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обрабатывать ошибки корректно, чтобы они не приводили к сбою скрипта. - Версионировать ваши конфиги. Когда игроки сообщают о багах, спрашивайте, какую версию они используют. Многие проблемы возникают из-за устаревших конфигурационных файлов после обновления.