События и планирование
В JavaScript события доступны через world.beforeEvents, world.afterEvents,
system.beforeEvents и system.afterEvents; в Python — через
world.before_events, world.after_events, system.before_events и
system.after_events. Обработчик подписывается через subscribe и снимается
через unsubscribe.
Before и After
Before Events выполняются синхронно во время native-события Minecraft. Отменяемое событие содержит cancel. Такой
обработчик должен быть коротким; отложенную работу планируйте через system.
After Events сообщают уже произошедший результат и не могут отменить его. QuestScript сохраняет их входные данные как ограниченный snapshot и доставляет обработчики на server thread в финальной scripting-фазе тика. События группируются по типу; occurrences и подписчики внутри группы сохраняют порядок постановки и регистрации.
Snapshot playerJoin содержит стабильные playerId и playerName, как в Bedrock, но не live-объект игрока. Если игрок
ещё подключён, получите его отдельно через world.getPlayer(event.playerId) / world.get_player(event.player_id).
Асинхронный playerBreakBlock также сохраняет playerId и playerName; его поле player nullable, потому что игрок
может отключиться до отложенной доставки. В синхронном Before Event player остаётся обязательным.
И JavaScript-имя system.beforeEvents.scriptUnload, и Python-имя
system.before_events.script_unload обозначают специальное Script Lifecycle Event, а не обычное native Before Event.
Сигнал выполняется синхронно перед окончательной очисткой Script Run, не может отменить teardown и допускает прямые
World Mutations. Полный контракт и ограничения описаны в
жизненном цикле Script.
Планировщик
system.run(callback)— выполнить callback в ближайшей доступной финальной фазе; вызов из System task переносится на следующий тик;- JavaScript
system.runTimeout(callback, ticks)/ Pythonsystem.run_timeout(callback, ticks)— выполнить один раз после задержки; значение0может снова выполниться в текущем тике, даже из другого System task; - JavaScript
system.runInterval(callback, ticks)/ Pythonsystem.run_interval(callback, ticks)— повторять с интервалом; - JavaScript
system.runJob(generator)/ Pythonsystem.run_job(generator)— продвигать generator по шагам; - JavaScript
system.waitTicks(ticks)/ Pythonsystem.wait_ticks(ticks)— Promise/awaitable, завершающийся после числа тиков.
Если прошлый вызов interval ещё стоит в очереди или выполняется, очередная точка расписания пропускается. Это фиксированный контракт планировщика QuestScript. Все созданные скриптом подписки и задачи очищаются при его выгрузке.
clearRun/clear_run и clearJob/clear_job отменяют также callback или шаг generator, уже перенесённый в очередь
executor, если guest-выполнение ещё не началось. Уже начавшийся callback или шаг не прерывается. Для interval отмена
по-прежнему удаляет все будущие точки расписания, но не отменяет detached-асинхронную работу, которую callback уже
успел запустить.
Обычные callbacks планировщика и событий имеют синхронный контракт void/None. JavaScript callback может запустить
async-функцию, но QuestScript не ожидает возвращённый Promise, не обрабатывает его rejection и не связывает его с
clearRun/clear_run или подпиской. Для interval следующая итерация не ждёт такой Promise. Продолжение после
system.waitTicks обслуживается общей continuation queue как detached-работа. Обрабатывайте ошибки внутри самой
асинхронной задачи. В Before Event решение, включая cancel, должно быть принято до синхронного возврата обработчика.
Только API с явно объявленным Promise/awaitable callback-контрактом может ожидать его результат.
После обычной игровой логики runtime сначала обслуживает ограниченную lifecycle-очередь, затем готовые шаги
runJob/run_job,
Promise/awaitable continuations, System tasks и группы After Events. Continuations проверяются после каждого System task
и после каждой завершённой группы событий. Фаза и очереди ограничены; рекурсивный
runTimeout(..., 0)/run_timeout(..., 0) не получает бесконечный тик.
Обычный callback может быть перенесён, если мягкий бюджет тика закончился. После 10 тиков ожидания он попадает в overdue lane своего вида работы: за тик допускается не более 8 таких входов, и только пока остаётся общий мягкий бюджет. Это повышает приоритет, но не заставляет сервер безусловно выполнить весь накопившийся backlog в одном тике. Будущие регистрации планировщика и уже готовые единицы вместе ограничены 4096 слотами глобально и 512 на Script Run; ожидающая точка interval или шаг job считается отдельно от сохранённой регистрации. Новая работа сверх лимита явно отклоняется. Выгрузка, reload, shutdown или quarantine завершают оставшиеся результаты ошибкой ровно один раз.
Бюджет server thread
Обычный бюджет финальной scripting-фазы вычисляется из времени, уже потраченного игрой до этой фазы, и медианы последних 10 таких измерений с множителем 1.2. Целевой тик — 50 мс, обычной Script-работе выделяется от 15 до 35 мс, дополнительно сохраняется фиксированный резерв 1 мс. Если до финальной фазы уже прошло 40 мс, обычные callback в этом тике не стартуют; lifecycle cleanup сохраняет отдельный мягкий резерв 5 мс.
Мягкий дедлайн управляет только запуском следующей единицы. Уже запущенный JavaScript/Python callback не приостанавливается между тиками и может растянуть тик до завершения или hard-watchdog interruption. Поэтому эти настройки не являются гарантией тика короче 50 мс. В выбранном прототипе осталось 18 тиков длиннее 50 мс на 16 000 измеренных тиков; команда приняла это как остаточный риск. Предсказание длительности callback и отдельные бюджеты Script/entity отложены на пост-релизную доработку.
Оператор может переопределить стартовые значения JVM properties с префиксом questscript.serverThreadBudget.:
targetTickMillis, minScriptBudgetMillis, maxScriptBudgetMillis, fixedReserveMillis, gameHeadroomMultiplier,
historyWindowTicks, emergencyPreFinalCutoffMillis, lifecycleReserveMillis, overdueAfterTicks,
maxOverdueEntriesPerKindPerTick, maxLifecycleQueue, maxOrdinaryQueue, maxOrdinaryQueuePerScript,
maxAfterEventOccurrences, maxAfterEventOccurrencesPerScript, maxLifecycleEntriesPerTick и
maxGuestEntriesPerTick. Некорректная комбинация отклоняется при запуске. Скрипт не может менять эти значения.
Для обычной проверки используйте /qs diagnostics threading [scriptId]: команда уровня оператора 2 возвращает
ограниченный JSON snapshot текущего бюджета, очередей, overdue-возраста, interval skips, overflow, watchdog и lifecycle
failures с привязкой к Script Run. Глобальный ответ включает не более 64 Script Run и 64 примеров ожидающей работы, а
каждый Script Run — не более 32 interval; поля *Truncated показывают усечение. Snapshot не входит в Graal Context и не
содержит исходный код, аргументы, event payload или результаты.
Для глубокой записи включите -Dquestscript.optionA.diagnostics=true. JFR events questscript.OptionAPhase,
questscript.OptionAAdmission и questscript.OptionAGuestUnit показывают рассчитанный бюджет, входные измерения,
emergency state, глубину очередей, возраст отложенной работы, overflow, Script Run и watchdog outcome. Exact trace
recorder нужен для восстановления полной временной шкалы тиков. Мягкое исчерпание admission-бюджета означает перенос ещё
не начатой работы; hard watchdog означает interruption уже запущенной единицы и не возвращает время, потраченное в
текущем тике.
Глобальные JavaScript-функции setTimeout, setInterval, clearTimeout и clearInterval отсутствуют. Все задержки
задаются в тиках через JavaScript system.runTimeout, system.runInterval, system.waitTicks или Python
system.run_timeout, system.run_interval, system.wait_ticks; значение определяет самый ранний
подходящий тик, а не точный wall-clock момент. Ожидание не блокирует server thread.
Script Events
/scriptevent <namespace>:<path> [message] доставляет broadcast-событие через JavaScript
system.afterEvents.scriptEventReceive или Python system.after_events.script_event_receive.
Namespace — выбранный автором канал сообщений. Он не выводится из Script ID, не указывает отправителя и не зависит от
способа загрузки Script. Если необязательный message не передан, обработчик получает пустую строку.
Сейчас публиковать такие события можно только операторской командой
/scriptevent; Script-side метода JavaScript system.sendScriptEvent / Python system.send_script_event ещё нет.
Полный идентификатор остаётся в event.id, а namespaces сравнивает только
часть до :. В JavaScript передавайте options object:
signal.subscribe(handler, { namespaces: ["arena"] }); в Python — keyword:
signal.subscribe(handler, namespaces=["arena"]). Такой фильтр принимает
arena:round_start и arena:admin/reset, но не lobby:round_start. Пропущенный
фильтр или namespaces: null / namespaces=None принимает все корректные
Script Events; явный пустой список не принимает ни одного.
Поля JavaScript sourceEntity, sourceBlock, dimension и соответствующие поля Python source_entity, source_block,
dimension содержат QuestScript Public Facades (Entity, Block и Dimension), а не нативные Java-объекты Minecraft.
Если живой source отсутствует, соответствующее поле равно null; стабильные JavaScript-поля id, sourceKey,
sourceEntityId, position и Python-поля id, source_key, source_entity_id, position остаются доступны. Поле
Entity ID содержит UUID Entity-источника и равно null для остальных source types.
Script Event ID использует Minecraft-форму <namespace>:<path> с явными непустыми частями. Некорректный ID отклоняется
до публикации события.
Точный список доступных событий и DTO смотрите в API reference.