Guide 2026-05-03

Techniques de débogage pour FiveM

TDYSKY

TDYSKY

Fondateur et développeur principal chez Agency Scripts

L'art du débogage des scripts FiveM

Le débogage des scripts FiveM est fondamentalement différent du débogage des applications standard. Tu as affaire à une architecture client-serveur divisée dans laquelle le code s'exécute sur deux environnements d'exécution distincts, les événements voyagent à travers le réseau et l'état du jeu change à chaque image. En cas de problème, l'erreur peut provenir du serveur mais se manifester sur le client, ou vice versa. Développer une approche systématique du débogage tu feras gagner d’innombrables heures et accélérera considérablement ton flux de travail de développement. Ce guide couvre les outils, techniques et modèles essentiels que les développeurs professionnels FiveM utilisent quotidiennement.

Utiliser efficacement les instructions d'impression

L'outil de débogage le plus fondamental de FiveM est le print() fonction, mais son utilisation efficace nécessite plus que simplement vider des variables. Structurez ton sortie de débogage avec des préfixes qui identifient le script, le côté (client ou serveur) et la fonction sur laquelle l'impression a lieu. Utilisez des codes couleurs pour faire ressortir les messages importants dans la console. Créez un utilitaire de débogage que tu peux activer et désactiver sans supprimer les lignes de débogage de ton code.

-- shared/debug.lua
local DEBUG_ENABLED = GetConvar('myresource_debug', 'false') == 'true'
local RESOURCE_NAME = GetCurrentResourceName()

function DebugLog(module, message, ...)
    if not DEBUG_ENABLED then return end

    local side = IsDuplicityVersion() and 'SERVER' else 'CLIENT'
    local formatted = type(message) == 'string' and message:format(...) or tostring(message)
    local timestamp = os.date('%H:%M:%S')

    print(('[^3%s^0][^5%s^0][^2%s^0] %s'):format(
        timestamp, RESOURCE_NAME, side .. ':' .. module, formatted
    ))
end

function DebugTable(module, tbl, depth)
    if not DEBUG_ENABLED then return end
    depth = depth or 0
    local indent = string.rep('  ', depth)

    if type(tbl) ~= 'table' then
        DebugLog(module, '%s%s', indent, tostring(tbl))
        return
    end

    for k, v in pairs(tbl) do
        if type(v) == 'table' then
            DebugLog(module, '%s%s = {', indent, tostring(k))
            DebugTable(module, v, depth + 1)
            DebugLog(module, '%s}', indent)
        else
            DebugLog(module, '%s%s = %s (%s)', indent, tostring(k), tostring(v), type(v))
        end
    end
end

Utilisation de l'utilitaire de débogage

Avec cet utilitaire en place, tu peux ajouter une journalisation de débogage dans tes scripts qui est complètement silencieuse en production. Activez-le en ajoutant set myresource_debug true à la configuration de ton serveur lorsque tu dois dépanner. La sortie structurée permet de parcourir facilement les journaux de la console et de trouver exactement ce que tu recherches.

-- server/jobs.lua
RegisterNetEvent('myresource:startJob', function(jobName)
    local src = source
    DebugLog('jobs', 'Player %d attempting to start job: %s', src, jobName)

    local playerData = GetPlayerData(src)
    DebugTable('jobs', playerData)

    if not playerData then
        DebugLog('jobs', 'ERROR: No player data found for source %d', src)
        return
    end

    if playerData.job == jobName then
        DebugLog('jobs', 'Player %d already has job %s, skipping', src, jobName)
        return
    end

    DebugLog('jobs', 'Job %s assigned to player %d successfully', jobName, src)
end)

Erreurs courantes FiveM et solutions

ERREUR DE SCRIPT : tentative d'indexation d'une valeur nulle

Il s'agit de l'erreur Lua la plus fréquente dans le développement de FiveM. Cela signifie que tu essayes d'accéder à une propriété ou une méthode sur une variable qui est nil. Les causes les plus courantes sont l'accès aux données du lecteur avant leur chargement, le référencement à une fonction de framework qui n'existe pas dans la version que tu utilises ou la lecture d'une valeur de configuration avec une faute de frappe dans la clé. Vérifiez toujours zéro avant d’accéder aux propriétés imbriquées.

