Skip to main content

Client Utilities

Verification boundary

revision-sensitive RuneLite UI, packet, or in-game outcomes; treat those as live-client verification pending unless the page records direct evidence.

com.n3plugins.sdk.client holds the low-level client access the rest of the SDK builds on: the client handle, thread marshalling, tick delays, camera reads, screenshots, developer diagnostics, line-of-sight checks, threat-map reads, and hint-arrow reads. GameVars handles varbit and varplayer reads; see game-vars.md.

note

The screenshot and metrics helpers on this page provide read/diagnostic surfaces. They do not click, walk, hop worlds, mutate game state, or replace Api.actions.* for gameplay automation.

N3Client

N3Client acts as the boundary to the RuneLite client. Higher layers read the client through it. This keeps components swappable in tests.

Areas and spatial queries

The sdk.area.Area contract supplies immutable, plane-aware geometry without caching live RuneLite entities. RectangleArea, PolygonArea, RegionArea, and SceneArea all expose inclusive containment and stable, immutable tile enumeration. SceneArea captures the supplied WorldView base when it is constructed, so callers should recreate it after an instance or scene transition.

Area courtyard = new PolygonArea(Arrays.asList(
new WorldPoint(3200, 3200, 0),
new WorldPoint(3206, 3200, 0),
new WorldPoint(3203, 3205, 0)));

List<NPC> occupants = NPCs.search().withinArea(courtyard).result();
List<WorldPoint> tiles = courtyard.tiles();

NPCQuery, PlayerQuery, TileObjectQuery, and TileItemQuery accept the same area contract, allowing a geometry definition to be reused across entity types.

if (N3Client.loggedIn()) {
WorldPoint here = N3Client.playerPosition();
InteractionResult moving = N3Client.isMoving();
}
N3Client.sendClientMessage("Done.");
MethodReturns
getClient()the RuneLite Client, or null in the test JVM
loggedIn()true when logged into a world
playerPosition()the local player WorldPoint
isMoving()true while the local player walks or runs
inRegion(int regionID)true when the player stands in that region
sendClientMessage(String message)prints a game-chat message
canPathToTile(WorldPoint tile)a PathResult with isReachable() and getDistance()

getClient() returns null under Gradle, so guard every read. GameStateGuard relies on this for its null-safe checks.

Delays

Delays waits in ticks or milliseconds. The tick waits suit automation counting game ticks; the millisecond waits suit off-thread scripts.

Delays.tick(); // wait one game tick
Delays.tick(3); // wait three game ticks
Delays.waitUntil(() -> Bank.isOpen()); // block until a condition holds
InteractionResult ok = Delays.waitUntil(() -> done(), 5000); // with a timeout, returns false on timeout
MethodDoes
tick() / tick(int ticks)wait one or N game ticks
sleep(long ms)sleep a fixed time
waitUntil(InteractionResultSupplier condition)block until the condition holds
waitUntil(InteractionResultSupplier condition, long timeoutMs)block with a timeout; false on timeout
waitUntilTicks(InteractionResultSupplier condition, int ticks)block up to N ticks; false on timeout
getCurrentTick()the current client tick count

Inject a fake clock with setAdapterForTesting(...) and restore it with resetAdapterForTesting() so tests advance ticks without a live client.

ClientThreadBridge

ClientThreadBridge marshals work to the correct thread and flags violations. Some client calls require the client thread; others forbid it.

if (ClientThreadBridge.isClientThread()) {
doRead();
} else {
ClientThreadBridge.invoke(this::doRead); // run on the client thread now
}
ClientThreadBridge.invokeLater(this::queueWork); // run on the next client cycle
ClientThreadBridge.requireOffThread("walker planning"); // throws if on the client thread
MethodDoes
isClientThread()true when the caller runs on the client thread
invoke(Runnable)run now if on the client thread, else marshal to it
invokeLater(Runnable)run on the next client cycle
requireOffThread(String operation)guard work that must not block the client thread

Swap the threading model in tests with setAdapterForTesting(...) and resetAdapterForTesting().

HintArrows

HintArrows.read(Client) returns a HintArrowInfo snapshot of the active hint arrow: the kind (NPC, player, tile, none) and its target. Tutorial Island uses a coordinate hint only as secondary positioning evidence: the object on the hinted tile must still match the explicit transition ID before an interaction authorizes. This prevents bank, poll, and door stages from treating an arbitrary hinted object as authority.

HintArrows.HintArrowInfo arrow = HintArrows.read(N3Client.getClient());

ClientScreenshots

ClientScreenshots copies RuneLite's current buffer-provider pixels into an in-memory PNG snapshot. It stores only the latest successful capture and never writes screenshot data to disk.

ClientScreenshot capture = ClientScreenshots.capturePng();
if (capture != null) {
log.debug("Captured {}x{} {}", capture.getWidth(), capture.getHeight(), capture.getMimeType());
}

