Zurück zum Blog
Tutorial7 Min. Lesezeit

FiveM-Mail-System-Skript: Nachrichten und Rechnungen im Spiel

Richte ein FiveM-Mail-System für Rechnungen, Jobnotizen und Serverankündigungen ein. Entdecke Top-Skripte, Framework-Support und Ideen für deine Jobs.

Agency Scripts

Gründer und leitender Entwickler bei Agency Scripts

Der Rollenspielwert physischer Post

In einer Zeit, in der die meisten FiveM-Server ausschließlich auf Telefonbenachrichtigungen und digitale Nachrichten setzen, sorgt ein physisches Post- und Briefsystem für eine zusätzliche Immersionsebene, die heraussticht. Spieler können Briefe im handschriftlichen Stil an andere Charaktere schreiben, Pakete mit Gegenständen verschicken, offizielle Behördenbescheide erhalten, Rechnungen von Unternehmen bekommen und sogar geheimnisvolle anonyme Nachrichten finden. So entstehen Rollenspielmöglichkeiten, die digitale Kommunikation einfach nicht bieten kann. Ein Drohbrief im Briefkasten hat mehr Gewicht als eine Textnachricht. Ein handgeschriebener Liebesbrief wirkt persönlicher als eine E-Mail. Gerichtsvorladungen, die per Post zugestellt werden, fühlen sich offizieller an. Neben dem Rollenspielwert schafft ein Postsystem eine natürliche Arbeitsmöglichkeit für Postboten, die Post in der Stadt abholen, sortieren und zustellen. In diesem Tutorial geht es um den Aufbau eines vollständigen Postsystems, vom Datenbankschema bis hin zu den Zustellungsmechaniken.

Datenbankschema für das Postsystem

Die Datenbank muss Briefe, Pakete, Briefkastenzuordnungen und Zustellungsstatus verwalten. Jeder Postgegenstand enthält eine Absenderkennung, eine Empfängerkennung, einen Typ zur Unterscheidung zwischen Briefen und Paketen, eine Betreffzeile, den Brieftext, angehängte Gegenstandsdaten für Pakete, Zeitstempel für Erstellung und Zustellung sowie Statusfelder, die angeben, ob die Post ausstehend, unterwegs, zugestellt oder gelesen ist. Briefkästen sind mit Grundstücksadressen verknüpft, sodass Spieler, die eine Unterkunft besitzen oder mieten, einen persönlichen Briefkasten haben. Öffentliche Briefkästen in der Stadt sind für Spieler ohne Unterkunft gedacht. Sie müssen ein Postamt aufsuchen, um ihre Post abzuholen. Das Schema sollte außerdem systemgenerierte Post unterstützen, etwa automatisierte Bescheide wie Rechnungen, Gerichtsdokumente, Bewerbungen und Regierungsmitteilungen, die Skripte ohne Absender eines Spielers auslösen können.

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

Briefe schreiben und versenden

Die Benutzeroberfläche zum Schreiben von Briefen sollte sich wie das Verfassen eines echten Briefes anfühlen. Zeige den Spielern eine NUI, die einem Blatt Papier ähnelt und Felder für den Namen des Empfängers, eine Betreffzeile und einen Freitext enthält. Verwende in der NUI eine Schriftart im Handschriftstil, um die Ästhetik eines physischen Briefes zu unterstreichen. Das Empfängerfeld sollte die Charakterdatenbank nach Namen durchsuchen, statt von den Spielern interne Kennungen zu verlangen. Wenn der Spieler mit dem Schreiben fertig ist und auf „Senden“ klickt, übermittelt der Client die Daten an den Server. Dieser validiert den Inhalt, prüft, ob der Absender genug Geld für das Porto hat, zieht die Kosten ab und fügt den Postgegenstand mit dem Status „ausstehend“ in die Datenbank ein. Lege angemessene Zeichenbeschränkungen fest, zum Beispiel 500 Zeichen für Standardbriefe und 1000 Zeichen für Premiumporto. So verhinderst du Missbrauch und ermöglichst trotzdem aussagekräftige Nachrichten. Der Brief wird anschließend in den Zustellungsprozess aufgenommen und wartet darauf, dass ein Postbote ihn bearbeitet oder der automatische Zustellungstimer abläuft.

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

Paketsystem mit angehängten Gegenständen

Pakete erweitern das Postsystem über Textnachrichten hinaus und ermöglichen es Spielern, einander physische Gegenstände zu schicken. Ein Spieler besucht ein Postamt, wählt Gegenstände aus seinem Inventar für ein Paket aus, schreibt eine optionale Nachricht und bezahlt das Paketporto. Der Server entfernt die Gegenstände aus dem Inventar des Absenders und speichert sie als JSON-Blob in der attachments-Spalte des Postgegenstands. Wenn der Empfänger das Paket an seinem Briefkasten oder im Postamt öffnet, deserialisiert der Server die Anhangsdaten und fügt jeden Gegenstand dem Inventar des Empfängers hinzu. Zuvor prüft er, ob ausreichend Platz vorhanden ist. Ist das Inventar des Empfängers voll, bleibt das Paket in seinem Briefkasten, bis er Platz geschaffen hat. So können Gegenstände auf legitime Weise an Spieler verschickt werden, die offline sind. Außerdem eröffnen sich Rollenspielmöglichkeiten für Versorgungspakete, Beweisübergaben und Geschenkaustausch. Validiere alle Gegenstandsdaten auf dem Server, um Duplizierungsexploits zu verhindern, und protokolliere jede Pakettransaktion zur Überprüfung durch Administratoren.

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

