Patch de entrega na engine
Quando o pagamento confirma, alguém precisa colocar o item na mão do comprador dentro do jogo. No OpenTibia isso é menos trivial do que parece: o market do TFS 0.3 vive em memória, o comprador pode estar offline, o vendedor pode estar online salvando por cima, e nada disso pode envolver mover moeda do jogo. O segundo entregável da bancada é o primitivo que resolve tudo isso: doLootfyDeliver, um binding nativo em C++ adicionado à engine.
Para o dono de servidor, a ideia é: a Lootfy avisa “o item X foi pago, entregue N unidades ao jogador Y”, e a engine faz isso de forma durável — funcione o comprador online ou offline, sem tocar em nenhuma moeda do jogo. Para o programador, o desafio real está em achar a oferta certa e consumir o estoque na fonte de verdade correta.
Por que um primitivo nativo, e não editar a tabela
Seção intitulada “Por que um primitivo nativo, e não editar a tabela”O market do TFS 0.3 vive em memória (IOMarket::marketMap); a tabela market_items do banco é só um retrato, reescrito em shutdown, global save ou save do jogador. Isso tem duas consequências que inviabilizam a solução ingênua de “editar market_items direto”:
- Não cola com o vendedor online. O save dele reescreve a tabela a partir da memória, apagando qualquer edição que você tenha feito no banco.
- Não entrega a um comprador offline. As funções de dar item ao jogador exigem o jogador carregado.
O primitivo mexe no marketMap (a verdade) e entrega pelo caminho durável do próprio market, então funciona com vendedor e comprador em qualquer combinação de online/offline.
A chave estável vs o id volátil
Seção intitulada “A chave estável vs o id volátil”Este é o detalhe mais importante do patch. O marketMap é indexado por um id volátil que renumera a cada save — usá-lo para localizar uma oferta dias depois entregaria o item errado. A oferta é encontrada, em vez disso, pela chave estável player_id + m_Time (o GUID do vendedor mais o timestamp de criação da oferta), que não muda ao longo da vida da oferta:
// Acha a oferta pela chave ESTAVEL (player_id + m_Time). O marketMap e keyed// pelo id volatil (renumera a cada save), entao varremos: na pratica ha uma// oferta por (player, time).uint32_t foundId = 0;MarketList marketList;for(MarketMap::const_iterator it = marketMap.begin(); it != marketMap.end(); ++it){ if(it->second.player_id == sellerId && it->second.m_Time == offerTime) { foundId = it->first; marketList = it->second; break; }}Os códigos de retorno
Seção intitulada “Os códigos de retorno”IOMarket::lootfyDeliver devolve um int e preenche um remaining por referência. O binding Lua doLootfyDeliver expõe os dois como (code, remaining):
code | Significa | remaining |
|---|---|---|
0 | entregue | estoque que sobrou na oferta |
1 | oferta inexistente ou expirada | 0 |
2 | estoque insuficiente | o estoque atual da oferta |
O remaining no caso 2 é útil para o chamador saber quanto ainda há — a plataforma pode reconciliar o estoque observado. A checagem de expiração é feita comparando time(NULL) com o m_Time da oferta.
Consumir em memória, realinhar o banco
Seção intitulada “Consumir em memória, realinhar o banco”A ordem de operações é deliberada. Primeiro consome-se o estoque na memória, que é a verdade do market — se zerou, apaga a oferta; se sobrou, atualiza a contagem:
remaining = marketList.item_Count - count;// Consome na MEMORIA (a verdade do market), como o buyMarket faz.if(remaining == 0) eraseMarket(foundId);else{ marketList.item_Count = remaining; setUpdateMarketItem(foundId, marketList);}Depois, realinha-se o snapshot market_items no banco pela mesma chave estável. A memória acima já é a verdade; esse UPDATE/DELETE só evita que a tabela fique defasada até o próximo save — e importa porque é dela que a sincronização e a reconciliação da Lootfy leem. Um save futuro reescreve o mesmo valor, então não há conflito.
if(remaining == 0) query << "DELETE FROM `market_items` WHERE `player_id` = " << sellerId << " AND `time` = " << offerTime << ";";else query << "UPDATE `market_items` SET `count` = " << remaining << " WHERE `player_id` = " << sellerId << " AND `time` = " << offerTime << ";";A entrega durável: bag ou depot, nunca o chão
Seção intitulada “A entrega durável: bag ou depot, nunca o chão”O item é entregue em pilhas de 100. O destino depende do estado do comprador:
- Comprador online → vai para a bag (
internalPlayerAddItem), item na mão. - Comprador offline, ou bag cheia → cai para o depot por nome (
playerNewMail), o mesmo caminho durável que obuyMarketdo jogo usa.
O ponto crítico é que o item nunca vai para o chão nem se perde. Se o add na bag falha (bag cheia), o engine pode ter tocado o item, então o código o recria antes de mandar para o depot:
if(onlineBuyer){ if(g_game.internalPlayerAddItem(NULL, onlineBuyer, item, false) == RET_NOERROR) continue; // Rejeitado (bag cheia): o engine nao ficou com o item. Recria para // o depot, ja que o add falho pode te-lo tocado. delete item; item = Item::CreateItem(marketList.item_Id, value); // ...}IOLoginData::getInstance()->playerNewMail(NULL, buyerName, 1, item);Os quatro arquivos a aplicar
Seção intitulada “Os quatro arquivos a aplicar”O patch são quatro arquivos-fonte aplicados na árvore do TFS 0.3, e depois é preciso recompilar a engine:
Directoryserver/src/
- iomarket.h declaração de
lootfyDeliverna seçãopublicdeIOMarket - iomarket.cpp implementação de
IOMarket::lootfyDeliver - luascript.h declaração de
luaDoLootfyDeliver - luascript.cpp registro
doLootfyDeliver+ implementação do binding
- iomarket.h declaração de
O binding é registrado junto aos outros lua_register e apenas desempilha os quatro argumentos e chama a implementação:
lua_register(m_luaState, "doLootfyDeliver", LuaScriptInterface::luaDoLootfyDeliver);
int32_t LuaScriptInterface::luaDoLootfyDeliver(lua_State* L){ //doLootfyDeliver(sellerId, offerTime, count, buyerName) std::string buyerName = popString(L); uint16_t count = (uint16_t)popNumber(L); uint64_t offerTime = (uint64_t)popNumber(L); uint32_t sellerId = (uint32_t)popNumber(L);
uint16_t remaining = 0; int result = IOMarket::getMarket()->lootfyDeliver(sellerId, offerTime, count, buyerName, remaining);
lua_pushnumber(L, result); lua_pushnumber(L, remaining); return 2;}Do lado do Lua de servidor, a entrega vira uma linha: quando o webhook de pagamento chega e o Agent o valida, o Lua de entrega chama doLootfyDeliver(sellerId, offerTime, count, buyerName) e lê o (code, remaining) para reportar à plataforma o sucesso (confirmar entrega, POST /v1/api/transactions/{transactionId}/delivery) ou a falha de estoque (reportar falha de entrega, POST /v1/api/transactions/{transactionId}/delivery-failure).