O valor da representação do correio físico
Em uma época em que a maioria dos servidores FiveM dependem inteiramente de notificações telefônicas e mensagens digitais, um sistema físico de correio e cartas adiciona uma camada de imersão que se destaca. Os jogadores podem escrever cartas manuscritas para outros personagens, enviar pacotes contendo itens, receber avisos oficiais do governo, receber contas de empresas e até mesmo encontrar misteriosas notas anônimas. Isto cria oportunidades de roleplay que a comunicação digital simplesmente não consegue replicar. Uma carta ameaçadora deixada na caixa de correio de alguém tem mais peso do que uma mensagem de texto. Uma carta de amor escrita à mão parece mais pessoal do que um e-mail. As intimações judiciais entregues pelo correio parecem mais oficiais. Além do valor da dramatização, um sistema de correio cria uma oportunidade natural de emprego para os funcionários dos correios que coletam, classificam e entregam correspondências em toda a cidade. Este tutorial aborda a construção de um sistema de correio completo desde o esquema do banco de dados até a mecânica de entrega.
Esquema de banco de dados para o sistema de correio
O banco de dados precisa lidar com cartas, pacotes, atribuições de caixas de correio e estados de entrega. Cada item de correspondência tem um identificador de remetente, um identificador de destinatário, um tipo que distingue cartas de pacotes, uma linha de assunto, o conteúdo do corpo das cartas, dados de itens anexados para pacotes, carimbos de data e hora para criação e entrega e sinalizadores de status que rastreiam se a correspondência está pendente, em trânsito, entregue ou lida. As caixas de correio estão vinculadas a endereços de propriedades para que os jogadores que possuem ou alugam casas tenham uma caixa de correio pessoal. Caixas de correio públicas espalhadas pela cidade atendem jogadores sem moradia, exigindo que eles visitem uma agência dos correios para coletar suas correspondências. O esquema também deve oferecer suporte a mensagens geradas pelo sistema para avisos automatizados, como contas, documentos judiciais, formulários de emprego e anúncios governamentais que os scripts podem ser acionados sem um remetente do jogador.
-- SQL schema
CREATE TABLE IF NOT EXISTS mail_items (
id INT AUTO_INCREMENT PRIMARY KEY,
sender_id VARCHAR(64),
sender_name VARCHAR(64) DEFAULT 'Unknown',
recipient_id VARCHAR(64) NOT NULL,
mail_type ENUM('letter','package','notice') DEFAULT 'letter',
subject VARCHAR(128) NOT NULL,
body TEXT,
attachments JSON DEFAULT '[]',
postage_paid INT DEFAULT 0,
status ENUM('pending','transit','delivered','read','returned')
DEFAULT 'pending',
mailbox_id INT DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
delivered_at TIMESTAMP NULL,
read_at TIMESTAMP NULL,
INDEX idx_recipient (recipient_id, status),
INDEX idx_status (status)
);
CREATE TABLE IF NOT EXISTS mailboxes (
id INT AUTO_INCREMENT PRIMARY KEY,
owner_id VARCHAR(64),
property_id INT DEFAULT NULL,
location_x FLOAT NOT NULL,
location_y FLOAT NOT NULL,
location_z FLOAT NOT NULL,
box_type ENUM('residential','public','business') DEFAULT 'residential',
capacity INT DEFAULT 20,
UNIQUE KEY unique_owner (owner_id)
);Escrevendo e enviando cartas
A interface de escrita de cartas deve ser semelhante a escrever uma carta real. Apresente aos jogadores um NUI que se assemelha a um pedaço de papel com campos para o nome do destinatário, uma linha de assunto e um corpo de texto livre. Use uma fonte estilo manuscrito no NUI para reforçar a estética da letra física. O campo do destinatário deve pesquisar o banco de dados de personagens pelo nome, em vez de exigir que os jogadores conheçam os identificadores internos. Quando o jogador termina de escrever e clica em enviar, o cliente envia os dados para o servidor que valida o conteúdo, verifica se o remetente tem dinheiro suficiente para cobrir a postagem, deduz o custo e insere o item de correio no banco de dados com status pendente. Imponha limites razoáveis no comprimento do corpo, talvez 500 caracteres para cartas padrão e 1.000 para postagem premium, para evitar abusos e ao mesmo tempo permitir mensagens significativas. A carta então entra no pipeline de entrega e espera que um funcionário dos correios a processe ou que o cronômetro de entrega automática expire.
-- server/mail.lua
local Config = {
postageCost = 50,
premiumPostage = 150,
maxBodyLength = 500,
premiumMaxBody = 1000,
autoDeliverTime = 900, -- 15 minutes if no postal worker
packagePostage = 200,
}
RegisterNetEvent('mail:send', function(data)
local src = source
local sender = GetPlayerIdentifier(src, 0)
-- Validate recipient exists
local recipient = MySQL.single.await([[
SELECT identifier, CONCAT(firstname,' ',lastname) as name
FROM characters WHERE CONCAT(firstname,' ',lastname) LIKE ?
LIMIT 1
]], {'%' .. data.recipientName .. '%'})
if not recipient then
TriggerClientEvent('mail:notify', src, 'Recipient not found.')
return
end
-- Check postage funds
local cost = data.premium and Config.premiumPostage or Config.postageCost
local maxLen = data.premium and Config.premiumMaxBody or Config.maxBodyLength
if #data.body > maxLen then
TriggerClientEvent('mail:notify', src, 'Letter exceeds maximum length.')
return
end
-- Deduct postage (framework-specific money removal)
local paid = exports['framework']:RemoveMoney(src, cost, 'cash')
if not paid then
TriggerClientEvent('mail:notify', src, 'Not enough cash for postage.')
return
end
-- Insert mail
MySQL.insert([[
INSERT INTO mail_items
(sender_id, sender_name, recipient_id, mail_type,
subject, body, postage_paid, status)
VALUES (?, ?, ?, 'letter', ?, ?, ?, 'pending')
]], {
sender, data.senderName, recipient.identifier,
data.subject, data.body, cost
})
TriggerClientEvent('mail:notify', src, 'Letter sent! Postage: $' .. cost)
end)Sistema de pacotes com anexos de itens
Os pacotes estendem o sistema de correio além do texto, permitindo que os jogadores enviem itens físicos uns aos outros. Um jogador visita uma agência dos correios, seleciona itens de seu inventário para incluir em um pacote, escreve uma nota opcional e paga a taxa de postagem do pacote. O servidor remove os itens do inventário do remetente e os armazena como um blob JSON na coluna de anexos do item de email. Quando o destinatário abre o pacote em sua caixa de correio ou agência postal, o servidor desserializa os dados do anexo e adiciona cada item de volta ao inventário do destinatário, verificando primeiro o espaço disponível. Se o inventário do destinatário estiver cheio, o pacote permanecerá na caixa de correio até que liberem espaço. Isso cria uma maneira legítima de enviar itens para jogadores offline e abre a possibilidade de roleplaying para pacotes de cuidados, entrega de evidências e troca de presentes. Valide todos os dados de itens no servidor para evitar explorações de duplicação e registre todas as transações de pacotes para revisão administrativa.
-- server/packages.lua
RegisterNetEvent('mail:sendPackage', function(data)
local src = source
local sender = GetPlayerIdentifier(src, 0)
-- Validate items exist in sender inventory
local validItems = {}
for _, item in ipairs(data.items) do
local hasItem = exports['ox_inventory']:Search(src, 'count', item.name)
if hasItem < item.count then
TriggerClientEvent('mail:notify', src,
'You do not have enough ' .. item.name)
return
end
table.insert(validItems, {
name = item.name,
count = item.count,
meta = item.metadata or {}
})
end
-- Remove items from sender
for _, item in ipairs(validItems) do
exports['ox_inventory']:RemoveItem(src, item.name, item.count)
end
-- Deduct postage
local paid = exports['framework']:RemoveMoney(
src, Config.packagePostage, 'cash')
if not paid then
-- Refund items if payment fails
for _, item in ipairs(validItems) do
exports['ox_inventory']:AddItem(src, item.name, item.count)
end
return
end
MySQL.insert([[
INSERT INTO mail_items
(sender_id, sender_name, recipient_id, mail_type,
subject, body, attachments, postage_paid, status)
VALUES (?, ?, ?, 'package', ?, ?, ?, ?, 'pending')
]], {
sender, data.senderName, data.recipientId,
data.subject or 'Package',
data.note or '',
json.encode(validItems),
Config.packagePostage
})
end)Trabalho do funcionário dos correios e rotas de entrega
O trabalho do carteiro transforma a entrega de correspondência de um processo em segundo plano em conteúdo de roleplay ativo. Os jogadores que chegam ao correio recebem uma van de entrega e uma rota de itens de correspondência pendentes para entrega. A rota é gerada consultando todos os itens de correspondência pendentes, agrupando-os por zona de entrega e criando uma sequência ordenada de pontos de referência que minimiza a distância de viagem. Cada parada na rota coloca um ponto no mapa e um marcador na caixa de correio de destino. Quando o funcionário dos correios alcança o marcador e pressiona a tecla de interação, o item de correio passa do status pendente para o status entregue e fica disponível na caixa de correio do destinatário. Os trabalhadores dos correios ganham um salário base mais bônus por entrega, criando uma fonte de renda honesta. Para servidores sem funcionários postais ativos, implemente um cronômetro de entrega automático que faça a transição do correio pendente para o status entregue após um atraso configurável, garantindo que o correio chegue mesmo quando ninguém estiver trabalhando na rota postal.
Interface de interação e leitura de caixa de correio
Quando um jogador se aproxima de sua caixa de correio e interage com ela, o servidor consulta todos os itens de correio endereçados a ele com status de entregue ou lido. O NUI exibe uma lista no estilo de caixa de entrada mostrando cada item de correio com seu nome de remetente, assunto, ícone de tipo e carimbo de data/hora. As letras não lidas aparecem com um destaque ou indicador de emblema. Clicar em uma carta a abre em modo de visualização completa, semelhante a uma carta física em papel, com o remetente e a data na parte superior, o corpo do texto no centro e um botão Fechar. Para pacotes, a visualização mostra a nota anexada e uma lista de itens contidos com um botão “Abrir Pacote” que transfere os itens para o inventário do jogador. Marque as cartas como lidas quando abertas e remova-as da caixa de correio após um período de retenção configurável para evitar excesso de banco de dados. Adicione um botão de resposta que preencha previamente o campo do destinatário com o nome do remetente original, tornando a troca de correspondência conveniente. Para avisos do sistema, estilize-os de forma diferente com um papel timbrado oficial para diferenciá-los das mensagens escritas pelos jogadores.
Notificações do sistema e correio automatizado
Além da comunicação entre jogadores, o sistema de e-mail se torna uma ferramenta poderosa para que outros scripts de servidor forneçam notificações envolventes. Em vez de notificações de notificação insípidas, os scripts podem enviar correspondência formal. Um sistema judicial envia intimações oficiais em papel timbrado do governo. Uma empresa envia faturas e lembretes de pagamento. Um script de gerenciamento de propriedade envia avisos de aluguel vencido e avisos de despejo. Um sistema de facção envia cartas de recrutamento ou instruções de missão. Crie uma exportação de servidor simples que qualquer recurso possa chamar para enfileirar um item de correio do sistema sem precisar entender os componentes internos do sistema de correio. A exportação aceita um identificador de destinatário, assunto, corpo, tipo de e-mail e anexos opcionais. Isso transforma seu sistema de e-mail em uma camada de notificação universal que parece integrada ao mundo do jogo, em vez de ser uma sobreposição de interface de usuário intrusiva que quebra a imersão.
-- server/exports.lua
-- Universal mail export for other resources
function SendSystemMail(recipientId, subject, body, mailType, attachments)
mailType = mailType or 'notice'
attachments = attachments or '[]'
if type(attachments) == 'table' then
attachments = json.encode(attachments)
end
MySQL.insert([[
INSERT INTO mail_items
(sender_id, sender_name, recipient_id, mail_type,
subject, body, attachments, status)
VALUES ('system', 'City of Los Santos', ?, ?, ?, ?, ?, 'pending')
]], {recipientId, mailType, subject, body, attachments})
end
exports('SendSystemMail', SendSystemMail)
-- Usage from another resource:
-- exports['mailsystem']:SendSystemMail(
-- playerId, 'Rent Due',
-- 'Your rent of $2,500 is due in 3 days.',
-- 'notice'
-- )
