Volver al blog
Guide8 min de lectura

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 ensure para reiniciar tu resource y restart para reiniciar un solo resource. Comprueba resmon para 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 pcall para 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.

¿Listo para empezar?

Consigue los scripts en nuestra tienda, o únete al Discord para soporte, novedades y un adelanto de lo que viene.