Suite Wiring
Packet Utils owns the optional UI-suppression headless mode, including the replacement status map, GPU stop-and-restore ownership, and the suite-wide synthetic-input boundary.
This page documents the committed source. Treat revision-sensitive RuneLite UI, packet, or in-game outcomes as pending live-client verification unless the page records direct evidence.
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, owns PacketUtils SOCKS proxy routing, and applies the disabled-by-default issued-action clipboard setting at startup, configuration changes, and shutdown. It also lifecycle-owns the default-off client telemetry observer: startup registers it before input locking only after revision health succeeds, and shutdown unregisters it before native-action trace clearing. It owns the shared Swing plugin-list branding decorator for replacing visible [n3] markers with the bundled n3 icon.
The PacketUtils package owns the native menu-action dispatcher used by listener-backed widgets and bank closing. It resolves the vanilla static method from runtime client bytecode, caches the revision-specific result, and does not require Client.menuAction(...) in the RuneLite API. See menu-action-dispatch.md for the cache and failure contract.
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. Packet Utils owns the internal Developer Tools runtime, its sidebar tab, canvas/menu utilities, attached Logger console, and panel-less Logger title-bar button. There is no standalone Developer Tools plugin-list entry. The full entrypoint list (class × config group) is the registered-plugins table in N3PLUGINS_SOURCE_OF_TRUTH.md. Do not register EthanApiPlugin in the default manifest.
The internal DevToolsPlugin runtime is a Packet Utils-owned singleton. It injects providers for its overlays and resolves those overlays only from start(), after the runtime singleton and Packet Utils panel graph finish injection. Overlay constructors receive that completed runtime directly. Repeated start/stop calls are safe. Packet Utils shutdown tolerates a startup that ended before the embedded panel initialized.
Suite GameTick Hook
// PacketUtilsPlugin.java
@Subscribe
public void onGameTick(GameTick e) {
ActionPacer.onTick(System.currentTimeMillis());
Walker.tick();
}
PacketUtils refreshes input locking and continuously sets the client idle-timeout threshold to the injected client's maximum of 90,000 client ticks when preventIdleLogout is enabled. Because the revision-240 setter clamps larger values to that maximum, PacketUtils also dispatches a harmless Shift press/release shortly before both client input-idle counters reach the threshold. The setting is independent of feature-plugin activity and leaves only the game's forced six-hour logout. Packet Utils owns this refresh; packet-click helpers do not create background executors or perform incidental idle prevention. PacketUtils runs account bootstrap only when at least one owner requested it with PacketUtilsPlugin.requireAccountBootstrap(owner).
Walker.tick() advances the active WalkerPath. Consumers such as Walk Assistant, mule walking, Market Alcher travel, and Api.actions.NavigationActions use the shared packaged walker under com.n3plugins.sdk.walker.
The suite does not package the walker's large pinned shortest-path resources in the jar. ShortestPathPlugin uses VendorResourceDownloader during startup to fetch the SHA-backed upstream archive into RuneLite.RUNELITE_DIR/n3Plugins/shortestpath/. Packet Utils remains the shared walker tick owner. Feature plugins must not download, embed, or independently tick shortest-path data. See walker.md for cache and validation details.
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, 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
Api.debug.SuiteRuntimeStatus.snapshot() returns a read-only diagnostic snapshot for user-facing plugins and debug tools. It includes the RevisionHealthCheck log, 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 use it to explain blocked or unhealthy states. Packet Utils remains the only suite component that ticks the walker and pacer.
Plugin-List Branding
The pinned RuneLite 1.12.38 API still does not provide a descriptor icon field. PacketUtilsPlugin 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 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.
The pacer's tick, jitter, and gate fields are one synchronized state transition. Diagnostic snapshots cannot observe a partially recorded action. The suite disables optional SessionFatigue scaling by default. A runtime that opts in must explicitly resume and pause it with the same monotonic millisecond time base used for pacing so logout, idle, and break time are not counted. It is a bounded timing policy, not an anti-detection guarantee.
Keep configuration changes suite-level. Feature plugins must not change the pacer profile for only their own actions unless the whole suite agrees 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.
Packet Utils owns SyntheticMouseService. It dispatches opt-in move-only paths with a lifecycle-managed Swing timer on the EDT and registers the bounded visualization overlay. It has one path owner, cancels on foreign/manual or client-layout transitions, and shuts down before InputLockService. It does not tick from onGameTick and does not change MousePackets, WidgetActions, or normal scene-action timing.
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 matters only 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 ...
WidgetActions.resumePause(widgetId, childId);
lastFiredTick = tick;
}
This prevents double-sending within a tick without depending on the shared pacer state. Because the server advances dialogue (or changes combat state) only 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, keep transient dialogue and modal handlers 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
NativeManifestTestlocks the native manifest entries and absence of loader and Ethan plugin entries.NativeRuntimeSourceTestrejects retired loader/Ethan runtime references in production Java source.N3ClientTickOwnershipTestlocks thatN3Clientis not a plugin tick owner and that Packet Utils owns walker ticking.ActionPacerJamTestlocks the pacer recovery invariant.N3PluginListBrandingTestandN3BrandingTestcover the plugin-list decorator and bundled brand image loader.LoggerConsoleControllerTestcovers Logger capture state, source/level filtering, and exact client content-pane restoration.IssuedActionClipboardTestcovers disabled defaults, dispatched-only output, stable formatting, and rejection suppression.PluginTransportActionResolverTestandNpcWalkerActionTestcover generic NPC/object target resolution, ambiguity rejection, Primio dispatch, and observed destination arrival.
Core Runtime Lifecycle & Flow
The following diagrams visualize the central runtime orchestration managed by PacketUtilsPlugin and the request-driven sequence.
Tick Hook Sequence
Requirements
Account bootstrap flows were removed. PacketUtilsPlugin.onGameTick now owns only suite tick infrastructure: action pacing, input-lock refresh, idle-logout prevention, optional Break Handler runtime ticking, logged-in Walker ticking, and Headless Mode map updates. Active automations must verify their own prerequisites instead of relying on a suite-wide bootstrap participant.
PacketUtils config owns:
preventIdleLogout- opt-in client-wide idle logout prevention until the game's forced six-hour logout, independent of feature-plugin activity.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.