# -*- coding: utf-8 -*-
r"""Le uma captura do hook e escreve TUDO o que se sabe decifrar dela, organizado.

    python tools/analisar_captura.py <log>              relatorio no ecra
    python tools/analisar_captura.py <log> --gravar     escreve tambem as tabelas
    python tools/analisar_captura.py                    a captura mais recente do Desktop

POR QUE EXISTE. Cada pergunta ("qual a EXP por mob?", "quanto tempo dura o respawn?", "o mob
vira-se para mim?") estava a gerar um script descartavel que lia a mesma captura outra vez,
com o mesmo codigo de parsing copiado. Isto le uma vez e responde a todas -- e o que nao
souber decifrar aparece na lista de opcodes por decifrar, em vez de desaparecer em silencio.

O QUE SAI, por seccao:

    sessao        duracao, personagem, nivel, quanto andou
    opcodes       tudo o que passou no fio, com quanto se percebe de cada
    mobs          que especies apareceram, quantas, e como se moveram
    combate       golpes dados e levados, dano por bandeira, cadencia
    mortes        de monstro e do jogador, com a EXP que cada uma rendeu ou custou
    exp           ganho por especie e por diferenca de nivel
    itens         equipar e desequipar, com o efeito na ficha
    ficha         atributos ao longo da sessao e o que o equipamento mudou

Com --gravar, cada seccao vai tambem para `analise/<nome-da-captura>/*.ini`, no formato
tabular do projeto -- prontas a comparar entre capturas.
"""
import collections
import glob
import math
import os
import struct
import sys

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

CR = chr(13) + chr(10)
MOB_MINIMO = 1000          # npc_id abaixo disto e jogador ou NPC de cidade

# PETS. A familia 11xxxxxx nao sao monstros: sao animais de estimacao, que andam com o dono.
#
# Sao 54 entradas no Mob.ini, 17 nomes com tres ids consecutivos cada -- os tres estagios de
# evolucao -- e todos com nivel 0, o unico grupo assim. O client tem petLevel.lod com onze
# registos, que sao os niveis 0 a 10.
#
# Isto tem de ser separado nas medidas. O "Miau ladrao" apareceu numa analise a perseguir a
# 87,6 unidades por segundo, mais do dobro dos 36-41 de qualquer monstro, e a conclusao
# apressada foi que havia especies muito mais rapidas. Nao ha: aquilo era um pet a acompanhar
# o dono, e o dono corre mais depressa que os monstros.
PET_MINIMO, PET_MAXIMO = 11000000, 12000000

# Acima disto nao e caminhada. A corrida mais rapida medida no oficial e o Miau ladrao com
# 88 u/s, e ele e um pet a acompanhar o dono; monstro nenhum passa dos 41. 300 deixa folga
# de sobra para uma amostra atrasada e ainda apanha qualquer teleporte.
VEL_IMPOSSIVEL = 300.0     # unidades por segundo
SALTO_MINIMO = 150.0       # abaixo disto e passo, por mais depressa que pareca ter sido


def eh_pet(npc_id):
    return PET_MINIMO <= int(npc_id or 0) < PET_MAXIMO

# O que cada opcode e, tanto quanto se sabe hoje. O que nao estiver aqui aparece no
# relatorio como "por decifrar" -- e essa lista e o mapa do que falta.
OPCODES = {
    "02 00": "lista de personagens", "02 01": "criar personagem",
    "02 05": "ficha inicial", "02 06": "entrar no mundo",
    "03 00": "chat", "03 06": "chat de area", "03 0a": "megafone", "03 0b": "chat de guilda",
    "04 00": "entrar/ressuscitar", "04 01": "movimento", "04 02": "entidade aparece",
    "04 03": "entidade sai",
    "05 00": "golpe (bote)", "05 01": "golpe do client", "05 02": "dano",
    "05 03": "ataque continuo", "05 09": "estado de combate", "05 0b": "fim de combate",
    "06 00": "equipar", "06 01": "desequipar", "06 03": "item some", "06 04": "item aparece",
    "06 05": "inventario", "06 06": "item muda de sitio", "06 1e": "loja de consignacao",
    "06 25": "item usado", "06 28": "item novo",
    "08 00": "estado do heroi", "08 01": "barra do heroi (HP/MP/EXP/nivel)",
    "08 02": "ficha de atributos", "08 06": "HP de entidade", "08 09": "pedido de estado",
    "08 0f": "resposta de estado",
    "0b 00": "keepalive", "0c 09": "missao", "11 0e": "efeito", "16 01": "sistema",
}


