# -*- coding: utf-8 -*-
r"""Extrai a tabela de SKILLS do client, por skill e por nivel.

    python tools/extrair_skills_lod.py  ->  server/Data/Skills/SkillLod.ini

DE ONDE VEM. C:\Games\Axion\ResUnpak\Interface\IE_Lod\ -- as tabelas do proprio client, ja
desempacotadas. Usam-se duas:

    skillBase.lod_pt    2710 registos    nome e icone de cada skill
    skillLevel.lod_pt   4623 registos    os valores, um registo por (skill, nivel)

O MODO DE DECIFRA E DIFERENTE NAS DUAS, e isso custou tempo:

    skillBase    decrypt_lod_stream(dados)                    (pular_zero=True, o padrao)
    skillLevel   decrypt_lod_stream(dados, pular_zero=False)

O proprio comentario do lod_crypto ja avisava que as tabelas binarias grandes precisam do
outro modo. Teste rapido para saber se uma tabela decifrou bem: contar valores de byte
distintos nos primeiros milhares. Bem decifrada da ~150; mal decifrada da 256 e quase nenhum
zero, que e entropia de ruido.

FORMATO DO skillLevel. Registo de tamanho variavel:

    284 bytes de bloco binario
    18 cadeias numericas (u32 de comprimento + texto, quase sempre "0")
    1  cadeia com a descricao

Com esta regra os 4623 registos leem-se de fio a pavio e sobram 32 bytes no fim.

O BLOCO BINARIO, mapeado a partir da dica do client que o jogador mandou em 29/08 --
"Golpe de ombro" nivel 1: mana 6, area 1,5m, Hits 2, Ataque fisico Bonus 4~11, Recuperacao
4,0s. Confirmado depois nos cinco niveis da mesma skill, onde os valores crescem:

    @  0  u32     id da skill          5001 nos cinco
    @  4  u32     nivel da skill       1 2 3 4 5
    @ 16  u32     nivel exigido        1 3 5 7 9
    @ 40  double  mana                 6 8 9 11 12
    @ 80  double  recuperacao (s)      4,0 nos cinco
    @ 88  double  ?                    600 nos cinco
    @104  double  ?                    15 nos cinco
    @172  double  ?                    10 nos cinco
    @180  double  dano minimo          4 7 11 16 22
    @188  double  dano maximo          11 15 21 28 36
    @252  double  ?                    0,70 0,75 0,80 0,85 0,90

CUIDADO COM O ALINHAMENTO: os doubles nao caem em fronteiras de 4 bytes. Ler u32 de 4 em 4
da valores como 16400 e 16422, que sao metades de doubles (0x4010 = 4.0, 0x4026 = 11.0) e
nao campos. Foi isso que escondeu tudo na primeira tentativa.

O QUE FICA POR IDENTIFICAR: a area (1,5 m) e os hits (2) da dica nao aparecem em nenhum dos
campos acima. Ou estao nos offsets ainda por decifrar (@88, @104, @172, @252), ou vem do
skillData, que tambem ja decifra (4146 registos) e ainda nao foi mapeado.
"""

# NOTA DE 30/08: o `campo104` passou a chamar-se `alcance10` -- e o ALCANCE EM DECIMOS
# DE METRO. Identificado por dois tooltips que o jogador mandou: a Corrosao (11002) diz
# "Area de atuacao 16.5m" e tem 165; o Golpe com ceptro (11005) diz 2.5m e tem 25.
#
# QUEM TRAVA E O CLIENT, nao o servidor -- nao ha verificacao de distancia do nosso
# lado. Por isso uma skill de 2,5 m obriga a encostar ao bicho tanto aqui como no
# oficial, e nao ha nada a corrigir quando isso parecer pouco.

import io
import os
import re
import struct
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import lod_crypto as L                                              # noqa: E402

PASTA = r"C:\Games\Axion\ResUnpak\Interface\IE_Lod"
SUFIXO = "_pt"
XOR = 0

