Beginner Scripting Guide & Thinking Patterns
revision-sensitive RuneLite UI, packet, or in-game outcomes; treat those as live-client verification pending unless the page records direct evidence.
Use this guide to learn the event-driven shape of n3Plugins automation. The examples show illustrative plugin fragments. They demonstrate the decision loop and real API signatures. They omit registration, configuration, Break Handler lifecycle, and shutdown cleanup required by a production plugin.
1. Core Concepts
A. The 600ms Server Tick
RuneScape advances game state on server ticks that last approximately 600 ms.
- Code runs on a tick timer: RuneLite invokes
@Subscribe public void onGameTick(GameTick event)once every 600ms. - Advance one decision at a time: Do not assume several dependent actions (withdraw, equip, close bank) can complete in one callback. Dispatch one action, return, and observe the resulting state on a later tick.
- React, don't sleep: Do not use
Thread.sleep()or busy-waiting loops. To wait, return from the tick handler and let the next tick evaluate the new state.
B. Stateless vs. Stateful Thinking
Stateful scripts click a tree, set a variable isChapping = true, and wait. When the player suffers an attack, walks away, or logs out, the script gets stuck because the code's state contradicts the game's reality.
- Prefer observed state: Query inventory, widgets, actors, and objects each tick instead of assuming the previous click succeeded.
- Store workflow state only when necessary: Pipelines and state machines require durable state, but you must reconcile that state with the live game before every action.
C. Runtime Gates And Ownership
The plugin's role dictates the required gates:
- Break Handler: Full automation plugins track breaks only while active, pausing actions during planned or active breaks. Helper plugins follow the explicit exemption policy in the SOT.
- Action pacing: Mutating
Api.actions.*methods apply shared handles that resolve by waiting and re-evaluating state.
2. Thinking Patterns: Designing a Script
Follow this structured mental process when designing a new script:
Proximity: Path Reachability vs. Euclidean Distance
The closest tree on your screen might sit behind a solid stone wall.
Flawed: "Get the closest tree to my player's coordinates." (Using nearestToPlayer()).
Correct: "Get the closest tree I can walk to without hitches." (Using nearestByPath()).
Named spells: resolve before acting
Spell names are not raw widget IDs. Use the shared resolver, backed by the Api.actions.Spell catalog, and handle a missing result without clicking anything:
Optional<Integer> spellId = MagicActions.resolveSpellInfo("High Level Alchemy");
if (!spellId.isPresent()) {
return; // Invalid config/name; retry only after the configuration changes.
}
// Cast by enum; name resolution uses MagicActions.resolveSpellInfo(name).
MagicActions.cast(Spell.HIGH_LEVEL_ALCHEMY, target);
The resolved value yields the packed RuneLite widget ID (for High Alchemy, InterfaceID.MagicSpellbook.HIGH_ALCHEMY). Avoid copying that number into a plugin or using removed widget-constant wrappers. Api.actions.spells owns friendly-name normalization and compatible suffix matching.
3. Beginner Walkthrough 1: Bone Buryer
This walkthrough demonstrates a simple skiller loop that finds bones in the inventory, buries them one-by-one, and pauses between ticks.
Step 1: The Loop Logic
- Deduplicate ticks (run once per 600ms).
- Search inventory for "Bones".
- If bones exist, bury one.
- Wait for the animation to finish before burying the next.
Step 2: The Implementation
package com.n3plugins.boneburyer;
import com.n3plugins.PacketUtils.PacketUtilsPlugin;
import com.n3plugins.Api.actions.InventoryActions;
import com.n3plugins.Api.common.InteractionResult;
import com.n3plugins.sdk.query.Inventory;
import net.runelite.api.Client;
import net.runelite.api.events.GameTick;
import net.runelite.api.widgets.Widget;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import javax.inject.Inject;
public class BoneBuryerPlugin extends Plugin {
@Inject private Client client;
private int lastFiredTick = -1;
@Subscribe
public void onGameTick(GameTick event) {
// 1. Deduplicate ticks (run once per 600ms)
int currentTick = client.getTickCount();
if (currentTick == lastFiredTick) return;
// 3. Animation Guard: don't click if we are already burying a bone
if (client.getLocalPlayer().getAnimation() != -1) {
return;
}
// 4. Query the inventory for bones
Widget bone = Inventory.search()
.withName("Bones")
.first()
.orElse(null);
if (bone != null) {
// 5. Interact (bury the bone)
InteractionResult res = InventoryActions.use(bone, "Bury");
// If the action went through, lock this tick
if (res.succeeded()) {
lastFiredTick = currentTick;
}
}
}
}
4. Beginner Walkthrough 2: State Machines
Use TypesafeCarouselStateMachine via BankWorkflowBuilder when executing a set sequence of actions or a robust workflow (e.g. open bank -> deposit items -> withdraw supplies -> close bank). It manages steps with enum-driven exhaustiveness and built-in telemetry.
Step 1: Design the Workflow
Define your target loadouts using BankRestockPlan, then create a machine:
BankRestockPlan plan = BankRestockPlan.builder()
.inventoryLoadout(myTargetLoadout)
.build();
TypesafeCarouselStateMachine<BankRestockState> bankMachine = BankWorkflowBuilder.create(plan);
Step 2: The Implementation
package com.n3plugins.bankrunner;
import com.n3plugins.sdk.workflow.BankRestockPlan;
import com.n3plugins.sdk.workflow.BankWorkflowBuilder;
import com.n3plugins.sdk.workflow.BankWorkflowBuilder.BankRestockState;
import com.n3plugins.sdk.workflow.CarouselResult;
import com.n3plugins.sdk.workflow.TypesafeCarouselStateMachine;
import com.n3plugins.sdk.workflow.WorkflowStatus;
import com.n3plugins.sdk.loadouts.InventoryLoadout;
import com.n3plugins.sdk.query.Inventory;
import net.runelite.api.Client;
import net.runelite.api.events.GameTick;
import net.runelite.client.eventbus.Subscribe;
import net.runelite.client.plugins.Plugin;
import javax.inject.Inject;
public class BankRunnerPlugin extends Plugin {
@Inject private Client client;
private TypesafeCarouselStateMachine<BankRestockState> bankMachine;
@Subscribe
public void onGameTick(GameTick event) {
int currentTick = client.getTickCount();
// Start banking if our inventory is full of Willow logs
boolean needsBanking = Inventory.search().withName("Willow logs").count() >= 27;
if (needsBanking && bankMachine == null) {
InventoryLoadout loadout = new InventoryLoadout();
// Configure loadout requirements if needed
bankMachine = BankWorkflowBuilder.create(
BankRestockPlan.builder()
.inventoryLoadout(loadout)
.build()
);
}
// Pulse the state machine on every game tick
if (bankMachine != null) {
CarouselResult<BankRestockState> result = bankMachine.pulse(currentTick);
// Clear the state machine when finished or failed
if (result.getSnapshot().getStatus() == WorkflowStatus.COMPLETED ||
result.getSnapshot().getStatus() == WorkflowStatus.FAILED) {
bankMachine = null;
}
}
}
}
5. Scripting Invariants: The Golden Rules
Your scripts must conform to these architectural standards to pass code review:
- Do not sleep: Let the tick dispatcher (
onGameTick) drive script progression. - Stateless dialogues: Avoid remembering dialog clicks. Click a dialogue widget if it is present; otherwise, do nothing.
- Use an intentional spatial selector: Use
nearestByPath()for player-relative reachable targets, orwalkable().nearestToPoint(anchor)for a stable work area. Do not usefirst()as a nearest-target policy. - Gate on Break Handler: Full automation plugins must pause gameplay actions during planned or active breaks.
- Return
InteractionResult: New mutating shared actions returnInteractionResult. Plugin decision code inspects it instead of comparing messages.