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

Жизненный цикл скрипта

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.