Skip to main content

Suite Wiring

PacketUtilsPlugin is the required shared runtime companion for the native n3 plugins. It stays enabledByDefault = true, owns RuneLite revision setup, initializes the packaged n3 SDK bridge, advances the shared walker, keeps the action pacer ticking, applies suite-wide idle-logout prevention when enabled, and owns PacketUtils SOCKS proxy routing. It also owns the shared Swing plugin-list branding decorator for replacing visible [n3] markers with the bundled n3 icon.

The suite has no loader runtime entrypoint and no external API-plugin runtime dependency.

Manifest Entries

runelite-plugin.properties registers Packet Utils and each user-facing n3 plugin directly, plus the disabled-by-default n3 developer tools panel. The full entrypoint list (class × config group) is the registered-plugins table in N3PLUGINS_SOURCE_OF_TRUTH.md. Do not register N3LoaderPlugin, EthanApiPlugin, or PathingTesting in the default manifest.

Suite GameTick Hook

// PacketUtilsPlugin.java
@Subscribe
public void onGameTick(GameTick e) {
ActionPacer.onTick(System.currentTimeMillis());
Walker.tick();
}

PacketUtils also refreshes input locking, applies the optional neverlog idle-timeout patch when preventIdleLogout is enabled, and runs account bootstrap only when at least one owner has requested it with PacketUtilsPlugin.requireAccountBootstrap(owner).

Walker.tick() advances the active WalkerPath. Consumers such as Walk Assistant, mule walking, Market Alcher travel, and InteractionApi.actions.NavigationActions use the shared packaged walker under com.n3plugins.sdk.walker.

ActionPacer.onTick(nowMs) increments the pacer's internal tick count. When the tick gate transitions from closed to open (the first tick after a recorded action's cooldown expires), it sets a random ms-jitter target within that tick window. It does not reset the jitter target on subsequent open ticks - only on the closed→open transition - so that isReady() calls in the same synchronous event dispatch do not permanently see a future target. See Pacing patterns below.

SDK Initialization

PacketUtilsPlugin.startUp() calls N3Client.initialize(). That method registers the SDK query/listener helpers with RuneLite's event bus:

  • inventory, bank, bank inventory, equipment, deposit box
  • NPCs, players, tile objects
  • shop and shop inventory

The SDK is packaged in the same plugin jar under com.n3plugins.sdk.*; it is not a RuneLite plugin and does not appear in the plugin list.

Runtime Status

InteractionApi.debug.SuiteRuntimeStatus.snapshot() returns a read-only diagnostic snapshot for user-facing plugins and debug tools. It includes the RevisionHealthCheck report, expected bundled client revision, live client revision when a client is available, mapping revision label, Packet Utils walker-tick ownership, active/walking walker state, and the current ActionPacerStatus.

This API is observational only. Feature plugins may use it to explain blocked or unhealthy states, but Packet Utils remains the only suite component that ticks the walker and pacer.

Plugin-List Branding

The pinned RuneLite 1.12.32 API does not provide a descriptor icon field. PacketUtilsPlugin therefore starts N3PluginListBranding after N3Client.initialize() and stops it during shutdown. The decorator scans visible Swing plugin-list rows for n3 descriptor markers, applies the bundled com/n3plugins/ui/brand/n3.png icon, strips the visible marker text, and restores any still-visible labels when the shared plugin shuts down.

Pacing patterns

RuneLite fires all @Subscribe onGameTick handlers synchronously on the client thread in a single dispatch cycle. PacketUtilsPlugin subscribes first and calls ActionPacer.onTick(nowMs). Every other plugin's handler fires a few milliseconds later with about the same nowMs. That timing shapes how you rate-limit per-tick behaviour.

State-aware throttling - use ActionPacer

Use ActionPacer when your plugin actively drives a repeated interaction across multiple ticks and needs human-like timing variance between those interactions: clicking an NPC, withdrawing from a bank, casting a spell, walking to a destination.

// Pattern: state machine or decision tree that fires once every 1–3 ticks
@Subscribe
public void onGameTick(GameTick e) {
long nowMs = System.currentTimeMillis();
if (!ActionPacer.isReady(nowMs)) return; // tick + jitter gate
// ... resolve the action ...
NPCActions.interact(npc, "Attack"); // internally calls recordAction()
}