def _p(v, q):
    if not v:
        return 0
    v = sorted(v)
    return v[min(len(v) - 1, int(q * len(v)))]


def carregar_tabelas():
    D.carregar()
    mobs = {}
    for r in D.tabela("Mob"):
        try:
            mobs[int(r["mob_id"])] = (r.get("nome") or "?", int(r.get("nivel") or 0))
        except (TypeError, ValueError):
            continue
    itens = {}
    for r in D.tabela("Item"):
        try:
            itens[int(r["item_id"])] = r.get("nome") or "?"
        except (TypeError, ValueError):
            continue
    return mobs, itens


def ler(caminho):
    """Passa a captura UMA vez e devolve tudo o que se conseguiu separar."""
    d = {
        "op": collections.Counter(), "tam": collections.defaultdict(collections.Counter),
        "npc_de": {}, "mov": collections.defaultdict(list),
        "golpes": [], "danos": [], "hp": [], "gage": [], "ficha": [],
        "equip": [], "jogador": [], "t0": None, "t1": None,
        # A VIDA DO JOGADOR VEM POR DOIS CAMINHOS, e e preciso guardar os dois.
        # Ver seccao_regeneracao para a historia de por que isto existe.
        "eu": None, "vida_gage": [], "vida_barra": [],
    }
    for t, op, c in pacotes(caminho, direcao="RECV"):
        k = op.hex(" ")
        d["op"][k] += 1
        d["tam"][k][len(c)] += 1
        if d["t0"] is None:
            d["t0"] = t
        d["t1"] = t
        if k == "04 02" and len(c) >= 74:
            ent = struct.unpack_from("<I", c, 8)[0]
            x, y, _z = struct.unpack_from("<3f", c, 12)
            d["npc_de"][ent] = struct.unpack_from("<I", c, 70)[0]
        elif k == "04 01" and len(c) >= 42:
            e = struct.unpack_from(">I", c, 8)[0]
            ox, oy, _z = struct.unpack_from("<3f", c, 12)
            dx, dy, _z2 = struct.unpack_from("<3f", c, 24)
            d["mov"][e].append((t, c[7], ox, oy, dx, dy,
                                struct.unpack_from("<f", c, 36)[0]))
        elif k == "05 00" and len(c) >= 55:
            d["golpes"].append((t, struct.unpack_from(">I", c, 7)[0],
                                struct.unpack_from(">I", c, 33)[0],
                                struct.unpack_from("<3f", c, 16)[:2],
                                struct.unpack_from("<3f", c, 37)[:2],
                                struct.unpack_from("<f", c, 28)[0]))
        elif k == "05 02" and len(c) == 51:
            b = struct.unpack_from(">I", c, 24)[0]
            d["danos"].append((t, struct.unpack_from(">I", c, 7)[0],
                               struct.unpack_from("<H", c, 22)[0],
                               b & 0xFF, (b >> 8) & 0xFF))
        elif k == "08 06" and len(c) >= 19:
            _ent = struct.unpack_from(">I", c, 7)[0]
            if len(c) >= 27:
                d["hp"].append((t, _ent,
                                struct.unpack_from(">I", c, 15)[0],
                                struct.unpack_from(">I", c, 19)[0]))
            # O 08 06 do PROPRIO jogador e por onde chegam os tiques de regeneracao.
            # Ver seccao_regeneracao: ignorar isto custou tres analises erradas em 28/08.
            if d["eu"] is not None and _ent == d["eu"]:
                d["vida_barra"].append((t, struct.unpack_from(">I", c, 15)[0], "08 06"))
        elif k == "08 01" and len(c) == 109:
            d["gage"].append((t, c[43],
                              struct.unpack_from("<Q", c, 69)[0],
                              struct.unpack_from("<Q", c, 77)[0],
                              struct.unpack_from("<I", c, 15)[0],
                              struct.unpack_from("<I", c, 19)[0]))
            # a entidade do jogador sai daqui, e e ela que identifica os 08 06 que sao DELE
            if d["eu"] is None:
                d["eu"] = struct.unpack_from(">I", c, 7)[0]
            d["vida_gage"].append((t, struct.unpack_from("<I", c, 15)[0], "08 01"))
        elif k == "08 02" and len(c) >= 794:
            d["ficha"].append((t, c))
        elif k in ("06 00", "06 01") and len(c) >= 17:
            d["equip"].append((t, k == "06 00", struct.unpack_from(">I", c, 13)[0]))
    for t, op, c in pacotes(caminho, direcao="SEND"):
        if op == b"\x04\x01" and len(c) >= 24:
            x, y, _z = struct.unpack_from("<3f", c, 12)
            d["jogador"].append((t, x, y))
        elif op == b"\x05\x01" and len(c) >= 28:
            x, y, _z = struct.unpack_from("<3f", c, 16)
            d["jogador"].append((t, x, y))
    d["jogador"].sort()
    return d


