Назад к блогу
Guide7 мин чтения

FiveM Техники отладки: быстро находите баги в Lua и JS

Отлаживайте скрипты FiveM быстрее. Логи сервера, вывод клиента, инструменты txAdmin, приёмы профилирования и проверенные рабочие процессы для исправления проблем до того, как их заметят игроки.

Agency Scripts

Основатель и ведущий разработчик Agency Scripts

Искусство отладки скриптов FiveM

Отладка скриптов FiveM принципиально отличается от отладки стандартных приложений. Вы работаете с разделённой клиент-серверной архитектурой, где код выполняется в двух отдельных средах, события передаются по сети, а состояние игры меняется каждый кадр. Когда что-то идёт не так, ошибка может возникать на сервере, но проявляться на клиенте, или наоборот. Разработка системного подхода к отладке сэкономит вам множество часов и значительно ускорит рабочий процесс разработки. В этом руководстве рассмотрены основные инструменты, техники и шаблоны, которые профессиональные разработчики FiveM используют ежедневно.

Эффективное использование Print Statements

Самый фундаментальный инструмент отладки в FiveM , это print() функция, но для эффективного использования требуется больше, чем просто вывод переменных. Структурируйте отладочный вывод с префиксами, идентифицирующими скрипт, сторону (клиент или сервер) и функцию, где происходит вывод. Используйте цветовые коды, чтобы важные сообщения выделялись в консоли. Создайте утилиту отладки, которую можно включать и выключать без удаления строк отладки из кода.

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

Использование утилиты Debug

С этим инструментом вы можете добавить отладочное логирование по всему скрипту, которое полностью тихое в продакшене. Включите его, добавив set myresource_debug true для вашей конфигурации сервера, когда нужно устранить неполадки. Структурированный вывод облегчает поиск по логам консоли и позволяет найти именно то, что вы ищете.

-- 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)

Распространённые ошибки FiveM и их решения

ОШИБКА СКРИПТА: попытка обратиться к nil значению

Это самая частая ошибка Lua в разработке FiveM. Это значит, что вы пытаетесь получить доступ к свойству или методу переменной, которая является nil. Наиболее частые причины , доступ к данным игрока до их загрузки, обращение к функции фреймворка, которой нет в используемой версии, или чтение значения конфигурации с опечаткой в ключе. Всегда проверяйте на nil перед доступом к вложенным свойствам.

-- 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'

ОШИБКА СКРИПТА: попытка вызвать nil значение

Эта ошибка возникает, когда вы пытаетесь вызвать функцию, которая не существует. В FiveM это часто происходит, когда вы вызываете экспорт из ресурса, который еще не запущен, используете функцию фреймворка, которая была переименована в обновлении, или забываете загрузить общий файл в вашем манифесте. Проверьте ваш fxmanifest.lua убедиться, что все необходимые файлы перечислены и находятся в правильном порядке.

-- 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)

Событие не было зарегистрировано

Когда вы вызываете событие, для которого ни один скрипт не зарегистрировал обработчик, FiveM тихо игнорирует его без ошибки в консоли. Это может быть очень сложно отлаживать, потому что всё выглядит правильно, но ничего не происходит. Используйте вспомогательную функцию, чтобы проверить регистрацию событий перед их вызовом, и добавляйте логирование вокруг вызовов событий во время разработки.

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

Отладка NUI с помощью DevTools

Для скриптов с NUI интерфейсами встроенные Chromium DevTools незаменимы. Откройте их через консоль F8, введя nui_devtools для доступа к полному Chrome inspector. Это даёт панель Elements для инспекции структуры DOM, Console для ошибок JavaScript, вкладку Network для загрузки ресурсов и панель Sources для установки точек останова. Для проблем с NUI-коммуникацией логируйте обе стороны моста сообщений.

// 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);
};

Профилирование с Resmon и Timing

Помимо базового resmon мониторинг, вы можете встроить точную инструментализацию времени в ваши скрипты. Измеряйте, сколько времени занимают конкретные операции, и записывайте предупреждения, когда они превышают допустимые пороги. Это особенно важно для запросов к базе данных, сложных вычислений и циклов, обрабатывающих множество сущностей.

-- 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)

Отладка State Bag

State bags , мощная, но иногда запутанная функция. Если значения state bag не обновляются как ожидалось, обычно это происходит из-за того, что вы устанавливаете их на неправильной сущности, обработчик не ловит правильное имя bag или репликация задерживается. Создайте команду инспектора state bag, которая выводит всё состояние для заданной сущности.

-- 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)

Основной список для отладки

  • Проверьте обе консоли. Всегда проверяйте консоль сервера (txAdmin или терминал) и клиентскую консоль (F8) на наличие ошибок. Ошибка на одной стороне часто объясняет неправильное поведение на другой.
  • Проверить состояние ресурса. Использовать ensure для перезапуска вашего ресурса и restart для перезапуска одного ресурса. Проверьте resmon убедиться, что ресурс действительно запущен.
  • Тест с чистой средой. Отключите другие скрипты, которые взаимодействуют с теми же системами. Многие ошибки возникают из-за конфликтов между ресурсами, а не из-за ошибок в одном скрипте.
  • Внимательно изучите стек вызовов ошибки. Lua stack traces показывают точный файл и номер строки. Читайте их снизу вверх, чтобы понять цепочку вызовов, приведшую к ошибке.
  • Используйте pcall для рискованных операций. Оборачивайте запросы к базе данных, вызовы экспортов и декодирование JSON в pcall обрабатывать ошибки корректно, чтобы они не приводили к сбою скрипта.
  • Версионировать ваши конфиги. Когда игроки сообщают о багах, спрашивайте, какую версию они используют. Многие проблемы возникают из-за устаревших конфигурационных файлов после обновления.

Готовы начать?

Возьмите скрипты в нашем магазине или заходите в Discord за поддержкой, обновлениями и анонсами.