Zurück zum Blog
Tutorial9 Min. Lesezeit

FiveM-Jagdlizenzsystem: Genehmigungen, Strafen und Zonen

Füge FiveM ein funktionierendes Jagdlizenzsystem hinzu. Genehmigungen, Schutzzonen, Strafen, Ranger-Berufe und Scripts für realistische Jagdgesetze.

Agency Scripts

Gründer und leitender Entwickler bei Agency Scripts

Lizenzen als Rollenspiel-Infrastruktur

Ein Lizenz- und Genehmigungssystem ist eine grundlegende Infrastruktur für jeden ernst zu nehmenden Rollenspielserver. Ohne ein solches System kann jeder Spieler vom ersten Tag an alles tun. Lizenzen knüpfen Aktivitäten an realistische Voraussetzungen und schaffen so Fortschritt, Konsequenzen und Rollenspielinteraktionen. Mit einem Führerschein müssen Spieler eine Prüfung bestehen, bevor sie legal Fahrzeuge fahren dürfen. Für einen Waffenschein sind Hintergrundüberprüfungen und eine Ausbildung erforderlich. Ein Jagdschein legt fest, wer wo jagen darf. Eine Gewerbeerlaubnis regelt, wer ein Unternehmen betreiben darf. Eine Angelerlaubnis, ein Pilotenschein, eine medizinische Zulassung, eine Anwaltszulassung, jede davon schafft eine Regulierungsebene, die reale Systeme widerspiegelt und natürliche Interaktionen zwischen Bürgern, Regierungsmitarbeitern und Strafverfolgungsbehörden ermöglicht. Wenn ein Polizist jemanden anhält und den Lizenzstatus überprüft, entsteht ein Rollenspielmoment, der nur durch das Lizenzsystem möglich wird. In diesem Tutorial erfährst du, wie du ein flexibles, erweiterbares Lizenzsystem aufbaust, das alle Genehmigungsarten unterstützt, die dein Server benötigt.

Flexible Konfiguration von Lizenztypen

Anstatt bestimmte Lizenztypen fest einzubauen, entwickelst du ein konfigurationsgesteuertes System, bei dem du einen neuen Lizenztyp einfach als Eintrag in einer Config-Tabelle hinzufügst. Die Definition jedes Lizenztyps enthält eine eindeutige Kennung, einen Anzeigenamen, eine Beschreibung, die Antragsgebühr, Angaben dazu, ob eine Prüfung erforderlich ist, gegebenenfalls die Prüfungsfragen, die Gültigkeitsdauer bis zur Verlängerung sowie die Jobs oder Rollen, die ihn ausstellen dürfen, und die Einschränkungen im Spiel, die gelten, wenn die Lizenz fehlt. So laufen dein Jagdschein, Führerschein, Waffenschein und deine Gewerbeerlaubnis alle über dieselbe zugrunde liegende Engine. Wenn dein Server wächst und du einen neuen Genehmigungstyp wie eine Taxikonzession oder Baugenehmigung brauchst, fügst du einen Config-Eintrag hinzu. Der gesamte Ablauf für Antragstellung, Ausstellung und Durchsetzung funktioniert dann automatisch, ohne dass du neuen Code schreiben musst.

-- shared/config.lua
Config = {}

Config.LicenseTypes = {
    driving = {
        label       = 'Driving License',
        description = 'Required to legally operate motor vehicles',
        cost        = 500,
        requiresExam = true,
        examType    = 'practical',
        validDays   = 365,
        issuedBy    = {'dmv', 'police'},
        restrictions = {'vehicle_operation'},
    },
    weapons = {
        label       = 'Weapons Permit',
        description = 'Required to legally carry firearms',
        cost        = 2500,
        requiresExam = true,
        examType    = 'written',
        validDays   = 180,
        issuedBy    = {'police'},
        restrictions = {'weapon_carry'},
        prerequisites = {'driving'},
    },
    hunting = {
        label       = 'Hunting License',
        description = 'Required for legal hunting activities',
        cost        = 750,
        requiresExam = true,
        examType    = 'written',
        validDays   = 90,
        issuedBy    = {'ranger', 'dmv'},
        restrictions = {'hunting_activity'},
    },
    fishing = {
        label       = 'Fishing Permit',
        description = 'Required for legal fishing activities',
        cost        = 200,
        requiresExam = false,
        validDays   = 30,
        issuedBy    = {'dmv', 'ranger'},
        restrictions = {'fishing_activity'},
    },
    business = {
        label       = 'Business License',
        description = 'Required to operate a commercial business',
        cost        = 5000,
        requiresExam = false,
        validDays   = 365,
        issuedBy    = {'government'},
        restrictions = {'business_operation'},
    },
    pilot = {
        label       = 'Pilot License',
        description = 'Required to operate aircraft',
        cost        = 10000,
        requiresExam = true,
        examType    = 'practical',
        validDays   = 180,
        issuedBy    = {'faa'},
        restrictions = {'aircraft_operation'},
        prerequisites = {'driving'},
    },
}

