# -*- coding: utf-8 -*-
"""Corta do stream de entrada tudo que o servidor OFICIAL nao manda nesse momento.

    python tools/filtrar_stream_oficial.py            mostra o que sairia
    python tools/filtrar_stream_oficial.py --aplicar  grava

O enterworld_nivel1.bin sao 171 frames gravados da sessao de OUTRA pessoa e reenviados a
todo jogador que entra. Medindo a mesma janela no oficial -- do charinfo (02 05) ate o
client pedir 04 00 -- ele manda 27 pacotes, nao 171.

A diferenca inteira e ruido:

    139 frames  entidades fantasma: 51 aparicoes, 60 movimentos e 28 sumicos de gente que
                estava na tela de quem gravou. O oficial manda ZERO 04 01/02/03 nessa
                janela -- as entidades so aparecem depois que o jogador pede 04 00.
      3 frames  08 05, 0a 13 e 0a 0a. CUIDADO com a leitura: o oficial MANDA os tres --
                so que DEPOIS do 04 00, quando o mundo ja carregou, nao antes. Um deles
                abre o calendario de eventos, e era por isso que o jogador via a janela
                surgir antes do mundo aparecer. Eles saem daqui porque estao na hora
                errada, nao porque sobrem; quando o servidor souber manda-los no momento
                certo, voltam -- pelo caminho normal, nao pelo replay.

Depois saiu mais um par, por outro motivo. O 08 0f e o 08 09 nao sao anuncio: sao a
RESPOSTA a um 08 09 que o proprio client envia. Medido na captura oficial, o client pergunta
em t+0,901 e o servidor responde em t+1,176; a resposta e o mesmo pacote de volta com
sequencia nova, precedido de um 08 0f de 10 bytes. Mandando isso no stream, a resposta
chegava ANTES da pergunta -- e quando o client perguntava, nada vinha. Ele entao esperava
ate desistir, e era a travada leve que sobrou depois de tirar as entidades fantasma. Agora
quem responde e o handler, no momento certo.

Uma tentativa anterior de filtrar quebrou a entrada porque cortou por palpite ("isto a
gente ja gera") e levou junto pacotes que o client precisa NAQUELE ponto. Aqui o criterio e
outro e verificavel: fica o que o oficial manda, sai o que ele nao manda.
"""
import collections
import os
import shutil
import struct
import sys

STREAM = r"D:\RoshEmulator\captures\enterworld_nivel1.bin"

# Opcodes que o oficial manda entre o charinfo e o 04 00, medidos em
# traffic_0823_1819.log. Tudo fora desta lista sai.
OFICIAL_NA_ENTRADA = {
    (0x02, 0x05), (0x04, 0x04), (0x05, 0x05), (0x05, 0x0c), (0x06, 0x05),
    (0x06, 0x25), (0x06, 0x28), (0x08, 0x00), (0x08, 0x01), (0x08, 0x02),
    (0x08, 0x07), (0x08, 0x08), (0x09, 0x02),
    (0x09, 0x04), (0x0a, 0x0c), (0x0a, 0x0f), (0x0b, 0x00), (0x0d, 0x0f),
    (0x0e, 0x0c), (0x0f, 0x11), (0x16, 0x01),
}

MOTIVO = {
    (0x04, 0x01): "movimento de entidade alheia",
    (0x04, 0x02): "aparicao de entidade que nao existe no nosso mundo",
    (0x04, 0x03): "sumico dessas entidades",
    (0x08, 0x05): "o oficial manda so DEPOIS do 04 00 -- aqui esta cedo demais",
    (0x08, 0x09): "e RESPOSTA ao 08 09 que o client pede -- ver responder_08_09",
    (0x08, 0x0f): "acompanha a resposta do 08 09 -- ver responder_08_09",
    (0x0a, 0x13): "o oficial manda so DEPOIS do 04 00 -- aqui esta cedo demais",
    (0x0a, 0x0a): "o oficial manda so DEPOIS do 04 00 (abre o calendario)",
}


def frames(dados):
    pos = 0
    while pos + 4 <= len(dados):
        (n,) = struct.unpack_from(">I", dados, pos)
        if n == 0 or n > 16384 or pos + 4 + n > len(dados):
            break
        yield dados[pos:pos + 4 + n], dados[pos + 4:pos + 4 + n]
        pos += 4 + n


def main() -> int:
    aplicar = "--aplicar" in sys.argv
    if not os.path.exists(STREAM):
        print("nao achei %s" % STREAM)
        return 1
    d = open(STREAM, "rb").read()

    manter, saem, ficam = [], collections.Counter(), collections.Counter()
    for bruto, corpo in frames(d):
        op = (corpo[0], corpo[1])
        if op in OFICIAL_NA_ENTRADA:
            manter.append(bruto)
            ficam[op] += 1
        else:
            saem[op] += 1

    novo = b"".join(manter)
    print("stream atual : %6d bytes, %3d frames" % (len(d), sum(saem.values()) + len(manter)))
    print("stream novo  : %6d bytes, %3d frames   (-%.0f%% dos bytes)"
          % (len(novo), len(manter), 100.0 * (len(d) - len(novo)) / len(d)))
    print("oficial      :              27 pacotes na mesma janela")
    print()
    print("SAI (%d frames):" % sum(saem.values()))
    for op, n in saem.most_common():
        print("   %02x %02x  x%-4d %s" % (op[0], op[1], n, MOTIVO.get(op, "")))
    print()
    print("FICA (%d frames):" % len(manter))
    print("   " + "  ".join("%02x %02x x%d" % (op[0], op[1], n)
                            for op, n in sorted(ficam.items())))

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

    bkp = STREAM + ".completo_backup"
    if not os.path.exists(bkp):
        shutil.copy2(STREAM, bkp)
        print()
        print("original guardado em %s" % os.path.basename(bkp))
    open(STREAM, "wb").write(novo)
    print("gravado: %d bytes" % len(novo))
    return 0


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