def seccao_sessao(d, out):
    dur = (d["t1"] - d["t0"]) if d["t0"] is not None else 0
    if dur < 0:
        dur += 86400
    out("SESSAO")
    out("   duracao          %.0f min (%.0f s)" % (dur / 60.0, dur))
    out("   pacotes lidos    %d" % sum(d["op"].values()))
    if d["gage"]:
        n0, n1 = d["gage"][0][1], d["gage"][-1][1]
        e0, e1 = d["gage"][0][2], d["gage"][-1][2]
        out("   nivel            %d -> %d" % (n0, n1))
        out("   EXP              %d -> %d" % (e0, e1))
        out("   limiar do nivel  %d" % d["gage"][-1][3])
    if len(d["jogador"]) > 1:
        # SO CONTA O QUE PODE TER SIDO ANDADO A PE.
        #
        # Somar todos os saltos dava 37 milhoes de unidades em 250 minutos -- 2.500 unidades
        # por segundo, cem vezes a velocidade de corrida. O que estava a entrar na conta eram
        # os teleportes, as trocas de mapa e o reaparecer depois de morrer, que no fio sao
        # apenas duas posicoes seguidas muito distantes.
        #
        # O criterio e a VELOCIDADE IMPLICITA entre duas amostras, e nao a distancia: uma
        # amostra pode faltar por segundos, e ai um salto grande e legitimo. Acima de
        # VEL_IMPOSSIVEL nao ha caminhada que explique.
        andou = saltos = 0.0
        n_saltos = 0
        for i in range(1, len(d["jogador"])):
            dt = d["jogador"][i][0] - d["jogador"][i-1][0]
            dist = math.hypot(d["jogador"][i][1] - d["jogador"][i-1][1],
                              d["jogador"][i][2] - d["jogador"][i-1][2])
            if dist < 0.01:
                continue          # duas amostras no mesmo sitio: nem andou nem saltou
            # SALTO PRECISA DAS DUAS COISAS: velocidade impossivel E distancia grande.
            #
            # So a velocidade nao chega. O log tem resolucao de milissegundo e o client manda
            # varias posicoes no mesmo instante, portanto dt sai zero muitas vezes -- e ai
            # qualquer passo de dois passos vira "velocidade infinita". Com so esse criterio
            # apareceram 52.296 saltos numa sessao, que seriam mais de tres teleportes por
            # segundo. Um teleporte de verdade move centenas de unidades de uma vez.
            if dist < SALTO_MINIMO:
                andou += dist
                continue
            if dt <= 0 or dist / dt > VEL_IMPOSSIVEL:
                saltos += dist
                n_saltos += 1
            else:
                andou += dist
        out("   o jogador andou  %.0f unidades" % andou)
        if n_saltos:
            out("   (mais %d saltos que somam %.0f -- teleporte, troca de mapa ou morte)"
                % (n_saltos, saltos))


