Run lifecycle
A run is one start()…stop(). It owns a world — the Tsum object, the
page router's binding, a scheduler with a table of jobs — and it dismantles
that world itself when it ends.
start()
- Pick the log catalogue for the run's language and open a new
runId, so every line from here on carries it. - If a run is already active,
stop()it first. If it will not go, logrun.startBusyand refuse rather than build a second world over the first. - Broadcast
run.startedto outside tooling. buildRun(settings): constructts = new Tsum(...), copy every setting onto it, attach the page router, reset the fever and Lorcana watchers, build aTsumTaskControllerand register one job per row ofrunTaskTable(settings).- Yield once so a
stop()that arrived mid-build can set its flag, thencontroller.start()— which does not return until the loop ends. - In a
finally,endRun(): cleargRunActive, stop the controller, detach the router, dropts, broadcastrun.stopped, close therunId.
loading...
The scheduler
TsumTaskController is a cooperative scheduler: on a short fixed tick it takes
the first job that is due and runs it to completion. A long job like a round
blocks the loop for the whole round; nothing else runs meanwhile. That is the
model — no timers, no workers, every native call synchronous — and it is why
async/await buys nothing here.
Which jobs there are is runTaskTable in runPlan.ts, a table compiled into
both the game bundle and the settings page, so the Run order card shows exactly
what a run will register:
loading...
When more than one job is due, the order is JobPriority (lowest first), and
every job's priority is distinct, so the order is the table's and nothing
else's. One-shot sweeps queued by a Now button go first; the app restart
next, so the chores after it run on a fresh app; then the coin-spending sweeps,
the mailbox, the hearts, and the round last, because it is the job that never
finishes early.
Repeated throws from one job restart the game app — counted per job, so a healthy round cannot mask a chore that throws every time.
settings-run-order-cardstop(), on a second thread
start() is still on the stack while the run goes, so every stop() arrives
on another thread. The host dispatches each runScript on its own pool
thread, and the engine hands the interpreter lock over at every sleep().
stop() therefore tears nothing down. It calls requestStop() — set
gStopRequested, clear ts.isRunning, remove every task and stop the
controller — and then waits, up to StopWaitMs, for gRunActive to clear. The
running task notices isRunning at its next loop boundary, the controller's
loop returns, and start()'s finally does the dismantling on the thread that
owns the world.
The rule that holds this together, and the one to keep in mind before moving
any of it: a run dismantles its own world. Clearing ts or detaching
gPages from inside stop() pulls the world out from under a task that is
still using it. index.ts's header comment says why at length.
requestStop() is also what a task body may call when it decides the run
should end — the Max Round Duration cap's "stop the script" action does — so a
task never has to know about threads.
Pause is not stop
Opening the settings panel or the Quick Bar pauses the run: the host sets
a flag that parks sleep() and every touch injector, so the script freezes
where it is. onPause() (quickbar.ts) is evaluated once after that flag is
set, and presses the game's own Pause button if a round is running, so the
round's clock stops too. Closing the panel resumes: onResume() runs before
the flag lifts, presses Continue and waits for the board. It has to come first
because the script is parked inside its next touch, often mid-chain, and that
touch would otherwise land on the pause menu.
Play with the panel open is the one thing that ends a run: it sends a fresh
start() with the settings on screen.
Rounds inside a run
The play job (taskPlayGameQuick) opens a round, plays it, and returns; the
scheduler calls it again shortly after. Each round gets a roundId that every
round.* event and log record carries. Play loop is the round.