Antrags- und Prüfungsablauf

Der Antragsprozess sollte sich wie eine echte Interaktion mit einer Behörde anfühlen. Spieler besuchen eine Kfz-Zulassungsstelle, ein Postamt oder ein passendes Regierungsgebäude und interagieren mit einem NPC oder Kiosk, um den Antrag zu starten. Die NUI zeigt die verfügbaren Lizenztypen, ihre Kosten und Voraussetzungen an. Nachdem ein Spieler eine Lizenz ausgewählt und die Gebühr bezahlt hat, prüft das System die Voraussetzungen. Wenn für die Lizenz eine schriftliche Prüfung erforderlich ist, wird ein Quiz mit Multiple-Choice-Fragen aus einem konfigurierbaren Fragenpool angezeigt. Mische die Reihenfolge der Fragen und Antwortmöglichkeiten, damit Spieler nicht durch Auswendiglernen schummeln können. Lege eine Mindestpunktzahl zum Bestehen fest, üblicherweise 70 bis 80 Prozent. Bei praktischen Prüfungen wie einer Fahrprüfung spawnst du ein Fahrzeug und erstellst eine Kontrollpunkt-Route, die der Spieler innerhalb eines Zeitlimits und unter Einhaltung der Verkehrsregeln absolvieren muss. Ein Fahrlehrer-NPC oder ein echter Spieler mit einem DMV-Job kann die Prüfung beaufsichtigen und Schüler durchfallen lassen, wenn sie rote Ampeln überfahren, stark zu schnell fahren oder Unfälle bauen. Speichere das Prüfungsergebnis und stelle bei bestandener Prüfung die Lizenz mit einer Gültigkeitsdauer aus, die ab dem aktuellen Zeitstempel läuft.

-- server/licenses.lua
function ApplyForLicense(playerId, licenseType)
    local config = Config.LicenseTypes[licenseType]
    if not config then return false, 'Invalid license type' end

    local identifier = GetPlayerIdentifier(playerId, 0)

    -- Check prerequisites
    if config.prerequisites then
        for _, prereq in ipairs(config.prerequisites) do
            local has = HasValidLicense(identifier, prereq)
            if not has then
                return false, 'Missing prerequisite: ' ..
                    Config.LicenseTypes[prereq].label
            end
        end
    end

    -- Check if already has active license
    local existing = MySQL.single.await([[
        SELECT id, status, expires_at FROM licenses
        WHERE identifier = ? AND license_type = ?
        AND status IN ('active','suspended')
    ]], {identifier, licenseType})

    if existing and existing.status == 'active' then
        return false, 'You already hold this license'
    end

    -- Deduct application fee
    local paid = exports['framework']:RemoveMoney(
        playerId, config.cost, 'bank')
    if not paid then
        return false, 'Insufficient funds ($' .. config.cost .. ' required)'
    end

    -- Create pending application
    local appId = MySQL.insert.await([[
        INSERT INTO license_applications
            (identifier, license_type, status, applied_at)
        VALUES (?, ?, 'pending', NOW())
    ]], {identifier, licenseType})

    if config.requiresExam then
        return true, 'Application submitted. Please complete the exam.', appId
    else
        -- No exam required, issue directly
        IssueLicense(identifier, licenseType)
        return true, 'License issued successfully!'
    end
end

function IssueLicense(identifier, licenseType)
    local config = Config.LicenseTypes[licenseType]
    local expiresAt = os.time() + (config.validDays * 86400)

    MySQL.insert([[
        INSERT INTO licenses
            (identifier, license_type, status, issued_at, expires_at)
        VALUES (?, ?, 'active', NOW(), FROM_UNIXTIME(?))
        ON DUPLICATE KEY UPDATE
            status = 'active', issued_at = NOW(),
            expires_at = FROM_UNIXTIME(?)
    ]], {identifier, licenseType, expiresAt, expiresAt})
end

function HasValidLicense(identifier, licenseType)
    local result = MySQL.single.await([[
        SELECT id FROM licenses
        WHERE identifier = ? AND license_type = ?
        AND status = 'active' AND expires_at > NOW()
    ]], {identifier, licenseType})
    return result ~= nil
