Pular para o conteúdo

Módulo de client de referência

O game_lootfy é o primeiro dos dois entregáveis da bancada: um módulo de client OTClient que faz o jogo mostrar as opções da Lootfy dentro do próprio market do servidor. Do ponto de vista do jogador, aparece um checkbox “Anunciar na Lootfy” na janela de venda e um botão “Comprar na Lootfy” na janela de compra — como se fossem nativos. Do ponto de vista do programador, é um exercício de enxertar UI num módulo de terceiro sem tocar o código dele.

Este módulo é a implementação de referência para o lado do client. Ele é sandboxed e depende só de game_interface, então instala soltando uma pasta em modules/, sem recompilar nada.

Na janela de venda (a confVend do market), o módulo injeta: um checkbox “Anunciar na Lootfy”, um campo “Preco R$:”, um label de status, um botão “Vincular personagem” e um botão “Atualizar”. Na janela de compra (a confimar), injeta: um botão “Comprar na Lootfy (R$)”, um label de status, um botão de vínculo e um botão de refresh. Tudo isso convive com o market original — o jogador continua podendo vender por moeda do jogo normalmente.

O manifesto do módulo é enxuto e diz tudo sobre como ele carrega:

Module
name: game_lootfy
sandboxed: true
autoload: true
dependencies: [ game_interface ]
scripts: [ lootfy ]
@onLoad: init()
@onUnload: terminate()

autoload faz o módulo subir sozinho; sandboxed o isola; a única dependência é game_interface, o que o torna portável entre distribuições de client. @onLoad chama init() e @onUnload chama terminate() — e é esse par que garante a instalação e a desinstalação limpas.

O market do jogo é um módulo de terceiro (na bancada, o poke_makert). A decisão central de desenho do game_lootfy é não forkar esse módulo: em vez de copiar e editar o código dele, o game_lootfy embrulha, em tempo de execução, três das funções dele. Isso mantém o market de terceiro atualizável e a integração isolada.

Como o market de terceiro pode carregar depois, o módulo faz polling por ele — até 60 tentativas de 500ms (cerca de 30 segundos). Se ao fim desse tempo não achar um módulo de market, faz no-op: o client simplesmente não tem a integração, sem erro.

-- Enxertar espera o poke_makert carregar. ~30s (60 * 500ms) e suficiente para
-- concluir que nao ha market a enxertar (client sem o modulo).
function hookPokeMakert()
hookAttempts = hookAttempts + 1
local poke = modules.poke_makert
if not poke or not poke.ConfVend or not poke.venda or not poke.showConfimarWindowDone
or not ensureSellWidgets() then
if hookAttempts < MAX_HOOK_ATTEMPTS then
scheduleEvent(hookPokeMakert, 500)
end
return
end
-- ... embrulha as três funções ...
end
  1. venda — abre a janela de venda (confVend). O wrapper deixa o original abrir a janela, depois reseta o checkbox e o preço e dispara a checagem de elegibilidade. Amarrar aqui, e não na função que só popula a janela escondida, garante que a elegibilidade e o auto-vínculo só aconteçam com a UI de fato na tela.

  2. ConfVend — o botão Vender. É o mais delicado: o wrapper captura o estado do checkbox e do preço ANTES de chamar o original (que limpa os campos e esconde a janela), deixa o original criar a oferta de market no jogo, e só então — se o checkbox estava marcado e o preço não vazio — dispara o announce@. Essa ordem é o que faz o anúncio referenciar a oferta certa.

  3. showConfimarWindowDone — abre a janela de compra (confimar). Essa janela só nasce na primeira compra, então o wrapper roda depois do original (com a janela já de pé), injeta os widgets de compra e pede a elegibilidade.

