Pular para o conteúdo

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.

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 / ENTREGUE

Observe 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.

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.

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 a resposta da plataforma (o checkoutUrl, o resultado da elegibilidade) e pode devolvê-la ao client pela mesma sessão de jogo.

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:

OpcodeSentidoQuem manda
220servidor → cliento opcode.lua / handlers de servidor
221client → servidoro 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).

kindPayloadSignifica
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)”
kindPayloadA 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 livreexibe 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.

Para amarrar tudo, o percurso de uma compra narrado como sequência:

  1. O jogador clica “Comprar na Lootfy”. O módulo game_lootfy resolve a oferta selecionada, lê a quantidade e manda buy@{...} no opcode 221. O botão se desabilita (debounce).

  2. O servidor recebe o 221. O opcode.lua valida tudo contra o cid da sessão — ignora qualquer ref no payload, computa o item_ref do próprio banco — e enfileira a operação no spool outbox/.

  3. 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 o checkoutUrl.

  4. O servidor devolve ao client. A resposta cai no inbox/, o servidor manda checkout@<url> no opcode 220, e o módulo abre a URL no navegador do jogador. O debounce do botão solta.

  5. 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.

  6. 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.