Arquitetura e protocolo
Uma venda no OpenTibia atravessa quatro processos distintos: o client na máquina do jogador, o servidor TFS, o Agent que roda ao lado dele e a plataforma Lootfy. Entender por onde cada mensagem passa — e, principalmente, por onde ela não passa — é o que faz o resto da seção fazer sentido.
Para o dono de servidor, a ideia essencial é simples: o jogador aperta um botão no jogo, e alguns saltos depois abre no navegador dele uma página de pagamento Pix. Para o programador, cada salto tem uma razão de existir, e é isso que esta página destrincha.
O caminho completo da chamada
Seção intitulada “O caminho completo da chamada”Este é o percurso de uma compra, do clique à entrega. Uma venda (anúncio) usa os mesmos primeiros saltos, parando na plataforma.
CLIENT (jogador) SERVIDOR TFS + Agent LOOTFY (plataforma) ───────────────── ───────────────────── ───────────────────
jogador clica "Comprar na Lootfy" │ opcode 221 buy@{...} └───────────────────▶ opcode.lua (creaturescript) valida no cid da sessão monta o envelope do banco │ spool outbox/ └──▶ AGENT (Node, loopback) assina JWS RS256 │ criar transação │ POST /v1/api/transactions └──────────────────────▶ transação criada devolve checkoutUrl ◀────────────────────── resposta no inbox/ ◀──┘ 220 checkout@<url> ◀───────────────────┘ abre a URL no navegador │ │ jogador paga o Pix, o PSP confirma ▼ ◀────────────────────────── webhook transfer_order (entrega) Agent verifica a assinatura da PLATFORM key │ │ chama o Lua de entrega ▼ doLootfyDeliver(...) ← patch C++ na engine consome a oferta, entrega o item │ └──▶ confirmar entrega POST .../delivery ──▶ PAGO / ENTREGUEObserve os três territórios. Entre client e servidor fala-se opcode extended (220/221). Entre servidor e Agent, um spool de arquivos no disco local. Entre Agent e Lootfy, HTTPS assinado. O único salto que sai da máquina do parceiro é o último — e ele é sempre o Agent quem faz, nunca o client.
Por que o client nunca fala com a Lootfy direto
Seção intitulada “Por que o client nunca fala com a Lootfy direto”O client roda na máquina do jogador. Os módulos são arquivos Lua em texto puro no disco dele — qualquer jogador pode ler e editar. Tratar esse ambiente como confiável seria entregar a chave da casa: um jogador poderia assinar “preço = 1 centavo” e a assinatura passaria em toda checagem, porque assinatura prova quem assinou, não que o assinado é verdade.
Por isso o desenho é inflexível: o client só pede e mostra. Ele manda ao servidor “quero anunciar” ou “quero comprar a oferta X”, e o servidor — que é a autoridade, conhece o banco e o personagem da sessão — monta o conteúdo real e delega a assinatura ao Agent. Todo campo sensível (o item, o personagem, o item_ref) sai do banco autoritativo, nunca do payload do client. O único dado que o client legitimamente informa é o preço em reais da própria oferta, que é dele definir.
Por que o servidor é quem assina
Seção intitulada “Por que o servidor é quem assina”Há duas assinaturas em jogo, e a família TFS não sabe fazer nenhuma delas em Lua puro — só tem md5 e sha1. A plataforma exige JWS RS256 (RSA, chave pública) em toda chamada de contrato. Essa é a primeira razão de existir o Agent: ele é o processo que guarda a chave privada do servidor e assina sob demanda.
A segunda razão é específica do OpenTibia antigo. O client 8.54 não consegue setar header HTTP customizado — a chamada HTTP dele não recebe headers. Como a assinatura viaja no header X-Lootfy-Signature, o client jamais poderia carregar o envelope assinado até a plataforma, mesmo que quisesse. O contrato prevê dois ramos para isso:
- Ramo A: o client carrega o envelope assinado (para clients que sabem setar header — OTClient novo, client custom).
- Ramo B: o servidor assina e envia; o client só pede pelo opcode 221 e recebe o resultado pelo 220. é o que a bancada usa
O case OpenTibia usa o ramo B por necessidade. Há um efeito colateral bom: no ramo B o servidor vê a resposta da plataforma (o checkoutUrl, o resultado da elegibilidade) e pode devolvê-la ao client pela mesma sessão de jogo.
O protocolo de opcodes
Seção intitulada “O protocolo de opcodes”Client e servidor conversam por extended opcodes — um canal genérico do protocolo OpenTibia para mensagens fora do jogo em si. A integração usa dois:
| Opcode | Sentido | Quem manda |
|---|---|---|
| 220 | servidor → client | o opcode.lua / handlers de servidor |
| 221 | client → servidor | o módulo game_lootfy |
O formato é o mesmo nos dois sentidos: uma string "<kind>@<payload>". O kind diz o tipo da mensagem; o payload, quando estruturado, é um JSON pequeno e raso (um objeto de topo, sem aninhamento profundo — o suficiente para o que trafega).
Kinds de saída (client → servidor, opcode 221)
Seção intitulada “Kinds de saída (client → servidor, opcode 221)”kind | Payload | Significa |
|---|---|---|
link | {} | inicie o vínculo do meu personagem |
elig | {} | quero saber se posso anunciar/comprar |
announce | {"brl":"<preço>"} | anuncie na Lootfy a oferta que acabei de criar |
buy | {"seller":..,"clientId":..,"count":..,"qty":..} | crie uma compra da oferta selecionada |
Kinds de entrada (servidor → client, opcode 220)
Seção intitulada “Kinds de entrada (servidor → client, opcode 220)”kind | Payload | A UI faz |
|---|---|---|
link | <url> | abre o link de vínculo no navegador |
checkout | <url> | abre a página de pagamento no navegador |
elig | <json> {allowed, linked, message, blocker} | destrava/tranca o checkbox ou o botão |
| (outro) | texto livre | exibe como mensagem de chat [Lootfy] |
Note a assimetria: link e checkout carregam só a URL como payload (não um JSON), porque a única coisa que a UI faz com eles é abrir o navegador. Já elig carrega um JSON estruturado, porque a UI precisa ler quatro campos dele.
Um passo de cada vez
Seção intitulada “Um passo de cada vez”Para amarrar tudo, o percurso de uma compra narrado como sequência:
-
O jogador clica “Comprar na Lootfy”. O módulo
game_lootfyresolve a oferta selecionada, lê a quantidade e mandabuy@{...}no opcode 221. O botão se desabilita (debounce). -
O servidor recebe o 221. O
opcode.luavalida tudo contra ocidda sessão — ignora qualquer ref no payload, computa oitem_refdo próprio banco — e enfileira a operação no spooloutbox/. -
O Agent assina e chama. Ele lê o spool, assina o envelope com JWS RS256 e faz
POST /v1/api/transactions(criar transação,signature-only). A plataforma cria a transação e devolve ocheckoutUrl. -
O servidor devolve ao client. A resposta cai no
inbox/, o servidor mandacheckout@<url>no opcode 220, e o módulo abre a URL no navegador do jogador. O debounce do botão solta. -
O jogador paga o Pix. No navegador, fora do jogo. O PSP confirma o pagamento e a Lootfy dispara o webhook de entrega
transfer_order, assinado com a chave da plataforma. -
O Agent verifica e a engine entrega. O Agent valida a assinatura do webhook e aciona o Lua de entrega, que chama o binding nativo
doLootfyDeliver— o patch de engine. O item vai para a bag (comprador online) ou o depot (offline). Nenhum dinheiro do jogo se move.