# -*- coding: utf-8 -*-
"""Extrai as curvas de progressao de uma captura do servidor oficial.

    python tools/analisar_progressao.py                 usa C:/RoshHook/traffic.log
    python tools/analisar_progressao.py <captura.log>

O que sai daqui, tudo do CCharInfoHeroGage (08 01), 109 bytes:

    [ 7:11] id do personagem   BE
    [15:19] HP atual   [19:23] HP maximo    (LE)
    [23:27] MP atual   [27:31] MP maximo    (LE)
    [43]    nivel
    [45:49] reputacao          LE
    [54:58] contador54 (SP?)   BE  -- sobe ~12 por abate, ainda sem nome confirmado
    [65:69] FP / Fleta         LE
    [69:73] EXP                LE  -- LE em 69, nao BE em 66: ver build_hero_gage
    [90:94] nivel - 1          BE

O script cruza isso com os abates (05 02 com dano que zera o HP do alvo, e o 08 06 que
move a barra) pra responder:

    1. quanta EXP cada especie de mob da
    2. quanta EXP falta pra subir em cada nivel
    3. quanto HP e MP se ganha por nivel, POR CLASSE
    4. se a EXP muda com a diferenca de nivel entre personagem e mob
    5. como o FP (Fleta) sobe

O ponto 3 e o mais urgente: hoje o banco usa 36 de HP e 10 de MP por nivel para as SETE
classes, medido numa Maga so.
"""
import collections
import os
import re
import struct
import sys

PADRAO = r"C:\RoshHook\traffic.log"

CLASSES = {100: "Guerreiro", 200: "Paladina", 300: "Vagabundo", 400: "Feiticeira",
           500: "Arqueira", 600: "Maga", 800: "Artilheiro"}

# EXP base ja medida SEM evento, pra reconhecer multiplicador na captura nova
EXP_BASE_CONHECIDA = {24: "Aranha pequena"}


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


def ler_gage(f):
    # HP e MP sao u32 LITTLE-endian em [15], [19], [23] e [27] -- nao big-endian em
    # [12], [16], [20], [24]. As duas leituras dao o MESMO numero enquanto o valor for
    # menor que 256, porque o byte baixo cai no mesmo lugar; por isso o erro atravessou
    # o projeto inteiro sem aparecer. So estourou quando um personagem passou de 255 de
    # HP: o HP maximo virou 16777328 (0x01000070) e o MP atual 16777354. Mesmo padrao do
    # bug da EXP, que tambem era LE deslocado tres bytes.
    return {
        "id": struct.unpack_from(">I", f, 7)[0],
        "hp": struct.unpack_from("<I", f, 15)[0],
        "hp_max": struct.unpack_from("<I", f, 19)[0],
        "mp": struct.unpack_from("<I", f, 23)[0],
        "mp_max": struct.unpack_from("<I", f, 27)[0],
        "nivel": f[43],
        "reput": struct.unpack_from("<I", f, 45)[0],
        "c54": struct.unpack_from(">I", f, 54)[0],
        "fp": struct.unpack_from("<I", f, 65)[0],
        "exp": struct.unpack_from("<I", f, 69)[0],
    }


