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.
O que ele adiciona ao jogo
Seção intitulada “O que ele adiciona ao jogo”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 .otmod
Seção intitulada “O .otmod”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 monkeypatch: enxertar sem forkar
Seção intitulada “O monkeypatch: enxertar sem forkar”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 ...endAs três funções embrulhadas
Seção intitulada “As três funções embrulhadas”-
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. -
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 oannounce@. Essa ordem é o que faz o anúncio referenciar a oferta certa. -
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 rendOs widgets injetados
Seção intitulada “Os widgets injetados”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,%.]', ''))endA conversão de encoding para o client 8.54
Seção intitulada “A conversão de encoding para o client 8.54”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 textendPrimeiro 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 0xE0–0xF4) 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.
Uninstall limpo
Seção intitulada “Uninstall limpo”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 ...endDepois 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.
Como instalar
Seção intitulada “Como instalar”-
Solte a pasta
game_lootfyemmodules/do seu client OTClient. -
Reinicie o client (ou recarregue os módulos). Como o
.otmodéautoload, ele sobe sozinho. -
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.