Skip to main content
Every mutating call your plugin makes travels one pipeline: target resolution, pacing, action resolution, dispatch, and a structured result. This page explains each stage and the tools you use to debug them.

InteractionResult

InteractionResult is the standard return type for mutating action APIs. Its fields tell you what the runtime saw:
Use succeeded() and failed(). Never compare messages.

Status reference

The execution pipeline

A dispatched action is not proof the game state changed. Observe the expected widget, interface, or actor state on a later tick before your workflow advances.

ActionResolver

ActionResolver matches menu actions and returns one-based RuneLite action indexes. It strips RuneLite text tags, trims whitespace, ignores null and empty actions, and matches case-insensitively.
When a caller accepts several menu verbs, pass them in preference order. The method returns the index of the first matching entry in the widget or entity action array, not the first requested verb:
Use ActionResolver.hasAction(...) inside query predicates instead of looping over action arrays yourself:
On failure, describeRequested(...) repeats the raw caller input in the message. Never index into composition action arrays with filtered-list arithmetic; sparse entity action arrays (for example Gemstone Crabs) break that assumption. Index directly into the raw array and clean values through ActionResolver. com.n3plugins.PacketUtils.reflection owns native menu dispatch through MenuDispatcher. The production singleton, ReflectionMenuDispatcher, invokes the vanilla client’s static obfuscated menu-action method. It does not call an injected Client.menuAction(...) API method. Current native-menu consumers:
  • WidgetActions for listener-backed CC_OP operations.
  • BankActions.close() for the client-local close operation.

Resolution and caching

The first dispatch for a client revision checks a disk cache at .runelite/cache/menu-action-plan.json. On a miss, MenuActionAsmResolver analyzes the runtime client bytecode with ASM data-flow analysis, accepting an invocation only when its descriptor reads exactly (IIIIIILjava/lang/String;Ljava/lang/String;II[BSIJ])V and every operand traces to a logical argument or a modeled transformation. Multiple calls, incomplete bindings, unsupported transformations, or descriptor drift fail closed. Cache schema version 2 records the revision, source hook, bytecode fingerprint, descriptor, and typed bindings. A revision or fingerprint mismatch, malformed plan, or missing method invalidates the entry and triggers re-resolution. Cache writes are best-effort: a read-only home directory leaves ASM-resolved methods working, and the next start scans again. Delete the file to force resolution.

Failure semantics

ReflectionMenuDispatcher throws IllegalStateException when the client drops offline, resolution fails, or invocation fails. WidgetActions and BankActions convert that exception to PACKET_NOT_QUEUED and skip recording the action in the pacer.

Synthetic dispatch path

When synthetic mouse is enabled and the target projects onto the canvas, the runtime plans a humanized mouse path, installs a one-shot menu entry, and completes with a native canvas click. Otherwise dispatch goes straight through the reflection dispatcher. See the humanization and safety playbook for the synthetic input boundary.

Menu entry snapshots (diagnostics only)

The com.n3plugins.sdk.menu package captures immutable snapshots of the client’s current menu entries for failure analysis:
entries() captures the current entries as an immutable list and returns an empty list when the client or entries disappear. firstMatching(option, target) matches normalized, case-insensitive text and treats a null argument as a wildcard. diagnostics() formats entries as option -> target strings for logging. Snapshots expose option, target, identifier, type, parameters, item ID, world view ID, and deprioritization state.
Never dispatch actions from MenuEntrySnapshot. Snapshots are for inspection. Route execution through Api.actions.* so pacing, locking, and result tracking stay intact.

Blocking events

BlockingEventActions handles the blockers that appear before normal automation can run: the welcome screen, death dialogues, and viewport layout. The class carries no credentials and performs no auto-login. In tests without RuneLite’s injector, client lookup fails closed with CLIENT_NOT_READY. Run blocker checks before your automation each tick. If a blocker appears, handle it and skip the rest of the tick so competing actions never queue:
setResizableMode(...) acts conservatively. Treat it as a state check and surface the failure to the operator when the layout is incompatible.