games / fivem
Scripting de missions et heists scénarisés
Explication
Ce que vous allez apprendre
- Modéliser une séquence de mission avec une machine à états côté serveur
- Refuser explicitement toute transition d'état non prévue
- Faire valider la réussite d'un minijeu par le serveur, jamais par le client seul
- Détecter un minijeu résolu en un temps physiquement impossible
- Persister un cooldown en base de données pour empêcher un abus après redémarrage
Dans quel contexte ?
Un serveur RP propose un braquage de banque scénarisé en plusieurs étapes (piratage, perçage, pillage, fuite), l'un des contenus les plus populaires auprès des joueurs — mais aussi l'un des plus visés par la triche, car il enchaîne plusieurs des concepts déjà vus (sécurité, économie, persistance) dans une séquence prolongée où chaque étape est une occasion potentielle d'exploit.
| État du heist | Transition autorisée vers |
|---|---|
IDLE | HACKING |
HACKING | DRILLING |
DRILLING | LOOTING |
LOOTING | ESCAPE |
Piège dangereux
Faire confiance au client pour déclarer directement la réussite d'une étape (par exemple "j'ai réussi le piratage") sans que le serveur ne valide un minimum de plausibilité (temps écoulé, séquence respectée) permet à un joueur de sauter des étapes entières ou de résoudre un minijeu instantanément via un client modifié.
Assembler les briques précédentes en un vrai scénario
Un braquage ("heist") scénarisé est l'un des contenus les plus attendus sur un serveur roleplay, et aussi l'un des plus complexes à sécuriser correctement, car il enchaîne plusieurs des concepts déjà vus (sécurité, économie, persistance) dans une seule séquence prolongée avec de multiples étapes successives.
Modéliser une séquence avec une machine à états
Un heist n'est pas une action isolée mais une suite ordonnée d'étapes (pirater un système, percer un coffre, récupérer le butin, s'échapper). Représenter cela comme une "machine à états" signifie qu'à chaque instant, le déroulement se trouve dans exactement un état précis, et que seules certaines transitions vers l'état suivant sont autorisées. Cela empêche structurellement un joueur de "sauter" une étape (par exemple valider directement le pillage sans être passé par le piratage), puisque toute transition non explicitement prévue est refusée par construction.
Pourquoi le serveur doit rester seul décisionnaire
Même si un minijeu de piratage se joue visuellement côté client, la RÉUSSITE de ce minijeu ne doit jamais être décidée par le client seul — exactement le même principe que dans la leçon Sécurité. Le serveur reste l'arbitre final : il peut par exemple détecter qu'un minijeu a été "réussi" en un temps physiquement impossible, un signal fort de triche à traiter en conséquence.
Des conséquences qui doivent survivre au serveur
Un cooldown entre deux tentatives d'un même heist, gardé uniquement en mémoire serveur, disparaîtrait au moindre redémarrage — permettant à un joueur de recommencer immédiatement en profitant de cette réinitialisation accidentelle. Persister ce cooldown en base de données garantit qu'il reste valable quoi qu'il arrive au processus du serveur lui-même.
Commandes & code
Scripting de missions et heists scénarisés
Un heist scénarisé est une machine à états avec des étapes séquentielles, des points de non-retour et une validation serveur à chaque transition.
-- Machine à états serveur pour un heist multi-étapes (source de vérité, le client ne fait qu'afficher)
local HeistStates = { IDLE = 'idle', HACKING = 'hacking', DRILLING = 'drilling', LOOTING = 'looting', ESCAPE = 'escape' }
local ActiveHeist = {
state = HeistStates.IDLE,
crew = {}, -- liste des sources des joueurs participants
startedAt = nil,
vaultLoot = 0,
}
local function transitionTo(newState)
local validTransitions = {
[HeistStates.IDLE] = { HeistStates.HACKING },
[HeistStates.HACKING] = { HeistStates.DRILLING },
[HeistStates.DRILLING] = { HeistStates.LOOTING },
[HeistStates.LOOTING] = { HeistStates.ESCAPE },
[HeistStates.ESCAPE] = { HeistStates.IDLE },
}
-- refuse toute transition non prévue explicitement : empêche un client de sauter une étape (skip du hack, ex.)
local allowed = validTransitions[ActiveHeist.state] or {}
if not TableContains(allowed, newState) then
print(('[Heist] Transition invalide refusée: %s -> %s'):format(ActiveHeist.state, newState))
return false
end
ActiveHeist.state = newState
TriggerClientEvent('technologik:heist:stateChanged', -1, newState)
return true
end-- Étape de minijeu (hack) : le client joue le minijeu, mais le SERVEUR décide seul de la réussite finale
RegisterNetEvent('technologik:heist:hackAttempt', function(success, timeTaken)
local src = source
if ActiveHeist.state ~= HeistStates.HACKING then return end -- refuse un event hors séquence
if not IsPlayerInCrew(src, ActiveHeist.crew) then return end
-- Validation anti-triche : un hack ne peut pas être résolu en moins d'un temps physiquement plausible
if timeTaken < 3000 then
LogSuspiciousActivity(src, ('hack résolu en %dms, suspect'):format(timeTaken))
DropPlayer(src, 'Comportement anormal détecté')
return
end
if success then
transitionTo(HeistStates.DRILLING)
else
ActiveHeist.failedAttempts = (ActiveHeist.failedAttempts or 0) + 1
if ActiveHeist.failedAttempts >= 3 then
TriggerServerEvent('technologik:heist:triggerAlarm') -- échec répété = conséquence scénarisée
end
end
end)-- Points de non-retour et conséquences persistantes (cooldown, alerte police) attachés à la machine à états
local function onHeistCompleted()
ActiveHeist.state = HeistStates.IDLE
local lootPerPlayer = math.floor(ActiveHeist.vaultLoot / #ActiveHeist.crew)
for _, src in ipairs(ActiveHeist.crew) do
AddPlayerBalanceAtomic(src, lootPerPlayer) -- réutilise la transaction atomique (voir leçon économie)
end
-- Cooldown persisté en DB pour empêcher un repeat immédiat (farming du heist)
MySQL.insert('INSERT INTO heist_cooldowns (heist_id, completed_at) VALUES (?, NOW())', { 'fleeca_bank' })
ActiveHeist.crew = {}
ActiveHeist.vaultLoot = 0
end
local function canStartHeist()
local lastCompletion = MySQL.scalar.await(
'SELECT completed_at FROM heist_cooldowns WHERE heist_id = ? ORDER BY completed_at DESC LIMIT 1',
{ 'fleeca_bank' }
)
if not lastCompletion then return true end
local elapsedMinutes = (os.time() - lastCompletion) / 60
return elapsedMinutes >= 120 -- cooldown de 2h avant de pouvoir relancer ce heist
endRésumé
- Un heist scénarisé se modélise comme une machine à états dont le serveur est la SEULE source de vérité.
- Chaque transition d'état doit être explicitement whitelistée pour empêcher un client de sauter une étape.
- Un minijeu résolu anormalement vite (hack en quelques millisecondes) est un signal de triche à traiter comme tel.
- Le cooldown entre deux tentatives d'un même heist doit être persisté en DB, jamais uniquement en mémoire serveur.
Exercices pratiques
Mission : empêcher de sauter les étapes du braquage
Objectif : Comprendre pourquoi une machine à états protège contre le skip d'étapes, étendre cette machine, et raisonner sur la persistance de l'état en cours.
Contexte
Un joueur curieux modifie son client pour envoyer directement l'event correspondant à l'étape LOOTING du braquage de la banque Fleeca, sans jamais avoir déclenché HACKING ni DRILLING au préalable. Le braquage reste bloqué à l'état IDLE côté serveur : rien ne se passe, et l'équipe veut comprendre pourquoi cette tentative échoue, avant d'étendre le système avec un nouvel état d'alarme.