def seccao_opcodes(d, out):
    out("")
    out("OPCODES  (o que passou no fio)")
    out("   %-8s %8s  %-30s %s" % ("opcode", "pacotes", "o que e", "tamanhos"))
    por_decifrar = []
    for k, n in d["op"].most_common():
        nome = OPCODES.get(k)
        tt = ", ".join(str(t) for t, _ in d["tam"][k].most_common(2))
        if nome is None:
            por_decifrar.append((k, n, tt))
            continue
        out("   %-8s %8d  %-30s %s" % (k, n, nome, tt))
    if por_decifrar:
        out("")
        out("   POR DECIFRAR -- estes passaram e ninguem sabe o que sao:")
        for k, n, tt in por_decifrar:
            out("   %-8s %8d  %-30s %s" % (k, n, "?", tt))


def seccao_mobs(d, mobs, out):
    out("")
    out("MONSTROS")
    vistos = collections.Counter()
    pets = collections.Counter()
    for ent, npc in d["npc_de"].items():
        if eh_pet(npc):
            pets[npc] += 1
        elif npc > MOB_MINIMO:
            vistos[npc] += 1
    if not vistos:
        out("   nenhum")
        return []
    linhas = []
    for npc, n in vistos.most_common():
        nome, niv = mobs.get(npc, ("?", 0))
        vel = {}
        for est in (0, 1):
            vs = []
            for ent, lst in d["mov"].items():
                if d["npc_de"].get(ent) != npc:
                    continue
                lst = sorted(lst)
                i = 0
                while i < len(lst):
                    if lst[i][1] != est:
                        i += 1
                        continue
                    j = i
                    while (j + 1 < len(lst) and lst[j+1][1] == est
                           and lst[j+1][0] - lst[j][0] < 2.0):
                        j += 1
                    if j > i:
                        dur = lst[j][0] - lst[i][0]
                        dist = sum(math.hypot(lst[k][2]-lst[k-1][2], lst[k][3]-lst[k-1][3])
                                   for k in range(i+1, j+1))
                        if dur > 0.5 and dist > 0:
                            vs.append(dist / dur)
                    i = j + 1
            vel[est] = _p(vs, .5)
        linhas.append((npc, nome, niv, n, vel[0], vel[1]))
    out("   %-10s %-26s %5s %6s %9s %11s" %
        ("npc", "nome", "nivel", "vistos", "passeio", "persegue"))
    for npc, nome, niv, n, v0, v1 in linhas:
        out("   %-10d %-26s %5d %6d %9.1f %11s"
            % (npc, nome[:26], niv, n, v0, ("%.1f" % v1) if v1 else "-"))
    if pets:
        out("")
        out("   PETS (nao entram nas medidas de monstro):")
        for npc, n in pets.most_common():
            nome, _ = mobs.get(npc, ("?", 0))
            out("      %-10d %-26s visto %d vezes" % (npc, nome[:26], n))
    return linhas


def seccao_combate(d, mobs, out):
    out("")
    out("COMBATE")
    alvos = collections.Counter(x[2] for x in d["danos"])
    jog = next((a for a, _ in alvos.most_common()
                if d["npc_de"].get(a, 0) <= MOB_MINIMO), None)
    if jog is None:
        out("   sem dano nesta captura")
        return None
    NOME_B = {1: "acertou", 2: "critico/forte", 4: "falhou", 5: "bloqueado", 6: "esquivado"}
    for lado, titulo in ((True, "GOLPES QUE O JOGADOR LEVOU"),
                         (False, "GOLPES QUE O JOGADOR DEU")):
        g = [x for x in d["danos"]
             if ((x[2] == jog) == lado)
             and (d["npc_de"].get(x[1], 0) > MOB_MINIMO) == lado]
        if not g:
            continue
        out("   %s  (n=%d)" % (titulo, len(g)))
        por_b = collections.defaultdict(list)
        for _t, _at, _al, dano, band in g:
            por_b[band].append(dano)
        for b in sorted(por_b):
            v = [x for x in por_b[b] if x > 0]
            med = ("dano mediano %d" % _p(v, .5)) if v else "dano 0"
            out("      bandeira %d %-14s %4d  %5.1f%%   %s"
                % (b, NOME_B.get(b, "?"), len(por_b[b]),
                   100.0 * len(por_b[b]) / len(g), med))
    # cadencia dos golpes de mob
    pares = collections.defaultdict(list)
    for t, at, alvo, _d, _b in d["danos"]:
        if d["npc_de"].get(at, 0) > MOB_MINIMO:
            pares[(at, alvo)].append(t)
    cad = []
    for _k, ts in pares.items():
        ts.sort()
        cad += [ts[i] - ts[i-1] for i in range(1, len(ts)) if 0 < ts[i] - ts[i-1] < 15]
    if cad:
        out("   cadencia dos golpes de mob: mediana %.2f s" % _p(cad, .5))
    return jog