end

exports('HasValidLicense', HasValidLicense)

Integration in die Strafverfolgung

Die wahre Stärke eines Lizenzsystems zeigt sich, wenn die Strafverfolgungsbehörden während Rollenspielinteraktionen Lizenzstatus abfragen und ändern können. Polizisten müssen bei Verkehrskontrollen den Lizenzstatus eines Bürgers überprüfen, bei Durchsuchungen Waffenscheine kontrollieren und Lizenzen als Konsequenz für kriminelles Verhalten aussetzen oder entziehen können. Baue eine Integration in das Polizei-MDT ein, die alle Lizenzen eines Bürgers samt Status, Ausstellungsdatum und Ablaufdatum anzeigt. Füge Befehle oder MDT-Aktionen hinzu, um eine Lizenz mit Begründung und Dauer auszusetzen, dauerhaft zu entziehen und nach einer Aussetzung wiederherzustellen. Bei einer Aussetzung sollte der Inhaber eine Benachrichtigung mit Begründung und Dauer erhalten. Integriere Lizenzprüfungen in bestehende Abläufe der Strafverfolgung. Wird ein Spieler ohne gültigen Führerschein beim Fahren erwischt, kann das Polizeisystem dies bei einer Verkehrskontrolle automatisch kennzeichnen. Wird jemand ohne Genehmigung beim Jagen erwischt, können Ranger ein Bußgeld verhängen und gegebenenfalls die Lizenz entziehen.

-- server/enforcement.lua
RegisterNetEvent('licenses:checkCitizen', function(targetId)
    local src = source
    -- Verify requesting player is law enforcement
    local job = exports['framework']:GetPlayerJob(src)
    if job ~= 'police' and job ~= 'ranger' and job ~= 'sheriff' then
        return
    end

    local targetIdentifier = GetPlayerIdentifier(targetId, 0)
    local licenses = MySQL.query.await([[
        SELECT license_type, status, issued_at, expires_at,
               suspended_reason, suspended_until
        FROM licenses WHERE identifier = ?
    ]], {targetIdentifier})

    TriggerClientEvent('licenses:showResults', src, licenses)
end)

RegisterNetEvent('licenses:suspend', function(targetIdentifier, licType, reason, days)
    local src = source
    local job = exports['framework']:GetPlayerJob(src)
    if job ~= 'police' and job ~= 'judge' then return end

    local suspendUntil = os.time() + (days * 86400)

    MySQL.update([[
        UPDATE licenses SET
            status = 'suspended',
            suspended_reason = ?,
            suspended_until = FROM_UNIXTIME(?)
        WHERE identifier = ? AND license_type = ?
    ]], {reason, suspendUntil, targetIdentifier, licType})

    -- Notify the affected player if online
    local targetPlayer = GetPlayerFromIdentifier(targetIdentifier)
    if targetPlayer then
        TriggerClientEvent('licenses:notify', targetPlayer,
            'Your ' .. Config.LicenseTypes[licType].label ..
            ' has been suspended for ' .. days .. ' days. Reason: ' .. reason)
    end

    -- Log the action
    MySQL.insert([[
        INSERT INTO license_logs
            (identifier, license_type, action, performed_by, reason)
        VALUES (?, ?, 'suspend', ?, ?)
    ]], {targetIdentifier, licType, GetPlayerIdentifier(src, 0), reason})
end)

Verlängerung und Umgang mit Ablaufdaten

Lizenzen mit Ablaufdatum sorgen für wiederkehrende Einnahmen und regelmäßige Interaktionen mit NPCs, durch die sich die Welt lebendig anfühlt. Wenn sich das Ablaufdatum einer Lizenz nähert, benachrichtigst du den Inhaber über das Telefonsystem oder das Postsystem aus einem separaten Tutorial. Gewähre eine Kulanzfrist von einigen Ingame-Tagen, in der die Lizenz zwar technisch abgelaufen ist, der Spieler aber nicht sofort bestraft wird und Zeit zur Verlängerung hat. Die Verlängerung sollte einfacher sein als die Erstanmeldung und nur die Gebühr erfordern, keine erneute Prüfung, es sei denn, die Lizenz wurde zuvor ausgesetzt oder entzogen. Implementiere einen geplanten serverseitigen Task, der regelmäßig abgelaufene Lizenzen prüft und ihren Status aktualisiert. Bei Lizenzen, die regelmäßig verlängert werden müssen, wie Angelerlaubnissen mit kurzen Laufzeiten, kannst du eine Verlängerung für mehrere Zeiträume auf einmal anbieten, mit einem kleinen Rabatt. So belohnst du engagierte Spieler, die vorausplanen, und hältst gleichzeitig den Verlängerungszyklus aufrecht, durch den sich das System dynamisch anfühlt.

