# -*- coding: utf-8 -*-
"""Extrai tudo que passar de SKILL numa captura do oficial.

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

Skill e o maior buraco do emulador: nenhum personagem tem uma, nao ha tabela de skill por
personagem, as 49 tabelas SkillTree_* do client nunca foram abertas, e seis opcodes da
familia 05 chegam do servidor sem que a gente responda nada.

Este script nao "sabe" o formato de nenhum deles -- e justamente o contrario. Ele agrupa
os pacotes desconhecidos, mostra o que varia dentro de cada um e cruza com o gasto de MP
e com o dano, pra que a leitura do formato comece de dado real em vez de chute.

Opcodes da familia de combate/skill vistos nas capturas:
    05 00  CAttackStartPacket        (tratado)
    05 01  CAttackDecidePacket       (tratado)
    05 02  CAttackDamagePacket       (tratado)
    05 03  CAttackSkillReadyPacket   <- nao tratado
    05 04  ?                         <- nao tratado
    05 05  ?                         <- nao tratado
    05 09  ?                         <- nao tratado
    05 0a  CCoolTimeSkillListPacket  <- nao tratado
    05 0c  ?                         <- nao tratado
"""
import collections
import os
import re
import struct
import sys

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

CONHECIDOS = {
    (5, 0): "CAttackStart (tratado)",
    (5, 1): "CAttackDecide (tratado)",
    (5, 2): "CAttackDamage (tratado)",
    (5, 3): "CAttackSkillReady",
    (5, 10): "CCoolTimeSkillList",
    (6, 47): "CCoolTimeList",
}


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 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

    tudo = list(frames(cam))
    fam5 = [(h, d, f) for h, d, f in tudo if len(f) >= 2 and f[0] == 5]
    mp = [(h, struct.unpack_from(">I", f, 20)[0], struct.unpack_from(">I", f, 24)[0])
          for h, d, f in tudo
          if d == "RECV" and f[:2] == bytes([8, 1]) and len(f) >= 109]

    print("captura: %s" % os.path.basename(cam))
    print("%d frames no total, %d da familia 05 (combate/skill)" % (len(tudo), len(fam5)))
    print()

    # ---------- 1. o que apareceu ----------
    print("=" * 84)
    print("OPCODES DA FAMILIA 05")
    print("=" * 84)
    cont = collections.Counter((d, (f[0], f[1])) for h, d, f in fam5)
    tam = collections.defaultdict(set)
    for h, d, f in fam5:
        tam[(d, (f[0], f[1]))].add(len(f))
    for (d, op), n in sorted(cont.items(), key=lambda kv: -kv[1]):
        nome = CONHECIDOS.get(op, "")
        print("   %s %02x %02x  %5d frames  tam=%-16s %s"
              % (d, op[0], op[1], n, str(sorted(tam[(d, op)])[:4]), nome))

    # ---------- 2. o miolo de cada desconhecido ----------
    print()
    print("=" * 84)
    print("DENTRO DE CADA OPCODE NAO TRATADO")
    print("=" * 84)
    naotratados = [(5, 3), (5, 4), (5, 5), (5, 9), (5, 10), (5, 12)]
    for op in naotratados:
        grupo = [(h, d, f) for h, d, f in fam5 if (f[0], f[1]) == op]
        if not grupo:
            continue
        print()
        print("### %02x %02x  %s  (%d frames)"
              % (op[0], op[1], CONHECIDOS.get(op, "sem nome"), len(grupo)))
        por_tam = collections.defaultdict(list)
        for h, d, f in grupo:
            por_tam[len(f)].append(f)
        for t, fs in sorted(por_tam.items()):
            print("   -- %d bytes, %d frames" % (t, len(fs)))
            print("      1o: %s" % " ".join("%02x" % x for x in fs[0][:48]))
            if len(fs) > 1:
                varia = [i for i in range(t) if len({f[i] for f in fs}) > 1]
                print("      offsets que variam: %s" % (varia[:24] or "nenhum"))
                for i in varia[:6]:
                    vals = collections.Counter(f[i] for f in fs)
                    print("         [%2d] %s" % (i, " ".join("%d(%d)" % v
                                                             for v in vals.most_common(5))))

    # ---------- 3. custo de MP ----------
    print()
    print("=" * 84)
    print("MP AO LONGO DA CAPTURA  (o custo da skill sai daqui)")
    print("=" * 84)
    if not mp:
        print("   nenhum 08 01 -- sem como ver o MP")
    else:
        ant = None
        quedas = collections.Counter()
        for h, atual, maximo in mp:
            if ant is not None and atual < ant:
                quedas[ant - atual] += 1
                print("   %-14s MP %d -> %d  (-%d)" % (h, ant, atual, ant - atual))
            ant = atual
        if quedas:
            print()
            print("   quedas de MP distintas (candidatas a custo de skill):")
            for v, n in quedas.most_common():
                print("      -%-4d  x%d" % (v, n))
        else:
            print("   o MP nunca caiu -- nenhuma skill foi usada nesta captura")
    return 0


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