MAATRIX GAMES / Блог / FiveM: конфликт ресурсов и скриптов — как найти и устранить

FiveM: конфликт ресурсов и скриптов — как найти и устранить

MAATRIX GAMES

Поставил новый скрипт — и вместо радости получил спам ошибок в консоли, команду, которая выполняет не то, что должна, или сервер, который вообще не поднимается после ensure. Знакомая ситуация для любого, кто хоть раз набирал resources/ до отказа. Хорошая новость: конфликт ресурсов почти всегда диагностируется по одной и той же схеме, без гадания — разберём, откуда берутся такие конфликты и как вычислить виновника, даже если у тебя установлено полсотни скриптов.

Как понять, что дело именно в конфликте, а не в баге одного скрипта

Первый сигнал — проблема появилась не сразу, а после установки нового ресурса или после смены версии одного из старых. Если сервер работал нормально месяц, а после ensure new_script в консоли посыпались ошибки в совершенно другом, ранее рабочем ресурсе — это почти наверняка конфликт, а не самостоятельный баг.

Второй сигнал — команда или функция срабатывает не так, как ожидалось, но без явной ошибки в консоли. Например, ты набираешь /car и вместо спавна машины из одного скрипта получаешь поведение другого — это классика при дублирующихся RegisterCommand.

Третий сигнал — ошибки вида attempt to call a nil value, attempt to index a nil value (global 'ESX') или QBCore is not defined в скриптах, которые раньше работали. Это почти всегда говорит о проблеме с порядком загрузки или о том, что фреймворк, под который писан ресурс, на сервере вообще не запущен (или запущен не тот).

Смотреть стоит в двух местах: серверная консоль (или вкладка Console в txAdmin) — там видны ошибки Lua и предупреждения о ресурсах; и клиентский F8-консоль вместе с resmon — там ловятся клиентские ошибки скриптов и падение FPS от конкретного ресурса. Если ты ещё не разбирался с базовой установкой — вот тут описан подъём FiveM-сервера с нуля, включая структуру resources/ и server.cfg.

Дублирующиеся команды и экспорты

Самый частый и самый тихий конфликт — два ресурса регистрируют одну и ту же чат-команду через RegisterCommand('give', ...). FiveM не всегда явно ругается на это в консоли — часто просто побеждает тот обработчик, что зарегистрировался последним по порядку загрузки, а первый молча перестаёт срабатывать. Разработчик первого скрипта может решить, что у него сломалась логика, хотя на деле команду просто перехватил другой ресурс.

Найти дубликаты можно быстро, не перебирая ресурсы вручную — грепом по всей папке resources/:

grep -rn "RegisterCommand(" resources/ --include="*.lua" | grep -oP "RegisterCommand\('\K[^']+" | sort | uniq -d

Команда выведет только те названия команд, которые встречаются больше одного раза — это и есть кандидаты на конфликт. Аналогично можно проверить экспорты внутри одного ресурса, если в нём несколько файлов:

grep -rn "exports(" resources/my_resource/ | grep -oP "exports\('\K[^']+"

Важный нюанс: экспорты в FiveM намespaced по имени ресурса (exports['resource_a']:GetSomething()), поэтому два *разных* ресурса с одинаковым именем функции внутри exports друг другу не мешают — конфликт возникает, только если внутри одного и того же ресурса функция с одинаковым именем регистрируется дважды (второй вызов exports() перезатирает первый).

Поднять сервер FiveM (GTA V) за пару минут

Готовый образ MAATRIX GAMES: NVMe, AMD EPYC, DDoS-защита, панель управления. Локации UK, US, RU. Оплата картой РФ, СБП или криптой.

Создать сервер

Дублирующиеся и пересекающиеся события

С событиями (AddEventHandler, RegisterNetEvent) ситуация тоньше, чем с командами: FiveM позволяет вешать на одно и то же имя события сколько угодно обработчиков, и по умолчанию сработают все они по очереди — это нормальная и рабочая механика, а не баг. Проблема начинается, когда два ресурса вешаются на одно и то же имя события, но ожидают разную структуру данных в аргументах.

Типичный сценарий: кастомный скрипт инвентаря шлёт TriggerEvent('inventory:itemUsed', itemName), а другой ресурс уже слушает событие с точно таким же именем, но из другого, несовместимого инвентаря, и ждёт таблицу с другими полями. Оба обработчика отработают, но один из них упадёт с ошибкой обращения к несуществующему полю.