ORIGENS = {
    "axion": (r"C:\Games\Axion\ResUnpak\Interface\IE_Lod", "_pt", 0),
    "arena": (r"C:\Games\Arena\ResUnpak\Interface\IE_Lod", "_en", 0x0D),
}
"""De onde ler. `--origem arena` le o client do servidor Arena, de 29/06/2026.

O DELE TEM QUASE O DOBRO: 8851 niveis de skill contra 4623, e 4991 skills contra 2710. Ver a
seccao 50 dos achados, que descreve como a cifra dele foi aberta -- um XOR de um byte (0x0D)
por cima do .lod normal, achado comparando um ficheiro de tres bytes cujo claro se conhecia.

So tem `_en` e `_sa`: os nomes das skills vem em ingles. Quem quiser os nomes em portugues
extrai do nosso e cruza pelo id."""
BIN_NIVEL = 284
N_NUMEROS = 18
# offsets dentro do bloco binario -- ver o cabecalho
OFF = {"skill_id": (0, "u32"), "nivel": (4, "u32"), "nivel_req": (16, "u32"),
       "mana": (40, "d"), "recuperacao": (80, "d"), "campo88": (88, "d"),
       "alcance10": (104, "d"), "campo172": (172, "d"),
       "dano_min": (180, "d"), "dano_max": (188, "d"), "campo252": (252, "d"),
       # DOIS PARES DE DANO, um por tipo. Descoberto em 29/08 quando o jogador reparou que
       # "batendo eu tiro mais dano do que soltando skill": a Flecha de energia tinha 0~0 em
       # @180/@188 e o bonus saia zero. Comparando uma skill fisica com uma magica nivel a
       # nivel, os campos que crescem sao outros:
       #
       #     Golpe de ombro (fisica)      @180/@188   4~11  7~15  11~21  16~28
       #     Flecha de energia (magica)   @212/@220   110,37~113,67  126,86~131,06 ...
       #
       # Cada skill tem um par preenchido e o outro a zero.
       "dano_mag_min": (212, "d"), "dano_mag_max": (220, "d"), "campo72": (72, "d")}
# no skillBase o id fica 196 bytes antes do nome
DESLOC_ID_BASE = -196


def cadeias(d, maxln=400):
    """Todas as cadeias nao vazias com o comprimento u32 a frente."""
    out = []
    i = 8
    n = len(d)
    while i + 4 < n:
        ln = struct.unpack_from("<I", d, i)[0]
        if 1 <= ln <= maxln and i + 4 + ln <= n:
            s = d[i + 4:i + 4 + ln]
            if all(32 <= b < 127 or b >= 0xA0 for b in s):
                out.append((i, s.decode("latin-1")))
                i += 4 + ln
                continue
        i += 1
    return out


def nomes_do_base():
    """{id da skill: nome}, do skillBase."""
    p = os.path.join(PASTA, "skillBase.lod" + SUFIXO)
    if not os.path.isfile(p):
        return {}
    d = L.decrypt_lod_stream(open(p, "rb").read(), xor=XOR)
    saida = {}
    for off, s in cadeias(d, 120):
        # o nome e a unica cadeia que nao e numero nem ficheiro
        if re.fullmatch(r"-?\d+", s) or "." in s:
            continue
        k = off + DESLOC_ID_BASE
        if k < 0:
            continue
        sid = struct.unpack_from("<I", d, k)[0]
        if 0 < sid < 10 ** 7 and sid not in saida:
            saida[sid] = s
    return saida