-- BAD: Will crash if GetPlayerData returns nil
local name = GetPlayerData(src).charinfo.firstname

-- GOOD: Defensive nil checks
local playerData = GetPlayerData(src)
if not playerData then
    print('[ERROR] Player data is nil for source: ' .. src)
    return
end

local charinfo = playerData.charinfo
if not charinfo then
    print('[ERROR] charinfo missing for source: ' .. src)
    return
end

local name = charinfo.firstname or 'Unknown'

ERREUR DE SCRIPT : tentative d'appel d'une valeur nulle

Cette erreur se produit lorsque tu essayes d'appeler une fonction qui n'existe pas. Dans FiveM, cela se produit généralement lorsque tu appeles une exportation à partir d'une ressource qui n'a pas encore démarré, utilisez une fonction d'infrastructure qui a été renommée dans une mise à jour ou oubliez de charger un fichier partagé dans ton manifeste. Vérifiez ton fxmanifest.lua pour tu assurer que tous les fichiers requis sont répertoriés et dans le bon ordre.

-- Safely calling an export that might not be available
local function SafeExport(resource, exportName, ...)
    local success, result = pcall(function(...)
        return exports[resource][exportName](...)
    end, ...)

    if not success then
        print(('[^1ERROR^0] Failed to call export %s:%s - %s'):format(
            resource, exportName, tostring(result)
        ))
        return nil
    end

    return result
end

-- Usage
local inventory = SafeExport('ox_inventory', 'GetInventory', src)

L'événement n'a pas été enregistré

Lorsque tu déclenches un événement pour lequel aucun script n'a enregistré de gestionnaire, FiveM le supprime silencieusement sans erreur dans la console. Cela peut être exaspérant à déboguer car tout semble correct mais rien ne se passe. Utilisez une fonction d'assistance pour vérifier que les événements sont enregistrés avant de les déclencher, et ajoutez une journalisation autour de tes déclencheurs d'événements pendant le développement.

-- server/debug_events.lua
-- Wrap TriggerClientEvent to log when events fire
local originalTrigger = TriggerClientEvent

if GetConvar('myresource_debug', 'false') == 'true' then
    TriggerClientEvent = function(eventName, target, ...)
        print(('[^3EVENT^0] TriggerClientEvent: %s -> target: %s'):format(
            eventName, tostring(target)
        ))
        return originalTrigger(eventName, target, ...)
    end
end

Débogage NUI avec DevTools

Pour les scripts avec les interfaces NUI, les Chromium DevTools intégrés sont inestimables. Ouvrez-les avec la console F8 en tapant nui_devtools pour accéder à l’inspecteur Chrome complet. Cela tu donne le panneau Éléments pour inspecter la structure DOM, la console pour les erreurs JavaScript, l'onglet Réseau pour le chargement des ressources et le panneau Sources pour définir les points d'arrêt. Pour les problèmes de communication NUI, enregistrez les deux côtés du pont de messages.

// nui/js/debug.js
// Log all incoming NUI messages
window.addEventListener('message', (event) => {
    if (event.data && event.data.action) {
        console.log(
            '%c[NUI Received]%c ' + event.data.action,
            'background: #2dd4bf; color: #000; padding: 2px 6px; border-radius: 3px;',
            'color: #94a3b8;',
            event.data
        );
    }
});

// Wrap fetch to log NUI callbacks
const originalFetch = window.fetch;
window.fetch = function(url, options) {
    const body = options?.body ? JSON.parse(options.body) : null;
    console.log(
        '%c[NUI Callback]%c ' + url,
        'background: #8b5cf6; color: #fff; padding: 2px 6px; border-radius: 3px;',
        'color: #94a3b8;',
        body
    );
    return originalFetch.apply(this, arguments);
};

Profilage avec Resmon et Timing

