Skip to main content

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:

  1. startUp() constructs stopped controller state, UI, overlay, and hotkey registrations. It registers the plugin with the Break Handler.
  2. A user action starts the AIO. Starting builds one top-level workflow and calls breakHandler.startPlugin(this).
  3. onGameTick checks login and break state, pulses the controller once, updates the immutable snapshot, and manages the shared input lock.
  4. A due break calls breakHandler.startBreak(this). The plugin stops pulsing gameplay work and releases its input lock until the break ends.
  5. 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.
  6. 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:

StateResponsibility
PREFLIGHTRun mandatory Tutorial Island or Rune Mysteries work before ordinary scheduling.
SELECT_TASKFilter enabled modules, goals, cooldowns, requirements, and blocked states; select one candidate.
PREPAREReconcile supplies, equipment, bank state, and acquisition.
EXECUTEPulse the selected module's child state machine once.
VERIFYConfirm terminal task or goal postconditions.
TRANSITIONRecord task result, clear module-owned transient state, and return to selection.
BLOCKEDExpose 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

EventRequired action
User stopCancel AIO workflow, cancel AIO-owned walk, release input, clear active module and retry state.
Break startsStop gameplay pulses, retain the selected task, force fresh observation/preparation before resume, and release input.
Logout or connection lossClear entity/widget references, AIO observation baselines, active walk, and transient action state. Keep user config.
World hopCancel scene-bound work and re-enter observation or preparation after login.
Plugin shutdownPerform full stop, unregister resources, clear transient account-derived state.
Config change while stoppedApply to the next start.
Config change while runningReset 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.