---
name: cabal-eventpass
description: "Estado do EventPass: periodo desbloqueado com patch binario no CabalMain.exe (funcionando); missao/resgate/compra ainda faltam (precisa servidor do zero)"
metadata: 
  node_type: memory
  type: project
  originSessionId: 153bd067-f5ce-4a37-9378-d46c0afa3789
  modified: 2026-08-17T16:18:57.866Z
---

O cliente **275** (`D:\Cabal\CabalMain 275 + Full English Patch`) **ja tem o
EventPass inteiro compilado dentro** — `Bin/x64/CabalMain.exe` traz
`Snake::Manager::EventPass` com `OnLoadData`, `OnDlgShow`, `GetRewardByGrade`,
`BuildSortedMissions` etc.  Nao precisa mexer no executavel.

Ele le a tabela de `Data/EventPass.enc` (1060 bytes, cabecalho diz 7022 = tamanho
depois de descomprimir).  O pacote `D:\Cabal\EventPass-[RaGEZONE]` tem os dois
lados dessa tabela: `EventPass.dec` (XML, exatamente 7022 bytes) e
`EventPass.scp` (formato do servidor, separado por TAB).  Secoes do `.scp`:
`Mission_Setting`, `Mission_List`, `Mission_Grade`, `Point_Limit`,
`Point_Exchange`, `Buff_Reward`, `PremiumBuff_Reward`, `Dungeon_EntryList`.

O cliente do **EP39** (`D:\Cabal\Episode39`) tem o mesmo codigo, mas o
`EventPass.enc` dele so' tem 89 bytes (vazio) e o binario carrega a string
`EventPass.h  server data empty` — essa versao espera os dados virem do
servidor.  Ou seja: **o 275 e' o caminho mais facil**, a tabela ja esta local.

**O servidor nao tem nada.**  `EventPass`, `SeasonPass`, `PassMission`,
`BattlePass` = 0 ocorrencias no WorldSvr e 0 no fonte em `D:\Cabal\source`.
Existe `/etc/cabal/Data/EventPass.scp` no container, mas nenhum codigo le.
A janela abre e desenha, e nenhum contador anda.  Implementar = escrever o lado
do servidor do zero num modulo nativo, respondendo aos opcodes que o cliente
manda.

Nao tentar logar com o client do EP39 para isso: o servidor exige
`client_version=275` e o EP39 se diz 293.  A chave `IgnoreClientVersion=1` no
`[LoginSvr]` do `LoginSvr_01.ini` pula a checagem (a chave existe no binario,
so' nao esta no ini), mas passar do login nao garante o resto do protocolo — e
de qualquer forma o 275 ja resolve.

Para descobrir os opcodes, ver o espiao de entrada em [[cabal-pacotes-chat]].

**A checagem de periodo (mensagem "indisponivel") e' 100% local, contra o
relogio do Windows do cliente — nao tem nada a ver com o servidor.** Testado e
confirmado duas vezes (17/ago/2026): mudando a data do Windows pra
dez/2024 a janela abre; voltando a data real, fecha de novo. Provas que levaram
a essa conclusao:
- O modulo DropAlert (`/root/patchwork/DropAlert.c` na VM) tem uma checagem
  incondicional (roda em todo pacote de saida, independente de teto de sniff)
  procurando opcode 148 pra trocar o timestamp — nunca disparou UMA VEZ em toda
  a sessao. Descobriu-se que opcode 148 e' na verdade um pacote que o
  **cliente manda** (`[in] op=148`), nao uma resposta do servidor — o
  comentario antigo no codigo ("Resposta do CSC_GETSVRTIME... op=148") estava
  errado.
- Testou-se limpar a tabela de captura do DropAlert (comando `zerar` via
  `/etc/cabal/Data/PluginCfg/DropAlert.test`, ver [[cabal-server-arquitetura]]
  pra outros hooks), tentar abrir o EventPass, e dar `dump` — **zero pacotes
  novos, de entrada ou saida, foram gerados so' por abrir a janela**. O
  cliente nao pergunta nada pro servidor nessa hora.
- `EventPass.dec`/`EventPass.scp` (`D:\Cabal\EventPass-[RaGEZONE]`) **nao tem
  nenhum campo de data/periodo** em nenhuma secao (`mission_setting`,
  `mission_list`, `mission_grade`, `point_limit`, `point_exchange`,
  `buff_reward`, `premiumbuff_reward`) — so' conteudo de missao/recompensa.
  A data de inicio/fim do "season" ta hardcoded dentro do `CabalMain.exe`.

**RESOLVIDO (17/ago/2026) — patch binario no CabalMain.exe, testado e
funcionando.** Ao inves de tentar enganar a hora (API nunca chegou a ser
chamada em nenhum teste com breakpoint em `GetLocalTime`/`GetSystemTime`/
`GetSystemTimeAsFileTime`/`GetTickCount`/etc — a checagem usa outra coisa por
baixo, nao importa mais o que), achamos o PONTO DE DECISAO direto:

- A mensagem "Not Event Pass period." (que aparece flutuando e no popup de
  NPC) e' o id de mensagem **2326** (0x916), confirmado batendo
  `<msg id="2326" cont="Not Event Pass period."/>` no `Data\Language\English\msg.enc`
  do cliente 275.
- Procurando `mov edx, 0x916` (bytes `BA 16 09 00 00`) no `CabalMain.exe`
  (`Bin\x64\CabalMain.exe`) achamos 9 ocorrencias; testando com breakpoint em
  cada uma (x64dbg, anexado no processo rodando), a que dispara ao clicar no
  EventPass e' **`cabalmain.exe+ABD37`** (RVA), isolada das outras 8 (que sao
  de outros lugares que reusam a mesma mensagem).
