Licences comme infrastructure de jeu de rôle
Un système de licences et de permis constitue une infrastructure fondamentale pour tout serveur de jeu de rôle sérieux. Sans cela, chaque joueur peut tout faire dès le premier jour. Les licences contrôlent les activités selon des exigences réalistes, créant ainsi une progression, des conséquences et des interactions de jeu de rôle. Un permis de conduire signifie que les joueurs doivent passer un test avant de conduire légalement un véhicule. Un permis d’armes nécessite une vérification des antécédents et une formation. Un permis de chasse restreint qui peut chasser et où. Une licence commerciale contrôle qui peut exploiter des entreprises commerciales. Un permis de pêche, une licence de pilote, une licence médicale, une licence juridique – chacun crée un niveau de réglementation qui reflète les systèmes du monde réel et génère des interactions organiques entre les citoyens, les fonctionnaires et les forces de l'ordre. Lorsqu'un policier arrête quelqu'un et vérifie son statut de permis, c'est un moment de jeu de rôle qui n'existe que parce que le système de permis le permet. Ce didacticiel explique comment créer un cadre de licence flexible et extensible qui prend en charge tout type de permis dont ton serveur a besoin.
Configuration flexible du type de licence
Plutôt que de coder en dur des types de licences spécifiques, créez un système basé sur la configuration dans lequel l'ajout d'un nouveau type de licence ne nécessite qu'une nouvelle entrée dans une table de configuration. Chaque définition de type de licence comprend un identifiant unique, un nom d'affichage, une description, le coût de candidature, si elle nécessite un examen, les questions de l'examen le cas échéant, la durée de validité avant le renouvellement nécessaire, les emplois ou les rôles qui peuvent la délivrer et les restrictions de jeu qui s'appliquent lorsque la licence est manquante. Cette approche signifie que ton permis de chasse, ton permis de conduire, ton permis d'armes et ton permis commercial fonctionnent tous sur le même moteur sous-jacent. Lorsque ton serveur grandit et a besoin d'un nouveau type de permis comme un médaillon de taxi ou un permis de construire, tu ajoutes une entrée de configuration et l'ensemble du pipeline d'application, de délivrance et d'application fonctionne automatiquement sans écrire de nouveau code.
-- 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'},
},
}
Flux de candidature et d’examen
Le processus de candidature doit ressembler à une véritable interaction gouvernementale. Les joueurs visitent un emplacement DMV, un bureau de poste ou un bâtiment gouvernemental concerné et interagissent avec un PNJ ou un kiosque pour commencer l'application. Le NUI présente les types de licences disponibles, leurs coûts et leurs exigences. Après avoir sélectionné une licence et payé les frais, le système vérifie les conditions préalables. Si la licence nécessite un examen écrit, le joueur entre dans une interface de quiz avec des questions à choix multiples tirées d'un pool de questions configurable. Randomisez l’ordre des questions et les positions des réponses pour éviter la tricherie lors de la mémorisation. Exiger une note de passage minimale, généralement de 70 à 80 pour cent. Pour les examens pratiques comme les examens de conduite, faites apparaître un véhicule et créez un itinéraire de point de contrôle que le joueur doit terminer dans un délai imparti tout en respectant le code de la route. Un PNJ instructeur d'auto-école ou un vrai joueur travaillant comme DMV peut surveiller le test et faire échouer les étudiants qui grillent des feux rouges, accélèrent excessivement ou s'écrasent. Stockez le résultat de l'examen et, en cas de réussite, délivrez la licence avec sa période de validité commençant à l'horodatage actuel.
-- 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)
Intégration des forces de l'ordre
Le véritable pouvoir d'un système de licence apparaît lorsque les forces de l'ordre peuvent interroger et modifier les statuts des licences lors d'interactions de jeu de rôle. Les policiers doivent pouvoir vérifier le statut du permis d'un citoyen lors des contrôles routiers, vérifier les permis d'armes lors des fouilles et suspendre ou révoquer les permis en cas de comportement criminel. Créez une intégration de police MDT qui affiche toutes les licences détenues par un citoyen ainsi que leur statut, leur date d'émission et leur date d'expiration. Ajoutez des commandes ou des actions MDT pour suspendre une licence avec un motif et une durée, la révoquer définitivement et la rétablir après une période de suspension. Lorsqu'une licence est suspendue, le titulaire doit recevoir une notification expliquant le motif et la durée de la suspension. Intégrez les contrôles de licence dans les flux de travail d’application existants. Si un joueur est surpris en train de conduire sans permis valide, le système de police peut le signaler automatiquement lors d'un contrôle routier. Si quelqu'un est surpris en train de chasser sans permis, les gardes forestiers peuvent émettre des citations entraînant des amendes et une révocation potentielle du permis.
-- 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)
Système de renouvellement et gestion des expirations
Les licences avec des dates d'expiration génèrent des revenus récurrents et des interactions régulières avec les PNJ qui permettent au monde de se sentir vivant. Lorsqu'une licence approche de sa date d'expiration, envoyez au titulaire une notification via le système téléphonique ou le système de messagerie couvert dans un didacticiel séparé. Accordez un délai de grâce de quelques jours dans le jeu lorsque la licence est techniquement expirée mais que le joueur n'est pas immédiatement pénalisé, ce qui laisse le temps de renouveler. Le renouvellement devrait être plus simple que la demande initiale, ne nécessitant que le paiement des frais et aucun réexamen à moins que la licence n'ait été précédemment suspendue ou révoquée. Implémentez une tâche planifiée côté serveur qui s'exécute périodiquement pour vérifier les licences expirées et mettre à jour leur statut. Pour les licences qui nécessitent un renouvellement périodique, comme les permis de pêche avec de courtes périodes de validité, envisagez de proposer des options de renouvellement groupé où les joueurs peuvent payer d'avance pour plusieurs périodes avec une légère réduction. Cela récompense les acteurs dévoués qui planifient à l’avance tout en maintenant le cycle de renouvellement qui permet au système de rester dynamique.
Application des restrictions dans les scripts
Le système de licence n'a d'importance que si d'autres scripts appliquent réellement les restrictions. Créez une fonction d'exportation centralisée que n'importe quel script peut appeler pour vérifier si un joueur détient une licence valide d'un type donné. Le script de ton véhicule vérifie un permis de conduire avant d'autoriser le démarrage du moteur ou lorsqu'un joueur prend place sur le siège du conducteur. Ton script d'armes vérifie un permis d'armes lorsqu'un joueur équipe une arme à feu. Ton script de chasse vérifie un permis de chasse avant d'autoriser la récolte d'animaux. Ton script de pêche vérifie un permis de pêche avant d'autoriser les captures. Chaque script décide de ses propres conséquences en cas de violations. Certains pourraient simplement empêcher l’action avec une notification. D'autres pourraient autoriser l'action mais signaler le joueur à l'attention des forces de l'ordre, déclenchant un niveau de recherche ou une alerte automatique. La rigueur dépend de la philosophie de jeu de rôle de ton serveur. Le système de licence fournit la couche de données ; les scripts d’application décident des conséquences. Cette séparation signifie que tu peux ajuster la rigueur de l'application sans toucher au système de licence lui-même.
Schéma de base de données et outils d'administration
Le schéma de base de données a besoin de tables pour les définitions de licences déjà dans la configuration, de licences actives, d'applications, de résultats d'examen et d'un journal d'audit. Le journal d’audit est essentiel à la responsabilisation. Chaque délivrance, suspension, révocation et rétablissement de permis est enregistré avec un horodatage, l'identifiant de l'agent exécutant et le motif. Les commandes d'administration doivent permettre au personnel de délivrer n'importe quelle licence directement à des fins d'événement, d'effacer toutes les suspensions lors d'événements d'amnistie, d'expirer en masse les licences d'un type spécifique pour les réinitialisations du système et d'afficher un historique complet des licences pour n'importe quel joueur. Créez un panneau d'administration dans le tableau de bord Web de ton serveur ou MDT qui fournit des statistiques en un coup d'œil sur les licences actives par type, les applications en attente, les suspensions récentes et les expirations à venir. Ces données tu aident à comprendre comment les joueurs interagissent avec le système et si les frais et les périodes de validité doivent être ajustés pour maintenir un engagement sain.
-- 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)
);
