# -*- coding: utf-8 -*-
"""Monta Data/Monsters/Mob.ini com TODOS os monstros do jogo.

    python tools/importar_mobs.py            mostra
    python tools/importar_mobs.py --aplicar  grava

Ate 25/08 o Mob.ini tinha QUATRO entradas. O catalogo de monstros do jogo tem quase mil.

Cada coluna vem de uma fonte diferente, e a diferenca importa mais do que o valor:

    nome, nivel     do CLIENT (npcInfo.lod). Completo, 1227 registros.
                    O nivel esta no offset 68 -- achado em 25/08 procurando o offset cujo
                    valor batesse com os tres niveis que ja tinhamos medidos. Bate nos tres
                    e tem 111 valores distintos na faixa 0..110, que e a cara de um nivel.

    hp_max          do FIO ([32:36] do 04 02). So existe para monstro que alguem ja viu.
                    NAO esta no client -- procurei o HP medido de 51 entidades em todos os
                    offsets do registro de 234 bytes e nenhum bate. Faz sentido: HP e estado
                    do servidor, e por isso ele vem no pacote de aparicao.

    exp_base        do FIO, por observacao de abate. So os que foram medidos.

A coluna `fonte` diz de onde veio cada linha, pra ninguem confundir nivel (que e catalogo,
confiavel) com HP (que e amostra, e so existe pro que foi visto).

O QUE NAO EXISTE EM LUGAR NENHUM: defesa, ataque, esquiva e bloqueio do monstro. Nao estao
no client e nao vem no fio -- sao do servidor. So saem por medicao de combate: o dano que
voce causa da a defesa por subtracao, o dano que voce recebe da o ataque, e as bandeiras do
05 02 (0x04 falha, 0x05 bloqueio, 0x06 esquiva) dao as taxas.
"""
import collections
import glob
import os
import re
import struct
import sys

RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(RAIZ, "server"))
sys.path.insert(0, os.path.join(RAIZ, "tools"))
import data_files as D                                              # noqa: E402
import lod_crypto                                                   # noqa: E402

LOD = r"C:\Games\Karos\ResUnpak\Interface\IE_Lod\npcInfo.lod_pt"
TAM = 234
OFF_ID, OFF_NIVEL = 0, 68
TIPO_NPC = 1          # no catalogo Npc.ini: 1 e NPC, o resto e monstro
CR = chr(13) + chr(10)
BARRA = chr(92)


def escapar(v):
    if v is None:
        return None
    s = str(v).replace(BARRA, BARRA + BARRA)
    return s.replace(chr(9), BARRA + "t").replace(chr(13), "").replace(chr(10), BARRA + "n")


def frames_de(cam):
    atual, blocos = None, []
    for l in open(cam, encoding="latin-1", errors="replace"):
        m = re.match(r"\[([\d:.]+)\] (SEND|RECV) sock=", l)
        if m:
            atual = [m.group(1), m.group(2), bytearray()]
            blocos.append(atual)
            continue
        if atual is not None and re.fullmatch(r"(?:[0-9A-Fa-f]{2} )+\s*", l):
            atual[2].extend(bytes.fromhex(l.strip()))
    for t, d, b in blocos:
        pos = 0
        while pos + 4 <= len(b):
            (n,) = struct.unpack_from(">I", b, pos)
            if n == 0 or n > 16384 or pos + 4 + n > len(b):
                break
            yield d, bytes(b[pos + 4:pos + 4 + n])
            pos += 4 + n


def hp_do_fio():
    """{npc_id: hp_max}, so onde todas as amostras concordam."""
    visto = collections.defaultdict(collections.Counter)
    for cam in sorted(glob.glob(r"C:\RoshHook\traffic*.log")):
        for d, c in frames_de(cam):
            if d == "RECV" and c[:2] == bytes([4, 2]) and len(c) >= 74:
                nid = struct.unpack_from("<I", c, 70)[0]
                if nid:
                    visto[nid][struct.unpack_from("<I", c, 32)[0]] += 1
    saida = {}
    for nid, v in visto.items():
        val, quantas = v.most_common(1)[0]
        # divergencia costuma ser pacote truncado, nao HP diferente; exige maioria folgada
        if val and quantas / sum(v.values()) >= 0.9:
            saida[nid] = val
    return saida


def nivel_do_client():
    d = lod_crypto.decrypt_lod_stream(open(LOD, "rb").read())
    n = struct.unpack_from("<I", d, 0)[0]
    saida = {}
    for k in range(n):
        r = d[4 + k * TAM: 4 + (k + 1) * TAM]
        if len(r) < TAM:
            break
        saida[struct.unpack_from("<I", r, OFF_ID)[0]] = struct.unpack_from("<I", r, OFF_NIVEL)[0]
    return saida