- Logica ao redor (endereco de imagem base 0x140000000, ou seja
  RVA+0x140000000 = VA visto no debugger):
  ```
  1400ABD2B: mov rcx, r15
  1400ABD2E: call cabalmain.1400AB820      ; funcao que decide periodo valido
  1400ABD33: test al, al
  1400ABD35: jne cabalmain.1400ABD41       ; se valido, pula pro caminho bom
  1400ABD37: mov edx, 0x916                ; senao, mostra "Not Event Pass period"
  1400ABD41: mov rcx, r15
  1400ABD44: call cabalmain.1400AA548      ; caminho "periodo valido" de verdade
  ```
- **O patch**: em `1400ABD35`, trocar o byte `75` (jne) por `EB` (jmp) —
  2 bytes, mesmo tamanho (`75 0A` -> `EB 0A`), forca sempre o caminho "periodo
  valido" sem importar o que `cabalmain.1400AB820` calcula por tras (nao
  precisou entender essa funcao nem lidar com VMProtect diretamente).
  Aplicado via x64dbg: Assemble (motor XEDParse) em `1400ABD35` com
  `jmp short 1400ABD41` + patch salvo no arquivo (`Patches > Patch File`).
  **Resultado**: janela do EventPass abre normal, mostra progresso
  (`50/50 Step`), abas Reward/Mission funcionam. O contador
  "44910 day(s) until end date" fica com numero estranho (efeito colateral
  cosmetico de pular o calculo real, sem importancia).
- Como achar o file-offset de um VA: `RVA = VA - 0x140000000`; sec `.text`
  desse build tem `RawPtr=0x400`, `VirtualAddress=0x1000`, entao
  `file_offset = 0x400 + (RVA - 0x1000)`.

**O que NAO foi resolvido — precisa virar projeto a parte, e' trabalho de
servidor de verdade, nao patch de 2 bytes:**
- A aba Mission mostra os contadores certos (`Daily 0/3` bate com o grupo 1 do
  `Mission_List` do `.scp`, `Weekly 0/20` bate com o grupo 2 — prova que o
  contador tambem le local), mas a lista de missoes individual aparece
  **vazia**, sem nenhuma linha. Tentativa de achar quem referencia a string
  `BuildSortedMissions` (RVA `0xA865F3`, secao `.rdata`) via x64dbg
  "Find references to" deu **zero resultados** — nao e' um enderecamento
  direto simples, precisa de outra abordagem (talvez breakpoint na hora de
  clicar na aba Mission, ou olhar `GetRewardByGrade`/`OnLoadData`, RVAs
  `0xA8626B` / `0xA863AB`, tambem sem xref direta encontrada ainda).
