Techniki debugowania FiveM: Szybko znajduj błędy w Lua i JS
Debuguj skrypty FiveM szybciej. Logi serwera, printy klienta, narzędzia txAdmin, triki z profilerem i sprawdzone metody pracy, aby naprawiać problemy, zanim gracze je zauważą.
Agency Scripts
Założyciel i główny deweloper w Agency Scripts
Sztuka debugowania skryptów FiveM
Debugowanie skryptów FiveM zasadniczo różni się od debugowania standardowych aplikacji. Masz do czynienia z podzieloną architekturą klient-serwer, w której kod działa w dwóch osobnych środowiskach wykonawczych, zdarzenia przesyłane są przez sieć, a stan gry zmienia się w każdej klatce. Kiedy coś pójdzie nie tak, błąd może powstać na serwerze, ale objawić się u klienta lub odwrotnie. Opracowanie systematycznego podejścia do debugowania zaoszczędzi ci niezliczone godziny i drastycznie przyspieszy twoją pracę programistyczną. Ten poradnik omawia niezbędne narzędzia, techniki i wzorce, z których profesjonalni programiści FiveM korzystają na co dzień.
Efektywne używanie instrukcji print
Najbardziej podstawowym narzędziem do debugowania w FiveM jest print() funkcja, ale jej efektywne wykorzystanie wymaga czegoś więcej niż zwykłego zrzucania zmiennych. Ustrukturyzuj dane wyjściowe debugowania za pomocą prefiksów identyfikujących skrypt, stronę (client lub server) oraz funkcję, w której następuje print. Używaj kodów kolorów, aby ważne komunikaty wyróżniały się w konsoli. Zbuduj narzędzie debugowania, które możesz włączać i wyłączać bez usuwania linii debugujących z kodu.
-- 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
Korzystanie z narzędzia debugowania
Dzięki temu narzędziu możesz dodać do swoich skryptów logowanie debugowania, które jest całkowicie wyciszone na serwerze produkcyjnym. Włącz je, dodając set myresource_debug true do konfiguracji Twojego serwera, gdy musisz rozwiązać problemy. Ustrukturyzowane wyjście ułatwia przeszukiwanie logów konsoli za pomocą grep i znajdowanie dokładnie tego, czego szukasz.
-- 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)
Typowe błędy FiveM i rozwiązania
SCRIPT ERROR: attempt to index a nil value
To najczęstszy błąd Lua podczas tworzenia skryptów FiveM. Oznacza, że próbujesz uzyskać dostęp do właściwości lub metody zmiennej, która jest nil. Najczęstszymi przyczynami są: dostęp do danych gracza przed ich załadowaniem, odwoływanie się do funkcji frameworka, która nie istnieje w używanej wersji, lub odczyt wartości configu z literówką w kluczu. Zawsze sprawdzaj obecność nil przed uzyskaniem dostępu do właściwości zagnieżdżonych.
-- 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
Ten błąd występuje, gdy próbujesz wywołać funkcję, która nie istnieje. W FiveM zdarza się to zazwyczaj wtedy, gdy wywołujesz export z zasobu, który jeszcze się nie uruchomił, używasz funkcji frameworka o zmienionej w aktualizacji nazwie lub zapomnisz załadować plik shared w manifeście. Sprawdź swój fxmanifest.lua aby upewnić się, że wszystkie wymagane pliki są wymienione i we właściwej kolejności.
-- 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)
Event nie został zarejestrowany
Gdy wywołujesz zdarzenie, dla którego żaden skrypt nie zarejestrował handlera, FiveM po cichu je ignoruje bez żadnego błędu w konsoli. Debugowanie tego bywa szalenie frustrujące, ponieważ wszystko wygląda poprawnie, ale nic się nie dzieje. Użyj funkcji pomocniczej, aby sprawdzić, czy zdarzenia są zarejestrowane przed ich wywołaniem, i dodaj logowanie wokół wywołań zdarzeń podczas developmentu.
-- 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
Debugowanie NUI za pomocą DevTools
W przypadku skryptów z interfejsem NUI wbudowane narzędzia Chromium DevTools są nieocenione. Otwórz je w konsoli F8, wpisując nui_devtools aby uzyskać dostęp do pełnego inspektora Chrome. Daje ci to panel Elements do sprawdzania struktury DOM, Console do błędów JavaScript, zakładkę Network do ładowania zasobów oraz panel Sources do ustawiania breakpointów. W przypadku problemów z komunikacją NUI loguj obie strony mostu wiadomości.
// 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);
};
Profilowanie za pomocą Resmon i Timing
Więcej niż podstawowe resmon monitorowania, możesz wbudować w swoje skrypty precyzyjne opomiarowanie czasu. Mierz czas trwania poszczególnych operacji i rejestruj ostrzeżenia w logach, gdy przekroczą one akceptowalne progi. Jest to szczególnie ważne w przypadku zapytań do bazy danych, złożonych obliczeń oraz pętli przetwarzających wiele bytów.
-- 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)
Debugowanie state bagów
State bagi to potężna, ale czasami myląca funkcja. Gdy wartości state baga nie aktualizują się zgodnie z oczekiwaniami, zwykle wynika to z ustawiania ich na niewłaściwym bycie, braku przechwytywania właściwej nazwy baga przez handler lub opóźnienia replikacji. Zbuduj komendę inspektora state bagów, która zrzuca cały stan dla danego bytu.
-- 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)
Niezbędna lista kontrolna debugowania
- Sprawdź obie konsole. Zawsze sprawdzaj błędy zarówno w konsoli serwera (txAdmin lub terminal), jak i w konsoli klienta (F8). Błąd po jednej stronie często wyjaśnia nieprawidłowe zachowanie po drugiej.
- Sprawdź stan resource. Użyj
ensureaby zrestartować swój zasób orazrestartaby zrestartować pojedynczy zasób. Sprawdźresmonaby upewnić się, że zasób rzeczywiście działa. - Testuj w czystym środowisku. Wyłącz inne skrypty, które wchodzą w interakcję z tymi samymi systemami. Wiele błędów wynika z konfliktów między zasobami, a nie z błędów w pojedynczym skrypcie.
- Przeczytaj uważnie stack trace błędu. Stack trace w Lua pokazuje dokładny plik i numer linii. Czytaj je od dołu do góry, aby zrozumieć łańcuch wywołań, który doprowadził do błędu.
- Używaj pcall do ryzykownych operacji. Otaczaj zapytania do bazy danych, wywołania exportów i dekodowanie JSON w
pcallaby elegancko wychwytywać błędy, zamiast dopuszczać do awarii skryptu. - Wersjonuj swoje konfiguracje. Gdy gracze zgłaszają błędy, zapytaj, jakiej wersji używają. Wiele problemów wynika z nieaktualnych plików konfiguracyjnych po aktualizacji.