События FiveM: RegisterNetEvent, TriggerServerEvent, TriggerClientEvent
Как работают события клиента и сервера FiveM: RegisterNetEvent, AddEventHandler, TriggerServerEvent, TriggerClientEvent с -1, переменная source и именование событий.
Большинство ошибок скрипта между игрой и сервером сводятся к событиям: обработчику, который никогда не запускается, событию, отправленному никому, или source, который указывает на неправильного игрока.
Эта статья объясняет, как работают сетевые события, как регистрировать и запускать их в обоих направлениях, и как их называть, чтобы два скрипта никогда не столкнулись.
Две стороны
Ресурс FiveM может иметь код на клиенте (одна копия за игрока) и на сервере (одна копия). Они общаются через события:
| От | Кому | Функция |
|---|---|---|
| Клиент | Сервер | TriggerServerEvent |
| Сервер | Один клиент | TriggerClientEvent(name, playerId, ...) |
| Сервер | Все клиенты | TriggerClientEvent(name, -1, ...) |
| Одна сторона | Одна сторона | TriggerEvent |
Аргументы сериализованы, поэтому вы можете отправить числа, строки, логические значения и таблицы. Вы не можете отправить функции. Дескрипторы сущностей локальны для каждой машины, поэтому отправьте сетевой id (NetworkGetNetworkIdFromEntity) вместо этого.
Регистрация обработчика
Обработчик — это функция, которая запускается при поступлении события. Для события, поступающего из сети, вы также должны пометить его как разрешенный:
RegisterNetEvent('my_script:client:notify', function(message)
print(message)
end)Это краткая форма: RegisterNetEvent с функцией как второй аргумент регистрирует событие и присоединяет обработчик. Длинная форма делает то же самое в два шага:
RegisterNetEvent('my_script:client:notify')
AddEventHandler('my_script:client:notify', function(message)
print(message)
end)Если вы только используете AddEventHandler, обработчик работает для локальных вызовов TriggerEvent, но TriggerServerEvent или TriggerClientEvent с другой стороны игнорируются. Это наиболее распространенная причина события, которое ничего не делает.
Клиент на сервер
-- client
TriggerServerEvent('my_script:server:buy', 'water', 2)-- server
RegisterNetEvent('my_script:server:buy', function(item, amount)
local src = source
print(('player %s wants %s x %s'):format(src, amount, item))
end)На сервере глобальное значение source содержит id игрока, который запустил событие. Клиент не отправляет его, поэтому он не может быть подделан клиентом, изменяющим его аргументы.
Сервер на клиент
-- server: to one player
TriggerClientEvent('my_script:client:notify', src, 'Purchase done')
-- server: to everyone
TriggerClientEvent('my_script:client:notify', -1, 'Server restart in 5 minutes')-- client
RegisterNetEvent('my_script:client:notify', function(message)
print(message)
end)Второй аргумент TriggerClientEvent всегда цель. Если вы его забудете, ваш первый аргумент данных читается как идентификатор игрока и событие никуда не уходит.
Сохраните source перед Wait
source — это не значение, которым вы владеете, это глобальное значение, которое среда выполнения устанавливает для работающего события. Если ваш обработчик вызывает Wait или вызывает что-то, что дает выход, такое как MySQL.query.await, другое событие может запуститься тем временем и source изменится.
RegisterNetEvent('my_script:server:buy', function(item, amount)
local src = source -- copy it first
Wait(500) -- or any await
print(src) -- still the right player
print(source) -- may be someone else now
end)Сделайте local src = source первой строкой каждого обработчика сервера и используйте src после этого. То же применяется внутри обратного вызова, который вы передаете другой функции.
Имена событий
События глобальны для всего сервера: любой ресурс может запустить любое имя события. Если два скрипта оба регистрируют buy или server:buy, оба обработчика запускаются, и игрок может запустить их по имени.
Предыдите каждое событие префиксом ресурса и стороны:
'my_script:server:buy'
'my_script:client:updateJob'Не переиспользуйте имена событий других скриптов и не называйте ваше событие чем-то общим, таким как giveMoney. Читаемое имя также делает очевидным в консоли, какой скрипт что отправил.
Никогда не доверяйте клиенту
Все, что приходит через TriggerServerEvent, может быть запущено читером с любыми аргументами в любое время. Проверьте на сервере:
- что игроку разрешено это делать (работа, расстояние, кулдаун),
- что количества и имена предметов действительны,
- что вы никогда не берете цену или награду из аргументов.
RegisterNetEvent('my_script:server:buy', function(item, amount)
local src = source
if type(amount) ~= 'number' or amount < 1 or amount > 10 then return end
-- look the price up on the server, do not read it from the client
end)Полный список находится в securing server events. Для результата, который должен вернуться к вызывающему, обратный вызов чище чем два события: смотрите server callbacks on ESX, QBCore and ox_lib.
Большие или частые события
Каждое событие — это сетевое сообщение. Не запускайте его каждый кадр и не отправляйте очень большую таблицу каждому игроку. Если консоль сервера печатает о надежных событиях переполнения, вы отправляете слишком много, слишком быстро: смотрите reliable network event overflow.
Контрольный список
| Симптом | Исправление |
|---|---|
| Обработчик никогда не запускается с другой стороны | Используйте RegisterNetEvent, а не только AddEventHandler |
TriggerClientEvent не достигает никого |
Передайте идентификатор игрока или -1 как второй аргумент |
source равна nil или неправильному игроку |
local src = source как первая строка, перед любым Wait |
| Два скрипта реагируют на то же событие | Предыдите имена префиксом ресурса: my_script:server:buy |
| Клиент может дать себе предметы или деньги | Валидируйте на сервере и смотрите цены там |
| Спам событий в консоли или лаг | Отправляйте меньше, реже: смотрите reliable network event overflow |
Короткие ответы
Какова разница между RegisterNetEvent и AddEventHandler?
RegisterNetEvent позволяет событию запускаться с другой стороны сети. AddEventHandler только присоединяет функцию. Обработчик сетевого события нуждается в обоих, или RegisterNetEvent с функцией как второй аргумент.
Как я отправляю событие каждому игроку?
На сервере TriggerClientEvent('my_script:client:thing', -1, data). -1 означает всех подключенных клиентов.
Почему source неправильно в моем событии сервера?
source — это глобальное значение, которое содержит игрока, который запустил текущее событие. Если вы вызываете Wait и тем временем запускается другое событие, оно меняется. Скопируйте его в локальное первым: local src = source.
Скрипты без этой проблемы
Advanced BoostingУгон машин на заказ через планшет: контракты от класса D до S+, команды и живая очередь.Смотреть скрипт →
Drug Dealer AppУличная торговля в виде приложения lb-phone: зоны, NPC-покупатели, уровни и вызовы полиции.Смотреть скрипт →
Shop CreatorМагазин меньше чем за минуту — владельцы, сотрудники, сейфы и ограбления в комплекте.Смотреть скрипт →