ClientScreenshot previous = ClientScreenshots.lastCapture();

Methods:

  • capturePng() - copies the current BufferProvider pixel array on the Swing EDT, encodes a PNG, caches it as the latest capture, and returns null when the client, buffer, pixels, dimensions, or encoding path proves unavailable.
  • lastCapture() - returns the latest successful in-memory capture, or null when none succeeds.
  • setAdapterForTesting(adapter) - installs a custom capture adapter and clears the cached capture.
  • resetAdapterForTesting() - restores the live RuneLite buffer adapter and clears the cached capture.

ClientScreenshot holds the immutable payload returned by capturePng(). It contains the canvas width and height, MIME type, base64-encoded PNG bytes, and capture timestamp:

  • getWidth()
  • getHeight()
  • getMimeType()
  • getBase64Data()
  • getCapturedAt()

Intended consumers include developer tooling, Agent Server/MCP diagnostics, and tests needing a serializable view of the current canvas. It offers neither a polling loop nor an action surface.

DeveloperMetrics

DeveloperMetrics builds a serializable diagnostic payload from the current client state. It supports developer heartbeats, support tooling, and debug snapshots.

Map<String, Object> snapshot = DeveloperMetrics.snapshot(N3Client.getClient(), false);

DeveloperMetrics.publishHeartbeat(
N3Client.getClient(),
webhookUrl,
secret,
true,
(url, payload) -> send(url, payload));

Methods:

  • snapshot(client, includeScreenshot) - returns a map containing timestamp, login/game state, world, revision, tick count, account hash, local player, skill levels, inventory, bank, session, and optional screenshot data.
  • publishHeartbeat(client, webhookUrl, secret, includeScreenshot, publisher) - wraps snapshot(...) in a developer_heartbeat envelope and passes it to the provided Publisher. Blank webhook URLs return false without publishing.
  • publishHeartbeat(client, webhookUrl, secret, includeScreenshot) - uses the default publisher.

Publishing serves as an optional diagnostic. It omits gameplay automation, issues no actions, and fails to drive plugin state machines.

WorldPoint and WorldArea utilities

WorldPointUtility and WorldAreaUtility hold the geometry helpers (distance, containment, plane handling) shared by the walker and query layers. They take no client state and remain safe to call in tests.

CameraApi

CameraApi acts as a read-only camera facade. CameraActions provides a separate event-driven movement boundary: it chooses the shortest wrapped yaw direction, plans bounded one-to-eight-tick arrow-key holds, and presses/releases through CanvasInput. It never writes camera yaw, pitch, or target fields. Callers own anticipation, hold timing, cancellation, and final-tolerance observation. Transient target candidates must not initiate movement.

CameraSnapshot camera = CameraApi.snapshot();
log.debug("Camera yaw={} pitch={} zoom={}",
camera.getYaw(),
camera.getPitch(),
camera.getZoom());

InteractionResult projected = CameraApi.isWorldPointInViewport(targetPoint);

Methods:

  • snapshot()
  • yaw()
  • pitch()
  • zoom()
  • isCanvasPointInViewport(point)
  • isWorldPointInViewport(worldPoint)
  • setAdapterForTesting(adapter), resetAdapterForTesting()

CameraActions.plan(snapshot, targetYaw, targetPitch, holdTicks) returns a CameraMovementPlan. Call press(client, plan), retain it for the planned game ticks, and always call release(client, plan) on completion, cancellation, or shutdown.

CameraSnapshot carries yaw, pitch, yaw target, pitch target, camera x/y/z, zoom, viewport width/height, and viewport x/y offsets. Viewport helpers answer only whether a canvas or projected world point sits inside the current viewport. They omit line-of-sight, reachability, or actionability implications.

The live adapter reads zoom from RuneLite gameval varclient IDs and projects world points through RuneLite Perspective. Zoom source and projection behavior remain live-verification pending until observed in-client.

LineOfSightApi

LineOfSightApi provides same-plane visibility helpers for world tiles, actors, and tile objects. Use the pure overload when the caller already owns a blocked-tile set. Use the client-backed overload to sample the current collision map from the live world view.

WorldPoint from = N3Client.playerPosition();
WorldPoint to = targetNpc.getWorldLocation();

InteractionResult visible = LineOfSightApi.hasLineOfSight(from, to);
int visibleSamples = LineOfSightApi.countVisible(from, sampleTiles, blockedTiles);

Methods:

  • hasLineOfSight(from, to, blockedTiles) - pure same-plane check against a caller-supplied collection of opaque tiles
  • hasLineOfSight(from, to) - client-backed same-plane check against the current collision map
  • canSee(actor) - local-player-to-actor visibility helper
  • canSee(tileObject) - local-player-to-object visibility helper
  • countVisible(anchor, samples, blockedTiles) - counts how many sample tiles have pure line of sight to the anchor
  • sampleLine(from, to) - samples the tiles touched by the same-plane line
  • setClientForTesting(client) - swaps the backing client; clear it with setClientForTesting(null)