Au-delà du basique resmon surveillance, tu peux intégrer une instrumentation de synchronisation précise dans tes scripts. Mesurez la durée d’opérations spécifiques et enregistrez les avertissements lorsqu’elles dépassent les seuils acceptables. Ceci est particulièrement important pour les requêtes de base de données, les calculs complexes et les boucles qui traitent de nombreuses entités.

-- shared/profiler.lua
local Profiler = {}

function Profiler.Start(label)
    return {
        label = label,
        startTime = GetGameTimer()
    }
end

function Profiler.Stop(timer, warnThresholdMs)
    local elapsed = GetGameTimer() - timer.startTime
    warnThresholdMs = warnThresholdMs or 5

    if elapsed >= warnThresholdMs then
        print(('[^1PERF WARNING^0] %s took %dms (threshold: %dms)'):format(
            timer.label, elapsed, warnThresholdMs
        ))
    elseif GetConvar('myresource_debug', 'false') == 'true' then
        print(('[^2PERF^0] %s completed in %dms'):format(timer.label, elapsed))
    end

    return elapsed
end

-- Usage in a server event
RegisterNetEvent('myresource:heavyOperation', function(data)
    local timer = Profiler.Start('heavyOperation')

    -- ... expensive processing ...
    local result = ProcessLargeDataSet(data)

    Profiler.Stop(timer, 10) -- warn if over 10ms
end)

Débogage du sac d'état

Les sacs d'État sont une fonctionnalité puissante mais parfois déroutante. Lorsque les valeurs du sac d'état ne sont pas mises à jour comme prévu, c'est généralement parce que tu les définissez sur la mauvaise entité, que le gestionnaire ne détecte pas le nom de sac correct ou que la réplication est retardée. Créez une commande d'inspecteur de sacs d'état qui vide tous les états d'une entité donnée.

-- server/debug_statebags.lua
RegisterCommand('debugstate', function(source, args)
    local targetId = tonumber(args[1])
    if not targetId then
        print('Usage: debugstate [playerId]')
        return
    end

    local playerPed = GetPlayerPed(targetId)
    if playerPed == 0 then
        print('Player not found: ' .. targetId)
        return
    end

    local entityState = Player(targetId).state
    print(('[^3STATE BAGS^0] Player %d:'):format(targetId))

    -- Print known state keys (state bags don't have an iterator)
    local keysToCheck = {'job', 'gang', 'duty', 'dead', 'phone', 'inventory'}
    for _, key in ipairs(keysToCheck) do
        local val = entityState[key]
        if val ~= nil then
            print(('  %s = %s (%s)'):format(key, tostring(val), type(val)))
        end
    end
end, true)

Liste de contrôle de débogage essentielle

  • Vérifiez les deux consoles. Regardez toujours à la fois la console du serveur (txAdmin ou terminal) et la console client (F8) pour détecter les erreurs. Une erreur d’un côté explique souvent un comportement défectueux de l’autre.
  • Vérifiez l’état de la ressource. Utiliser ensure pour redémarrer ton ressource et restart pour redémarrer une seule ressource. Vérifier resmon pour tu assurer que la ressource est réellement en cours d'exécution.
  • Testez avec un environnement propre. Désactivez les autres scripts qui interagissent avec les mêmes systèmes. De nombreux bugs proviennent de conflits entre ressources plutôt que de bugs au sein d’un seul script.
  • Lisez attentivement la trace de la pile d’erreurs. Les traces de pile Lua tu montrent le numéro exact du fichier et de la ligne. Lisez-les de bas en haut pour comprendre la chaîne d’appels qui a conduit à l’erreur.
  • Utilisez pcall pour les opérations risquées. Encapsulez les requêtes de base de données, les appels d'exportation et le décodage JSON dans pcall pour détecter les erreurs avec élégance au lieu de les laisser planter ton script.
  • Versionnez tes configurations. Lorsque les joueurs signalent des bugs, demandez quelle version ils utilisent. De nombreux problèmes proviennent de fichiers de configuration obsolètes après une mise à jour.

Partager cet article

Prêt à améliorer votre serveur ?

Découvrez nos scripts FiveM premium dans la boutique Agency Scripts ou rejoignez notre communauté Discord pour le support et les mises à jour.