Pular para o conteúdo

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.

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;
}
}

IOMarket::lootfyDeliver devolve um int e preenche um remaining por referência. O binding Lua doLootfyDeliver expõe os dois como (code, remaining):

codeSignificaremaining
0entregueestoque que sobrou na oferta
1oferta inexistente ou expirada0
2estoque insuficienteo 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.

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 << ";";

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 o buyMarket do 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);

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 lootfyDeliver na seção public de IOMarket
    • iomarket.cpp implementação de IOMarket::lootfyDeliver
    • luascript.h declaração de luaDoLootfyDeliver
    • luascript.cpp registro doLootfyDeliver + implementação do binding

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