# -*- coding: utf-8 -*-
"""Guarda TRAJETORIAS REAIS de monstro, tiradas das capturas do oficial.

    python tools/extrair_trilhas_mob.py            mostra
    python tools/extrair_trilhas_mob.py --aplicar  grava em Data/Monsters/MobTrack.ini

POR QUE ISTO EXISTE

Em 25/08 passei o dia tentando reproduzir o movimento dos monstros por modelagem: medi a
distancia do passo, a velocidade, a distribuicao de virada, o tamanho do trecho, a duracao da
pausa. TODAS as estatisticas agregadas bateram com o oficial -- e o movimento na tela
continuou errado, com o bicho girando em torno do proprio eixo.

O jogador filmou. Em modo diagnostico, andando em LINHA RETA, sem virar nem parar, o lagarto
ainda girava 180 graus em meio segundo. Ou seja: o defeito nao estava em nenhuma das
estatisticas que eu vinha ajustando.

A saida e a mesma que ja funcionou duas vezes hoje, nos pacotes de aparicao e nos ids de
entidade: parar de modelar e usar o que o servidor oficial mandou. Uma trilha gravada nao
precisa de teoria sobre ritmo, velocidade ou virada -- ela E o movimento, com o tempo entre
pacotes, os campos e as posicoes que o client comprovadamente aceita.

O QUE E GUARDADO

Cada passo da trilha, relativo ao inicio dela:

    ms          quando, em milissegundos desde o inicio
    x, y        posicao, relativa ao ponto de partida
    dx, dy      destino declarado, relativo a posicao daquele passo
    vel         o campo [36:40]
    estado      o byte [7]: 0 andando, 2 parado

Na hora de usar, o mob soma a propria ancora e reproduz. Como a trilha veio de um mob real
perambulando em volta da ancora DELE, ela ja fica contida -- nao precisa de raio.
"""
import collections
import glob
import math
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"))
import data_files as D                                              # noqa: E402

CR = chr(13) + chr(10)
MIN_PASSOS = 60           # trilha curta demais fica com laco visivel
MAX_PASSOS = 400          # ~4 minutos de movimento; o suficiente pra nao parecer repeticao

# TRILHAS POR ESPECIE, escolhidas por PERCENTIL DE ATIVIDADE.
#
# A primeira versao pegava as tres MAIS LONGAS de cada especie, e isso escolhia justamente
# os bichos mais agitados da captura. O jogador viu na hora: "no original eles andam tao
# menos". Medindo a velocidade media de cada monstro ao longo da vida dele, em 400
# entidades:
#
#     p10  0,03    p25  0,07    p50  2,89    p75  5,26    p90  5,99    u/s
#
# As trilhas que eu tinha escolhido andavam a 11,1 -- o percentil 94. Um quarto dos monstros
# do oficial fica praticamente parado, e nenhum dos nossos ficava.
#
# Agora sao seis trilhas por especie, uma em cada percentil abaixo, e cada entidade sorteia
# uma. Isso reproduz a POPULACAO, nao um individuo.
PERCENTIS = (10, 25, 40, 55, 75, 90)
POR_ESPECIE = len(PERCENTIS)


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 t, d, bytes(b[pos + 4:pos + 4 + n])
            pos += 4 + n


def para_segundos(t):
    h, m, s = t.split(":")
    return int(h) * 3600 + int(m) * 60 + float(s)


BURACO_S = 7.0
"""Acima disto, duas leituras do mesmo mob nao sao seguidas: ele saiu do campo de visao de
quem gravou e voltou. Medido no oficial em 163 mil intervalos: mediana 0,61, p99 6,79, e
depois um salto para 98 s no p99,9 -- a assinatura de outra coisa."""

SALTO_U = 20.0
"""Idem para a posicao. O passo normal e 6,6 u; 20 e o triplo disso."""