- Clicar em **"Purchase"** (resgatar/comprar passe premium) **derruba o
  jogo**. Quase certo que tenta um round-trip com o servidor (opcode de
  compra) que o WorldSvr nao entende / nao responde, e o cliente quebra
  esperando resposta que nunca vem certo — mesma causa raiz de sempre:
  servidor nao tem NENHUM opcode de EventPass (missao, ponto, resgate,
  compra) implementado. Ver [[cabal-server-arquitetura]] pro padrao de como
  os outros modulos (LootAnnouncer) foram implementados do zero, seria o
  mesmo tipo de trabalho aqui so' que bem maior (tabela de progresso no
  banco, opcodes novos, entrega de item).
- **Nao clicar em "Purchase" nem "Receive Reward"/"Receive All"** ate isso
  ser implementado — so' navegar pelas abas e' seguro.

**ATUALIZACAO 17/ago/2026 — causa raiz do crash confirmada com dado real,
banco ja preparado, falta so' o formato da resposta:**

Um post do RaGEZONE (fonte externa, sobre o extender Linux "EP35") revelou que
o nosso proprio `/etc/cabal_bin/lib/lib.so` (confirmado via `nm -D`, simbolo
`_Z15OnCSC_EVENTPASSP13CProcessLayerP18PROCESSDATACONTEXT` em `0x11e014`) ja
tem o **opcode CSC_EVENTPASS = 2999** registrado no dispatcher
(`ProcSystem::OpenProcedure`, tabela `g_sUsrProcedureMap`), mas o handler so'
faz `return 0` — nao processa nem responde nada. `CSC_PVPBATTLETIME = 3000`
esta no mesmo caso (stub vizinho, registrado logo depois).

Confirmado ao vivo com o espiao de entrada do DropAlert (comando `in:on` via
`/etc/cabal/Data/PluginCfg/DropAlert.test`, depois `zerar` pra limpar e `dump`
pra listar): ao clicar em "Purchase" e o jogo travar, o pacote que chegou
**antes** do crash foi exatamente:
```
op=2999  via=1  len=10  vezes=3  | e2 b7 0a 00 44 47 e4 b3 b7 0b
op=3000  via=1  len=10  vezes=3  | e2 b7 0a 00 86 c6 7c 42 b8 0b
```
10 bytes total, sem payload alem do proprio opcode (offset 8-9 = `b7 0b` LE =
2999) — um pedido "puro", sem parametro. O cliente manda 3x seguidas (deve
reenviar esperando resposta), nao recebe nada, e trava.

**Banco ja preparado**: tabela `dbo.cabal_eventpass_table` e procedure
`dbo.cabal_sp_eventpass_sync` criadas no banco `Account` do `sql_main`
(FK em `cabal_auth_table.UserNum`, colunas `StartDate`/`EndDate` (BIGINT),
`MissionPageCount`/`MissionSlotCount`/`RewardSlotCount` (INT),
`MissionPageData`/`MissionSlotData`/`RewardSlotData` (VARBINARY(MAX))).
Baseado num schema MySQL que o usuario tinha de outra fonte (provavelmente
outra implementacao de EventPass de outro server privado) — traduzido pra
T-SQL, com upsert (insert se nao existir, senao update) em vez do update-only
original. Script em
`C:\Users\ADMINI~1\AppData\Local\Temp\claude\D--Cabal\153bd067-f5ce-4a37-9378-d46c0afa3789\scratchpad\eventpass_schema.sql`
(scratchpad de sessao, nao persiste — recriar se precisar rodar de novo, o
SQL em si esta documentado aqui).

