>
Guia 2026-04-26

Dicas Avançadas de Gestão de Inventário para FiveM

TDYSKY

TDYSKY

Fundador & Lead Developer na Agency Scripts

Por que a arquitetura de inventário é importante

O sistema de inventário é a espinha dorsal de qualquer servidor de roleplay FiveM. Cada interação do jogador com os itens, desde pegar uma arma até entregar uma chave a alguém, flui através do inventário. Um inventário mal projetado leva a explorações de duplicação de itens, problemas de dessincronização e jogadores frustrados que perdem seus equipamentos. Os sistemas de inventário mais robustos seguem um modelo autoritativo de servidor, onde o cliente apenas exibe o que o servidor lhe diz, nunca confiando no cliente para relatar seu próprio estado. Essa decisão arquitetônica por si só elimina a maioria das explorações de duplicação que afetam os servidores com inventários confiáveis ​​dos clientes. Ao planejar seu inventário, pense primeiro no fluxo de dados: o servidor possui a verdade, o cliente a renderiza e cada mutação passa por um evento de servidor validado.

Sistemas baseados em peso versus slots

A escolha entre sistemas de inventário baseados em peso e em slots molda fundamentalmente a experiência do jogador. Em um sistema baseado em slots, cada slot contém um tipo de item com tamanho máximo de pilha, e o número total de slots define a capacidade de carga. Em um sistema baseado em peso, cada item tem um valor de peso e o jogador tem um peso máximo para carregar. Muitas estruturas modernas combinam ambas as abordagens, usando slots para organização, mas limitando a capacidade total por peso. Aqui está um exemplo de uma definição combinada de item de peso e slot:

-- Shared item definitions (items.lua)
QBCore.Shared.Items = {
    ['water_bottle'] = {
        name = 'water_bottle',
        label = 'Water Bottle',
        weight = 500,        -- grams
        type = 'item',
        image = 'water_bottle.png',
        unique = false,
        useable = true,
        shouldClose = true,
        description = 'A refreshing bottle of water',
        stackSize = 10,      -- max per slot
    },
    ['lockpick'] = {
        name = 'lockpick',
        label = 'Lockpick',
        weight = 200,
        type = 'item',
        image = 'lockpick.png',
        unique = false,
        useable = true,
        shouldClose = true,
        description = 'Used to pick locks',
        stackSize = 5,
    },
}

O campo weight é armazenado em gramas para precisão, e stackSize controla quantos itens cabem em um único slot. Quando um jogador tentar pegar um item, verifique se há espaço disponível e se o peso total não excede o máximo. Essa validação dupla evita que os jogadores carreguem quantidades irrealistas de itens pesados, mesmo que tenham slots vazios.

Validação do lado do servidor e anti-exploração

Cada ação de inventário deve ser validada no servidor antes de entrar em vigor. Quando um jogador arrasta um item do slot 3 para o slot 7, o cliente envia uma solicitação de movimentação e o servidor verifica se o slot de origem realmente contém esse item, se o slot de destino pode aceitá-lo e se as quantidades são consistentes. Nunca deixe o cliente especificar contagens de itens ou criar itens do nada. Aqui está um manipulador de movimentação seguro do lado do servidor:

RegisterNetEvent('inventory:server:moveItem', function(fromSlot, toSlot, fromAmount)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local fromItem = Player.PlayerData.items[fromSlot]
    if not fromItem then
        -- Source slot is empty, possible exploit attempt
        DropPlayer(src, 'Invalid inventory operation')
        return
    end

    if fromAmount > fromItem.amount or fromAmount < 1 then
        DropPlayer(src, 'Invalid inventory amount')
        return
    end

    local toItem = Player.PlayerData.items[toSlot]

    if toItem and toItem.name == fromItem.name and not fromItem.unique then
        -- Stack items together
        local maxStack = QBCore.Shared.Items[fromItem.name].stackSize or 50
        local canStack = maxStack - toItem.amount
        local moveAmount = math.min(fromAmount, canStack)

        if moveAmount > 0 then
            toItem.amount = toItem.amount + moveAmount
            fromItem.amount = fromItem.amount - moveAmount
            if fromItem.amount <= 0 then
                Player.PlayerData.items[fromSlot] = nil
            end
        end
    else
        -- Swap items between slots
        Player.PlayerData.items[toSlot] = fromItem
        Player.PlayerData.items[fromSlot] = toItem
    end

    Player.Functions.SetPlayerData('items', Player.PlayerData.items)
end)

Observe que DropPlayer exige operações claramente impossíveis. Registrar esses eventos em uma tabela de auditoria separada ajuda a identificar tentativas e padrões de exploração. Considere implementar também a limitação de taxa em eventos de inventário, porque jogadores legítimos raramente realizam mais do que algumas operações de inventário por segundo, enquanto explorações automatizadas geralmente enviam centenas de solicitações rapidamente.