poke.ConfVend = function(...)
local doAnnounce = sellCheck and sellCheck:isChecked() and sellCheck:isEnabled()
local brl = doAnnounce and sanitizeBrl(sellBrl:getText()) or ''
local r = savedConfVend(...) -- deixa o original criar a oferta
if doAnnounce then
if brl == '' then
tell('Preco em reais vazio: anuncio na Lootfy nao enviado.')
else
sendToServer('announce', '{"brl":"' .. brl .. '"}')
end
end
return r
end

Os widgets são criados programaticamente com g_ui.createWidget e ancorados na janela do market, cada um com um id próprio (lootfySellCheck, lootfyBrl, lootfyBuyBtn…) para poder ser encontrado e destruído depois. Os botões de vínculo começam invisíveis e só aparecem quando a elegibilidade indica que o personagem não está vinculado. O botão de compra começa desabilitado e só destrava quando a elegibilidade confirma o vínculo.

O preço em reais passa por um sanitizador simples antes de ir para o payload, para não quebrar o JSON nem aceitar lixo:

-- So digitos, virgula e ponto: o preco em reais e nada mais, e mantem o JSON valido.
local function sanitizeBrl(text)
return (string.gsub(text or '', '[^%d,%.]', ''))
end

O protocolo 8.54 carrega bytes crus e o client os renderiza como Latin-1. A plataforma responde em UTF-8. Sem conversão, uma mensagem com acento (que o português usa o tempo todo) apareceria com caracteres quebrados. O módulo converte UTF-8 → Latin-1 antes de exibir qualquer texto vindo do servidor, e o algoritmo é sensível à ordem:

-- ORDEM IMPORTA: o descarte das sequencias de 3-4 bytes vem PRIMEIRO, na string
-- ainda UTF-8; a conversao de 2 bytes abaixo produz bytes Latin-1 em 0xE0-0xF4
-- que colidiriam com a faixa de lead de 3-4 bytes se rodasse antes.
local function toLatin1(text)
if not text then return text end
text = string.gsub(text, '[\224-\244][\128-\191]+', '?')
text = string.gsub(text, '([\194\195])([\128-\191])', function(prefix, tail)
local code = string.byte(tail)
if prefix == '\194' then return string.char(code) end
return string.char(code + 64)
end)
return text
end

Primeiro descartam-se as sequências de 3–4 bytes (emoji, símbolos fora do Latin-1) enquanto a string ainda é UTF-8; só depois convertem-se as sequências de 2 bytes, que cobrem os acentos do português. Se a ordem se invertesse, os bytes Latin-1 já produzidos (na faixa 0xE00xF4) colidiriam com a faixa de lead de 3–4 bytes UTF-8. Esse conversor é o espelho de client do que o servidor faz do lado dele.

Porque o módulo monkeypatcha código de terceiro, ele precisa saber desfazer tudo. O terminate() restaura as três funções originais do market e destrói todos os widgets que injetou:

function terminate()
ProtocolGame.unregisterExtendedOpcode(OPCODE_IN)
local poke = modules.poke_makert
if poke then
if savedConfVend then poke.ConfVend = savedConfVend end
if savedVenda then poke.venda = savedVenda end
if savedShowConfimar then poke.showConfimarWindowDone = savedShowConfimar end
end
for _, w in ipairs({ sellCheck, sellBrl, sellStatus, sellLinkBtn, sellRefreshBtn,
buyBtn, buyStatus, buyLinkBtn, buyRefreshBtn }) do
if w then w:destroy() end
end
-- ... zera as referências ...
end

Depois de um terminate(), o market de terceiro fica exatamente como estava antes de o game_lootfy carregar. Isso é o que torna seguro distribuir o módulo: se o jogador o remover, nada do market original fica corrompido.

  1. Solte a pasta game_lootfy em modules/ do seu client OTClient.

  2. Reinicie o client (ou recarregue os módulos). Como o .otmod é autoload, ele sobe sozinho.

  3. Abra o market do jogo. Se houver um módulo de market compatível carregado, os widgets Lootfy aparecem. Se não houver, o módulo faz no-op em silêncio após ~30s.