Durchsetzung von Einschränkungen in anderen Skripten

Das Lizenzsystem ist nur dann relevant, wenn andere Skripte die Einschränkungen tatsächlich durchsetzen. Erstelle eine zentrale Export-Funktion, mit der jedes Skript prüfen kann, ob ein Spieler eine gültige Lizenz eines bestimmten Typs besitzt. Dein Fahrzeugsystem prüft vor dem Starten des Motors oder beim Einsteigen auf den Fahrersitz, ob ein Führerschein vorliegt. Dein Waffenskript prüft beim Ausrüsten einer Schusswaffe, ob ein Waffenschein vorliegt. Dein Jagdskript prüft vor dem Zerlegen eines Tieres, ob ein Jagdschein vorhanden ist. Dein Angelskript prüft vor dem Angeln, ob eine Angelerlaubnis vorliegt. Jedes Skript legt selbst fest, welche Konsequenz ein Verstoß hat. Manche verhindern die Aktion einfach mit einer Benachrichtigung. Andere erlauben die Aktion, markieren den Spieler aber für die Strafverfolgung und lösen automatisch einen Fahndungsstatus oder Alarm aus. Wie streng du vorgehst, hängt von der Rollenspiel-Philosophie deines Servers ab. Das Lizenzsystem stellt die Daten bereit, die Durchsetzungsskripte legen die Konsequenzen fest. Durch diese Trennung kannst du die Durchsetzung anpassen, ohne das Lizenzsystem selbst anzufassen.

Datenbankschema und Admin-Tools

Das Datenbankschema benötigt Tabellen für aktive Lizenzen, Anträge, Prüfungsergebnisse und ein Audit-Log. Lizenzdefinitionen sind bereits in der Config enthalten. Das Audit-Log ist entscheidend für die Nachvollziehbarkeit. Jede Ausstellung, Aussetzung, jeder Entzug und jede Wiederherstellung einer Lizenz wird mit Zeitstempel, Kennung des handelnden Beamten und Begründung aufgezeichnet. Admin-Befehle sollten dem Team ermöglichen, zu Event-Zwecken jede Lizenz direkt auszustellen, bei Amnestie-Events alle Aussetzungen aufzuheben, für Systemneustarts Lizenzen eines bestimmten Typs gesammelt ablaufen zu lassen und die vollständige Lizenzhistorie eines Spielers einzusehen. Erstelle ein Admin-Panel im Web-Dashboard oder MDT deines Servers, das auf einen Blick Statistiken zu aktiven Lizenzen nach Typ, ausstehenden Anträgen, kürzlichen Aussetzungen und bevorstehenden Ablaufdaten anzeigt. Anhand dieser Daten erkennst du, wie Spieler mit dem System interagieren und ob Gebühren oder Gültigkeitsdauern angepasst werden müssen, damit das Engagement stabil bleibt.

-- SQL schema
CREATE TABLE IF NOT EXISTS licenses (
    id                INT AUTO_INCREMENT PRIMARY KEY,
    identifier        VARCHAR(64) NOT NULL,
    license_type      VARCHAR(32) NOT NULL,
    status            ENUM('active','suspended','revoked','expired')
                      DEFAULT 'active',
    issued_at         TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at        TIMESTAMP NULL,
    suspended_reason  VARCHAR(255) DEFAULT NULL,
    suspended_until   TIMESTAMP NULL,
    UNIQUE KEY unique_license (identifier, license_type),
    INDEX idx_status (status),
    INDEX idx_expires (expires_at)
);

CREATE TABLE IF NOT EXISTS license_applications (
    id            INT AUTO_INCREMENT PRIMARY KEY,
    identifier    VARCHAR(64) NOT NULL,
    license_type  VARCHAR(32) NOT NULL,
    status        ENUM('pending','passed','failed','cancelled')
                  DEFAULT 'pending',
    exam_score    INT DEFAULT NULL,
    applied_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    completed_at  TIMESTAMP NULL
);

CREATE TABLE IF NOT EXISTS license_logs (
    id            INT AUTO_INCREMENT PRIMARY KEY,
    identifier    VARCHAR(64) NOT NULL,
    license_type  VARCHAR(32) NOT NULL,
    action        VARCHAR(32) NOT NULL,
    performed_by  VARCHAR(64),
    reason        VARCHAR(255),
    created_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_identifier (identifier)
);

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.