Metadados de itens e itens exclusivos

Os metadados transformam itens simples em objetos ricos e exclusivos. Uma arma pode conter seu número de série, durabilidade e modificações anexadas. Um telefone pode armazenar o número atribuído e a referência da lista de contatos. Os alimentos podem ter um carimbo de data e hora de validade. Os metadados são armazenados como uma tabela Lua serializada em JSON no banco de dados, anexada a cada instância de item. A principal distinção é entre itens empilháveis, que compartilham os mesmos metadados, e itens exclusivos, cada um com sua própria tabela de metadados. Veja como criar uma arma com metadados completos:

-- Creating a weapon with metadata
function CreateWeaponItem(src, weaponName, serial)
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return false end

    local metadata = {
        serial = serial or GenerateSerial(),
        durability = 100.0,
        ammo = 0,
        attachments = {},
        registered = false,
        registeredTo = nil,
        quality = math.random(85, 100),
        created = os.time(),
    }

    return Player.Functions.AddItem(weaponName, 1, nil, metadata)
end

function GenerateSerial()
    local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    local serial = ''
    for i = 1, 10 do
        local idx = math.random(1, #chars)
        serial = serial .. chars:sub(idx, idx)
    end
    return serial
end

Ao exibir itens com metadados na NUI, passe os metadados junto com as informações do item para que a IU possa mostrar detalhes como barras de durabilidade, números de série e classificações de qualidade. Isso dá aos jogadores uma conexão mais profunda com seus itens e oferece suporte a cenários de RPG avançados, como sistemas de registro de armas ou investigações forenses que rastreiam um número de série até seu proprietário.

Desempenho NUI de arrastar e soltar

A IU do inventário é um dos elementos NUI mais sensíveis ao desempenho porque os jogadores interagem com ela constantemente. Evite renderizar novamente toda a grade de inventário a cada atualização. Em vez disso, use uma abordagem de DOM virtual ou atualizações de elementos direcionados que modifiquem apenas os slots que foram alterados. Quando um jogador arrasta um item, lide com o arrasto inteiramente em JavaScript usando eventos de mouse em vez de enviar atualizações de posição ao cliente Lua durante o arrasto. Envie apenas o resultado final da eliminação como um único retorno de chamada NUI. Aqui está um padrão de manipulador de arrasto de alto desempenho:

// Inventory NUI - performant drag and drop
let draggedItem = null;
let dragElement = null;

document.addEventListener('mousedown', (e) => {
    const slot = e.target.closest('.inv-slot[data-has-item="true"]');
    if (!slot) return;

    draggedItem = {
        slot: parseInt(slot.dataset.slot),
        item: JSON.parse(slot.dataset.itemInfo),
    };

    dragElement = slot.cloneNode(true);
    dragElement.classList.add('dragging-ghost');
    dragElement.style.position = 'fixed';
    dragElement.style.pointerEvents = 'none';
    dragElement.style.zIndex = '9999';
    document.body.appendChild(dragElement);

    moveDragElement(e.clientX, e.clientY);
});

document.addEventListener('mousemove', (e) => {
    if (!dragElement) return;
    moveDragElement(e.clientX, e.clientY);
});

document.addEventListener('mouseup', (e) => {
    if (!draggedItem) return;

    const targetSlot = e.target.closest('.inv-slot');
    if (targetSlot) {
        const toSlot = parseInt(targetSlot.dataset.slot);
        // Send only the final result to Lua
        fetch(`https://${GetParentResourceName()}/moveItem`, {
            method: 'POST',
            body: JSON.stringify({
                fromSlot: draggedItem.slot,
                toSlot: toSlot,
                amount: draggedItem.item.amount,
            }),
        });
    }

    if (dragElement) dragElement.remove();
    draggedItem = null;
    dragElement = null;
});

Para a renderização visual, use CSS Grid para o layout dos slots e evite animações CSS pesadas em itens de inventário, pois os jogadores podem ter dezenas de slots visíveis simultaneamente. Sprites de imagem para ícones de itens carregam mais rápido do que arquivos de imagem individuais e reduzem as solicitações HTTP quando o NUI é aberto pela primeira vez.

Sistemas de armazenamento e contêineres

Além do inventário pessoal, os jogadores precisam de acesso a armazenamento externo, como baús de veículos, esconderijos domésticos e armazenamento organizacional compartilhado. Cada tipo de contêiner deve ter seus próprios limites de capacidade e regras de controle de acesso. Os baús de veículos usam a placa do veículo como um identificador exclusivo, os esconderijos domésticos usam IDs de propriedade e os esconderijos de trabalho usam o nome do trabalho combinado com uma verificação de nota. Armazene os inventários de contêineres em uma tabela de banco de dados dedicada, separada dos inventários dos jogadores, para manter as consultas eficientes:

CREATE TABLE IF NOT EXISTS stash_items (
    id INT AUTO_INCREMENT PRIMARY KEY,
    stash_id VARCHAR(100) NOT NULL,
    slot INT NOT NULL,
    item_name VARCHAR(50) NOT NULL,
    amount INT DEFAULT 1,
    metadata LONGTEXT DEFAULT '{}',
    UNIQUE KEY unique_stash_slot (stash_id, slot),
    INDEX idx_stash_id (stash_id)
);

-- Example stash_id values:
-- 'trunk_ABC123'        (vehicle trunk by plate)
-- 'house_42'            (house stash by property id)
-- 'police_evidence_1'   (job stash with identifier)

Quando um jogador abre um contêiner, carregue seu conteúdo do banco de dados e bloqueie-o para evitar o acesso simultâneo de vários jogadores. Use uma tabela de bloqueio do lado do servidor que rastreie quais IDs de stash estão abertos no momento e por quem. Solte a trava quando o player fechar o container ou desconectar. Isso evita a clássica exploração de duplicação, onde dois jogadores abrem o mesmo baú e ambos retiram os mesmos itens.

Sincronização e persistência de inventário

Manter os dados de inventário sincronizados entre a memória do servidor, o banco de dados e a exibição do cliente requer uma coordenação cuidadosa. Salve o inventário do jogador no banco de dados periodicamente, e não a cada alteração de item, para reduzir a carga de gravação do banco de dados. Um intervalo de salvamento de 30 a 60 segundos funciona bem para a maioria dos servidores. Além disso, sempre salve na desconexão do jogador e no desligamento do servidor usando o evento playerDropped e um manipulador de desligamento. Implemente um sistema de sinalização suja que só grava no banco de dados quando o inventário realmente mudou desde o último salvamento:

local inventoryDirty = {}

-- Mark inventory as needing save
function MarkDirty(citizenid)
    inventoryDirty[citizenid] = true
end

-- Periodic save loop
CreateThread(function()
    while true do
        Wait(30000) -- 30 seconds
        for citizenid, dirty in pairs(inventoryDirty) do
            if dirty then
                local Player = QBCore.Functions.GetPlayerByCitizenId(citizenid)
                if Player then
                    SaveInventoryToDatabase(citizenid, Player.PlayerData.items)
                end
                inventoryDirty[citizenid] = nil
            end
        end
    end
end)

AddEventHandler('playerDropped', function()
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if Player then
        local citizenid = Player.PlayerData.citizenid
        if inventoryDirty[citizenid] then
            SaveInventoryToDatabase(citizenid, Player.PlayerData.items)
            inventoryDirty[citizenid] = nil
        end
    end
end)

Para o lado do cliente, atualizações de NUI em lote para que múltiplas mudanças rápidas de inventário, como o recebimento de vários itens de uma operação de elaboração, resultem em uma única atualização de UI em vez de uma por item. Isso elimina o efeito tremeluzente que os jogadores veem quando os itens aparecem um de cada vez em sua grade de inventário.

Monitoramento e otimização de desempenho

Monitore o desempenho do seu sistema de inventário rastreando as principais métricas: tempo médio de economia do banco de dados, taxa de processamento de eventos e frequência de renderização NUI. Use o criador de perfil FiveM para identificar gargalos em seus retornos de chamada de inventário. Armadilhas de desempenho comuns incluem iterar todos os inventários de jogadores em cada quadro para recursos baseados em proximidade, como coleta de itens terrestres, serializar grandes objetos de metadados desnecessariamente e deixar quadros NUI ativos quando o inventário é fechado. Para itens terrestres, use um sistema de grade espacial que verifica apenas os itens dentro da célula do jogador, em vez de verificar cada item descartado no servidor. Armazene as definições de item em cache em uma tabela de pesquisa indexada pelo nome do item para que a busca das propriedades do item seja uma pesquisa de hash O(1) em vez de uma varredura de array O(n). Por fim, considere implementar a paginação de inventário para contêineres com capacidades muito grandes, carregando apenas os slots visíveis e buscando linhas adicionais conforme o player rola, o que mantém as consultas ao banco de dados e a renderização NUI leves, mesmo para armazéns com centenas de slots.

Partilhar este artigo

Pronto para melhorar o teu servidor?

Explora os nossos scripts FiveM premium na loja Agency Scripts ou junta-te à nossa comunidade no Discord para suporte e atualizações.