def seccao_mortes_e_exp(d, mobs, out):
    out("")
    out("MORTES E EXP")
    # morte de mob = HP a zero num npc de monstro
    mortes = [(t, d["npc_de"][e]) for t, e, hp, _m in d["hp"]
              if hp == 0 and d["npc_de"].get(e, 0) > MOB_MINIMO]
    out("   monstros mortos: %d" % len(mortes))
    if d["gage"]:
        quedas = [(a[0], a[2], b[2]) for a, b in zip(d["gage"], d["gage"][1:]) if b[2] < a[2]]
        for t, antes, dep in quedas:
            out("   o jogador morreu: EXP %d -> %d, perdeu %d (%.2f%% do nivel)"
                % (antes, dep, antes - dep,
                   100.0 * (antes - dep) / d["gage"][-1][3] if d["gage"][-1][3] else 0))
    # EXP por especie, separando pela diferenca de nivel
    ev = [(t, "m", npc, 0) for t, npc in mortes]
    ev += [(g[0], "e", g[2], g[1]) for g in d["gage"]]
    ev.sort(key=lambda x: x[0])
    ganhos = collections.defaultdict(list)
    ult, pend, nivel = None, [], 1
    for t, tipo, a, b in ev:
        if tipo == "e":
            nivel = b or nivel
            if ult is not None and a > ult and pend:
                ganhos[(pend[-1], nivel)].append(a - ult)
                pend = []
            ult = a
        else:
            pend.append(a)
    if not ganhos:
        return []
    out("")
    out("   EXP POR MONSTRO")
    out("   %-24s %6s %7s %5s %5s %9s" %
        ("mob", "nv_mob", "nv_jog", "dif", "n", "exp"))
    linhas = []
    for (npc, nj), v in sorted(ganhos.items(), key=lambda kv: (kv[0][0], kv[0][1])):
        if len(v) < 3:
            continue
        nome, nm = mobs.get(npc, ("?", 0))
        out("   %-24s %6d %7d %5d %5d %9d"
            % (nome[:24], nm, nj, nj - nm, len(v), _p(v, .5)))
        linhas.append((npc, nome, nm, nj, nj - nm, len(v), _p(v, .5)))
    return linhas


def seccao_itens(d, itens, out):
    out("")
    out("ITENS")
    if not d["equip"]:
        out("   nenhuma troca de equipamento")
        return
    t0 = d["t0"] or 0
    for t, pos, item in d["equip"]:
        out("   +%7.1fs  %-9s item %-10d %s"
            % (t - t0, "EQUIPOU" if pos else "TIROU", item,
               (itens.get(item) or "?")[:44]))