def escolher_origem():
    """Le --origem da linha de comando e ajusta PASTA/SUFIXO/XOR."""
    global PASTA, SUFIXO, XOR
    nome = "axion"
    if "--origem" in sys.argv:
        i = sys.argv.index("--origem")
        if i + 1 < len(sys.argv):
            nome = sys.argv[i + 1]
    if nome not in ORIGENS:
        print("origem desconhecida: %s (ha %s)" % (nome, ", ".join(ORIGENS)))
        raise SystemExit(2)
    PASTA, SUFIXO, XOR = ORIGENS[nome]
    print("origem: %s -> %s (sufixo %s, xor 0x%02X)" % (nome, PASTA, SUFIXO, XOR))
    return nome


def main() -> int:
    escolher_origem()
    p = os.path.join(PASTA, "skillLevel.lod" + SUFIXO)
    if not os.path.isfile(p):
        print("nao encontrei %s" % p)
        return 1
    d = L.decrypt_lod_stream(open(p, "rb").read(), pular_zero=False, xor=XOR)
    n = struct.unpack_from("<I", d, 4)[0]

    def ler_str(q):
        ln = struct.unpack_from("<I", d, q)[0]
        return d[q + 4:q + 4 + ln].decode("latin-1"), q + 4 + ln

    linhas = []
    q = 8
    for _ in range(n):
        if q + BIN_NIVEL > len(d):
            break
        b = d[q:q + BIN_NIVEL]
        q += BIN_NIVEL
        for _k in range(N_NUMEROS):
            _s, q = ler_str(q)
        desc, q = ler_str(q)
        r = {}
        for nome, (off, tipo) in OFF.items():
            if tipo == "u32":
                r[nome] = struct.unpack_from("<I", b, off)[0]
            else:
                v = struct.unpack_from("<d", b, off)[0]
                r[nome] = 0.0 if v != v else v
        # TIRA TODO O CARACTER DE CONTROLO, e nao so a mudanca de linha e o tab.
        # Havia descricoes com retorno de carro pelo meio, e cada uma partia a
        # linha do .ini em duas: saiam 4634 linhas para 4623 registos.
        r["descricao"] = re.sub(r"[\x00-\x1f]+", " ", desc).strip()
        linhas.append(r)
    print("registos lidos: %d de %d (consumiu %d de %d bytes)" % (len(linhas), n, q, len(d)))

    nomes = nomes_do_base()
    print("nomes vindos do skillBase: %d" % len(nomes))
    com_nome = sum(1 for r in linhas if r["skill_id"] in nomes)
    print("registos com nome: %d" % com_nome)

    destino = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                           "..", "server", "Data", "Skills", "SkillLod.ini")
    nl, tb = chr(10), chr(9)
    cab = ["//skill_id", "nivel", "nivel_req", "mana", "recuperacao",
           "dano_min", "dano_max", "campo88", "alcance10", "campo172",
           "campo252", "dano_mag_min", "dano_mag_max", "campo72",
           "nome", "descricao"]
    with io.open(destino, "w", encoding="utf-8", newline="") as f:
        f.write("; SkillLod -- tabela de skills do CLIENT, por skill e por nivel." + nl)
        f.write("; Gerado por tools/extrair_skills_lod.py, de skillBase.lod_pt e" + nl)
        f.write("; skillLevel.lod_pt. Ver o cabecalho dessa ferramenta: os campos com" + nl)
        f.write("; nome 'campoNN' estao decifrados mas ainda nao identificados." + nl)
        f.write(tb.join(cab) + nl)
        for r in linhas:
            f.write(tb.join([
                str(r["skill_id"]), str(r["nivel"]), str(r["nivel_req"]),
                "%g" % r["mana"], "%g" % r["recuperacao"],
                "%g" % r["dano_min"], "%g" % r["dano_max"],
                "%g" % r["campo88"], "%g" % r["alcance10"], "%g" % r["campo172"],
                "%g" % r["campo252"],
                "%g" % r["dano_mag_min"], "%g" % r["dano_mag_max"],
                "%g" % r["campo72"],
                nomes.get(r["skill_id"], ""), r["descricao"][:110]]) + nl)
    print("escrito em %s" % os.path.normpath(destino))
    return 0


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