Client Utilities
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.
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.");
| Method | Returns |
|---|---|
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
| Method | Does |
|---|---|
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
| Method | Does |
|---|---|
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 currentBufferProviderpixel array on the Swing EDT, encodes a PNG, caches it as the latest capture, and returnsnullwhen the client, buffer, pixels, dimensions, or encoding path proves unavailable.lastCapture()- returns the latest successful in-memory capture, ornullwhen 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)- wrapssnapshot(...)in adeveloper_heartbeatenvelope and passes it to the providedPublisher. Blank webhook URLs returnfalsewithout 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 tileshasLineOfSight(from, to)- client-backed same-plane check against the current collision mapcanSee(actor)- local-player-to-actor visibility helpercanSee(tileObject)- local-player-to-object visibility helpercountVisible(anchor, samples, blockedTiles)- counts how many sample tiles have pure line of sight to the anchorsampleLine(from, to)- samples the tiles touched by the same-plane linesetClientForTesting(client)- swaps the backing client; clear it withsetClientForTesting(null)
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_FULLas opaque. canSee(...)helpers use the local player's world tile as the origin.sampleLine(...)returns aSet<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 tileisTileThreatenedByHazard(tile, filter)- checks whether any matching active graphics object currently occupies the tileisTileThreatenedByNpc(tile, isAggressive, maxAttackRange)- checks whether any aggressive NPC threatens the tile by range and line of sightisTileThreatened(tile, projectileFilter, hazardFilter, npcFilter, maxNpcAttackRange)- combined single-tile threat check across enabled source typesisPathThreatened(path, projectileFilter, hazardFilter, npcFilter, maxNpcAttackRange)- returnstruewhen any path tile proves threatenedthreatenedTiles(path, projectileFilter, hazardFilter, npcFilter, maxNpcAttackRange)- returns an immutable list of the threatened tiles from the pathsetClientForTesting(client)- swaps the backing client; clear it withsetClientForTesting(null)
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 == nullskips 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
falseor an empty list rather than throwing.
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 characterbackspace(Client)- sends one backspace press/release pair
CanvasInput methods:
arrowPress(Client, keyCode)andarrowRelease(Client, keyCode)- camera arrow-key nudgesrightClick(Client, x, y)- opens a right-click menu at a canvas coordinateleftClick(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.