Postbotenjob und Zustellrouten

Der Postbotenjob macht die Postzustellung von einem Hintergrundprozess zu aktivem Rollenspielinhalt. Spieler, die sich im Postamt zum Dienst melden, erhalten einen Lieferwagen und eine Route mit ausstehender Post, die zugestellt werden muss. Die Route wird erstellt, indem alle ausstehenden Postgegenstände abgefragt, nach Zustellgebiet gruppiert und zu einer geordneten Abfolge von Wegpunkten zusammengestellt werden, die die Fahrstrecke minimiert. Jeder Stopp auf der Route wird auf der Karte durch einen Blip und am Briefkasten des Ziels durch eine Markierung angezeigt. Wenn der Postbote die Markierung erreicht und die Interaktionstaste drückt, wechselt der Postgegenstand vom Status „ausstehend“ zu „zugestellt“ und steht im Briefkasten des Empfängers bereit. Postboten erhalten ein Grundgehalt und zusätzliche Prämien pro Zustellung. So entsteht eine ehrliche Einkommensquelle. Wenn auf dem Server keine aktiven Postboten unterwegs sind, richte einen automatischen Zustellungstimer ein, der ausstehende Post nach einer konfigurierbaren Verzögerung als zugestellt markiert. So kommt die Post auch dann irgendwann an, wenn niemand die Zustellroute fährt.

Briefkasteninteraktion und Leseoberfläche

Wenn sich ein Spieler seinem Briefkasten nähert und mit ihm interagiert, fragt der Server alle an ihn adressierten Postgegenstände mit dem Status „zugestellt“ oder „gelesen“ ab. Die NUI zeigt eine Liste im Stil eines Posteingangs mit Absendername, Betreff, Typsymbol und Zeitstempel für jeden Postgegenstand. Ungelesene Briefe werden hervorgehoben oder mit einem Symbol gekennzeichnet. Beim Anklicken eines Briefes wird er in einer Vollbildansicht geöffnet, die wie ein physischer Brief auf Papier gestaltet ist. Oben stehen Absender und Datum, in der Mitte der Text und darunter eine Schaltfläche zum Schließen. Bei Paketen zeigt die Ansicht die angehängte Nachricht und eine Liste der enthaltenen Gegenstände sowie eine Schaltfläche „Paket öffnen“, über die die Gegenstände ins Inventar des Spielers übertragen werden. Markiere Briefe beim Öffnen als gelesen und entferne sie nach einer konfigurierbaren Aufbewahrungsdauer aus dem Briefkasten, um ein übermäßiges Datenbankwachstum zu verhindern. Füge eine Antwortschaltfläche hinzu, die das Empfängerfeld mit dem Namen des ursprünglichen Absenders vorausfüllt und so den Schriftwechsel erleichtert. Gestalte Systembescheide mit einem offiziellen Briefkopf anders, damit sie sich von der Post anderer Spieler unterscheiden.

Systembenachrichtigungen und automatisierte Post

Neben der Kommunikation zwischen Spielern wird das Postsystem zu einem leistungsstarken Werkzeug, mit dem andere Serverskripte immersive Benachrichtigungen versenden können. Statt unauffälliger Toast-Benachrichtigungen können Skripte formelle Post verschicken. Ein Gerichtssystem sendet offizielle Vorladungen auf Briefpapier der Regierung. Ein Unternehmen verschickt Rechnungen und Zahlungserinnerungen. Ein Immobilienverwaltungsskript versendet Mietfälligkeitshinweise und Räumungswarnungen. Ein Fraktionssystem verschickt Rekrutierungsbriefe oder Missionsbriefings. Erstelle einen einfachen serverseitigen Export, den jede Ressource aufrufen kann, um einen System-Postgegenstand in die Warteschlange zu stellen, ohne die internen Abläufe des Postsystems kennen zu müssen. Der Export akzeptiert eine Empfängerkennung, Betreff, Text, Posttyp und optionale Anhänge. So wird dein Postsystem zu einer universellen Benachrichtigungsebene, die in die Spielwelt eingebunden ist, statt zu einem aufdringlichen UI-Overlay, das die Immersion unterbricht.

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

Das passende Script

Fertig für deinen Server, Updates inklusive.

Bereit loszulegen?

Hol dir die Scripts in unserem Shop, oder komm auf den Discord für Support, Updates und einen Blick auf das, was kommt.