Соглашения
Правила, которым следует API и следовать которым ожидает от вас: как называются вещи, что возвращает вызов, что остаётся стабильным между релизами и из чего складывается строка лога. Ничего нового здесь нет - это форма того, что описывают остальные страницы, собранная в одном месте.
- Сетевые события -
<domain>:<verb>, в нижнем регистре, с одним двоеточием:chat:send,race:start. Собственный трафик сервера использует доменыchat,vehicle,player,session,worldиmodules; ресурс выбирает свой домен, обычно своё имя.node:зарезервировано за фреймворком. Имена событий не несут версии: переименованное событие не падает, а замолкает - поэтому не меняйте имена после публикации, а добавляйте новые рядом. - Хуки движка пишутся в camelCase и никогда не уходят в сеть:
playerJoined,vehicleSpawned. Уведомление -<subject><Verb-ed>: прошедшее время после субъекта (vehicleEdited,playerSeatChanged,serverShutdown); поток -…Changed; запрос, который обработчик может отклонить, -<subject><Action>Request(vehicleSpawnRequest,relayRequest) - без префиксаon, его уже говоритnode.on. Два написания, два вида - читатель с первого взгляда отличает сетевое сообщение от серверного хука. Написания серверов до 1.2.0 (playerJoin,onVehicleSpawnRequest,canRelay) - устаревшие псевдонимы: они работают и один раз предупреждают на ресурс; см. События → Именование. - Сообщения шины между ресурсами следуют сетевому правилу:
chat:say,chat:command,dimensions:changed. - Имена ресурсов состоят из букв, цифр,
_,-и.и начинаются с буквы или цифры. Имя - это папка, тег лога, хранилищеnode.storage(storage/<name>.json, не более 64 символов) и имя, под которым клиентский мод регистрирует переданные скрипты (не более 128). - Ключи хранилища - строки
kind:idдо 256 символов:playtime:42,lastSeen:42. В качестве ключа используйтеplayer.accountIdилиplayer.name, никогдаplayer.id. - Модульные каналы - идентификаторы
u32, которые вы выбираете сами; пространство идентификаторов общее для всех модулей и ресурсов сервера, так что публикуйте свои.0x44494D53(DIMS) принадлежитdimensions. - Собственные консольные теги (
node.log.custom, Clog_custom) не длиннее шести символов, чтобы колонки консоли не расползались.
Объекты и идентификаторы
Заголовок раздела «Объекты и идентификаторы»Сервер мыслит идентификаторами: идентификатор игрока (маленький, назначается при подключении и
переиспользуется после отключения) и глобальный идентификатор машины (уникальный на всё время
жизни сервера). Поверхность node оборачивает их в объекты; node.raw и C ABI оставляют идентификаторы.
PlayerилиVehicle- таблица сidи метатаблицей. Чтение любого другого поля один раз забирает запись и кэширует её на объекте;refresh()сбрасывает кэш; методы действуют по идентификатору, так что объект переживает запись (player:isConnected(),vehicle:exists()скажут, ссылается ли он ещё на что-то). Два объекта равны по==, когда совпадают их идентификаторы;tostringдаётPlayer#3 Alice,Vehicle#12 Alice.- Идентификаторы внутри записи, называющие другую сущность, приходят объектами с сырым
идентификатором рядом:
vehicle.driver-Player,vehicle.driverId- число;player.vehicle-Vehicle,player.vehicleId- число (-1пешком). - Каждый вызов
node, принимающий игрока или машину, берёт и объект, и идентификатор:node.send(target, ...),node.players.get(id),vehicle:seat(player),node.bans.add(who). - Где важна скорость, остаются идентификаторы:
node.raw.onпередаёт сырые аргументы, а фильтр ретрансляции (relayRequest) получает(fromPid, toPid, category, subtype, globalId), потому что выполняется на каждый пакет. - Сырое имя - это имя из C в camelCase:
kick_player- этоnode.raw.kickPlayer,get_vehicle_transform_json-node.raw.getVehicleTransform, возвращающий раскодированную таблицу.
Формы возвращаемых значений
Заголовок раздела «Формы возвращаемых значений»Справочник Lua помечает каждую форму в сигнатуре; вот формы, которые он использует.
| Форма | Где используется | Примеры |
|---|---|---|
boolean |
действия: true, когда сделано, false, когда цели нет или запрос не принят |
player:kick, node.send, node.storage.set, vehicle:setTag, node.resources.reload |
значение или nil (помечено ?) |
поиск и чтение того, чего может ещё не быть | node.players.find, vehicle:transform(), node.fs.read, node.json.decode (nil при ошибке разбора) |
| массив, возможно пустой | списки | node.players.all(), player:vehicles(), node.bans.all() |
number |
счётчики и идентификаторы | node.off (снятые обработчики), node.after (идентификатор таймера), player:resync() (отправленные пакеты) |
result, err |
один фоновый вызов | node.await(workFn, args) отдаёт результат или nil и строку ошибки |
status, body, headers |
один HTTP-вызов в корутине | node.http.fetch: 0, "request not queued", {}, когда запрос не удалось запустить; -1 и текст ошибки в body, когда отказал транспорт |
| аргументы колбэка | асинхронные формы | cb(status, body, headers) для HTTP, cb(ok) для node.fs.writeAsync, doneFn(result, err) для node.job |
| значение по умолчанию | node.storage.get(key, default) |
возвращает default, когда ключа нет |
| ошибка (бросает) | неверное использование, а не отказ во время работы | node.on с именем не строкой или обработчиком не функцией, node.commands.add с неверной сигнатурой, node.sleep вне node.async |
Ничто в node не возвращает объект ошибки: неудавшееся действие - это false, отсутствующая
вещь - nil, ошибка программиста - исключение. node.on(name, fn) с тем же fn дважды - одна
подписка: второй вызов заменяет первый, обработчик выполняется один раз на событие, а
node.off(name, fn) снимает его и возвращает 1 (События → Именование
говорит то же об устаревших написаниях). Две разные функции - два обработчика. Подписывайтесь при
загрузке; перезагрузка начинает с чистого состояния.
В C формы - целые числа: действие возвращает 0 при успехе и -1 при неудаче, вопрос - 1 или
0, заполнение буфера - число записанных байтов или -1, когда буфер мал, а запрос размера
(buf = NULL) - длину, которую записал бы. Цикл с буфером - на странице
Нативные модули.
Нагрузки
Заголовок раздела «Нагрузки»- Таблица, которую вы передаёте в
player:send,node.broadcast,node.bus.emit,node.storage.setилиnode.http.post, кодируется в JSON за вас: массив, когда её ключи -1..n, иначе объект; функции становятсяnull; вложенность останавливается на 32 уровнях. Строка уходит как есть. dataсетевого события приходит на сервер строкой, которую прислал клиент, - раскодируйте еёnode.json.decodeи проверьте тип. Нагрузка уведомления или запроса приходит раскодированной там, где на проводе это JSON (конфигурация, вызов сцепки), и строкой там, где это имя (роль на месте).dataшины - тоже строка; модульный канал несёт байты и ничего не разбирает.- На клиенте
node.emitServer(name, data)шлётtostring(data): кодируйте таблицу там черезjsonEncodeи раскодируйте на сервере.NodeMP.events.triggerServerкодирует за вас.
Версии и совместимость
Заголовок раздела «Версии и совместимость»| Поверхность | Версия | Что обещано |
|---|---|---|
| Сетевой протокол | v18 (Wire::ProtoVersion) |
Точное совпадение. Лаунчер, клиентский мод и сервер выходят вместе; несовпадение отклоняется на рукопожатии с текстом Protocol version mismatch: launcher speaks v17, server speaks v18 - update the outdated side. |
| C ABI | 1.12 (NODE_ABI_VERSION_MAJOR 1, MINOR 12) |
Мажор - это раскладка: внутри неё ничто не двигается, новые записи добавляются в конец и поднимают минор, выведенная из употребления запись становится заглушкой, сохраняющей место. Модуль, собранный против более старого 1.x, продолжает работать; другой мажор загрузчик отклоняет. |
Lua node и node.raw |
сервер 1.2.1 |
Генерируются из одной схемы, sdk/api.toml, вместе с node.h и страницами справочника; apigen.py docs --check падает при расхождении, так что справочник говорит то, что делает сервер. node.raw - зеркало записей C один к одному. |
Клиентская таблица node |
клиентский мод 1.4.0 |
Десять функций со страницы Клиентские скрипты. |
NodeMP.* |
клиентский мод 1.4.0 (NodeMP.VERSION) |
Одна стабильная глобальная таблица; исходные плоские помощники остаются псевдонимами вызовов из пространств имён. NodeMP.internal и имена модулей с точками под ним могут меняться между версиями. |
Журнал изменений провода живёт в server/include/net/Protocol.h, история ABI - в описаниях записей
sdk/node.h (ABI 1.8, ABI 1.9, …). Ни то ни другое здесь не дублируется.
Окружение Lua
Заголовок раздела «Окружение Lua»Каждый ресурс работает в собственном Lua-стейте версии 5.4 с открытыми стандартными библиотеками -
string, table, math, os, io, coroutine, utf8, debug - и убранными только
загрузчиками C-модулей (Нативные модули).
node добавляет сервер и не дублирует стандартную библиотеку, поэтому у нескольких вопросов «как
мне…» ответ - стандартный Lua, а у нескольких ответа пока нет:
| Мне нужно | Используйте | Примечания |
|---|---|---|
Случайное число с плавающей точкой в [0, 1) |
math.random() |
Lua 5.4 сеет генератор случайным зерном при создании стейта; вызывать math.randomseed не нужно. |
Случайное целое в [a, b] |
math.random(a, b) |
|
Случайное число с плавающей точкой в [a, b) |
a + (b - a) * math.random() |
Помощника в node для этого нет. |
| Случайные байты для токена или ключа | node.crypto.randomBytes(n), node.crypto.randomHex(n?) |
Криптографические; math.random - нет. |
| Сколько времени что-то заняло | node.server.uptime() до и после: монотонные секунды с дробной частью |
os.clock() - процессорное время процесса по всем потокам, так что оно измеряет код, нагружающий процессор в рабочем потоке, и мало что ещё. Вызова со статистикой по обработчикам вроде Util.DebugExecutionTime из BeamMP нет; сервер сам логирует каждый срез обработчика дольше 250 мс (Конкурентность). |
| Часы | node.server.time() (unix, с дробной частью), node.server.unixTime() (целые секунды), os.date, os.time |
|
| Память, занятая этим ресурсом | collectgarbage("count") * 1024 - байты этого Lua-стейта |
Только текущий стейт: цифры по всем стейтам вместе или по процессу нет, а node.server.metrics() несёт счётчики (игроки, машины, глубина очереди), не байты. |
| Операционная система | в node нет |
package.config:sub(1, 1) - это "\\" на Windows и "/" в остальных случаях, что и нужно для пути; имя и версия ОС не раскрываются. node.server.version() - версия самого сервера. |
| JSON | node.json.encode(value), node.json.decode(text) |
encode пишет компактный JSON и не принимает опций: ни красивой печати, ни минификации, ни flatten (RFC 6901), ни diff или patch (RFC 6902) - у Util.JsonPrettify, JsonFlatten, JsonDiff и JsonDiffApply из BeamMP аналога нет. Красивую печать для файла, который читают люди, делают несколько строк Lua; diff - сравнение таблиц, которое вы пишете сами. |
local t0 = node.server.uptime()local sum = 0for _ = 1, 100000 do sum = sum + math.random(1, 6) endnode.log("100000 dice rolls in %.2f ms, average %.3f", (node.server.uptime() - t0) * 1000, sum / 100000)
node.log("float %.3f · int %d · float in [10, 20) %.3f · token %s", math.random(), math.random(1, 6), 10 + 10 * math.random(), node.crypto.randomHex(4))node.log("this Lua state uses %d KB · path separator %s", math.floor(collectgarbage("count")), package.config:sub(1, 1))
-- expect: 100000 dice rolls in \d+\.\d\d ms, average 3\.\d+-- expect: this Lua state uses \d+ KBЛогирование
Заголовок раздела «Логирование»Каждая строка консоли - HH:MM:SS Tag › message: время, тег, дополненный до шести символов,
› и сообщение; logs/server.log получает ту же строку без цветов. При [General] Debug = true
метка времени получает миллисекунды, а после › вставляется колонка с именем пишущего потока
(Res › PluginFramework race · …), так что парсер не должен считать, что сообщение начинается
сразу за ним. Куда попадает ваш вывод:
node.log(msg, ...)печатает под тегомResс именем ресурса впереди:race · Player#0 Alice is ready. Дополнительные аргументы - аргументыstring.format.node.log.warnиnode.log.errorиспользуют тегиWarnиErrorс тем же префиксом. Ошибки, о которых сервер сообщает за вас, читаются какrace · error in event 'race:ready': ...с трассировкой стека (error in resource event '…'для обработчика шины,error in timer callback,error in async task), а обработчик, держащий рабочий поток дольше 250 мс, отмечается как зависание, с ресурсом и видом в скобках. Не каждая строка, которую сервер печатает о вашем коде, несёт префикс:emitClient: invalid player ID '0'(node.sendигроку, которого нет) иnode.sleep called outside a node.async taskне называют ресурс.node.log.tag(tag, msg)пишет под одним из собственных тегов сервера -Core,Net,Res,Mods,Module,Join,Leave,Kick,Veh,Warn,Error,Debug; неизвестный тег сводится кModule.node.log.custom(tag, rgb, msg)использует ваш собственный тег цветом0xRRGGBB,node.log.raw(text)пропускает префикс,node.log.sink(fn)наблюдает каждую строку,node.log.titleзадаёт заголовок консоли.log_infoнативного модуля попадает подModule;log_custom- под его собственный тег; приёмник лога на C может подавлять строки, на Lua - только наблюдать.
На клиенте beamng.log помечает строки мода: node.res - доставка и активация ваших файлов,
node.events - node.log и ошибки обработчиков (Error in event handler for "race:start" from source "node.res/race": ...), node.net и node.session - связь и список игроков,
nodemp.events - ошибки внутри обработчиков NodeMP.events.on (Handler for 'race:start' errored: ...).
- Справочник Lua API - каждая сигнатура с пометкой формы.
- События - правило именования в применении к каждому виду событий.
- Нативные модули - соглашения C полностью.
- Сетевой протокол - где определён
v18.
