Skip to main content

Beginner Scripting Guide & Thinking Patterns

Use this guide to learn the event-driven shape of n3Plugins automation. The examples are illustrative plugin fragments: they show the decision loop and real API signatures, but omit registration, configuration, Break Handler lifecycle, and shutdown cleanup required by a production plugin.


1. The Core Concepts You Must Grasp

A. The 600ms Server Tick

RuneScape advances game state on server ticks that are approximately 600 ms.

  • Your 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: Never use Thread.sleep() or busy-waiting loops in your scripts. If you need to wait, return from the tick handler and let the next tick evaluate the new state.

B. Stateless vs. Stateful Thinking

Beginners often write "stateful" scripts: they click a tree, set a variable isChapping = true, and wait. If the player gets attacked, walks away, or logs out, the script is stuck because the code’s state does not match 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 needed: Pipelines and state machines need durable state, but that state must be reconciled with the live game before every action.

C. Runtime Gates And Ownership

The required gates depend on the plugin's role:

  1. Account bootstrap: Active automation that needs account setup calls PacketUtilsPlugin.requireAccountBootstrap(owner) and releases that owner when idle or stopped. Passive overlays and bookkeeping do not need the gate.
  2. Break Handler: Full automation plugins use active-only Break Handler tracking and pause actions during planned or active breaks. Helper plugins follow the explicit exemption policy in the SOT.
  3. Action pacing: Mutating InteractionApi.actions.* methods apply shared pacing where their contract requires it and can return PACED. The plugin handles that result by waiting and re-evaluating state.

2. Thinking Patterns: How to Design a Script

When designing a new script, follow this structured mental process:

Proximity: Path Reachability vs. Euclidean Distance

When looking for objects, the closest tree on your screen might be on the other side of a solid stone wall.

  • Improper thinking: "Get the closest tree to my player's coordinates." (Using nearestToPlayer()).
  • Optimal thinking: "Get the closest tree that I can actually walk to without hitches." (Using nearestByPath()).

3. Beginner Walkthrough 1: My First 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

  1. Register and verify account bootstrap while the loop is active.
  2. Search inventory for "Bones".
  3. If bones exist, bury one.
  4. 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.InteractionApi.actions.InventoryActions;
import com.n3plugins.InteractionApi.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;

// 2. Register this active owner and wait for account setup.
if (!PacketUtilsPlugin.requireAccountBootstrap(this)) 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: Linear Task Pipeline (The Bank Runner)

When you need to execute a set sequence of actions (e.g. open bank -> deposit items -> withdraw supplies -> close bank), a TaskPipeline is the optimal tool. It manages the steps without cluttering your code with state flags.

Step 1: Design the Steps

  • open-bank: Click the nearest bank booth or NPC.
  • wait-bank: Wait until the bank interface is open.
  • deposit: Deposit gathered logs.
  • withdraw: Withdraw a fresh supply of coins.
  • close-bank: Close the interface.

Step 2: The Implementation

package com.n3plugins.bankrunner;

import com.n3plugins.PacketUtils.PacketUtilsPlugin;
import com.n3plugins.InteractionApi.actions.BankActions;
import com.n3plugins.InteractionApi.plan.StepResult;
import com.n3plugins.InteractionApi.plan.StepStatus;
import com.n3plugins.InteractionApi.plan.TaskPipeline;
import com.n3plugins.sdk.query.Inventory;
import com.n3plugins.sdk.widgets.BankApi;
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 TaskPipeline bankPipeline;
private int lastFiredTick = -1;

@Subscribe
public void onGameTick(GameTick event) {
int currentTick = client.getTickCount();
if (currentTick == lastFiredTick) return;
lastFiredTick = currentTick;

if (!PacketUtilsPlugin.requireAccountBootstrap(this)) return;

// Start banking if our inventory is full of Willow logs
boolean inventoryNeedsBanking = Inventory.search().withName("Willow logs").count() >= 27;

if (inventoryNeedsBanking && bankPipeline == null) {
bankPipeline = createBankingPipeline();
}

// Advance the pipeline
if (bankPipeline != null) {
StepResult result = bankPipeline.tick();

// If completed or failed, clear the pipeline
if (bankPipeline.complete() || result.getStatus() == StepStatus.FAILED) {
bankPipeline = null;
}
}
}

private TaskPipeline createBankingPipeline() {
return TaskPipeline.create()
// 1. Open the bank
.step("open-bank", () -> StepResult.fromInteraction(BankActions.openNearest()))

// 2. Wait 1 tick for the widget to appear
.step("wait-bank", () -> BankApi.isOpen()
? StepResult.success()
: StepResult.waitTicks(1, "Opening bank UI"))

// 3. Deposit the inventory (a production workflow would preserve
// required tools through its loadout/banking policy)
.step("deposit-inventory", () -> StepResult.fromInteraction(BankActions.depositAll()))

// 4. Close the bank
.step("close-bank", () -> StepResult.fromInteraction(BankActions.close()));
}
}

5. Scripting Invariants: The Golden Rules of n3Plugins

To pass code review, your scripts must conform to these architectural standards:

  1. Do not sleep: Always let the tick dispatcher (onGameTick) drive script progression.
  2. Stateless dialogues: Do not try to remember dialog clicks. If a dialogue widget is present, click it. If not, do nothing.
  3. Use an intentional spatial selector: Use nearestByPath() for player-relative reachable targets, or walkable().nearestToPoint(anchor) for a stable work area. Do not use first() as a nearest-target policy.
  4. Own bootstrap lifecycle: Active automation that needs account setup registers an owner, gates gameplay on readiness, and releases the owner when idle or stopped.
  5. Return InteractionResult: New mutating shared actions return InteractionResult; plugin decision code inspects it instead of comparing messages.