def _cortar(v):
    """Parte a leitura de um mob nas COSTURAS. E A CORRECCAO DE 30/08.

    A versao anterior agrupava por (captura, entidade, especie) e nunca cortava. Sempre que
    o bicho saia de vista e voltava -- ou o id de entidade era reaproveitado -- os passos
    continuavam a ser somados a MESMA trilha. As 55 trilhas gravadas ficaram assim:

        54 das 55 com buraco de tempo acima de 3 s, o maior de 9872 s (2h45)
        30 das 55 com salto de posicao acima de 20 u, o maior de 151 u

    Na tela isso e um monstro que congela e depois marcha em linha recta por 151 unidades.
    O jogador disse-o em 30/08: "nossa trilha ta tudo bugadas, a ferramenta nao foi precisa".

    Cortar rende MAIS material, nao menos: 2044 pedacos limpos com 60 passos ou mais,
    contra as 55 trilhas coladas de antes.
    """
    out, act = [], [v[0]]
    for a, b in zip(v, v[1:]):
        if (b[0] - a[0]) > BURACO_S or math.hypot(b[1] - a[1], b[2] - a[2]) > SALTO_U:
            out.append(act)
            act = [b]
        else:
            act.append(b)
    out.append(act)
    return out


FONTES = (
    r"C:\RoshHook\traffic*.log",
    r"C:\Users\Administrator\Desktop\CapturaKaros\*.log",
)
"""Onde estao as capturas do OFICIAL.

A segunda pasta esteve de fora ate 30/08 e e a maior das duas: so o
`karos_20260827_0838.log` tem 108 MB, mais do que tudo o que estava na primeira. Foi o
jogador que a apontou -- "nessa pasta e tudo do original tbm".

NAO PODE ENTRAR AQUI CAPTURA NOSSA. Uma trilha tirada do nosso servidor seria o nosso
movimento a servir de modelo para si proprio, e o defeito que as trilhas vieram
corrigir voltava por dentro sem dar sinal.
"""


def fontes():
    """Os ficheiros de captura, sem repetidos e por ordem."""
    vistos, saida = set(), []
    for padrao in FONTES:
        for c in sorted(glob.glob(padrao)):
            chave = os.path.abspath(c).lower()
            if chave in vistos:
                continue
            vistos.add(chave)
            saida.append(c)
    return saida


def coletar():
    """{(captura, entidade, npc_id, pedaco): [(t, ox, oy, dx, dy, vel, estado)]}"""
    trilhas = collections.defaultdict(list)
    quem = {}
    for cam in fontes():
        for t, d, c in frames_de(cam):
            if d != "RECV":
                continue
            if c[:2] == bytes([4, 2]) and len(c) >= 74:
                quem[struct.unpack_from("<I", c, 8)[0]] = struct.unpack_from("<I", c, 70)[0]
            elif c[:2] == bytes([4, 1]) and len(c) == 42:
                eid = struct.unpack_from(">I", c, 8)[0]
                nid = quem.get(eid)
                if not nid:
                    continue
                ox, oy, _ = struct.unpack_from("<3f", c, 12)
                dx, dy, _ = struct.unpack_from("<3f", c, 24)
                v = struct.unpack_from("<f", c, 36)[0]
                trilhas[(cam, eid, nid)].append(
                    (para_segundos(t), ox, oy, dx, dy, v, c[7]))
    # CORTAR NAS COSTURAS antes de devolver -- ver _cortar.
    saida = {}
    for (cam_, eid, nid), v in trilhas.items():
        for i, pedaco in enumerate(_cortar(v)):
            saida[(cam_, eid, nid, i)] = pedaco
    return saida


