>
Tutoriel 2026-05-16

Système de courrier en jeu pour FiveM

TDYSKY

TDYSKY

Fondateur et développeur principal chez Agency Scripts

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

Partager cet article

Prêt à améliorer votre serveur ?

Découvrez nos scripts FiveM premium dans la boutique Agency Scripts ou rejoignez notre communauté Discord pour le support et les mises à jour.