Lifecycle and State
Purpose
This report defines the runtime model for the N3BuilderF2P port. The full development plan owns product scope, phases, and acceptance gates.
The old builder runs AIO_F2P_AllSkillsCore.onLoop() on DreamBot's script thread. Its module states often dispatch one action and block inside Sleep.sleepUntil(...) before returning a delay. RuneLite event subscribers run on the client thread. Blocking that thread prevents normal client processing.
The port must yield after one state evaluation and resume on a later GameTick or routed event.
Target lifecycle
Use one native RuneLite plugin and one controller:
startUp()constructs stopped controller state, UI, overlay, and hotkey registrations. It registers the plugin with the Break Handler.- A user action starts the AIO. Starting builds one top-level workflow and calls
breakHandler.startPlugin(this). onGameTickchecks login and break state, pulses the controller once, updates the immutable snapshot, and manages the shared input lock.- A due break calls
breakHandler.startBreak(this). The plugin stops pulsing gameplay work and releases its input lock until the break ends. - A user stop cancels the active workflow and only the Walker handle owned by the AIO, clears transient state, calls
breakHandler.stopPlugin(this), and releases the input lock. shutDown()performs the same cleanup and unregisters UI, hotkey, overlay, and Break Handler resources.
Keep startup and shutdown idempotent. Do not auto-enable or auto-start the AIO during normal RuneLite startup.
AutomationLoop ownership
Build the loop through an existing factory:
machine = TypesafeCarouselStateMachine
.builder(AioState.class, "aio_builder")
.owner(this)
.initial(AioState.SELECT_TASK)
.on(AioState.SELECT_TASK, this::selectTask)
.on(AioState.PREPARE, this::prepareTask)
.on(AioState.EXECUTE, this::executeTask)
.on(AioState.TRANSITION, this::finishTask)
.on(AioState.BLOCKED, this::blocked)
.build();
loop = AutomationLoop.fromStateMachine(
AutomationLoopConfig.builder(this)
.workflowId("aio_builder_" + UUID.randomUUID())
.tickSupplier(client::getTickCount)
.build(),
machine);
The controller calls loop.pulse() once for each new game tick. AutomationLoop supplies same-tick deduplication, snapshots, terminal state, and any supervisor or break gates supplied through its configuration. Do not add an executor, timer, or second gameplay loop.
The workflow ID must distinguish concurrent or restarted sessions without retaining raw account names. Use an in-memory UUID when the registry requires uniqueness.
Scheduler states
The top-level scheduler owns these decisions:
| State | Responsibility |
|---|---|
PREFLIGHT | Run mandatory Tutorial Island or Rune Mysteries work before ordinary scheduling. |
SELECT_TASK | Filter enabled modules, goals, cooldowns, requirements, and blocked states; select one candidate. |
PREPARE | Reconcile supplies, equipment, bank state, and acquisition. |
EXECUTE | Pulse the selected module's child state machine once. |
VERIFY | Confirm terminal task or goal postconditions. |
TRANSITION | Record task result, clear module-owned transient state, and return to selection. |
BLOCKED | Expose a terminal no-progress reason when no candidate can run. |
Keep one active child workflow. The scheduler pulses that child from EXECUTE and maps the child's WorkflowStatus into its own next state. CarouselResult has no child-delegation method.
State-machine translation
The old enum states can remain domain states when they describe gameplay. Use the current CarouselResult signatures:
return TypesafeCarouselStateMachine.builder(MiningState.class, "aio_mining")
.owner(this)
.initial(MiningState.FIND_ROCK)
.on(MiningState.FIND_ROCK, context -> {
InteractionResult result = findAndMineRock();
if (result.accepted()) {
return CarouselResult.transitionTo(
MiningState.WAITING_FOR_MINING,
"mine_dispatched",
result.getMessage());
}
if (result.getStatus() == InteractionStatus.PACED) {
return CarouselResult.stay("mine_paced", result.getMessage());
}
return CarouselResult.fail("mine_failed", result.getMessage());
})
.on(MiningState.WAITING_FOR_MINING, this::verifyMining)
.build();
Every enum value needs one handler. stay, transitionTo, complete, and fail require a stable reason code and detail string. A handler must request at most one meaningful action.
Do not retain old states that only emulate a blocking sleep or a DreamBot cache refresh. Replace those states with an event/postcondition wait or delete them.
Waiting and event routing
A TaskPipeline step can wait for a filtered event:
InteractionResult result = ObjectActions.interact(tree, "Chop down");
if (result.getStatus() == InteractionStatus.PACED) {
return StepResult.fromInteractionPaced(result);
}
if (!result.accepted()) {
return StepResult.failed(result.getMessage());
}
return StepResult.waitForEvent(
AnimationChanged.class,
event -> event.getActor() == client.getLocalPlayer()
&& client.getLocalPlayer().getAnimation() != -1,
"Waiting for woodcutting animation",
8);
The timeout uses ticks, not milliseconds. Filter the event to the actor, item, widget, var, location, or operation that owns the step. When one event cannot prove the outcome, use a later state to inspect SdkEvents and current client state.
Use tick delays only for pacing or a documented protocol window. Prefer state predicates over fixed delays.
Reset and continuity rules
| Event | Required action |
|---|---|
| User stop | Cancel AIO workflow, cancel AIO-owned walk, release input, clear active module and retry state. |
| Break starts | Stop gameplay pulses, retain the selected task, force fresh observation/preparation before resume, and release input. |
| Logout or connection loss | Clear entity/widget references, AIO observation baselines, active walk, and transient action state. Keep user config. |
| World hop | Cancel scene-bound work and re-enter observation or preparation after login. |
| Plugin shutdown | Perform full stop, unregister resources, clear transient account-derived state. |
| Config change while stopped | Apply to the next start. |
| Config change while running | Reset only when the changed field invalidates the active task; otherwise apply at the next task boundary. |
Do not serialize active enum state or cached entities for restart recovery. Reconstruct state from current client observations.
Failure and retry policy
Each state classifies failures as:
- Transient: pacing, movement in progress, interface opening, expected respawn, or event delay.
- Recoverable: re-query target, reopen interface, replan route, return to bank, reacquire supplies, or choose another location.
- Module blocked: insufficient funds, buy limit, missing requirement, unsupported method, or cooldown.
- Terminal session failure: inconsistent state, repeated no-progress, unavailable required infrastructure, or no runnable modules.
Bound retries by ticks, attempts, or both. Reset the counter after observed progress. Surface the last interaction status and recovery reason in the snapshot.
Tests
Add focused lifecycle and workflow tests for:
- Stopped-by-default startup and explicit start.
- One pulse per game tick and same-tick deduplication.
- Active-only Break Handler tracking and break preemption.
- Input-lock ownership and release on wait, break, stop, logout, and shutdown.
- Child workflow completion, failure, cancellation, and replacement.
- Empty candidates, all blocked, target complete, and task switch.
- Filtered event success, unrelated event rejection, and timeout.
- Logout, hop, connection loss, config change, stop/restart, and idempotent shutdown.
- Snapshot retention of terminal reason and observed progress.
Static lifecycle tests cannot prove gameplay. Live acceptance must observe a full select, prepare, execute, verify, transition cycle plus stop and break behavior.