A small, readable OSRS client with a plugin panel, entity visuals, and a worked example bot — in about two thousand lines you can hold in your head.
It exists to be learned from and hacked on. There is no auto-updater, no account manager, no plugin store. What there is:
- Everything is Java. Plugins, settings, the control panel, and all the drawing. No C++ toolchain needed to write a bot or an overlay.
- Overlays are plain Java2D. Your plugin gets a
Graphics2Dover the game window and draws whatever it likes — shapes, alpha, antialiased text, images. Nothing had to expose a "draw box" primitive. - Settings build their own UI. Declare a setting; the control panel grows the right widget for it. No plugin writes a line of Swing.
- No network code at all. We never build a packet. We call the game's own "do this menu action" function and let it build and send the packet. That is the single biggest reason this codebase is small, and the reason it survives most game updates.
- Ten native methods. That is the entire unsafe surface, all in one file.
launcher injected into the game
┌──────────────┐ ┌────────────────────────────────┐
│ one button │ ── inject ──> │ kewlklient.dll │
└──────────────┘ │ reads memory │
│ starts a JVM ───────────────┼──> kewlklient.jar
│ 10 natives ─────────────────┼──> api + your plugins
│ <── one finished image/frame ─┼── overlays (Java2D)
└────────────────────────────────┘ control panel (Swing)
Java draws, C++ shows. Java renders the whole overlay into an image and hands it back once a frame; C++ puts it on screen and otherwise stays out of the way. That inversion is why a plugin can draw anything Java2D can draw, and it costs one memcpy a frame.
Windows only. The whole thing is Win32 — there is no Linux or Mac build and there is not going to be one.
You need three things installed. The first two you probably have; the third is the one people miss:
| why | get it | |
|---|---|---|
| JDK 17+ | the plugins are Java | IntelliJ ships one, or adoptium.net |
| CMake | the injected part is C++ | cmake.org/download — tick Add CMake to the system PATH |
| A C++ compiler | same | Build Tools for Visual Studio → Desktop development with C++. The full IDE is not needed. |
You do not need Gradle — the wrapper in this repo fetches it.
- File → Open and pick the folder you cloned. It imports as a Gradle project.
- Pick the KewlKlient run configuration (it is checked into the repo) and press Run.
- Start OSRS and log in.
- In the little window that appeared, press LAUNCH OSRS CLIENT NOW.
That is it — no config file to edit. The build points kewlklient.ini at whichever JDK IntelliJ is
using, and puts everything in build\dist\.
gradlew runSame thing. gradlew dist builds without launching, and build.bat is a wrapper around it for people
who prefer a double-click.
A control panel opens beside the game with a switch and settings for every plugin. The same plugins have hotkeys:
| key | does |
|---|---|
| F1 | player visuals on/off |
| F2 | NPC visuals on/off |
| F5 | the woodcutter on/off |
| F3, F4, F6–F8 | free, for your plugins |
If nothing appears, see Troubleshooting.
Extend Plugin, override what you need, add one line to the list. This one draws a marker on the
nearest cow and counts them:
package kewl.plugins;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import kewl.Plugin;
import kewl.api.Entity;
import kewl.api.Npcs;
import kewl.ui.Hud;
public final class CowSpotter extends Plugin {
public CowSpotter() {
config.number("range", "Range", "How far to look", 10, 1, 30);
config.colour("colour", "Colour", "", new Color(255, 120, 200));
}
@Override public String name() { return "Cow spotter"; }
@Override public String description() { return "Marks the nearest cow."; }
@Override public int hotkey() { return 5; } // F6
@Override
public void render(Graphics2D g) {
Entity cow = Npcs.nearestWithin(config.number("range"), 2805);
if (cow == null) return;
Point at = cow.screen();
if (at != null) Hud.entityBox(g, at, 16, 30, config.colour("colour"));
}
}Add it in java/kewl/KewlKlient.java:
private static final List<Plugin> PLUGINS = new ArrayList<>(List.of(
new kewl.plugins.PlayerVisuals(),
new kewl.plugins.NpcVisuals(),
new kewl.plugins.Woodcutter(),
new kewl.plugins.CowSpotter() // <- yours
));Rebuild, restart the client. Your plugin is in the panel with a range slider and a colour picker you never wrote. That list is the entire plugin system — no scanning, no annotations, no manifest, nothing that can silently fail to find your class.
Read Woodcutter.java next. It is the worked example and does
all four things at once: settings, a decision loop that acts on the game, a world overlay, and a
statistics panel.
| for | rules | |
|---|---|---|
tick() |
deciding and acting | runs every frame while enabled |
render(Graphics2D) |
drawing, and nothing else | runs after every plugin has ticked |
Keeping them apart means you can turn the drawing off without changing behaviour.
Neither may block. They run about thirty times a second on the overlay thread; a Thread.sleep in
either freezes the overlay. For "every few seconds", keep a long lastX field and compare
System.currentTimeMillis() — Woodcutter shows the pattern, and also shows why you want a timer
and an animation check rather than either alone.
// kewl.api.Game -- the world, as of the start of this frame
Game.ready() Game.me()
Game.npcs() Game.players() Game.entities()
Game.toScene(worldX, worldY) // world -> scene, null if not loaded
// projection, through the game's own camera maths
Game.projectWorld(x, y) // -> Point, or null when off screen
Game.tileOutlineWorld(x, y) // -> Polygon lying flat on the ground
// kewl.api.Npcs / Players -- finding things
Npcs.nearest(1278) Npcs.nearestWithin(10, 2805)
Npcs.withId(ids...) Players.nearest()
// an Entity
e.id() e.uid() e.worldX() e.worldY() e.distance()
e.animation() e.isIdle() e.orientation() e.screen()
// you
Game.me().worldX() .isIdle() .health() .healthPercent() .runEnergy()
// stats
Skills.level(Skill.WOODCUTTING) Skills.experience(...) Skills.boost(...)
// kewl.api.Actions -- doing things, in WORLD coordinates
Actions.walkTo(x, y)
Actions.object(treeId, x, y) // chop / mine / open
Actions.npc(entity) // first option
Actions.npc(entity, 2) // second optionTwo coordinate systems, and mixing them up is the most common bug here. World coordinates are what your minimap shows (Lumbridge ≈ 3222, 3218). Scene coordinates are 0–104 within the loaded chunk, and they are what the game's click function actually wants. Everything a plugin touches is in world coordinates; the conversion happens in one place.
render() hands you a Graphics2D over the whole game window, already antialiased, with (0,0) at the
top-left of the game's client area. Anything Java2D can do works. kewl.ui.Hud has the three things
every overlay ends up wanting:
Hud.entityBox(g, point, 16, 30, colour); // a box standing on a point
Hud.tile(g, Game.tileOutlineWorld(x, y), colour); // a tile highlight lying on the ground
Hud.text(g, "hello", x, y, colour); // text with an outline, readable on any background
Hud.panel(g, 12, 12, "My plugin", new Hud.Lines() // a statistics panel that sizes itself
.add("state", "running")
.add("xp/hr", 41234));Tile outlines go through the game's own projection corner by corner, so they sit on the ground with the right perspective and stay correct while the camera turns.
launcher/main.cpp — finds osclient.exe, writes the DLL path into it, and calls LoadLibraryA on a
remote thread. The oldest, most boring injection there is, in about thirty lines.
client/offsets.hpp— every game-specific number, in one file, each with a note on how to re-find it.client/game.hpp— guarded memory reads, entity enumeration, projection, anddoAction.client/overlay.hpp— the transparent window, and the one function that puts pixels in it.client/jvm.hpp— starts the JVM and registers the ten natives. The whole unsafe surface.client/dllmain.cpp— finds the game window and runs the 30 fps loop.
java/kewl/Natives.java— the ten natives, declared. You will not call these directly.java/kewl/api/— the world with names on it:Game,Entity,Local,Npcs,Players,Skills,Actions.java/kewl/ui/—Theme(all the colours),Hud(drawing helpers),Sidebar(the control panel).java/kewl/config/— settings that build their own controls.java/kewl/plugins/— the plugins.
The overlay is a transparent always-on-top window, not a renderer hook. Hooking would need a detour library, a graphics API to get right, and it crashes inside someone else's render loop when you get it wrong. A layered window is a page of code you can read in one sitting. The cost: it will not show up in screenshots or recordings, and it can flicker. That trade is on purpose.
Java does the drawing. C++ used to draw the boxes and it was the wrong split: every new visual meant a new primitive exposed across the JNI boundary. Now Java renders the frame into an image and C++ shows it, so a plugin has the whole of Java2D and C++ has one job. It costs one memcpy of a full-screen image per frame — about 8 MB at 1080p, well under a millisecond, and we were already blitting those pixels.
Bot clients usually reimplement the game's network protocol — a big table of opcodes and byte layouts that changes every update and fails silently when it drifts.
KewlKlient does not. doAction is the client's own menu-action entry point: the same function that runs
when you right-click a tree and choose "Chop down". We call it with the same arguments and the client
builds and sends the packet itself. Consequences:
- We never need to know the wire format.
- Anti-cheat sees a normally-constructed packet, because it is one.
- One function to re-find after an update instead of a hundred opcodes.
Jagex rebuilds the client roughly weekly. Two kinds of number in offsets.hpp rot at different speeds:
- Function RVAs (
DO_ACTION,WORLD_TO_SCREEN) — assume these are wrong after any patch. - Struct offsets (
ENTITY_SCENE_X,SCENE…) — stabler, but they do move. One of them shifted by0x10between builds a few weeks apart.
BUILD_ID is the fingerprint. If it does not match, do not just bump it — every other number was
measured on that build.
.\tools\ghidra_headless.ps1 -Ghidra "C:\ghidra_11.4" -Script FindOffsets.java
type tools\offsets_found.txtFirst run analyses 16 MB and takes 10–20 minutes; it is cached after that.
The method: anchor on a name the client uses for itself, then walk to what you want. The client ships
a Lua binding layer that registers its own functions by name, and those names are plain text in the
binary — worldToScreenCoord, npcCoord, playerCoord. Find the string, find its references, and you
are inside the registration code for the function you are after. FindOffsets.java does the finding;
you open the referencing function and read off the pointer it registers.
Do not scan for byte patterns. A byte pattern is a guess about instructions the compiler may rearrange. When it breaks it does not error — it gives you an address that decompiles into something plausible and wrong.
Prefer IDA if you own it (better decompiler on this binary): .\tools\ida_headless.ps1. It works on a
copy and deletes the stale database when the exe changes, because IDA will happily answer from last
month's build without telling you.
There is also a Claude skill in .claude/skills/deob/ that carries this whole method — if you use
Claude Code, ask it to find an offset and it will follow it.
Game.OPLOC1 = 3 is "first option on a scenery object". Other actions have other numbers. To find one,
attach a debugger to DO_ACTION, do the action by hand in game, and read the opcode argument. That is
how every number in this repo was found — none of them were guessed.
Genuinely useful, roughly easiest first:
- Find the nearest tree automatically. The woodcutter needs a typed-in tile because nothing here can enumerate scenery — that means walking the scene's object grid, about six more offsets. Land this and every gathering plugin gets shorter, and the woodcutter loses its most awkward setting. (the highest-value one on this list, by a distance)
- Names. NPC and player names live behind a pointer chain the client's own
npcNamebinding walks. Everything currently shows an id where it wants to show a name. - Inventory reading. Nearly every bot needs "am I full yet".
- Ground items. Pairs with the above to make a looter possible.
- Saving settings. Config lives in memory and dies with the client. A small properties file next to the jar, written on change, would do it — no new offsets, pure Java, a good first PR.
- A minimap overlay. All the pieces exist; it just needs the minimap's own projection.
Please keep the three rules this codebase is built on: no packet building, every offset gets a comment saying how it was found, and nothing claims to work until it has been seen working.
Nothing happens when I press the button.
Is the game actually running and logged in? The launcher looks for osclient.exe by name. If it says the
game refused the DLL, you built 32-bit — the game is x64 and rejects a 32-bit DLL silently.
The overlay says "java: could not load jvm.dll".
java= in kewlklient.ini is wrong. It needs a folder with bin\server\jvm.dll under it. A JDK always
has that; some JREs do not.
The build says "CMake is not installed (or not on your PATH)".
Install it from the table above. If you just installed it, restart IntelliJ — it caches the PATH
it was started with, so a new install is invisible to it until then.
The build says "No CMAKE_CXX_COMPILER could be found". No C++ compiler. Install the Build Tools from the table above.
The overlay says "kewl/KewlKlient not found".
kewlklient.jar must sit next to kewlklient.dll. gradlew dist puts all four files in build\dist\
and now checks they are actually there before claiming success — it used to be possible for the build
to pass while producing no DLL at all.
Boxes are in the wrong place, or the client crashes on inject. The game updated and the offsets moved. See When the game updates.
Boxes flicker. Expected — it is a layered window, not a renderer hook. See "The native half".
GPL-3.0. See LICENSE. In plain terms:
- Anyone can use it, for anything, including commercially.
- It has to stay open. If you distribute a modified version — or anything built on it — you have to ship the source under the GPL too. You cannot take this closed.
- Credit stays with it. Keep the copyright notices and say what you changed.
If you fork it, a link back here is appreciated on top of what the licence requires.
Copyright (C) 2026 StoneShorts and the KewlKlient contributors.
This is a personal-use tool for your own account. Automating a game breaks its rules and can get that account banned. That is your call to make, and yours to live with.