Retour au cours

games / fivem

Scripting de missions et heists scénarisés

Leçon 221 exercice

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 heistTransition autorisée vers
IDLEHACKING
HACKINGDRILLING
DRILLINGLOOTING
LOOTINGESCAPE

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.

lua
-- 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
lua
-- É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)
lua
-- 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
end

Ré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

1 disponible
1

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.

Résoudre l’exercice →