Первые шаги
На этой странице ресурс hello собирается с пустой папки до работающего сервера: серверная
половина приветствует каждого игрока и раз в минуту считает их, а клиентская половина каждые
десять секунд отчитывается обратно. Он повторяет demo-numbers, самый маленький сквозной пример,
и добавляет вызовы, которые понадобятся вам в первый же день. Кроме сервера ничего устанавливать
не нужно: Lua встроен.
Вам нужны работающий Node-Server 1.2.1 (Быстрый старт), открытый в
соседней вкладке справочник Lua API и, для клиентской половины, лаунчер на
машине, которая может подключиться к серверу - серверную половину можно проверить без игры
(ниже). Два вызова ниже - player:tell и node.chat.say - говорят через
пример ресурса chat, который рисует чат и владеет командами /: установите chat рядом с
hello (скопируйте examples/chat из архива релиза в resources/; examples/README.txt
перечисляет остальные) или знайте, что без него эти два вызова не делают вообще ничего - ни строки
в чате, ни строки в консоли. В консоли видно node.log.
Ресурсы лежат в resources/ внутри рабочего каталога сервера, рядом с server.toml. Создайте
такое дерево:
resources/└── hello/ ├── resource.toml ├── server/ │ └── main.lua # runs on the server └── client/ └── main.lua # streamed to every playerИмя папки - имя ресурса по умолчанию; манифест может его переопределить.
resource.toml
Заголовок раздела «resource.toml»name = "hello"version = "1.0"
[server]main = "server/main.lua"
[client]files = ["main.lua"]obfuscation = "none"Всё здесь необязательно - папка без манифеста является Lua-ресурсом с именем папки, точкой входа
server/main.lua и всеми .lua из client/, передаваемыми игрокам. obfuscation = "none"
отдаёт клиентский файл открытым текстом, пока вы разрабатываете, чтобы beamng.log оставался
читаемым; уберите строку перед публикацией, и применится уровень light по умолчанию. Полная
схема - на странице Ресурсы.
Серверная половина
Заголовок раздела «Серверная половина»server/main.lua выполняется один раз при запуске сервера. Код верхнего уровня регистрирует
обработчики; сервер вызывает их по мере событий.
node.log("hello loaded, players online: %d", node.players.count())
-- an engine event: the handler receives a Player objectnode.on("playerJoined", function(player) node.log("%s joined from %s", tostring(player), player.ip) player:tell("Welcome, %s", player.name) player:send("hello:greet", { name = player.name })end)
node.on("playerLeft", function(player) node.log("%s left", tostring(player)) -- the name is still known hereend)
-- a wire event from the client half: (player, data), data as the client sent itnode.on("hello:count", function(player, data) node.log("%s -> %s", tostring(player), tostring(data))end)
-- a repeating timer, on the same worker thread as every handlernode.every(60000, function() node.chat.say("%d player(s) online", node.players.count())end)
-- expect: hello loaded, players online: 0-- expect: Player#\d+ Alice joined from \S+-- expect: Player#\d+ Alice -> 1-- expect: Player#\d+ Alice leftНа что опирается каждая строка:
node.log(msg, ...)печатает под именем ресурса; дополнительные аргументы - это аргументыstring.format.node.on("playerJoined", fn)подписывается на событие движка. Обработчик получаетPlayer:player.id,player.name,player.ip- поля,player:tellиplayer:send- методы, аtostring(player)читается какPlayer#0 Alice. В справочнике событий перечислено каждое имя с его аргументами.player:send(event, data)отправляет сетевое событие этому игроку; таблица кодируется в JSON за вас. Имя следует правилу<domain>:<verb>для сетевых событий: строчные буквы и одно двоеточие.node.on("hello:count", fn)- имя, которое не является событием движка, это сетевое событие от клиентской половины. Его обработчик получает(player, data), гдеdata- строка, которую прислал клиент.node.every(ms, fn)выполняетfnв рабочем потоке доnode.cancel(id);node.after(ms, fn)- одноразовая форма.
Клиентская половина
Заголовок раздела «Клиентская половина»client/main.lua передаётся каждому игроку после синхронизации контента и выполняется внутри
BeamNG с полным доступом к игре, используя клиентскую таблицу node.
local M = {}
local n = 0 -- the number we sendlocal acc = 0 -- seconds since the last send
node.log("hello client script loaded")
-- sent by server/main.lua on playerJoined; data is the JSON text of the table it sentnode.on("hello:greet", function(data) local greeting = jsonDecode(data) node.log("server greets " .. tostring(greeting.name))end)
-- a returned table with on* functions is registered as a game extension,-- so onUpdate(dt) ticks every framefunction M.onUpdate(dt) acc = acc + (dt or 0) if acc < 10 then return end acc = 0 n = n + 1 node.emitServer("hello:count", tostring(n))end
return MТри отличия от серверной стороны. Обработчики node.on(name, fn) получают только data - строку,
которую прислал сервер; JSON декодируйте игровой функцией jsonDecode. node.emitServer(name, data)
отправляет сетевое событие на сервер; data уходит строкой. node.log(msg) пишет в beamng.log
под тегом node.events. Остальная часть клиентской таблицы (node.off, node.emitLocal,
модульный канал, node.requestVehicleTrigger) описана на странице
Клиентские скрипты.
Запустите сервер. Две строки под тегом Res подтверждают ресурс, за ними идёт сводка:
hello · hello loaded, players online: 0hello v1.0 loaded — lua · server 1 file · 1 client file1 resource · 0 modules loadedПервая строка - ваш вызов node.log: точка входа выполняется во время сканирования, до строки о
загрузке. Подключитесь через лаунчер. Консоль сервера показывает подключение и каждые десять
секунд - счётчик клиента:
hello · Player#0 Alice joined from 203.0.113.5hello · Player#0 Alice -> 1hello · Player#0 Alice -> 2Если установлен chat, Alice видит Welcome, Alice как системную строку, а все видят
1 player(s) online раз в минуту. На машине Alice beamng.log (в
%LOCALAPPDATA%\BeamNG\BeamNG.drive\current\) содержит клиентскую сторону под тегами node.res и
node.events:
Activated server resource "hello" (1 ge file(s), 0 vehicle file(s))hello client script loadedserver greets AliceПерезагрузка без перезапуска
Заголовок раздела «Перезагрузка без перезапуска»node.resources.reload(name) перезагружает ресурс: его обработчики, таймеры и корутины
сбрасываются, а серверная половина выполняется заново с main.lua - в рабочем потоке, после того
как текущий обработчик вернёт управление. Ресурс может перезагрузить сам себя. У сервера нет
консольного ввода, поэтому запускайте перезагрузку из Lua - обычно командой чата, для которой
нужен ресурс chat:
node.commands.add("reload", function(player, args) local name = args[1] or "hello" if node.resources.reload(name) then player:tell("reloading %s", name) else player:tell("no resource named %s", name) endend, { role = "admin" })
-- expect-client: Alice chat:msg .*You are not allowed to use /reloadrole ограничивает команду игроками, чью роль на сессию другой обработчик назначил через
player:setRole("admin"); без этого /reload мог бы набрать кто угодно. Консоль подтверждает
строкой hello reloaded — lua. Два ограничения: состояние Lua теряется (то, что должно выжить,
держите в node.storage, которое сохраняется между перезагрузками и перезапусками), а клиентская
половина упаковывается один раз при запуске, поэтому изменение в client/ требует перезапуска
сервера. Без chat перезапускайте сервер - или дайте ресурсу один раз перезагрузить себя из
таймера, с флагом в node.storage, чтобы новый экземпляр этого не повторял; заодно это прогоняет
resourceUnload("reload") (Ресурсы → Перезагрузка).
Тестирование без игры
Заголовок раздела «Тестирование без игры»У сервера нет консольного ввода, и ничему на нём не нужен игрок: каждый серверный вызов работает
на одном сервере, а команду чата можно прогнать через контракт шины, на котором говорит chat
(События → node.bus). Второй ресурс - или несколько
строк в конце вашего собственного - публикует строку, которую набрал бы игрок, как chat:command
и смотрит ответ как chat:say:
-- the command under testnode.commands.add("hello", function(player, args, raw) player:tell("Hello, %s! You typed: %s", tostring(player.name), raw)end)
-- the test bench: watch what commands answer on the bus ...node.bus.on("chat:say", function(source, data) local reply = node.json.decode(data) node.log("%s says to pid %s: %s", source, tostring(reply.pid), reply.text)end)
-- ... and, half a second in, pretend that player 0 typed "/hello world"node.after(500, function() node.bus.emit("chat:command", { pid = 0, name = "hello", args = { "world" }, raw = "/hello world" })end)
-- expect: \S+ says to pid 0: Hello, nil! You typed: /hello worldname должно быть в нижнем регистре - chat приводит набранное игроком к нижнему регистру
перед публикацией, а прелюдия ищет обработчик по точной строке. Игрока 0 здесь нет, поэтому
player.name равно nil, а ответ никому не доставить; проверяется строка на шине. Всё остальное -
таймеры, node.storage, node.pg, node.http, перезагрузка, serverShutdown и resourceUnload
по Ctrl+C - работает одинаково с игроками и без; единственное, чему нужен клиент, - обработчик
сетевого события, потому что отправить его может только клиентский мод. Клиентскую половину вне
игры не запустить вовсе: сервер упаковывает файлы и проверяет их синтаксис, но не выполняет их, и
первое место, где видна ошибка времени выполнения, - beamng.log игрока.
Где видны ошибки
Заголовок раздела «Где видны ошибки»Серверные ошибки попадают в консоль и в logs/server.log под тегом Error, с именем ресурса и
местом, где произошла ошибка, и трассировкой стека:
hello · error in event 'hello:count': .../resources/hello/server/main.lua:16: attempt to concatenate a nil valueОшибка во время выполнения main.lua при загрузке сообщается так же, с путём к файлу; ресурс
всё равно считается загруженным - с теми обработчиками, которые успели зарегистрироваться до
сбойной строки (при синтаксической ошибке - ни с одним). Ошибка внутри обработчика никогда не
выгружает ресурс и никогда не отклоняет отменяемый запрос. Обработчик, занимающий рабочий поток
дольше 250 мс, отмечается строкой
plugin worker job stalled the thread for 300 ms (resource hello, event 'hello:count') — move heavy work to node.await/node.job:
каждый обработчик, колбэк таймера, отрезок корутины и колбэк завершения измеряется отдельно, а
строка называет ресурс и то, что зависло (до 1.2.1 измерялись только целые задания рабочего потока -
медленный колбэк таймера или отрезок node.async оставался незамеченным, - и ресурс не назывался);
см. Конкурентность.
Клиентские ошибки - в beamng.log игрока: файл, который не компилируется, пропускается с
Resource "hello" (main.lua): compile error: ... -- file skipped, а ошибка внутри обработчика
выглядит как Error in event handler for "hello:greet" from source "node.res/hello": .... Сервер
не выполняет клиентские файлы, но разбирает их при упаковке: файл, который не разбирается,
сообщается при загрузке строкой
hello · client file 'main.lua' has a syntax error: main.lua:1: unexpected symbol near '=' (the file ships anyway; the game's Lua will very likely refuse it too)
независимо от настройки обфускации (до 1.2.1 единственным признаком было предупреждение
Prometheus, а при выключенной обфускации его не было вовсе). Где лежит лог и как открыть
внутриигровую консоль диагностики - на странице
Устранение неполадок.
- Ресурсы - манифест полностью, порядок загрузки, клиентские файлы и
обфускация,
node.fs,node.storage,node.config. - События - каждый вид событий и как отклонить запрос.
- Конкурентность - таймеры, корутины и фоновые задания.
- Примеры:
chat- полный протокол сетевых событий,gatekeeper-example- отменяемые запросы,vehicle-cleanup-node.configи таймер политик.