def main() -> int:
    aplicar = "--aplicar" in sys.argv
    D.carregar()
    mob = D.indice("Mob", "mob_id")
    presentes = {int(r["npc_id"]) for r in D.tabela("NpcSpawn")}

    trilhas = coletar()
    boas = {k: v[:MAX_PASSOS] for k, v in trilhas.items()
            if len(v) >= MIN_PASSOS and k[2] in presentes}
    def atividade(passos):
        """Velocidade media da trilha: o que separa um mob parado de um agitado."""
        dur = passos[-1][0] - passos[0][0]
        if dur <= 0:
            return 0.0
        d = 0.0
        for i in range(1, len(passos)):
            d += ((passos[i][1] - passos[i-1][1]) ** 2 +
                  (passos[i][2] - passos[i-1][2]) ** 2) ** 0.5
        return d / dur

    por_especie = collections.defaultdict(list)
    for k, v in boas.items():
        por_especie[k[2]].append((atividade(v), k, v))
    escolhidas = {}
    for nid, lista in por_especie.items():
        lista.sort(key=lambda x: x[0])
        n = len(lista)
        vistos, saida = set(), []
        for p in PERCENTIS:
            i = min(n - 1, max(0, (n * p) // 100))
            if i in vistos:
                continue
            vistos.add(i)
            saida.append(lista[i][2])
        escolhidas[nid] = saida

    print("especies no mundo        : %d" % len(presentes))
    print("especies com trilha real : %d" % len(escolhidas))
    print()
    print("%-10s %-28s %-9s %s" % ("mob_id", "nome", "trilhas", "u/s de cada uma"))
    print("-" * 62)
    total = 0
    for nid in sorted(escolhidas, key=lambda k: -len(escolhidas[k])):
        ns = [len(t) for t in escolhidas[nid]]
        total += sum(ns)
        vels = []
        for t in escolhidas[nid]:
            dur = t[-1][0] - t[0][0]
            d = sum(((t[i][1]-t[i-1][1])**2 + (t[i][2]-t[i-1][2])**2) ** 0.5
                    for i in range(1, len(t)))
            vels.append(d/dur if dur > 0 else 0)
        print("%-10s %-28s %-9d %s" % (nid, str((mob.get(nid) or {}).get("nome"))[:28],
                                       len(ns), " ".join("%.1f" % v for v in vels)))
    faltam = sorted(presentes - set(escolhidas))
    print()
    print("sem trilha (vao usar a de outra especie): %d  %s"
          % (len(faltam), [str((mob.get(n) or {}).get("nome"))[:18] for n in faltam[:6]]))
    print("passos no total: %d" % total)

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

    destino = D.caminho("MobTrack", "Monsters")
    with open(destino, "w", encoding="utf-8", newline=CR) as fp:
        w = fp.write
        w("; MobTrack -- trajetorias REAIS de monstro, gravadas do servidor oficial." + CR)
        w("; colunas: chave, npc_id, trilha, passos, dados" + CR)
        w(";" + CR)
        w("; Cada passo e  ms:x:y:dx:dy:vel:estado  separado por espaco, relativo ao inicio" + CR)
        w("; da trilha. x e y sao a posicao; dx e dy o destino declarado, relativo a essa" + CR)
        w("; posicao; estado e o byte [7] do 04 01 (0 andando, 2 parado)." + CR)
        w(";" + CR)
        w("; Existe porque modelar o movimento por estatistica NAO FUNCIONOU. Distancia de" + CR)
        w("; passo, velocidade, distribuicao de virada, tamanho de trecho e duracao de pausa" + CR)
        w("; foram todos medidos e casados com o oficial, e o monstro continuava girando em" + CR)
        w("; torno do proprio eixo na tela. Um video do jogador mostrou o giro acontecendo" + CR)
        w("; ate em modo diagnostico, andando em linha reta -- ou seja, o defeito nao estava" + CR)
        w("; em nada que eu vinha ajustando." + CR)
        w(";" + CR)
        w("; Trilha gravada nao precisa de teoria: ela E o movimento que o client aceita." + CR)
        w(CR)
        for nid in sorted(escolhidas):
            for i, passos in enumerate(escolhidas[nid], 1):
                t0, x0, y0 = passos[0][0], passos[0][1], passos[0][2]
                partes = []
                for t, ox, oy, dx, dy, v, st in passos:
                    partes.append("%d:%.1f:%.1f:%.1f:%.1f:%.2f:%d"
                                  % (int((t - t0) * 1000), ox - x0, oy - y0,
                                     dx - ox, dy - oy, v, st))
                w("[%d_%d]" % (nid, i) + CR)
                w("; %s" % ((mob.get(nid) or {}).get("nome") or "?") + CR)
                w("npc_id = %d" % nid + CR)
                w("trilha = %d" % i + CR)
                w("passos = %d" % len(passos) + CR)
                w("dados = " + " ".join(partes) + CR + CR)
    print()
    print("gravado: %s (%s bytes)" % (destino, "{:,}".format(os.path.getsize(destino))))
    return 0


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