> ## Documentation Index
> Fetch the complete documentation index at: https://runelite.zip/llms.txt
> Use this file to discover all available pages before exploring further.

# Interactions and menu dispatch

> How actions resolve, pace, dispatch, and report: InteractionResult, ActionResolver, menu dispatch, and diagnostic snapshots.

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:

| Field                      | Meaning                                                 |
| -------------------------- | ------------------------------------------------------- |
| `status`                   | Machine-readable `InteractionStatus`                    |
| `message`                  | Short human-readable explanation                        |
| `targetType`, `targetName` | The target searched or acted on                         |
| `requestedAction`          | Cleaned action name or requested action list            |
| `actionIndex`              | One-based menu action index when the resolver found one |
| `availableActions`         | Cleaned actions seen on the target                      |

```java theme={null}
InteractionResult result = NPCActions.interact("Banker", "Bank");
if (result.failed()) {
    log.debug("{} target={} action={}",
        result.getStatus(),
        result.getTargetName(),
        result.getRequestedAction());
}
```

Use `succeeded()` and `failed()`. Never compare messages.

### Status reference

| Status                                     | Meaning                                                                                                                                 |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `TARGET_NOT_FOUND`                         | The collection query lacked a target                                                                                                    |
| `TARGET_NULL`                              | The caller passed a null direct target                                                                                                  |
| `ACTION_NOT_FOUND`                         | The target existed but lacked the requested action                                                                                      |
| `WIDGET_NOT_FOUND`                         | Widget-specific target problem                                                                                                          |
| `WIDGET_HIDDEN`                            | The widget is hidden or a required side tab is not visible. Tab prerequisites target `tab/<TAB_NAME>` and never open the tab implicitly |
| `BANK_NOT_OPEN`                            | Bank-only action failed because the bank interface was unavailable                                                                      |
| `TANNING_NOT_OPEN`                         | Tanning action failed because the tanner interface was closed                                                                           |
| `TANNING_SLOT_NOT_FOUND`                   | No, or more than one, tanning slot matched the requested hide label                                                                     |
| `MOVEMENT_NOT_QUEUED`, `PACKET_NOT_QUEUED` | Low-level queueing failed                                                                                                               |
| `PACED`                                    | The pacer is still cooling down; re-evaluate next tick                                                                                  |
| `INPUT_LOCKED`                             | An input lock is held; the action did not dispatch                                                                                      |
| `CLIENT_NOT_READY`, `NOT_LOGGED_IN`        | Client or session preconditions failed                                                                                                  |

## The execution pipeline

```mermaid theme={null}
flowchart TD
    A[Action call] --> B{Client and login guards}
    B -->|Fail| BF[CLIENT_NOT_READY / NOT_LOGGED_IN]
    B -->|Pass| C{ActionPacer ready and no input lock?}
    C -->|No| CF[PACED / INPUT_LOCKED]
    C -->|Yes| D[Resolve target]
    D -->|Query empty| DF[TARGET_NOT_FOUND]
    D -->|Found| F{Target null or stale?}
    F -->|Yes| FF[TARGET_NULL / TARGET_STALE]
    F -->|No| G{ActionResolver finds index?}
    G -->|No| GF[ACTION_NOT_FOUND]
    G -->|Yes| H[Dispatch: menu dispatcher or synthetic canvas click]
    H -->|Queue failure| HF[PACKET_NOT_QUEUED]
    H -->|Dispatched| K[Record in pacer]
    K --> L[Return result with metadata]
```

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.

```java theme={null}
int index = ActionResolver.findActionIndex(widget.getActions(), "Withdraw-10");
if (index <= 0) {
    return InteractionResult.fail(InteractionStatus.ACTION_NOT_FOUND, "No withdraw action");
}
```

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:

```java theme={null}
int index = ActionResolver.findActionIndex(
    item.getActions(),
    "Wear",
    "Wield",
    "Equip"
);
```

Use `ActionResolver.hasAction(...)` inside query predicates instead of looping over action arrays yourself:

```java theme={null}
Optional<Widget> teleport = Inventory.search()
    .filter(item -> ActionResolver.hasAction(item.getActions(), "Teleport"))
    .first();
```

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`.

## Menu dispatch

`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](/guides/humanization-safety) 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:

```java theme={null}
List<MenuEntrySnapshot> entries = MenuEntriesApi.entries();
Optional<MenuEntrySnapshot> bank = MenuEntriesApi.firstMatching("Bank", "Banker");
boolean hasTakeCoins = MenuEntriesApi.contains("Take", "Coins");
log.debug("Current menu entries: {}", MenuEntriesApi.diagnostics());
```

`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.

<Warning>
  Never dispatch actions from `MenuEntrySnapshot`. Snapshots are for inspection. Route execution through `Api.actions.*` so pacing, locking, and result tracking stay intact.
</Warning>

## Blocking events

`BlockingEventActions` handles the blockers that appear before normal automation can run: the welcome screen, death dialogues, and viewport layout.

| Method                                       | Behavior                                                                         |
| -------------------------------------------- | -------------------------------------------------------------------------------- |
| `isWelcomeScreenOpen()`                      | Checks the click-to-play widget                                                  |
| `continueWelcomeScreen()`                    | Queues a click when the welcome screen is visible                                |
| `continueDeathDialog()`                      | Delegates to `DialogActions.continueSpace()` when a continuable dialogue is open |
| `isFixedViewport()`, `isResizableViewport()` | Layout visibility checks                                                         |
| `setResizableMode(...)`                      | Returns success when already matching, otherwise fails closed                    |

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:

```java theme={null}
@Subscribe
public void onGameTick(GameTick event) {
    if (BlockingEventActions.isWelcomeScreenOpen()) {
        InteractionResult result = BlockingEventActions.continueWelcomeScreen();
        if (result.failed()) {
            log.debug("Welcome screen continue failed: {}", result.getMessage());
        }
        return;
    }

    if (BlockingEventActions.continueDeathDialog().succeeded()) {
        return;
    }

    runAutomationTick();
}
```

`setResizableMode(...)` acts conservatively. Treat it as a state check and surface the failure to the operator when the layout is incompatible.
