Skip to main content
This page teaches the event-driven shape of n3Plugins automation. The examples show the decision loop and real API signatures. A production plugin additionally needs registration, configuration, break handler lifecycle, and shutdown cleanup; the plugin development playbook covers the surrounding skeleton.

Design process

Work through the same five questions every time:

Walkthrough: bone buryer

A minimal skiller loop: find bones in the inventory, bury one per tick, and wait out the animation.

Walkthrough: bank runner with a state machine

A full inventory of willow logs triggers a bank restock through BankWorkflowBuilder, which manages opening, depositing, withdrawing, and closing:

Walkthrough: stateless dialogue helper

Dialogue widgets exist only while the server presents them. React within the tick and keep no memory across ticks:

Pattern: pacing

ActionPacer is suite-wide and driven by Packet Utils. The action APIs check it internally, so never gate your own tick loop on ActionPacer.isReady(...). Packet Utils increments the tick count and applies jitter before your handler runs, so a same-tick isReady() check returns false at the wrong moment and starves your automation.

Pattern: spatial selection

Entity lists are not sorted by distance. first() grabs whatever the raw scan returned and produces unstable target choice. Select by path reachability or by a stable anchor:
QueryResults.nearestTo(...) and sortedByDistanceTo(...) use straight-line tile distance. When reachability decides the action target, use nearestByPath().

Pattern: spell resolution

Spell widgets are revision-backed addresses. Resolve names through the shared resolver backed by the Api.actions.Spell catalog, and treat an empty result as a configuration error:
Never copy a packed spell widget ID into a plugin or depend on removed widget-constant wrappers. Use resolveSpellWidget(...) when the workflow needs the live widget; it stays empty when the spell is not visible in the active spellbook.

Pattern: query freshness

Query builders scan and cache per tick. Caching a builder across ticks reads stale state:

Pattern: ordered tick decisions

When several combat policies must run in a fixed order once per tick, use TickDecisionList instead of duplicating lastFiredTick guards. See tick decisions for the full contract.

Loop ergonomics

AutomationLoop wraps the observe-decide-act cycle when a plugin wants loop-style ergonomics while keeping ownership in its own onGameTick:
AutomationLoop.fromPipeline(...) and AutomationLoop.fromStateMachine(...) adapt existing workflows without creating another pacer, walker, or global tick owner.

Read, then act

Pair a read surface with its result-aware write:
Quest and diary progress reads mirror requirement helpers:
DiaryProgressApi exposes only explicitly mapped varbit and varplayer-backed tasks. Unknown diary coverage is absent rather than guessed.

The rules reviewers enforce

  1. Do not sleep. Let the tick dispatcher drive progression.
  2. Keep dialogue handlers stateless: click what is visible, otherwise do nothing.
  3. Use an intentional spatial selector: nearestByPath() for reachable targets, walkable().nearestToPoint(anchor) for a work area. Never first() as a nearest-target policy.
  4. Gate full automation on the break handler; pause actions during planned or active breaks.
  5. Inspect InteractionResult at the decision point. Do not compare messages and do not treat dispatch as completion.
  6. Resolve IDs from gameval.ItemID, IdMapRegistry, and WidgetCatalog. Never hardcode identifiers.
  7. Route every write through Api.actions.*. Do not call raw packet queues directly.