def seccao_ficha(d, out):
    out("")
    out("FICHA DE ATRIBUTOS  (08 02)")
    if len(d["ficha"]) < 2:
        out("   menos de dois pacotes -- nada a comparar")
        return
    a, b = d["ficha"][0][1], d["ficha"][-1][1]
    CAMPOS = {62: "alcance", 90: "atq fisico", 94: "vel ataque", 98: "classif ataque",
              102: "protecao fisica", 118: "atq magico", 126: "protecao magica",
              134: "vel invocacao", 138: "MULTIPLICADOR DE CRITICO", 170: "chance critico"}
    out("   %-26s %12s %12s %10s" % ("campo", "primeiro", "ultimo", "delta"))
    for off, nome in sorted(CAMPOS.items()):
        try:
            va = struct.unpack_from("<f", a, off)[0]
            vb = struct.unpack_from("<f", b, off)[0]
        except struct.error:
            continue
        if abs(vb - va) < 0.001:
            continue
        out("   %-26s %12.3f %12.3f %10.3f" % (nome, va, vb, vb - va))
    for i, nome in enumerate(("forca", "destreza", "vitalidade",
                              "inteligencia", "sabedoria")):
        off = 24 + i * 2
        try:
            va = struct.unpack_from("<H", a, off)[0]
            vb = struct.unpack_from("<H", b, off)[0]
        except struct.error:
            continue
        if va != vb:
            out("   %-26s %12d %12d %10d" % ("bonus de " + nome, va, vb, vb - va))