ActionPacer uses two-layer variance: a tick gate (random 1–3 tick gap, configurable) and a ms jitter gate (bounded humanized reaction-time sample within the configured tick window). The tick gate is what gives action classes their anti-detection cadence. The ms jitter activates on the tick after the gate first opens, because same-tick isReady() calls see nowMs < jitterTargetMs - by the next game tick (600 ms later) the jitter has trivially elapsed.

ActionPacerConfig is the public configuration value:

  • ActionPacerConfig.defaults() returns the suite default: 1-3 ticks and 0-200 ms jitter.
  • ActionPacerConfig.of(minTicks, maxTicks, minMs, maxMs) creates an explicit range.
  • ActionPacer.configure(config) swaps the active configuration.
  • ActionPacer.reset() restores defaults and clears cooldown state.

Keep configuration changes suite-level. Feature plugins should not change the pacer profile for only their own actions unless the whole suite has agreed to that timing model.

sdk.random.N3Random owns shared non-blocking random helpers for action pacing, humanizer scheduling, and gesture target selection. It does not sleep or block. sdk.humanizer.HumanizerService owns suite-level opt-in anti-ban scheduling for passive gestures; plugin wrappers provide the actual gesture executor.

Per-tick self-limiting - use a tick counter

Use client.getTickCount() when your plugin responds to transient game state that already limits how often it can fire: dialogue widgets only exist while dialogue is open; a combat idle alert only matters when the player stops attacking. The state itself is the rate-limiter - no cross-tick cooldown is needed.

private int lastFiredTick = -1;

@Subscribe
public void onGameTick(GameTick e) {
int tick = client.getTickCount();
if (tick == lastFiredTick) return; // one action per server tick max
// ... check transient widget / condition ...
WidgetApi.resumePause(widgetId, childId);
lastFiredTick = tick;
}

This prevents double-sending within a tick without depending on the shared pacer state. Because the server only advances dialogue (or changes combat state) once per tick, the guard is always sufficient.

Keep transient handlers stateless

The closed-to-open transition rule prevents the pacer from moving its jitter target forward on every open tick. Even so, transient dialogue and modal handlers should remain stateless: when the visible widget exists, dispatch the appropriate action; otherwise do nothing. Do not add retry locks, backoff, or a cross-tick pacer gate to that workflow.

Test Coverage

  • NativeManifestTest locks the native manifest entries and absence of loader and Ethan plugin entries.
  • NativeRuntimeSourceTest rejects retired loader/Ethan runtime references in production Java source.
  • N3ClientTickOwnershipTest locks that N3Client is not a plugin tick owner and that Packet Utils owns walker ticking.
  • ActionPacerJamTest locks the pacer recovery invariant.
  • N3PluginListBrandingTest and N3BrandingTest cover the plugin-list decorator and bundled brand image loader.

Core Runtime Lifecycle & Account Bootstrap Flow

The following diagrams visualize the central runtime orchestration managed by PacketUtilsPlugin and the request-driven account bootstrap sequence.

Tick Hook Sequence

Account Bootstrap Requirements

Bootstrap is no longer an unconditional suite-wide gate. Active automations and explicit script surfaces call PacketUtilsPlugin.requireAccountBootstrap(owner) while they need PacketUtils to enforce account setup, then call releaseAccountBootstrap(owner) when stopped or idle. isAccountBootstrapReady() returns true when no owner has requested bootstrap; this keeps passive helpers from being blocked by setup they did not ask for.

PacketUtils config now also owns:

  • preventIdleLogout - opt-in suite-wide neverlog-style idle timeout handling.
  • proxyEnabled, proxyHost, proxyPort, proxyUsername, proxyPassword - SOCKS proxy routing fields under Proxy Settings.

Disabling preventIdleLogout or shutting down PacketUtils restores the client idle timeout captured when the toggle was first applied. Proxy disable restores the JVM proxy selector/authenticator that existed before PacketUtils enabled its SOCKS selector.

Account Bootstrap Step Machine