Caution

hasLineOfSight(from, to, blockedTiles) acts as the pure overload. It respects only the blockedTiles collection you pass in and skips RuneLite collision data. For live NPC or tile visibility checks, prefer hasLineOfSight(from, to).

Behavior notes:

  • Plane mismatch returns false.
  • The client-backed overload samples intermediate tiles and treats CollisionDataFlag.BLOCK_MOVEMENT_FULL as opaque.
  • canSee(...) helpers use the local player's world tile as the origin.
  • sampleLine(...) returns a Set<WorldPoint>; treat membership as authoritative and avoid relying on iteration order.
  • Client-backed collision and visibility behavior remains live-verification pending for revision-sensitive cases.

ThreatMapApi

ThreatMapApi provides a read-only tile-safety facade built on live projectiles, graphics objects, NPC locations, and LineOfSightApi. It bypasses walking, pausing, clicking, or issuing gameplay actions. Callers use it inside their own loops to decide whether a destination or path appears currently unsafe.

Filters operate on live Projectile, GraphicsObject, and NPC instances. They usually mirror the same predicates you pass to the helpers described in Scene Query Actions.

List<WorldPoint> dangerousTiles = ThreatMapApi.threatenedTiles(
path,
projectile -> projectile.getId() == DANGEROUS_PROJECTILE_ID,
graphics -> graphics.getId() == HAZARD_GRAPHICS_ID,
npc -> npc.getCombatLevel() > 0,
1
);

if (!dangerousTiles.isEmpty()) {
log.debug("Threatened path tiles: {}", dangerousTiles);
}

Methods:

  • isTileThreatenedByProjectile(tile, filter) - checks whether any matching projectile currently targets the tile
  • isTileThreatenedByHazard(tile, filter) - checks whether any matching active graphics object currently occupies the tile
  • isTileThreatenedByNpc(tile, isAggressive, maxAttackRange) - checks whether any aggressive NPC threatens the tile by range and line of sight
  • isTileThreatened(tile, projectileFilter, hazardFilter, npcFilter, maxNpcAttackRange) - combined single-tile threat check across enabled source types
  • isPathThreatened(path, projectileFilter, hazardFilter, npcFilter, maxNpcAttackRange) - returns true when any path tile proves threatened
  • threatenedTiles(path, projectileFilter, hazardFilter, npcFilter, maxNpcAttackRange) - returns an immutable list of the threatened tiles from the path
  • setClientForTesting(client) - swaps the backing client; clear it with setClientForTesting(null)
Caution

For isTileThreatened(...), isPathThreatened(...), and threatenedTiles(...), a null filter means "skip this source entirely", not "match everything". Pass projectile -> true, graphics -> true, or npc -> true to include every source of that type.

Behavior notes:

  • Projectile targeting prefers Projectile.getTargetPoint() when RuneLite exposes a resolved world tile and falls back to local target coordinates only when the world target proves unavailable.
  • Hazard checks use the top-level world view and ignore finished graphics objects.
  • NPC checks operate same-plane only and use straight-line tile distance for maxAttackRange. They omit path distance.
  • isTileThreatenedByNpc(tile, null, range) still considers NPCs interacting with the local player. In the combined helpers, npcFilter == null skips NPC checks entirely.
  • NPC threat confirmation uses LineOfSightApi.hasLineOfSight(npcTile, tile). Blocked collision tiles can suppress an otherwise in-range NPC threat.
  • Null client, null world view, null tile, null path, and empty path inputs return false or an empty list rather than throwing.
note

Live verification remains pending for revision-sensitive projectile targets, graphics-object hazard semantics, and NPC threat modeling. Treat this API as a useful read surface, not as a proof that a tile stays universally safe in every encounter.

Canvas And Keyboard Input

KeyboardHelper and CanvasInput dispatch synthetic AWT events to the RuneLite client canvas on the calling thread. They skip executors and sleeps; callers space press/release pairs across game ticks.

KeyboardHelper methods:

  • type(Client, String) - sends pressed/typed/released events for each character
  • backspace(Client) - sends one backspace press/release pair

CanvasInput methods:

  • arrowPress(Client, keyCode) and arrowRelease(Client, keyCode) - camera arrow-key nudges
  • rightClick(Client, x, y) - opens a right-click menu at a canvas coordinate
  • leftClick(Client, x, y) - neutral click, commonly used to dismiss a menu

HotkeyEventListener adapts a configured RuneLite Keybind supplier to an SDK press callback. Plugins register the listener with RuneLite's KeyManager, but keep the listener implementation and callback semantics in sdk.input. HotkeyCaptureEventListener converts a focused component's next key press into the persisted Keybind without duplicating key-event parsing in plugin panels.

These helpers target UI-level input behaviors lacking a server-packet equivalent, such as camera fidgets or text-entry flows. Prefer packet/action APIs for normal game interactions.