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.
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.");
| 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 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
| Method | Does |
|---|---|
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
| 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
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 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 is unavailable.lastCapture()- returns the latest successful in-memory capture, ornullwhen 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)- 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 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 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) 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_FULLas opaque. canSee(...)helpers use the local player's world tile as the origin.sampleLine(...)returns aSet<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 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 is 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 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 == nullskips 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
falseor an empty list rather than throwing.
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 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 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.