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
ensurepour redémarrer ton ressource etrestartpour redémarrer une seule ressource. Vérifierresmonpour 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
pcallpour 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.