def seccao_regeneracao(d, out):
    """A vida do jogador ao longo do tempo, pelos DOIS caminhos por onde ela chega.

    ISTO EXISTE POR CAUSA DE UM ERRO. Em 28/08 media-se a regeneracao pelo 08 01 -- a barra
    do heroi -- e dava sempre zero, o que levou a conclusao "o oficial nao regenera a nivel
    baixo". Errado: a barra do heroi so muda quando se leva dano ou se ganha EXP, e os tiques
    de regeneracao chegam pelo 08 06, a barra de ENTIDADE, com o id do proprio jogador.
    Foram tres analises seguidas a olhar para o sitio errado, e nenhuma ferramenta avisava.

    Agora avisa: esta seccao mostra os dois lado a lado e diz de qual vem cada subida.
    """
    eu = d.get("eu")
    if not eu:
        out("")
        out("VIDA DO JOGADOR")
        out("   nao identifiquei a entidade do jogador (sem 08 01 na captura)")
        return
    serie = sorted(d["vida_gage"] + d["vida_barra"])
    if len(serie) < 3:
        out("")
        out("VIDA DO JOGADOR")
        out("   menos de tres amostras")
        return
    out("")
    out("VIDA DO JOGADOR  (entidade %d)" % eu)
    out("   %d amostras: %d pelo 08 01 (barra do heroi), %d pelo 08 06 (barra de entidade)"
        % (len(serie), len(d["vida_gage"]), len(d["vida_barra"])))

    subidas = [(t, hp - ant[1], fonte)
               for (t, hp, fonte), ant in zip(serie[1:], serie)
               if hp > ant[1]]
    descidas = sum(1 for (t, hp, _f), ant in zip(serie[1:], serie) if hp < ant[1])
    out("   %d subidas, %d descidas" % (len(subidas), descidas))
    if not subidas:
        return
    por_fonte = collections.Counter(f for _t, _d, f in subidas)
    out("   subidas por caminho: %s" % dict(por_fonte))

    # tiques regulares: os que se repetem com o mesmo passo
    passos = collections.Counter(p for _t, p, _f in subidas)
    out("   passos mais comuns: %s"
        % ", ".join("+%d x%d" % (k, v) for k, v in passos.most_common(5)))
    reg = [t for t, p, _f in subidas if p == passos.most_common(1)[0][0]]
    if len(reg) >= 3:
        gaps = [round(b - a, 2) for a, b in zip(reg, reg[1:]) if 0.5 < b - a < 60]
        if gaps:
            gaps.sort()
            out("   intervalo entre tiques do passo dominante: mediana %.2f s (%d amostras)"
                % (gaps[len(gaps) // 2], len(gaps)))
    # atraso desde o ultimo dano
    if d["danos"]:
        ult = None
        atrasos = []
        marcos = sorted(x[0] for x in d["danos"])
        for t, _p, _f in subidas:
            antes = [m for m in marcos if m < t]
            if antes and (ult is None or t - antes[-1] > 0):
                atrasos.append(t - antes[-1])
        seguidos = [a for a in atrasos if a < 120]
        if seguidos:
            seguidos.sort()
            out("   atraso desde o ultimo pacote de dano: minimo %.1f s, mediana %.1f s"
                % (seguidos[0], seguidos[len(seguidos) // 2]))


def gravar_tabelas(nome, mobs_linhas, exp_linhas):
    """Escreve as tabelas no formato do projeto, para comparar entre capturas."""
    pasta = os.path.join(RAIZ, "analise", nome)
    os.makedirs(pasta, exist_ok=True)
    escritos = []
    if mobs_linhas:
        cam = os.path.join(pasta, "Mobs.ini")
        with open(cam, "w", encoding="latin-1", newline="") as f:
            f.write("; Monstros vistos em %s" % nome + CR)
            f.write("; colunas: npc_id, nome, nivel, vistos, vel_passeio, vel_persegue" + CR)
            f.write(CR)
            f.write("//npc_id\tnome\tnivel\tvistos\tvel_passeio\tvel_persegue" + CR)
            for npc, n, niv, q, v0, v1 in mobs_linhas:
                f.write("%d\t%s\t%d\t%d\t%.1f\t%.1f" % (npc, n, niv, q, v0, v1) + CR)
        escritos.append(cam)
    if exp_linhas:
        cam = os.path.join(pasta, "ExpPorMob.ini")
        with open(cam, "w", encoding="latin-1", newline="") as f:
            f.write("; EXP que cada monstro rendeu em %s" % nome + CR)
            f.write("; colunas: npc_id, nome, nivel_mob, nivel_jogador, diferenca, amostras, exp" + CR)
            f.write(CR)
            f.write("//npc_id\tnome\tnivel_mob\tnivel_jogador\tdiferenca\tamostras\texp" + CR)
            for npc, nome_m, nm, nj, dif, n, e in exp_linhas:
                f.write("%d\t%s\t%d\t%d\t%d\t%d\t%d" % (npc, nome_m, nm, nj, dif, n, e) + CR)
        escritos.append(cam)
    return escritos


def main() -> int:
    args = [a for a in sys.argv[1:] if not a.startswith("--")]
    gravar = "--gravar" in sys.argv
    if args:
        caminho = args[0]
    else:
        cands = sorted(glob.glob(os.path.join(os.path.expanduser("~"), "Desktop",
                                              "CapturaKaros", "*.log")),
                       key=os.path.getmtime, reverse=True)
        cands += sorted(glob.glob("C:" + os.sep + "RoshHook" + os.sep + "traffic.log"))
        if not cands:
            print("nenhuma captura encontrada")
            return 1
        caminho = cands[0]
    if not os.path.exists(caminho):
        print("nao existe: %s" % caminho)
        return 1

    linhas = []

    def out(s=""):
        linhas.append(s)
        print(s)

    out("=" * 76)
    out("%s   (%.1f MB)" % (os.path.basename(caminho), os.path.getsize(caminho) / 1e6))
    out("=" * 76)
    mobs, itens = carregar_tabelas()
    d = ler(caminho)
    seccao_sessao(d, out)
    seccao_opcodes(d, out)
    mobs_linhas = seccao_mobs(d, mobs, out)
    seccao_combate(d, mobs, out)
    exp_linhas = seccao_mortes_e_exp(d, mobs, out)
    seccao_itens(d, itens, out)
    seccao_ficha(d, out)
    seccao_regeneracao(d, out)

    if gravar:
        nome = os.path.splitext(os.path.basename(caminho))[0]
        pasta = os.path.join(RAIZ, "analise", nome)
        os.makedirs(pasta, exist_ok=True)
        with open(os.path.join(pasta, "relatorio.txt"), "w",
                  encoding="latin-1", errors="replace", newline="") as f:
            f.write(CR.join(linhas) + CR)
        escritos = gravar_tabelas(nome, mobs_linhas, exp_linhas)
        out("")
        out("gravado em analise/%s/  (relatorio.txt%s)"
            % (nome, "".join(", " + os.path.basename(e) for e in escritos)))
    return 0


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