Перейти к содержимому

Соглашения

Правила, которым следует 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, C log_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-стейте версии 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 = 0
for _ = 1, 100000 do sum = sum + math.random(1, 6) end
node.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: ...).