Script lifecycle
QuestScript loads individual files below config/questscript/scripts/. It supports JavaScript ESM .js/.mjs and Python .py. CommonJS .cjs/.cts and TypeScript do not run yet.
Loading
/qs load <path> immediately accepts and schedules creation of a dedicated
Graal environment and top-level evaluation on the server thread's reserved lifecycle lane. Command success
means "scheduled", not "already Loaded". A Script becomes a Loaded Script only
after its top-level code succeeds. Event subscriptions, timers, jobs, and other
resources continue to run while the Script is loaded.
The Script ID comes from the filename without its extension: it is lowercased and spaces become _. Directory names are not part of the ID.
QuestScript does not currently load Scripts automatically when the server starts.
Read current state through /qs lifecycle status or /qs lifecycle status <scriptId>. The command emits machine-readable JSON with loaded,
transitioning, and recentFailures arrays. Transition states are loading,
reloading, and unloading; a normal successful load becomes loaded, a
one-time run or unload terminates as completed, a failed operation becomes
failed, and a forcibly closed hung Script Run becomes quarantined. Lifecycle JSON uses schema version 2 and adds a nullable operation runId once a concrete Script Run is known, allowing load failures and quarantines to be correlated with threading diagnostics. Query a specific accepted operation by its returned operationId through /qs lifecycle operation <operationId>.
Top-level async limitation
Script Run has a synchronous public contract. QuestScript rejects top-level
await before publishing a Loaded Script and reports that deferred startup must
use system scheduling. In JavaScript, use system.run, system.runTimeout,
system.runInterval, or system.runJob; in Python use system.run,
system.run_timeout, system.run_interval, or system.run_job.
Unload and reload
/qs unload <scriptId> first closes admission for new invocations, then closes the environment and cleans the Script's resources on the server thread. /qs once <path> evaluates through the same lifecycle lane, closes the successful environment, and never publishes it into the Loaded Script set.
/qs reload <scriptId> closes the old environment before loading the same source again. If the replacement fails, the old environment is not restored and the Script ID remains unloaded.
A successfully initialized Script Run receives one final synchronous cleanup
entry through JavaScript system.beforeEvents.scriptUnload or Python
system.before_events.script_unload. The immutable event.reason
is unload, reload, serverShutdown, or oneTimeComplete. The signal is
delivered exactly once before native resource cleanup and Context close. A
Script Run Failure or an already quarantined Context skips user lifecycle code
and proceeds directly to native cleanup.
The lifecycle handler may read and write world.storage, read the world, and
perform direct synchronous World Mutation through typed QuestScript facades.
Ordinary validation, capability, and quota rules still apply. The handler
cannot cancel or defer teardown, use the scheduler or waitTicks/wait_ticks, create new
Script-Owned Resources, execute commands, or call another Script through
imports/exports. Promises and awaitables are not supported handler results. One
handler failure does not skip later handlers or native cleanup.
Unload, reload, quarantine, and server stopping cancel pending timers, jobs, continuations, events, and export invocations exactly once. During stopping, loads accepted before the shutdown barrier finish first; the runtime then unloads the resulting Loaded Script set in one pass, and rejects new loads after the barrier.
Errors
A top-level error or watchdog interruption never publishes a partially loaded Script. A successful non-destructive interruption of an ordinary callback keeps the Script loaded only after Context and lifecycle health checks pass. Failed interruption, an unhealthy Context, or repeated hangs closes and quarantines the whole Script Run; it must be loaded again before it can continue. Load, unload, and reload transitions for one Script ID do not run concurrently.
Modules and other Loaded Scripts
A Language Import (import in JavaScript or Python) organizes files that belong
to one Script. Until Script Bundles are implemented, a filesystem-backed Script
can only read modules from the shared config/questscript/scripts root:
JavaScript uses relative ./…/../… ESM specifiers, while Python can import
modules and packages located there. Root escapes, absolute outside paths,
symlink escapes, and file writes are denied. /qs eval receives no filesystem
and cannot import files; bundled language modules are not filesystem imports.
JavaScript importScript("script_id") and Python import_script("script_id")
read exports from another already Loaded Script and never load it implicitly; a
missing Script ID throws an error.
An import object is bound to one exact Script Run. A retained object does not
switch to a replacement environment with the same Script ID after unload/reload;
its next invocation fails. Arguments and results cross environments only as
primitives, approved Public Facades, or bounded immutable copies of arrays and
objects. A guest function or any other Graal Value cannot escape through this
boundary.
A Script Export call is synchronous: a Promise/awaitable argument or result is
not awaited and is rejected at the boundary between Script Contexts.
Current limitation: a Public Facade cannot yet be passed as an argument to a Script Export whose target Script is Python. Primitives and detached array/object copies work for all four language pairs, and a Public Facade returned to a Python caller receives the Python projection.
Cross-Script callables are registered explicitly only during top-level Script Run:
function startQuest(playerId) {
return true;
}
script.export(startQuest, { description: "Starts a quest" });
@script.export(description="Starts a quest")
def start_quest(player_id):
return True
script.export returns the original function. An anonymous function needs a
name; a duplicate name fails Script Run. Registration closes after the
top-level run. JavaScript ESM exports and Python globals/__all__ remain
Language Exports and are not published to another Script. Unrestricted
Polyglot bindings are unavailable inside a Script Context.