Técnicas de depuración en FiveM: encuentra errores rápido en Lua y JS
Depura scripts de FiveM más rápido con registros del servidor, mensajes del cliente, herramientas de txAdmin y flujos de trabajo eficaces.
Agency Scripts
Fundador y desarrollador principal en Agency Scripts
El arte de depurar scripts de FiveM
Depurar scripts de FiveM es fundamentalmente distinto de depurar aplicaciones estándar. Trabajas con una arquitectura cliente-servidor dividida, donde el código se ejecuta en dos entornos distintos, los eventos viajan por la red y el estado del juego cambia en cada fotograma. Cuando algo falla, el error puede originarse en el servidor y manifestarse en el cliente, o viceversa. Desarrollar un método sistemático de depuración te ahorrará incontables horas y agilizará enormemente tu flujo de trabajo de desarrollo. Esta guía cubre las herramientas, técnicas y patrones esenciales que los desarrolladores profesionales de FiveM usan a diario.
Uso eficaz de las instrucciones print
La herramienta de depuración más básica de FiveM es la print() función, pero usarla eficazmente requiere algo más que volcar variables. Estructura la salida de depuración con prefijos que identifiquen el script, el lado (cliente o servidor) y la función desde la que se ejecuta la instrucción print. Usa códigos de color para destacar los mensajes importantes en la consola. Crea una utilidad de depuración que puedas activar y desactivar sin quitar las líneas de depuración del código.
-- 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
Uso de la utilidad de depuración
Con esta utilidad, puedes añadir registros de depuración a tus scripts que permanecerán completamente silenciosos en producción. Actívala añadiendo set myresource_debug true a la configuración de tu servidor cuando necesites solucionar un problema. La salida estructurada facilita buscar en los registros de la consola y encontrar exactamente lo que buscas.
-- 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)
Errores frecuentes de FiveM y soluciones
SCRIPT ERROR: attempt to index a nil value
Este es el error de Lua más frecuente en el desarrollo de FiveM. Significa que intentas acceder a una propiedad o un método de una variable que es nil . Las causas más comunes son acceder a los datos del jugador antes de que se carguen, hacer referencia a una función del framework que no existe en la versión que usas o leer un valor de configuración con un error tipográfico en la clave. Comprueba siempre que el valor no sea nil antes de acceder a propiedades anidadas.
-- 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'
SCRIPT ERROR: attempt to call a nil value
Este error se produce cuando intentas llamar a una función que no existe. En FiveM, suele ocurrir cuando llamas a un export de un recurso que todavía no se ha iniciado, usas una función del framework cuyo nombre cambió en una actualización o te olvidas de cargar un archivo compartido en el manifest. Revisa tu fxmanifest.lua para asegurarte de que todos los archivos necesarios estén incluidos y en el orden correcto.
-- 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)
El evento no está registrado
Cuando activas un evento para el que ningún script ha registrado un controlador, FiveM lo descarta silenciosamente y no muestra ningún error en la consola. Depurarlo puede ser desesperante porque todo parece estar bien, pero no ocurre nada. Usa una función auxiliar para comprobar que los eventos estén registrados antes de activarlos y añade registros alrededor de las activaciones de eventos durante el desarrollo.
-- 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
Depuración de NUI con DevTools
Para los scripts con interfaces NUI, las DevTools integradas de Chromium son indispensables. Ábrelas desde la consola F8 escribiendo nui_devtools para acceder al inspector completo de Chrome. Tendrás el panel Elements para inspeccionar la estructura del DOM, Console para los errores de JavaScript, Network para la carga de recursos y Sources para establecer puntos de interrupción. Si tienes problemas con la comunicación NUI, registra ambos lados del puente de mensajes.
// 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);
};
Análisis de rendimiento con Resmon y medición de tiempos
Además de la supervisión básica de resmon puedes añadir a tus scripts una instrumentación precisa para medir tiempos. Mide cuánto tardan operaciones específicas y registra advertencias cuando superen los límites aceptables. Esto es especialmente importante para las consultas a la base de datos, los cálculos complejos y los bucles que procesan muchas entidades.
-- 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)
Depuración de State Bag
Los state bags son una función potente, pero a veces confusa. Cuando los valores de los state bags no se actualizan como esperas, normalmente se debe a que los estás asignando a la entidad equivocada, el handler no detecta el nombre de bag correcto o la replicación se retrasa. Crea un comando de inspección de state bags que muestre todos los datos de estado de una entidad determinada.
-- 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)
Lista esencial de depuración
- Revisa ambas consolas. Consulta siempre tanto la consola del servidor (txAdmin o terminal) como la consola del cliente (F8) para detectar errores. Un error en un lado suele explicar por qué algo no funciona en el otro.
- Verifica el estado del resource. Usa
ensurepara reiniciar tu resource yrestartpara reiniciar un solo resource. Compruebaresmonpara asegurarte de que el resource se esté ejecutando realmente. - Prueba en un entorno limpio. Desactiva otros scripts que interactúen con los mismos sistemas. Muchos errores se deben a conflictos entre resources, no a fallos de un solo script.
- Lee atentamente el stack trace del error. Los stack traces de Lua muestran el archivo y el número de línea exactos. Léelos de abajo hacia arriba para entender la cadena de llamadas que provocó el error.
- Usa pcall para las operaciones de riesgo. Envuelve las consultas a la base de datos, las llamadas a exports y la decodificación JSON en
pcallpara capturar los errores de forma controlada en lugar de dejar que hagan fallar tu script. - Versiona tus configuraciones. Cuando los jugadores reporten errores, pregúntales qué versión están usando. Muchos problemas se deben a archivos de configuración desactualizados tras una actualización.