**O que falta pra fechar de vez**: descobrir o formato exato do pacote de
**resposta** que o `OnCSC_EVENTPASS` deveria mandar de volta (que opcode de
saida, como os campos da tabela viram bytes). Sem isso, nao da' pra escrever
o handler de verdade. Proximo passo natural: hook por interposicao de simbolo
no `lib.so` (LD_PRELOAD de um `.so` novo ANTES do `lib.so` na lista —
ver ordem atual em `/etc/cabal_scripts/cabal_services/cabal_server1.conf`,
`LD_PRELOAD="... /etc/cabal_bin/lib/lib.so ... /etc/cabal_bin/lib/libdropalert.so"`,
`lib.so` vem primeiro, entao um hook por simbolo precisa vir ANTES dele na
lista pra vencer a resolucao de simbolo do linker) pra interceptar
`OnCSC_EVENTPASS` de verdade (nao so' espiar, poder controlar a resposta) —
tecnica dada de exemplo no post do RaGEZONE (`dlsym(RTLD_NEXT, ...)` +
log + call-through), mesma familia do `DropAlert.c` so' que por nome de
simbolo em vez de patch de endereco.

**TENTATIVA 1 (17/ago/2026) — eco vazio, FALHOU, licao importante.** Em vez
de mexer no `lib.so` via LD_PRELOAD/simbolo, foi mais simples e mais seguro
estender o proprio `hkRecv` do `DropAlert.c` (que ja intercepta TODO pacote
de entrada antes de chegar no `lib.so`, no hook em `0xB3D7F8`): quando
`op==2999` ou `op==3000`, chamar `((fnUnicastE)ADDR_UNICAST_E)(self, buf,
len)` **diretamente** (sem fila/`Enfileira`) — funciona pq `hkRecv` ja roda
numa thread de jogo valida, mesmo padrao que `hkItemLooting` ja usa.

Resposta testada: eco vazio, so' o header (`magic e2b7, len=10,
opcode=2999/3000`, sem nenhum campo depois). **Resultado: piorou.** Antes,
so' clicar em "Purchase" derrubava o jogo. Com o eco vazio ativo, **ate
entrar no mundo normalmente passou a derrubar o cliente** (confirmado no log:
`hkRecv` disparou o eco duas vezes pro mesmo `self`, servidor ficou saudavel
o tempo todo, mas a conexao do cliente fechou ~1s depois do
`C2S_INITIALIZED` no `WorldSvr_01_01.log`). Ou seja, **CSC_EVENTPASS/
CSC_PVPBATTLETIME tambem disparam na entrada normal no mundo**, nao so' no
clique do Purchase — e o cliente **espera campos de verdade** na resposta;
mandar so' o header sem payload faz ele ler memoria fora do buffer (provavel
causa do crash mais cedo e mais forte que o "nao responder nada" original).

**Reverto feito e confirmado**: a chamada ficou desativada com `if (0 && ...)`
em volta (nao apagada, so' comentada via `0 &&` pra reaproveitar o codigo
depois) em `RespondeEventPassEco`/`hkRecv` no `DropAlert.c`, `sniffin`
voltou pra `0` no cfg, canal reiniciado, confirmado que login+entrada no
mundo voltaram ao normal (so' o crash original do Purchase continua, que ja'
era esperado).

**Conclusao pro proximo passo**: nao adianta mais tentar respostas
"chutadas" — precisa descobrir o formato real do payload que o cliente
espera ler ANTES de tentar de novo. Caminho mais provavel: x64dbg no
`CabalMain.exe`, achar onde o cliente **processa pacote de entrada com
opcode 2999** (dispatcher de entrada do lado do cliente, provavelmente perto
de onde ele registra o handler de resposta — parecido com o que fizemos pra
achar `OnDlgShow` etc, so' que pro lado de RECEBER, nao mandar), ver
exatamente quantos bytes/quais campos ele le do buffer antes de decidir
o formato certo pra construir do lado do servidor.

Ferramenta usada: x64dbg instalado em `D:\Cabal\Tools\x64dbg\` (baixado do
site oficial x64dbg.com / snapshot SourceForge, 17/ago/2026). O patch original
foi comparado contra o projeto de source aberto **CabalRealmEP33**
(thread RaGEZONE "Cabal Realm EP33 Source") que corresponde exatamente a
`D:\Cabal\source\Cabal EP33 main\CabalEP33` (mesmos nomes de pasta/hook:
Basic, Discord, DLLSec, GameVersion, PacketManager, Proc) — mas esse source
tem enderecos hardcoded pra build **32-bit** (Win32), enquanto o cliente real
rodando e' o de **64-bit** (`Bin\x64\CabalMain.exe`), entao os enderecos de
la nao valem direto, so' a tecnica (hook por `call`/patch de instrucao) serviu
de referencia. O `Olly.dll` que vem junto no `Bin\x64` e' 32-bit por engano
de empacotamento do modder (nao carrega no processo 64-bit, confirmado via
`pefile`/leitura de PE header) — resto de build, nao faz nada no cliente
atual.

**RESOLVIDO PARCIALMENTE (17/ago/2026) — Ghidra achou o parser real do
cliente, resposta minima ja funciona, crash geral sumiu.**

Instalado Ghidra 12.1.2 + JDK 21 em `D:\Cabal\Tools\` (headless, roda local
nessa maquina, nao precisa do PC do usuario — so' o x64dbg precisava estar la
por anexar em processo vivo; Ghidra e' analise estatica, so' precisa do
`.exe`). Projeto do Ghidra em `D:\Cabal\Tools\ghidra_projects\`
(`CabalMain275_v2`). **Nota:** o script de Ghidra headless precisa ser
`.java`, nao `.py` — a versao 12.1.2 tirou o Jython embutido, precisaria de
PyGhidra configurado a parte pra Python funcionar. Scripts usados ficam em
`D:\Cabal\Tools\ghidra_projects\Explore*.java` (uso: decompilar funcao dado
um endereco via `getFunctionContaining`/`createFunction` + `DecompInterface`).

O construtor da classe (`FUN_1400a5fe0`, contem
`*param_1 = Snake::Manager::EventPass::vftable`) registra via
`Snake::AddRecvProc` (nome real, achado por RTTI) um bloco de **8 opcodes de
entrada consecutivos: 2998 a 3005** — 2999=EventPass, 3000=PVPBattleTime
(ja sabiamos), os outros 6 (2998, 3001-3005) ainda nao identificados mas
devem ser sub-mensagens do mesmo sistema (uma delas, 2998, tem handler que
referencia literalmente `"...\Snake\EventPass.cpp"` no codigo fonte
original do jogo — confirma que a faixa toda e' do EventPass).

**Formato do pacote de resposta pro opcode 2999** (achado decompilando
`FUN_1400a72dc`, a funcao que o cliente chama ao RECEBER esse opcode):
```
offset 0-1:  magic = e2 b7
offset 2-3:  tamanho total (2 bytes)
offset 4-7:  4 bytes, significado ainda desconhecido (mandado como 0, funcionou)
offset 8-9:  opcode = b7 0b (2999)
offset 10-13: COUNT (int32) -- quantos registros vem a seguir
offset 14+:  COUNT registros de 36 bytes cada (9 campos de 4 bytes),
             so' processados se COUNT>0
```
Dentro de cada registro de 36 bytes (indices 0-8 de 4 bytes cada, ainda nao
totalmente decifrados): campo[0]=parece ID/indice (usado como lookup),
campo[1]=flag de tipo (0 ou 1, escolhe entre variantes de 8 ou 88 bytes na
struct interna -- pode ser Normal vs Premium), campo[7] e campo[8]=flags que
escolhem entre dois caminhos de inicializacao ("registro novo" vs "registro
existente/update").

**Testado e confirmado ao vivo:** mandar so' o header vazio (10 bytes, sem
COUNT) fazia o cliente ler lixo de memoria como se fosse o COUNT e crashar
**mais forte que o original** (derrubava ate a entrada normal no mundo, nao
so' o clique no EventPass). Mandar header + `COUNT=0` explicito (14 bytes)
**resolveu isso** — janela abre, navega entre abas, contadores de missao
aparecem certos (`0/3`, `0/20`, batendo com os grupos do `Mission_List` do
`.scp`), mas sem linhas de missao (porque COUNT=0 = nenhum registro, esperado).

Implementado direto no `hkRecv` do `DropAlert.c` (nao precisou de hook por
simbolo/LD_PRELOAD no `lib.so` como cheguei a cogitar) — chama
`UnicastE(self, buf, len)` direto, ja que `hkRecv` roda numa thread de jogo
valida. **Atencao:** esse hook so' e' instalado se `sniffin=1` no
`DropAlert.cfg` NA INICIALIZACAO do canal (nao adianta ligar depois em
runtime) — atualmente esta' ligado por causa disso, nao por precisar
espiar pacote.

**O que ainda falta:**
- **Lista de missao vazia**: precisa decifrar os 9 campos do registro de 36
  bytes e mandar dados reais (id da missao, progresso, etc.) em vez de
  COUNT=0. Nao tentado ainda.
- **Crash no "Purchase"**: continua acontecendo, e e' uma causa RAIZ
  DIFERENTE do que acabamos de consertar — o crash achado antes no x64dbg
  (`FUN_1400b05d4`, chamando `FUN_1401a49ac(&DAT_140cccf40, 0x24)` que
  retorna nulo) parece ser busca de recurso do **Cash Shop** (preco/item da
  compra premium), sistema totalmente separado que nunca foi tocado — tem os
  bancos `CabalCash` e `ItemShop` no SQL Server que nunca foram usados.
  Precisaria de investigacao propria.
- Os outros 6 opcodes da faixa 2998-3005 (so' 2998 foi espiado de relance,
  tem uma logica bem diferente de 2999 -- nao processa registros em laco,
  usa offsets diferentes) ainda nao foram mapeados.

**Tentativas de mandar registro de verdade (17/ago/2026) — ambas crasharam,
causa NAO e' o valor dos campos.** Testado ao vivo, servidor sempre revertido
pra `COUNT=0` (seguro) depois de cada tentativa:
- Tentativa 3: 1 registro, grade=1, tipo=0 (Normal), resto zerado -> crash
  ao clicar na aba Reward.
- Tentativa 4: 2 registros, grade=1 tipo=0 E tipo=1 (Normal+Premium juntos,
  hipotese de que faltava inicializar os dois lados do slot) -> crash igual.
- Com x64dbg anexado pegando a tentativa 4 ao vivo: o crash NAO e' nos
  campos que a gente manda. E' em `140753EC3`, escrevendo
  `mov dword ptr [rdi+58], eax` dentro de uma funcao que faz uma sequencia
  de `call funcao_X(); mov [rdi+offset], eax; call funcao_Y(); mov
  [rdi+offset+4], eax; ...` (parece estar preenchendo um OBJETO DE UI campo
  por campo, chamando um getter diferente pra cada campo). `rdi` (o "objeto"
  sendo preenchido) esta' nulo/invalido nesse ponto.
- **Hipotese pra proxima vez**: so' mandar os DADOS no pacote 2999 nao
  basta — o sistema de UI (linha/icone visual de cada grade na aba Reward)
  precisa que os "slots" ja' estejam alocados/criados antes disso, talvez
  via uma contagem total separada (numero maximo de grades?) que o cliente
  usa pra pre-alocar os objetos de UI, ou precise reabrir a janela depois
  que os dados chegam (a janela pode so' criar os objetos de UI no
  `OnDlgShow`/`OnLoadData`, nao dinamicamente quando um pacote chega com a
  janela ja' aberta). Vale tentar: (a) mandar o pacote de resposta ANTES do
  jogador abrir a janela pela primeira vez (ex: assim que entra no mundo,
  nao so' quando ele clica em algo), ou (b) rastrear no Ghidra quem CHAMA
  essa funcao de preenchimento (em `140753E9F`-ish) pra achar onde `rdi`
  deveria ter sido alocado.
- Parada aqui por hoje (17/ago/2026) a pedido do usuario — "vamos dar um
  tempo". Servidor deixado no estado seguro (`COUNT=0`, sem crash).