def main() -> int:
    aplicar = "--aplicar" in sys.argv
    D.carregar()
    catalogo = {int(r["npc_id"]): r for r in D.tabela("Npc")}
    niveis = nivel_do_client()
    hps = hp_do_fio()
    antigos = {int(r["mob_id"]): r for r in D.tabela("Mob")}

    mobs = {k: v for k, v in catalogo.items() if v.get("tipo") != TIPO_NPC}
    print("catalogo Npc.ini : %d entradas (%d monstros, %d NPCs)"
          % (len(catalogo), len(mobs), len(catalogo) - len(mobs)))
    print("nivel do client  : %d registros" % len(niveis))
    print("HP do fio        : %d entidades" % len(hps))
    print("Mob.ini de hoje  : %d entradas" % len(antigos))
    print()

    linhas = []
    for mid in sorted(mobs):
        c = mobs[mid]
        velho = antigos.get(mid, {})
        niv = niveis.get(mid)
        hp = hps.get(mid) or velho.get("hp_max")
        exp = velho.get("exp_base")
        fonte = ["client"] if niv is not None else []
        if hp:
            fonte.append("fio")
        linhas.append({"mob_id": mid, "nome": c.get("nome"), "nivel": niv,
                       "hp_max": hp, "exp_base": exp,
                       "fonte": "+".join(fonte) or "so nome"})

    com_nivel = sum(1 for l in linhas if l["nivel"] is not None)
    com_hp = sum(1 for l in linhas if l["hp_max"])
    com_exp = sum(1 for l in linhas if l["exp_base"])
    print("a lista nova teria %d monstros:" % len(linhas))
    print("   com nivel   : %d" % com_nivel)
    print("   com hp_max  : %d" % com_hp)
    print("   com exp_base: %d" % com_exp)
    print()
    print("exemplos (os que tem HP medido):")
    for l in [x for x in linhas if x["hp_max"]][:12]:
        print("   %-10s %-28s nivel %-4s hp %-6s exp %-5s [%s]"
              % (l["mob_id"], str(l["nome"])[:28], l["nivel"], l["hp_max"],
                 l["exp_base"], l["fonte"]))

    perdidos = [m for m in antigos if m not in mobs]
    if perdidos:
        print()
        print("SAEM da lista (nao sao monstro no catalogo): %s" % perdidos)

    if not aplicar:
        print()
        print("nada gravado. rode com --aplicar")
        return 0

    destino = D.caminho("Mob", "Monsters")
    cols = ("mob_id", "nome", "nivel", "hp_max", "exp_base", "fonte")
    with open(destino, "w", encoding="utf-8", newline=CR) as fp:
        w = fp.write
        w("; Mob -- catalogo de monstros." + CR)
        w("; colunas: " + ", ".join(cols) + CR)
        w(";" + CR)
        w("; De onde vem cada coluna, que e o que mais importa aqui:" + CR)
        w(";   nome, nivel  do CLIENT (npcInfo.lod, nivel no offset 68). COMPLETO." + CR)
        w(";   hp_max       do FIO ([32:36] do 04 02). So pra monstro que alguem ja viu --" + CR)
        w(";                nao esta no client, procurei em todos os offsets e nao bate." + CR)
        w(";   exp_base     do FIO, por abate observado. So os medidos." + CR)
        w(";   fonte        diz o que a linha realmente tem, pra nao confundir catalogo" + CR)
        w(";                com amostra." + CR)
        w(";" + CR)
        w("; NAO EXISTE AQUI: defesa, ataque, esquiva e bloqueio. Nao estao no client nem vem" + CR)
        w("; no fio -- sao do servidor. A defesa hoje e uma constante unica no entity_sim" + CR)
        w("; (DEFESA_DE_QUALQUER_MOB = 3), porque bater num mob fraco e num forte tira o mesmo" + CR)
        w("; dano; o que muda com o nivel e a chance de errar." + CR)
        w(CR)
        for l in linhas:
            w("[%d]" % l["mob_id"] + CR)
            for c in cols:
                if c == "mob_id":
                    continue
                v = escapar(l.get(c))
                if v not in (None, ""):
                    w("%s = %s" % (c, v) + CR)
            w(CR)
    print()
    print("gravado: %s (%s bytes, %d monstros)"
          % (destino, "{:,}".format(os.path.getsize(destino)), len(linhas)))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
