La valeur de jeu de rôle du courrier physique
À une époque où la plupart des serveurs FiveM reposent entièrement sur les notifications téléphoniques et la messagerie numérique, un système physique de courrier et de courrier ajoute une couche d'immersion qui se démarque. Les joueurs peuvent écrire des lettres manuscrites à d'autres personnages, envoyer des colis contenant des objets, recevoir des notifications officielles du gouvernement, obtenir des factures d'entreprises et même trouver de mystérieuses notes anonymes. Cela crée des opportunités de jeu de rôle que la communication numérique ne peut tout simplement pas reproduire. Une lettre de menace laissée dans la boîte aux lettres de quelqu'un a plus de poids qu'un message texte. Une lettre d’amour écrite à la main semble plus personnelle qu’un e-mail. Les convocations au tribunal envoyées par courrier semblent plus officielles. Au-delà de la valeur du jeu de rôle, un système de courrier crée une opportunité d'emploi naturelle pour les postiers qui récupèrent, trient et distribuent le courrier dans toute la ville. Ce didacticiel couvre la création d'un système de messagerie complet à partir du schéma de base de données jusqu'aux mécanismes de livraison.
Schéma de base de données pour le système de messagerie
La base de données doit gérer les lettres, les colis, les affectations de boîtes aux lettres et les états de livraison. Chaque élément de courrier possède un identifiant d'expéditeur, un identifiant de destinataire, un type distinguant les lettres des colis, une ligne d'objet, le contenu du corps des lettres, les données des éléments joints pour les colis, des horodatages pour la création et la livraison et des indicateurs d'état permettant de savoir si le courrier est en attente, en transit, livré ou lu. Les boîtes aux lettres sont liées aux adresses des propriétés afin que les joueurs qui possèdent ou louent un logement disposent d'une boîte aux lettres personnelle. Des boîtes aux lettres publiques placées un peu partout dans la ville desservent les joueurs sans logement, les obligeant à se rendre dans un bureau de poste pour récupérer leur courrier. Le schéma doit également prendre en charge le courrier généré par le système pour les notifications automatisées telles que les factures, les documents judiciaires, les candidatures à un emploi et les annonces gouvernementales que les scripts peuvent déclencher sans l'expéditeur d'un joueur.
-- 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)
);
Rédaction et envoi de lettres
L’interface de rédaction de lettres devrait donner l’impression de composer une vraie lettre. Présentez aux joueurs un NUI qui ressemble à un morceau de papier avec des champs pour le nom du destinataire, une ligne d'objet et un corps de texte libre. Utilisez une police de style script manuscrite dans le NUI pour renforcer l'esthétique physique de la lettre. Le champ destinataire devrait rechercher dans la base de données des personnages par nom plutôt que d'exiger que les joueurs connaissent les identifiants internes. Lorsque le joueur a fini d'écrire et clique sur envoyer, le client envoie les données au serveur qui valide le contenu, vérifie que l'expéditeur dispose de suffisamment d'argent pour couvrir les frais de port, déduit le coût et insère l'envoi de courrier dans la base de données avec un statut en attente. Imposer des limites raisonnables à la longueur du corps, peut-être 500 caractères pour les lettres standard et 1 000 pour les envois premium, afin d'éviter les abus tout en permettant des messages significatifs. La lettre entre ensuite dans le pipeline de livraison et attend qu'un postier la traite ou que le délai de livraison automatique 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)
Système de package avec pièces jointes
Les packages étendent le système de messagerie au-delà du texte en permettant aux joueurs de s'envoyer des objets physiques. Un joueur visite un bureau de poste, sélectionne les articles de son inventaire à inclure dans un colis, rédige une note facultative et paie les frais de port du colis. Le serveur supprime les éléments de l'inventaire de l'expéditeur et les stocke sous forme de blob JSON dans la colonne des pièces jointes de l'élément de courrier. Lorsque le destinataire ouvre le colis dans sa boîte aux lettres ou au bureau de poste, le serveur désérialise les données de la pièce jointe et ajoute chaque élément à l'inventaire du destinataire, en vérifiant d'abord l'espace disponible. Si l'inventaire du destinataire est plein, le colis reste dans sa boîte aux lettres jusqu'à ce qu'il libère de la place. Cela crée un moyen légitime d'envoyer des objets aux joueurs hors ligne et ouvre le jeu de rôle pour les colis de soins, les preuves et les échanges de cadeaux. Validez toutes les données d'élément sur le serveur pour éviter les exploits de duplication et enregistrez chaque transaction de package pour examen par l'administrateur.
-- 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)
Emploi des postiers et itinéraires de livraison
Le travail de postier transforme la livraison du courrier d'un processus d'arrière-plan en un contenu de jeu de rôle actif. Les joueurs qui pointent au bureau de poste reçoivent une camionnette de livraison et un itinéraire de courrier en attente à livrer. L'itinéraire est généré en interrogeant tous les éléments de courrier en attente, en les regroupant par zone de livraison et en créant une séquence ordonnée de points de cheminement qui minimise la distance parcourue. Chaque arrêt sur l'itinéraire place un repère sur la carte et un marqueur sur la boîte aux lettres de destination. Lorsque le postier atteint le marqueur et appuie sur la touche d'interaction, l'envoi passe du statut en attente au statut livré et devient disponible dans la boîte aux lettres du destinataire. Les postiers gagnent un salaire de base plus des primes par livraison, créant ainsi une source de revenus honnête. Pour les serveurs sans postiers actifs, implémentez un minuteur de livraison automatique qui fait passer le courrier en attente à l'état livré après un délai configurable, garantissant ainsi que le courrier arrive finalement même lorsque personne ne travaille sur la route postale.
Interaction de boîte aux lettres et interface de lecture
Lorsqu'un joueur s'approche de sa boîte aux lettres et interagit avec elle, le serveur interroge tous les éléments de courrier qui lui sont adressés avec un statut livré ou lu. Le NUI affiche une liste de type boîte de réception affichant chaque élément de courrier avec son nom de l'expéditeur, son objet, son icône de type et son horodatage. Les lettres non lues apparaissent avec un indicateur de surbrillance ou de badge. Cliquer sur une lettre l'ouvre en mode plein écran, semblable à une lettre physique sur papier, avec l'expéditeur et la date en haut, le corps du texte au centre et un bouton de fermeture. Pour les packages, la vue affiche la note jointe et une liste des éléments contenus avec un bouton « Ouvrir le package » qui transfère les éléments vers l'inventaire du joueur. Marquez les lettres comme lues une fois ouvertes et supprimez-les de la boîte aux lettres après une période de conservation configurable pour éviter l'engorgement de la base de données. Ajoutez un bouton de réponse qui pré-remplit le champ du destinataire avec le nom de l'expéditeur d'origine, ce qui facilite les échanges de correspondance. Pour les notifications système, stylisez-les différemment avec un papier à en-tête officiel pour les distinguer du courrier écrit par les joueurs.
Notifications système et courrier automatisé
Au-delà de la communication entre joueurs, le système de messagerie devient un outil puissant permettant à d'autres scripts serveur de fournir des notifications immersives. Au lieu de notifications fades, les scripts peuvent envoyer du courrier formel. Un système judiciaire envoie des convocations officielles sur papier à en-tête du gouvernement. Une entreprise envoie des factures et des rappels de paiement. Un script de gestion immobilière envoie des avis de loyer dû et des avertissements d'expulsion. Un système de faction envoie des lettres de recrutement ou des briefings de mission. Créez une exportation de serveur simple que n'importe quelle ressource peut appeler pour mettre en file d'attente un élément de courrier système sans avoir besoin de comprendre les composants internes du système de messagerie. L'exportation accepte un identifiant de destinataire, un objet, un corps, un type de courrier et des pièces jointes facultatives. Cela transforme ton système de messagerie en une couche de notification universelle qui semble intégrée au monde du jeu plutôt qu'en une superposition d'interface utilisateur intrusive qui brise l'immersion.
-- 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'
-- )