def main() -> int:
    cam = sys.argv[1] if len(sys.argv) > 1 else PADRAO
    if not os.path.exists(cam):
        print("nao achei a captura: %s" % cam)
        return 1

    linha_tempo = []          # (hora, gage)
    npcs_vistos = {}          # entity_id -> npc_id
    classes = collections.Counter()
    nomes = collections.Counter()
    for h, d, f in frames(cam):
        if d != "RECV":
            continue
        if f[:2] == bytes([8, 1]) and len(f) >= 109:
            linha_tempo.append((h, ler_gage(f)))
        elif f[:2] == bytes([4, 2]) and len(f) >= 74:
            npcs_vistos[struct.unpack_from("<I", f, 8)[0]] = \
                struct.unpack_from("<I", f, 70)[0]
            # 04 02 de JOGADOR: tem nome no fim e a classe em [246:248]. Nos de mob
            # esse offset nem existe -- sao 260 bytes secos, sem nome.
            if len(f) > 260:
                cl = struct.unpack_from("<H", f, 246)[0]
                if cl in CLASSES:
                    classes[cl] += 1
                    tam = struct.unpack_from(">H", f, 258)[0]
                    if 0 < tam < 60 and 260 + tam <= len(f):
                        nomes[f[260:260 + tam].decode("latin-1", "replace")] += 1

    if not linha_tempo:
        print("nenhum 08 01 nesta captura -- ela pegou RECV? (so SEND nao serve)")
        return 1

    # Uma captura pode ter VARIOS personagens: da pra trocar de personagem sem fechar o
    # jogo, e o hook segue gravando no mesmo arquivo. Separa por id e analisa um por um,
    # em vez de misturar as curvas de duas classes.
    por_id = collections.defaultdict(list)
    for h, g in linha_tempo:
        por_id[g["id"]].append((h, g))
    if len(por_id) > 1:
        print("%d personagens nesta captura:" % len(por_id))
        for pid, amostras in sorted(por_id.items(), key=lambda kv: -len(kv[1])):
            niveis = sorted({g["nivel"] for _, g in amostras})
            print("   id %-8d %4d amostras, niveis %s-%s"
                  % (pid, len(amostras), niveis[0], niveis[-1]))
        print()
        alvo = max(por_id.items(), key=lambda kv: len(kv[1]))[0]
        print("analisando o de mais amostras (id %d). Pra ver outro:" % alvo)
        print("   python tools/analisar_progressao.py %s <id>" % os.path.basename(cam))
        print()
        if len(sys.argv) > 2:
            try:
                alvo = int(sys.argv[2])
            except ValueError:
                pass
        linha_tempo = por_id[alvo]

    print("%d amostras de progressao, %d entidades vistas" % (
        len(linha_tempo), len(npcs_vistos)))
    if classes:
        print("classes vistas: %s" % ", ".join(
            "%s (%d)" % (CLASSES.get(c, c), n) for c, n in classes.most_common(4)))
    if nomes:
        print("personagens   : %s" % ", ".join(n for n, _ in nomes.most_common(4)))
    print()

    # ---------- 1. a curva bruta ----------
    print("=" * 76)
    print("LINHA DO TEMPO (so quando algo muda)")
    print("=" * 76)
    print("%-14s %-3s %-9s %-9s %-8s %-7s %-7s %s" %
          ("hora", "Nv", "HP", "MP", "EXP", "reput", "c54(SP?)", "FP"))
    ant = None
    for h, g in linha_tempo:
        chave = (g["nivel"], g["hp_max"], g["mp_max"], g["exp"], g["reput"], g["c54"], g["fp"])
        if chave == ant:
            continue
        ant = chave
        print("%-14s %-3d %-9s %-9s %-8d %-7d %-8d %d" %
              (h, g["nivel"], "%d/%d" % (g["hp"], g["hp_max"]),
               "%d/%d" % (g["mp"], g["mp_max"]), g["exp"], g["reput"], g["c54"], g["fp"]))

    # ---------- 2. EXP por abate ----------
    print()
    print("=" * 76)
    print("GANHOS ENTRE AMOSTRAS  (cada linha e um evento)")
    print("=" * 76)
    ganhos = collections.Counter()
    ant = None
    for h, g in linha_tempo:
        if ant is not None:
            d_exp = g["exp"] - ant["exp"]
            d_rep = g["reput"] - ant["reput"]
            d_c54 = g["c54"] - ant["c54"]
            d_fp = g["fp"] - ant["fp"]
            if g["nivel"] == ant["nivel"] and (d_exp or d_rep or d_c54 or d_fp):
                print("%-14s EXP %+6d   reput %+4d   c54 %+4d   FP %+d"
                      % (h, d_exp, d_rep, d_c54, d_fp))
                if d_exp > 0:
                    ganhos[d_exp] += 1
        ant = g
    if ganhos:
        print()
        print("EXP por abate (valores distintos):")
        for val, qtd in ganhos.most_common():
            linha = "   %6d  x%d" % (val, qtd)
            for m in (2, 3, 5, 10):
                if val % m == 0 and (val // m) in EXP_BASE_CONHECIDA:
                    linha += "   = %d x%d (%s)" % (val // m, m,
                                                   EXP_BASE_CONHECIDA[val // m])
                    break
            else:
                if val in EXP_BASE_CONHECIDA:
                    linha += "   = base, sem evento (%s)" % EXP_BASE_CONHECIDA[val]
            print(linha)
        print()
        print("   ATENCAO AO EVENTO DE EXP. O servidor oficial roda eventos de EXP")
        print("   multiplicada, e isso NAO aparece em nenhum campo do pacote -- so no")
        print("   valor ganho. A mesma Aranha pequena deu 24 numa captura e 72 em outra:")
        print("   a segunda foi durante um evento 3x. Antes de gravar qualquer EXP de mob")
        print("   no banco, confirme se havia evento ativo e divida.")
        print()
        print("   O que o evento NAO multiplica (medido nas duas capturas): reputacao (+8)")
        print("   e o contador54 (+12) vieram iguais com e sem evento.")

    # ---------- 3. subida de nivel ----------
    print()
    print("=" * 76)
    print("SUBIDAS DE NIVEL")
    print("=" * 76)
    ant = None
    achou = False
    for h, g in linha_tempo:
        if ant is not None and g["nivel"] > ant["nivel"]:
            achou = True
            print("%-14s nivel %d -> %d   HP max %d -> %d (%+d)   MP max %d -> %d (%+d)"
                  % (h, ant["nivel"], g["nivel"], ant["hp_max"], g["hp_max"],
                     g["hp_max"] - ant["hp_max"], ant["mp_max"], g["mp_max"],
                     g["mp_max"] - ant["mp_max"]))
            print("%-14s   EXP antes de subir: %d" % ("", ant["exp"]))
        ant = g
    if not achou:
        print("   nenhuma nesta captura -- pra medir a curva e preciso SUBIR de nivel")

    # ---------- 4. o que aparece ----------
    print()
    print("=" * 76)
    print("FP / FLETA")
    print("=" * 76)
    fps = sorted({g["fp"] for _, g in linha_tempo})
    print("   valores vistos: %s" % fps[:16])
    if fps == [0]:
        print("   sempre zero -- este personagem nao tem Fleta, ou o FP so aparece")
        print("   depois de algum evento (usar item, entrar numa area, completar quest)")
    return 0


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