Linux-установщик на Bash помещает исполняемый файл zevvoryn и запускаемый
лаунчер run_server.sh в выбранную папку сервера. Запускайте его командой
./run_server.sh; мастер первого запуска появится в терминале. В конце
установщик напечатает папку установки и команду запуска. В сборке с поддержкой
плагинов мастер спросит, включать ли их,
а затем позволит выбрать C++, Luau и Java/JVM по отдельности. Рантайм, который
не включён при сборке сервера, нельзя активировать одним флагом конфига.
Linux-лаунчер устанавливает рабочую папку в каталог сервера, поэтому
settings.properties создаётся там. При прямом запуске бинарника уже
существующий settings.properties рядом с ним имеет приоритет;
если файла ещё нет, мастер первого запуска сохраняет его в текущую рабочую
папку. Чтобы не засорять корень исходного репозитория мирами, логами и конфигом,
запускайте сервер из отдельной папки установки.
Если мастер уже пройден, поменяйте эти параметры в settings.properties и
перезапустите сервер:
enable-plugins=true
enable-cpp-plugins=false
enable-luau-plugins=true
enable-jvm-plugins=false
Помещайте плагины в каталог соответствующего runtime внутри папки сервера:
plugins/cpp/<id>/, plugins/luau/<id>/ или plugins/jvm/<id>/. Консоль
показывает результат загрузки каждого плагина; /plugins выводит статусы,
/reload перезагружает плагины, /stop корректно останавливает сервер.
Используйте только доверенные плагины и перед включением нативного C++ или Java
кода прочитайте раздел о безопасности ниже.
Доступность: эта публикация на GitHub содержит только документацию и не добавляет runtime плагинов в публичный бинарник сервера. Инструкции применимы к сборкам, в которые включён соответствующий runtime.
Эта публикация содержит документацию, но не исходный код plugin runtime.
Нативные плагины устанавливаются рядом с сервером пакетами
plugins/cpp/<каталог>/plugin.json и entry-модулем (.dll на Windows, .so на
Linux). Рабочий код — в src/plugins/, тесты — в DevInfo/tools/tests/;
исходники и fixtures не создают plugins/ в корне проекта. Сборочные примеры
живут в отдельном build-каталоге; копируйте готовый пакет в папку установленного
сервера или в отдельную тестовую среду, не в DevInfo/.
Импортируемый символ — zevvoryn_plugin_init, тип ZevvorynPluginInitV1.
Windows использует LoadLibraryW/GetProcAddress, Linux — dlopen/dlsym;
символ экспортируется через ZEVVORYN_PLUGIN_EXPORT. Внешний ABI не переносит
STL или исключения. Нативные модули остаются отображёнными до конца процесса,
так как реестр команд может хранить указатели на их callbacks.
Несовместимый ABI-модуль используется только тестом и собирается отдельно в
plugin-test-fixtures/, вне runtime-каталога.
Загрузчик принимает только пакеты с plugin.json, проверяет runtime, диапазон
API, относительный entry-путь и наличие файла. При запуске по каждому кандидату
печатается ENABLED или FAILED с причиной;
/plugins выводит список состояний. /reload отключает и повторно включает
C++-плагины; /stop остаётся корректной остановкой. Падение нативного кода
может завершить весь процесс: crash context указывает активный плагин, но это
не изоляция в отдельном процессе.
Для C ABI доступен submit_background(work, workUser, completion,
completionUser): одна фоновая очередь с лимитом 4096 задач выполняет work на
worker thread и completion на основном tick thread. Worker не должен читать или
изменять мир и не должен синхронно ждать completion. Перед on_disable runtime
дожидается worker-задач и доставляет результаты, чтобы plugin-owned task data
можно было безопасно освободить. Этот механизм сам по себе не выдаёт разрешений
на сеть или VFS; нативные C++-модули остаются доверенным кодом.
Главный поток плагинов привязывается к потоку, который действительно
выполняет серверный tick (он может отличаться от потока создания сервера);
completion фоновых задач и изменения мира не выполняются на UI/startup-потоке.
Синхронные команды и cancellable-события, пришедшие не с главного потока,
проходят через отдельную ограниченную очередь (4096 записей): переполнение
отклоняет команду, а событие отменяется по принципу fail-closed. Подписки,
добавленные из callback, начинают участвовать со следующего dispatch.
plugins/cpp/<id>/ в папке установленного сервера. Это
runtime-каталог установленной сборки или отдельная тестовая среда, а не
корень исходного репозитория или DevInfo/. Положите туда plugin.json и библиотеку (.dll
Windows / .so Linux). Исходники
своего плагина можно хранить отдельно; используйте заголовок API из сборки
сервера и соблюдайте описанную ниже ABI-версию.zevvoryn_plugin_init и заполните дескриптор ABI. ABI-границу
составляют только C-типы, function pointers и opaque void*; не передавайте
std::string, STL-контейнеры, классы C++ и исключения между DLL/SO и сервером.Минимальная заготовка:
#include "plugin_api.hpp"
namespace {
int32_t onLoad(void*) { return 1; }
void onTick(void*, double) {}
void onDisable(void*) {}
}
extern "C" ZEVVORYN_PLUGIN_EXPORT int32_t zevvoryn_plugin_init(
const ZevvorynHostApiV1* host, ZevvorynPluginApiV1* plugin) {
if (!host || !plugin || host->abi_version != ZEVVORYN_PLUGIN_ABI_VERSION ||
host->struct_size < sizeof(ZevvorynHostApiV1)) return 0;
plugin->abi_version = ZEVVORYN_PLUGIN_ABI_VERSION;
plugin->name = "hello-native"; // Должно совпасть с plugin.json
plugin->version = "1.0.0"; // Должно совпасть с plugin.json
plugin->on_load = &onLoad;
plugin->on_tick = &onTick;
plugin->on_disable = &onDisable;
return 1;
}
Сборка Linux из корня репозитория (выходной файл сразу в отдельной папке пакета установленного сервера):
c++ -std=c++20 -fPIC -shared -fvisibility=hidden -Isrc hello.cpp \
-o /path/to/server/plugins/cpp/hello-native/hello_plugin.so
Для Windows используйте MSVC /LD и /I<repo>\src, выходной файл назовите
hello_plugin.dll. Не копируйте сгенерированный runtime-пакет в корень
исходного репозитория.
После on_load сервер вызывает on_enable; затем on_tick на главном потоке,
on_disable при /reload или остановке и on_unload при завершении. Для
команды используйте host->register_command, для игрового хука —
host->subscribe_event или host->subscribe_event_filtered. Право
commands.register нужно для команд; blocks.read — для подписки на событие и
чтения блока; blocks.modify дополнительно нужно для отмены ломания. Пример
Полный пример C++-плагина должен покрывать команды, фильтры событий,
generation-safe handles, доступ к миру и фоновые задачи.
После сборки создайте plugin.json по схеме ниже, перезапустите сервер и
проверьте строку [Plugins] ENABLED hello-native .... При отказе загрузки
консоль покажет точный плагин и причину; /plugins покажет статусы.
classDiagram
class IPluginRuntime {
<<interface>>
+initialize(error)
+loadPlugin(path, error)
+unloadPlugin(name)
+tick(deltaSeconds)
+shutdown()
}
class PluginManager {
+loadDirectory(path, registry)
+enableAll()
+disableAll()
+statuses()
+dispatch(event)
}
class LuauRuntime {
+loadDirectory(path)
+dispatch(event)
+shutdown()
}
class JvmRuntime {
+loadDirectory(path)
+tick(deltaSeconds)
+shutdown()
}
class EventDispatcher {
+subscribe(typeId, priority, callback, filter)
+unsubscribe(token)
+dispatch(event)
}
class PluginTaskScheduler {
+submit(work, completion)
+runCompletions()
+waitIdleAndDrain()
+shutdown()
}
class EntityWorldApi {
+isValid(EntityId, Generation)
+getBlock(dimension, x, y, z)
}
class PluginManifest {
+name
+version
+runtime
+apiRange
+permissions
}
IPluginRuntime <|.. PluginManager
IPluginRuntime <|.. LuauRuntime
IPluginRuntime <|.. JvmRuntime
PluginManager --> EventDispatcher
PluginManager --> PluginTaskScheduler
PluginManager --> EntityWorldApi
PluginManager --> PluginManifest
LuauRuntime --> EventDispatcher
JvmRuntime --> EventDispatcher
stateDiagram-v2
[*] --> Discovered
Discovered --> Loaded: manifest, ABI, permissions accepted
Discovered --> Failed: invalid manifest / load / API mismatch
Loaded --> Enabled: on_load + on_enable succeed
Loaded --> Failed: lifecycle callback fails
Enabled --> Enabled: tick / events / commands
Enabled --> Disabled: /reload or /stop
Disabled --> Enabled: /reload, on_enable
Unloaded --> Discovered: /reload, scan package again
Enabled --> Unloaded: explicit unload / shutdown
Disabled --> Unloaded: shutdown
Enabled --> Failed: callback error
/reload intentionally retains native module mappings and command stubs: the
registry currently has no safe command-removal contract, so reinitializing a
module could leave stale callback pointers or duplicate subscriptions. It runs
disable/enable without adding duplicate event subscriptions. /stop runs
on_disable and on_unload while the main tick thread and world still exist.
For JVM and Luau, /reload closes the old class loader/VM states, removes event
subscriptions, and loads the package again; native code stays mapped because
the command registry has no safe API for removing plugin command callbacks.
Luau включается -DZEVVORYN_ENABLE_LUAU=ON (закреплённый upstream Luau 0.742);
по умолчанию выключен. Скрипты .luau загружаются из runtime-каталога
plugins/luau/, каждый получает отдельный VM; os, io, debug, package,
require и загрузчики файлов удалены. Функции readFile(path) и
writeFile(path, contents) появляются только при одобрении vfs.read и
vfs.write, работают в plugins/data/<name>/ и ограничивают размер одного
файла 4 MiB. Скрипт entry возвращает таблицу хуков; callback
onPlayerBlockBreak получает поля playerId, x, y, z, методы
isCancelled() и cancel().
Watchdog проверяет safepoints с бюджетом 250 000 шагов / 20 ms; каждый VM также
ограничен 32 MiB памяти. Это не прерывает нативный код операционной системой.
VFS-нормализатор проверяет относительность, нормализацию и symlink resolution
через std::filesystem до открытия файла.
{
"schema_version": 1,
"name": "example-luau",
"version": "1.0.0",
"runtime": "luau",
"entry": "main.luau",
"api": { "min": 1, "max": 1 },
"permissions": {
"chat.read": false,
"chat.send": false,
"blocks.read": false,
"blocks.modify": false,
"entities.read": false,
"commands.register": false,
"network.access": false,
"vfs.read": false,
"vfs.write": false
}
}
permissions принимает массив известных строк или объект булевых флагов. Это
запрос плагина, а не автоматическое разрешение. Администратор одобряет права в
plugins/permissions.json, например:
{
"hello": ["commands.register", "blocks.read", "entities.read"],
"moderation": ["blocks.read", "blocks.modify"]
}
Без файла одобрено ноль прав; плагин получает только пересечение запрошенных и
одобренных. Регистрация команд требует commands.register; хук ломания блока —
blocks.read, отмена события дополнительно требует blocks.modify. Права на
entities.read открывает проверку дескриптора сущности через пару
entity_id + generation; устаревший handle после отключения игрока не проходит
проверку. C ABI также предоставляет get_block(dimension, x, y, z, out) при
одобренном blocks.read (0 Overworld, -1 Nether, 1 End). Оба метода
работают только на серверном tick thread; для недоступного измерения или
неверного потока возвращают отказ. Права на чат и сеть пока не дают возможностей,
так как API для них ещё не открыто.
Нативный ABI также поддерживает subscribe_event_filtered: лёгкий C-фильтр
получает компактный event view, а при результате false основной callback
плагина не вызывается. Событие ломания блока несёт исходный protocol ID игрока
и отдельный generation-safe player_entity_id + player_generation для новых
плагинов; Luau получает те же поля. Регистрация событий всё равно требует
blocks.read.
JVM-runtime выключен по умолчанию и включается при сборке через
-DZEVVORYN_ENABLE_JVM=ON (нужны JDK и JNI). Для запуска также нужна доступная
JRE/JDK с server VM; при переносе бинарника на другой хост задайте JAVA_HOME.
CMake собирает API JAR и пример
в runtime-каталог рядом с сервером: plugins/jvm/api/ и
plugins/jvm/example/. Не кладите сборочные артефакты в корень репозитория.
Для onPlayerBlockBreak пример вызывает
context.subscribePlayerBlockBreak() в onLoad; запрос требует выданное
администратором blocks.read. Регистрация доступна на главном потоке сервера;
из собственного Java-потока вызов отклоняется.
Минимальный класс реализует dev.zevvoryn.api.ZevvorynPlugin:
package com.example;
import dev.zevvoryn.api.*;
public final class HelloPlugin implements ZevvorynPlugin {
private PluginContext context;
public void onLoad(PluginContext context) {
this.context = context;
context.log("Hello from Java!");
}
public void onPlayerBlockBreak(PlayerBlockBreakEvent event) {
if (event.x() == 0 && event.canCancel()) event.setCancelled(true);
}
}
Соберите plugin JAR против plugins/jvm/api/zevvoryn-plugin-api.jar (например,
javac -classpath <api-jar> -d <build-classes> HelloPlugin.java, затем
jar --create --file plugin.jar -C <build-classes> .). Файл JAR и манифест
расположите в отдельном runtime-пакете plugins/jvm/<plugin-id>/; это каталог
установленного сервера, не исходников проекта. main_class — полное имя
класса, entry — относительный путь к JAR:
{
"schema_version": 1,
"name": "hello-java",
"version": "1.0.0",
"runtime": "jvm",
"entry": "plugin.jar",
"main_class": "com.example.HelloPlugin",
"api": { "min": 1, "max": 1 },
"permissions": ["blocks.read", "blocks.modify", "entities.read"]
}
Методы onLoad, onEnable, onTick, onPlayerBlockBreak, onDisable и
onUnload вызываются последовательно на серверном потоке. Доступное API:
PluginContext.log, isEntityValid(id, generation) и getBlock(dimension,x,y,z);
чтение сущностей/блоков требует соответственно entities.read и blocks.read.
Событие PlayerBlockBreakEvent передаёт компактное представление координат,
protocol player id и generation-safe entity handle; отмена реально применяется
только с blocks.modify. Ошибка/исключение фиксируется с именем конкретного
плагина и отключает его callback.
JNI/JVM плагины считаются доверенным исполняемым кодом администратора.
Разрешения ограничивают только возможности, выданные через Zevvoryn API; это не
ограничивает Java-код, который использует java.io, сокеты, reflection,
threads, JNI или собственные библиотеки. Современный Java Security Manager
не является песочницей для этого runtime: API deprecated for removal, а в JDK
24 он permanently disabled; поэтому мы не включаем его и не обещаем изоляцию.
Если автор плагина не полностью доверенный, не запускайте его в том же
процессе: используйте отдельный OS user/container/VM с ограничением файлов,
сети, CPU и памяти.
Log4Shell — не «уязвимость JVM» и не разрешение-плагин: это уязвимость в затронутых версиях Apache Log4j 2, связанная с обработкой JNDI lookup. Если плагин добавляет Log4j или другие зависимости, проверяйте весь transitive dependency tree/SBOM, используйте поддерживаемые исправленные версии и не логируйте недоверенные данные через небезопасные lookup/configuration. Этот runtime сам по себе не встраивает Log4j и не может исправить уязвимую библиотеку, упакованную разработчиком плагина. Поскольку исполняемый плагин считается доверенным и разделяет адресное пространство, он может обходить permissions matrix; одобрение администратора обязательно.
Справка: Security Manager в JDK 24, Apache Logging Services: политика и известные уязвимости.
Runtime можно отключить в settings.properties: enable-plugins=false
отключает все плагины, enable-luau-plugins=false отключает только Luau, а
enable-jvm-plugins=false — только JVM. Для изолированного набора проверок используйте
-DZEVVORYN_PLUGIN_TESTS_ONLY=ON -DBUILD_TESTING=OFF; полная старая
BUILD_TESTING=ON конфигурация в текущем checkout ссылается на отсутствующие
тестовые исходники вне подсистемы плагинов.