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