Ищутся такие пересечения тем же способом, что и команды:

grep -rn "RegisterNetEvent(" resources/ --include="*.lua" | grep -oP "RegisterNetEvent\('\K[^']+" | sort | uniq -d

Если находишь совпадение между двумя явно не связанными ресурсами (не форк одного и того же скрипта, не части одного пакета) — это повод открыть оба файла и свериться, что за данные каждый из них реально передаёт и ожидает.

Ключевая проблема: ESX и QBCore, запущенные одновременно

Отдельная и самая болезненная категория конфликтов — попытка держать на одном сервере одновременно es_extended (ESX) и qb-core (QBCore). Это два самостоятельных фреймворка, каждый со своим объектом игрока, своей структурой инвентаря и своим набором глобальных функций (ESX.GetPlayerFromId() против QBCore.Functions.GetPlayer()). Подробное сравнение того, чем они отличаются архитектурно, разбирали в статье про ESX и QBCore — здесь же про то, что бывает, если попытаться запустить оба сразу.

Такое иногда происходит не специально, а по ошибке: скачал «универсальный» пак ресурсов с форума, в котором часть скриптов ESX, часть — QBCore, и ensure их все скопом. Оба core пытаются занять одни и те же роли — регистрируют похожие по смыслу, но несовместимые команды и события, каждый пытается управлять инвентарём и деньгами игрока по-своему. Результат — постоянные ошибки в консоли у ресурсов, написанных под «не тот» фреймворк, и рассинхрон данных игрока.

Держать оба ядра запущенными одновременно без специальной прослойки не стоит — выбирай один core под фактический набор ресурсов, которые собираешься использовать. Существуют совместимые слои и bridge-ресурсы, которые позволяют части community-скриптов работать поверх произвольного core, но это отдельная, более сложная настройка, а не то, что стоит городить, если цель — просто быстро запустить сервер.

Порядок `ensure` в server.cfg — почему он не косметика

Многие резко забивают на порядок строк ensure в server.cfg, потому что на первый взгляд кажется — какая разница, все ресурсы всё равно запустятся. Разница есть, и она критична для ресурсов с зависимостями. Если скрипт вызывает exports['es_extended']:GetSharedObject() при своей загрузке, а es_extended в этот момент ещё не поднят — вызов упадёт с ошибкой, потому что ресурса с таким именем ещё не существует в памяти сервера.

Часть современных ресурсов решает это через fx_manifest.lua, объявляя зависимость явно:

fx_version 'cerulean'
game 'gta5'

dependency 'oxmysql'
dependency 'es_extended'

server_scripts { 'server.lua' }

Директива dependency заставляет FiveM подгрузить нужный ресурс раньше, даже если в server.cfg он записан позже. Но так делают не все авторы, особенно в старых или самописных ресурсах — поэтому базовое правило безопаснее соблюдать руками:

# базовые зависимости — всегда в начале
ensure oxmysql
ensure ox_lib

# фреймворк
ensure es_extended

# ресурсы, зависящие от фреймворка
ensure esx_multicharacter
ensure esx_menu_default
ensure esx_society

# остальные пользовательские скрипты
ensure my_custom_garage

Общий принцип: сначала база данных (oxmysql/mysql-async), затем общие библиотеки (ox_lib), затем фреймворк, затем всё, что от него зависит, и только потом самостоятельные скрипты без внешних зависимостей. Если после ensure какого-то ресурса в консоли вылетает ошибка про nil value при обращении к функциям фреймворка — в первую очередь проверь именно порядок строк, а не сам код ресурса.

Метод бинарного поиска: как найти виновника среди полусотни ресурсов

Когда конфликт есть, но непонятно, между какими именно ресурсами — не нужно проверять их по одному. Быстрее и надёжнее закомментировать примерно половину строк ensure в server.cfg, перезапустить сервер и посмотреть, воспроизводится ли проблема.

