Неизвестные поля manifest являются ошибкой установки. Ошибки валидации показываются в интерфейсе и не игнорируются молча.
Ограничения
Нет native code, файлов и сети. Используются только Lua и декларативный JSON.
Важно: UI callback должен быть глобальной функцией по имени. Например function openPage() ... end, а не локальной функцией.
Свойства UI допускают только scalar-значения: number, string, bool, а также children и row. Вложенные style-объекты не используются.
Хранилище плагина является app-wide. Каждый ключ должен начинаться с mnd.questId, иначе возможно пересечение данных между квестами.
02Структура плагина
Плагин является папкой с manifest и Lua entry-файлом.
my-plugin/
├── plugin.json
└── main.lua
plugin.json
Manifest плагина. Описывает идентификатор, версию, entry, permissions, hooks, content types и slots.
main.lua
Логика плагина. Файл, который указан в поле entry.
Плагин описывает намерение, а движок выполняет команды и отрисовывает UI. Host написан на Rust и существует один на процесс. Установка плагина является свойством приложения, а не отдельной игровой сессии.
Наружу передаётся JSON-список команд из фиксированного набора: toast, render, close, goto_node, play_sound, stop_audio, diff переменных и snapshot storage.
03plugin.json
Все разрешённые поля manifest.
Поле
Тип
Обяз.
Описание
id
string
да
Минимум 2 сегмента lowercase Latin через точку. Например my.stuff.counter.
name
string
да
Человекочитаемое имя.
version
string
да
Любая непустая строка.
sdk
string
да
Точно "1".
entry
string
да
Относительный путь к .lua. Нельзя использовать .., абсолютный путь или \.
Разрешение проверяется при каждом соответствующем вызове mnd.*.
ui
mnd.ui.toast, mnd.ui.render, mnd.ui.close.
quest
mnd.vars.*.
navigation
mnd.game.go(node).
audio
mnd.audio.play, mnd.audio.stop.
storage
mnd.storage.*.
events
mnd.emit(name, payload).
Без нужного permission Lua получает ошибку. Она попадает в dispatch errors и debug panel. Filesystem, network и shell отсутствуют и физически недоступны.
Plugin-defined custom event, который создаётся только через mnd.emit.
Способы обработки
mnd.on
Подписка внутри плагина через mnd.on(event, function(payload) end).
Global handlers
on_quest_start, on_node_enter, on_item_reveal и другие.
Просто перечислить hook в manifest недостаточно для логики: hook нужен, чтобы core вызывал плагин для соответствующего события. UI callback onPress и другие callback resolve-ятся по имени глобальной функции и получают один аргумент.
08Render targets
mnd.ui.render(target, spec)
slot:<id>
Slot, объявленный в manifest. Отсутствующий slot является ошибкой.
item:<id>
Контент конкретного item с type: 'plugin'. Живёт ровно одну ноду и удаляется при переходе.
screen
Полноэкранный modal поверх slots. Сохраняется между нодами до закрытия. В приложении может быть только один screen; последний plugin выигрывает.
Slot areas hud, overlay, drawer, menu монтируются на игровом экране. Пустая область ничего не рисует.
09UI
Все 22 типа виджетов, доступные в SDK 1.
column
row
stack
box
card
list
grid
scroll
text
heading
caption
badge
icon
image
input
button
chip
switch
slider
progress
divider
spacer
Контейнеры
column, row, stack, box, card, list, grid, scroll могут иметь children.
list / grid
Дополнительно поддерживают row как шаблон одного элемента. Данные находятся в items.
10UI properties
Свойства распределены по поддерживаемым типам.
Общие и контейнеры
text
Для text, heading, caption, badge, chip label.
label
Для button, chip, switch, slider, badge. Button также понимает text.
size
Для text, icon, badge, spacer. pt/px.
color
Имя цвета или #RRGGBB / #AARRGGBB.
background
Имя цвета или #RRGGBB / #AARRGGBB.
border
Имя цвета или #RRGGBB / #AARRGGBB.
radius
Доступно везде, кроме card, box, stack.
padding
Доступно везде, кроме card, box, stack.
opacity
Доступно везде, кроме card, box, stack.
width / height
Leaf widgets. card, box, stack игнорируют width.
Text
weight
100…900 или 'bold'.
bold
Для text.
maxLines
Для text.
Layout
align
Containers и children stack: start, end, center, stretch, top, bottom, topStart, bottomEnd.
justify
column/row: center, end, space.
gap
column, row, list, grid, card. Значение в px.
fill
column: true означает занять всю высоту.
expand
Любой node внутри column/row: true → Expanded.
list / grid
items
Literal array или '{name}'.
columns
grid: от 1 до 6.
ratio
grid: aspect ratio ячейки.
cellWidth
Минимальная ширина ячейки в px для adaptive columns.
icon / image
icon
Для icon, button, chip, image. Имя из конечного списка.
Callback получает { value = 'typed', args = '<from spec>' }. Если args был object, его keys merge-ятся с value.
value является one-way binding: пока input не в focus, spec управляет значением; во время набора client не трогает значение и cursor. Не следует делать render на каждый onChange, потому что focus теряется.
13Adaptive grid
Renderer адаптирует сетку через cellWidth.
Plugin не видит ширину viewport и не имеет media queries. Renderer адаптирует grid через cellWidth.
{
type = 'grid',
items = '{bag_items}',
columns = 5,
cellWidth = 104,
gap = 8,
ratio = 0.72,
row = {
type = 'button',
label = '{name}',
icon = '{icon}',
args = '{key}',
onPress = 'examine'
}
}
Формула
actual columns = floor((width + gap) / (cellWidth + gap)), максимум columns, минимум 1.
Без cellWidth
Используется ровно columns, поэтому на mobile cells могут сжиматься.
Если button не помещается, renderer убирает icon и оставляет label с ellipsis.
14Content types
Новые типы node content, которых нет встроенными в engine.
Примеры назначения: combination lock, diary, radio. Поля являются form/data, а не code.
Content type появляется в add content на node и может быть размещён в row/column/stack.
Незаполненные поля используют default. Без default: text = пустая строка, number = min/0, bool = false, select = первый option.
Плагин определяет item через pluginTypeId. Заполненные поля находятся в payload.item_json.pluginData во время item_reveal.
15Лимиты
Ограничения одного plugin pass и UI specification.
commands per call64
log records per call100
UI nodes400
spec depth24
properties per node22
children / array length64
string length2000
payload64 KiB
storage keys256
storage value size16 KiB
Lua memory per plugin16 MiB
Lua instructions per call5,000,000
mnd.emit nesting4
При превышении лимита возникает ошибка для плагина в этом pass. Quest продолжается, game loop не падает. Instruction budget защищает от бесконечных циклов, замораживающих игру.
16Минимальный рабочий плагин
Полный пример из SDK.
plugin.json
{
"id": "my.counter",
"name": "Счётчик",
"version": "1.0.0",
"sdk": "1",
"entry": "main.lua",
"description": "Считает открытия сцены и показывает счётчик в углу.",
"author": "Ваше имя",
"permissions": ["ui", "quest", "storage"],
"hooks": ["quest_start", "node_enter"],
"slots": [
{ "id": "counter", "area": "hud", "priority": 30 }
]
}
main.lua
local function storageKey()
return tostring(mnd.questId) .. '.counter'
endlocal function current()
local value = mnd.vars.get('visits')
if type(value) == 'number' then return value end
if type(value) == 'string' then return tonumber(value) or 0 end
return 0
end
local function render()
mnd.ui.render('slot:counter', {
type = 'card',
align = 'stretch',
background = '#1B1B22',
border = '#3A3A46',
radius = 16,
padding = 12,
gap = 6,
children = {
{
type = 'badge',
label = 'Сцена',
background = '#26262F',
color = 'gray',
radius = 8
},
{
type = 'heading',
text = 'Открытий: {visits}',
color = 'white'
},
{
type = 'button',
label = 'Забыть',
variant = 'outline',
color = 'white',
border = '#3A3A46',
onPress = 'reset'
}
}
})
end
function on_quest_start()
if not mnd.vars.has('visits') then
mnd.vars.set('visits', mnd.storage.get(storageKey()) or 0)
end
render()
end
function on_node_enter()
mnd.vars.set('visits', current() + 1)
mnd.storage.set(storageKey(), current())
render()
end
function reset()
mnd.vars.set('visits', 0)
mnd.storage.set(storageKey(), 0)
mnd.ui.toast('Счётчик сброшен', 1500)
render()
end
17Частые ошибки
Ошибки, которые отдельно описаны в SDK.
1
Локальный callbackonPress='openPage' с local function openPage() не работает. Handler должен быть глобальным.
2
Неверное поле manifestНапример authorName вместо разрешённого поля приводит к ошибке установки.