Wróć do bloga
Guide7 min czytania

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 ensure aby zrestartować swój zasób oraz restart aby zrestartować pojedynczy zasób. Sprawdź resmon aby 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 pcall aby 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.

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.