Порядок действий:

  1. Сделай копию рабочего server.cfg перед экспериментами — cp server.cfg server.cfg.bak.
  2. Раздели список ensure пополам. Первую половину закомментируй символом # в начале строки.
  3. Перезапусти сервер и проверь, повторяется ли ошибка.
  4. Если ошибка исчезла — виновник в закомментированной половине. Если осталась — виновник во второй половине, которая осталась активной.
  5. Повтори деление пополам уже внутри виновной половины, пока не останется 1-2 ресурса.

Для ускорения итераций необязательно перезапускать весь сервер целиком: пока сервер поднят, можно включать и выключать ресурсы прямо из консоли без даунтайма остальных игроков —

stop resource_name
start resource_name

или, если менял код внутри ресурса, refresh (перечитать список ресурсов) и затем ensure resource_name заново. Это особенно удобно, когда тестируешь конфликт на живом сервере с онлайном и не хочешь дёргать всех разом полным рестартом.

Если конфликт клиентский (просадка FPS, визуальные баги, ошибки в F8-консоли), тот же принцип бинарного поиска работает и там — только смотреть нужно клиентский resmon (открывается по F3 в дебаг-режиме или через консольную команду) и клиентскую консоль F8, а не серверный лог. Список активных ресурсов на сервере в любой момент можно получить командой resources в серверной консоли — она покажет статус каждого (started, stopped, starting), что тоже помогает быстро понять, не завис ли какой-то ресурс на старте из-за ожидания зависимости.

Что проверить в манифесте ресурса перед установкой

Часть конфликтов можно предотвратить ещё до ensure, если пробежаться по fx_manifest.lua нового ресурса. Смотри на три вещи: во-первых, fx_version и game 'gta5' — устаревший или нестандартный манифест может вести себя непредсказуемо на актуальном артефакте сервера. Во-вторых, секции dependency, если они есть — это прямая подсказка, что ресурс ожидает конкретный фреймворк или библиотеку, и без них лучше не ставить. В-третьих, открой client_scripts/server_scripts и глазами пробеги имена функций — если видишь сплошь ESX. или QBCore., а у тебя на сервере другой core, ресурс без адаптации не заработает вообще, вне зависимости от порядка загрузки.

Если ставишь ресурс с GitHub без внятного README — не поленись открыть основной серверный файл и поискать явные упоминания фреймворка или зависимостей прямо в коде, прежде чем тратить время на ensure и разбор ошибок постфактум. Более подробно о самом процессе установки кастомных скриптов и типичных ошибках на этом этапе — в статье про установку кастомных скриптов на FiveM.

Поднять сервер FiveM (GTA V) за пару минут

Готовый образ MAATRIX GAMES: NVMe, AMD EPYC, DDoS-защита, панель управления. Локации UK, US, RU. Оплата картой РФ, СБП или криптой.

Создать сервер

Частые вопросы

Сервер не падает, но в консоли постоянно скроллится один и тот же warning — это конфликт?

Не обязательно критично, но игнорировать не стоит: часто это как раз повторяющийся вызов сломанного обработчика события или устаревший API, который скоро уберут из артефакта. Найди источник по тексту warning через grep по resources/ и обнови или замени ресурс.

Можно ли одновременно держать esx_вариант и «универсальные» ресурсы под оба фреймворка?

Технически некоторые современные ресурсы пишутся с поддержкой обоих core через условные проверки внутри кода, но это исключение, а не правило — по умолчанию считай, что ресурс работает только под тот фреймворк, под который написан явно.

После обновления одного ресурса всё сломалось — как понять, в нём ли дело?

Временно откати ресурс до предыдущей версии (если сохранил бэкап папки) и сравни поведение. Если проблема исчезла — смотри changelog новой версии на предмет изменившихся имён событий или экспортов, это частая причина после мажорных обновлений.

Обязательно ли использовать dependency в fx_manifest для собственных ресурсов?

Не обязательно, но настоятельно рекомендуется, если ресурс дёргает функции другого ресурса при старте — это избавляет от необходимости вручную следить за порядком строк в server.cfg и защищает от поломки при случайной перестановке ensure другим админом.

Есть ли способ автоматически проверять конфликты перед запуском сервера?

Готового встроенного линтера под все виды конфликтов в FiveM нет, но связку из grep-проверок на дублирующиеся команды/события (см. выше) можно оформить в простой shell-скрипт и гонять перед каждым добавлением новых ресурсов — это займёт пару секунд и снимет львиную долю ручной диагностики.