Жизненный цикл скрипта
QuestScript загружает отдельные файлы из config/questscript/scripts/. Поддерживаются JavaScript ESM .js/.mjs и Python .py. CommonJS .cjs/.cts и TypeScript пока не исполняются.
Загрузка
/qs load <path> немедленно принимает и планирует создание отдельного
Graal-окружения и выполнение top-level кода на server thread в зарезервированной lifecycle lane. Успех команды
означает «операция запланирована», а не «Script уже загружен». Скрипт считается
Loaded Script только после успешного завершения top-level кода. Подписки на
события, таймеры, jobs и другие ресурсы продолжают работать, пока скрипт загружен.
Script ID строится из имени файла без расширения: имя переводится в нижний регистр, а пробелы заменяются _. Каталоги не входят в ID.
QuestScript пока не загружает скрипты автоматически после запуска сервера.
Текущее состояние можно получить через /qs lifecycle status или
/qs lifecycle status <scriptId>. Команда печатает machine-readable JSON со
списками loaded, transitioning и recentFailures. Состояния переходов —
loading, reloading и unloading; успех обычной загрузки становится
loaded, однократное выполнение и выгрузка завершаются как completed,
неуспешная операция — как failed, а принудительно закрытый зависший Script Run — как quarantined. Lifecycle JSON использует schema version 2 и добавляет nullable runId к операции, когда конкретный Script Run уже известен; это позволяет связать load failure или quarantine с threading diagnostics. Конкретную принятую операцию можно найти по выданному operationId через /qs lifecycle operation <operationId>.
Ограничение top-level async
Script Run имеет синхронный публичный контракт. QuestScript отклоняет top-level
await до публикации Loaded Script и сообщает, что отложенный старт нужно
оформить через system scheduling. В JavaScript используйте system.run,
system.runTimeout, system.runInterval или system.runJob; в Python —
system.run, system.run_timeout, system.run_interval или system.run_job.
Выгрузка и перезагрузка
/qs unload <scriptId> сначала прекращает admission новых вызовов, затем закрывает окружение и очищает ресурсы скрипта на server thread. /qs once <path> выполняет файл через тот же lifecycle lane, закрывает успешное окружение и не публикует его в Loaded Script set.
/qs reload <scriptId> сначала закрывает старое окружение, затем загружает исходный файл заново. Если новая версия завершится ошибкой, старое окружение не восстанавливается и Script ID остаётся выгруженным.
Успешно инициализированный Script Run получает финальный синхронный cleanup-вход
через JavaScript system.beforeEvents.scriptUnload или Python
system.before_events.script_unload. Неизменяемое поле event.reason
равно unload, reload, serverShutdown или oneTimeComplete. Сигнал
доставляется ровно один раз перед нативной очисткой ресурсов и закрытием
Context. Script Run Failure и уже quarantined Context пропускают пользовательский
handler и сразу переходят к нативной очистке.
Lifecycle handler может читать и записывать world.storage, читать мир и
выполнять прямые синхронные World Mutations через типизированные QuestScript
facades. Обычные validation, capability и quota rules продолжают действовать.
Handler не может отменить или отложить teardown, использовать планировщик или
waitTicks/wait_ticks, создавать новые Script-Owned Resources, выполнять команды либо
обращаться к другому Script через import/export. Promise и awaitable не являются
поддержанным результатом handler. Ошибка одного handler не пропускает следующие
handlers и не отменяет нативную очистку.
Выгрузка, reload, quarantine и остановка сервера отменяют ожидающие таймеры, jobs, continuations, события и вызовы экспортов ровно один раз. При остановке операции загрузки, принятые до shutdown-барьера, сначала завершаются, после чего runtime одним проходом выгружает итоговый набор Loaded Scripts; новые загрузки после барьера отклоняются.
Ошибки
Ошибка или watchdog interruption top-level кода не публикует частично загруженный скрипт. Успешное non-destructive interruption обычного callback оставляет Script загруженным только после проверки Context и lifecycle-инвариантов. Неудачное interruption, нездоровый Context или повторные зависания закрывают и quarantines весь Script Run; для продолжения его нужно загрузить снова. Операции загрузки, выгрузки и перезагрузки одного Script ID не выполняются одновременно.
Модули и другие загруженные скрипты
Language Import (import в JavaScript или Python) организует файлы одного
скрипта. Пока Script Bundles не реализованы, файловый скрипт может только читать
модули из общего config/questscript/scripts: JavaScript использует относительные
ESM-specifier ./…/../…, а Python может импортировать находящиеся там модули и
пакеты. Выход за корень, абсолютные внешние пути, symlink escape и запись файлов
запрещены. /qs eval не получает файловую систему и не может импортировать
файлы; встроенные модули языка не считаются файловым импортом.
importScript("script_id") в JavaScript и import_script("script_id") в Python
обращаются к экспортам другого уже загруженного скрипта и не загружают его
автоматически; отсутствующий Script ID вызывает ошибку.
Объект импорта привязан к конкретному Script Run. Сохранённый объект не
переключается на новое окружение с тем же Script ID после unload/reload, а его
следующий вызов завершается ошибкой. Аргументы и результаты между окружениями
передаются как примитивы, разрешённые Public Facades или ограниченные
неизменяемые копии массивов и объектов; guest-функцию или другой Graal Value
вернуть через эту границу нельзя.
Вызов Script Export синхронный: Promise/awaitable в аргументе или результате не
ожидается и отклоняется на границе между Script Context.
Текущее ограничение: Public Facade пока нельзя передать аргументом в Script Export, если целевой Script написан на Python. Примитивы и отделённые копии массивов/объектов поддерживаются для всех четырёх языковых пар, а Public Facade, возвращённый вызывающему Python Script, получает Python-проекцию.
Cross-Script callable регистрируются явно только во время top-level Script Run:
function startQuest(playerId) {
return true;
}
script.export(startQuest, { description: "Запускает квест" });
@script.export(description="Запускает квест")
def start_quest(player_id):
return True
script.export возвращает исходную функцию. Для анонимной функции нужен
name; дубликат имени завершает Script Run ошибкой. После top-level регистрация
закрывается. JavaScript ESM exports и Python globals/__all__ являются только
Language Exports и не публикуются для другого Script. Unrestricted Polyglot
bindings недоступны в Script Context.