Skip to main content

Client Utilities

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 (varbit and varplayer reads) has its own page; see game-vars.md.

note

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

N3Client

N3Client is the boundary to the RuneLite client. Higher layers read the client through it. This keeps them swappable in tests.

if (N3Client.loggedIn()) {
WorldPoint here = N3Client.playerPosition();
boolean 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 that counts 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
boolean 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(BooleanSupplier condition)block until the condition holds
waitUntil(BooleanSupplier condition, long timeoutMs)block with a timeout; false on timeout
waitUntilTicks(BooleanSupplier 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

Some client calls must run on the client thread, others must not. ClientThreadBridge marshals work to the right thread and flags violations.

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 reads it to follow the tutorial's pointer through the bank and poll booths.

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 is unavailable.
  • lastCapture() - returns the latest successful in-memory capture, or null when none has succeeded.
  • 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 is 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 are developer tooling, Agent Server/MCP diagnostics, and tests that need a serializable view of the current canvas. It is not a polling loop or action surface.

DeveloperMetrics

DeveloperMetrics builds a serializable diagnostic payload from the current client state. It is intended for 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 is optional and diagnostic. It is not part of gameplay automation, does not issue actions, and should not be used to drive a plugin state machine.

WorldPoint and WorldArea utilities

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

CameraApi

CameraApi is a read-only camera facade. It intentionally does not rotate, zoom, or move the camera; camera writes can affect anti-ban behavior and should only be added later as opt-in result-aware actions if a real caller needs them.

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

boolean projected = CameraApi.isWorldPointInViewport(targetPoint);

Methods:

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

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 is inside the current viewport; they do not imply line-of-sight, reachability, or actionability.

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, and use the client-backed overload when you want the current collision map sampled from the live world view.

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

boolean 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) is the pure overload. It only respects the blockedTiles collection you pass in and does not read 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 do not rely on iteration order.
  • Client-backed collision and visibility behavior is still live-verification pending for revision-sensitive cases.

ThreatMapApi

ThreatMapApi is a read-only tile-safety facade built on live projectiles, graphics objects, NPC locations, and LineOfSightApi. It does not walk, pause, click, or issue any gameplay action. Callers use it inside their own loops to decide whether a destination or path is currently unsafe.

Filters operate on live Projectile, GraphicsObject, and NPC instances, so they usually mirror the same predicates you would 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 is 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 when you want 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 is unavailable.
  • Hazard checks use the top-level world view and ignore finished graphics objects.
  • NPC checks are same-plane only and use straight-line tile distance for maxAttackRange; they do not use 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), so 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 is still 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 is 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 do not create executors and they do not sleep; 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 are for UI-level input behaviors that have no server-packet equivalent, such as camera fidgets or text-entry flows. Prefer packet/action APIs for normal game interactions.