Sign in Sign up
kretrod/lodestone Public
Branches
master
PreviewCode160 lines (126 loc) · 7.4 KB Raw

Lodestone

A cheat client for Minecraft Java Edition, and the tooling used to build it. Verified against Minecraft 26.2 (Fabric, Java 25) on Windows 11.

It comes in two halves, which is worth knowing before reading the code:

The client (crates/client) is a DLL injected into the game. It hooks glfwSwapBuffers, draws its menu into the game's own OpenGL context, and drives the game through JNI — the JVM's own API — so field writes go through the GC's write barriers and methods can simply be called.

The explorer (crates/cli) is a fully external process that never puts a byte of code inside the game: OpenProcess + ReadProcessMemory only. It was the original project, and it is still how every name the client binds gets verified against the running game. The section below on VMStructs describes it.

The client is deliberately client-side only. Every module changes how your own client behaves, which is the whole of what a client can do. Nothing reaches for the integrated server — no editing world time, no server-side health — so nothing here depends on being the one running the world.

The problem with an external for a Java game

A normal game trainer follows a static pointer chain: a fixed offset in the main module, a few dereferences, a player struct. A JVM gives you none of that. There are no exported symbols for game objects, the layout of a Java class is decided at runtime, and the garbage collector moves objects, so any address you write down is stale the moment a GC runs.

What HotSpot does give you is a description of itself.

VMStructs: the VM describing its own C++ layout

jvm.dll exports a table that jhsdb and the Serviceability Agent use to debug a live JVM from outside the process:

gHotSpotVMStructs            -> VMStructEntry[]  { typeName, fieldName, isStatic, offset, address }
gHotSpotVMTypes              -> VMTypeEntry[]    { typeName, superclassName, size }
gHotSpotVMStructEntryArrayStride, …Offset        -> how to walk the arrays above

Read that table and you know the byte offset of every field of every HotSpot internal structure for this exact build — no hardcoded offsets, nothing to update when the JDK changes. lodestone probe dumps it:

VMStructs: 581 fields, 332 types, 346 int consts, 97 long consts
compressed oops   base 0x0  shift 3
compressed klass  base 0x0  shift 0
Klass           _name              +24
InstanceKlass   _fieldinfo_stream  +448

From there to player.position.x

ClassLoaderDataGraph::_head          static address, straight out of VMStructs
  -> ClassLoaderData::_klasses       linked list of Klass* via _next_link
     -> Klass::_name                 Symbol* -> "net/minecraft/client/Minecraft"
  -> InstanceKlass::_fieldinfo_stream UNSIGNED5 records -> field names + offsets
  -> Klass::_java_mirror             the java.lang.Class object: where statics live
     -> static field `instance`      the Minecraft singleton
        -> instance field `player`   LocalPlayer
           -> `position`             Vec3
              -> `x`                 a double, at a known offset

Two details make this work at all:

Field names. JDK 21 replaced the old u2[] field array with _fieldinfo_stream, a stream of UNSIGNED5-packed records (name sig offset access flags Optionals(flags)). Lodestone decodes it, then resolves each name/signature index through the class's constant pool — except for VM-injected fields, which index HotSpot's own vmSymbols table instead.

Objects move, metadata does not. Klass and the field stream live in Metaspace and never move, so offsets can be cached forever. Object addresses cannot: every tick re-walks from Minecraft.instance, a static field in a class mirror, which is a stable root.

What it will not do

It never writes an object reference. The JIT emits GC write barriers around reference stores; forging one from outside without them can leave the collector with a pointer it does not know about. Lodestone writes primitives only — doubles, floats, ints, booleans — and refuses anything else. Teleporting works by overwriting the components of the player's own Vec3 in place, and it refuses to do even that to a shared constant like Vec3.ZERO.

It never writes in multiplayer. Every write is gated on Minecraft.singleplayerServer != null. Attached to a server, it reads and shows state and nothing more. In single player the integrated server is in the same JVM, so the trainer edits the authoritative ServerPlayer as well as the client's — which is why flight and noclip stick instead of rubber-banding.

Use

lodestone-inject.exe                      load the client into the running game
lodestone-inject.exe --eject              stop it again

lodestone.exe procs                       find the game
lodestone.exe probe                       dump the VMStructs database
lodestone.exe classes minecraft/client    search 45k loaded classes
lodestone.exe class net/minecraft/client/Minecraft        fields + live statics
lodestone.exe get  net/minecraft/client/Minecraft instance player position y
lodestone.exe set  net/minecraft/client/Minecraft instance player abilities flying true
lodestone.exe find net/minecraft/client/Minecraft instance --of ClockState
lodestone.exe obj 0x715774258             identify and dump any object
lodestone.exe vmtype InstanceKlass        what HotSpot says about its own type
lodestone.exe methods <class> [filter]     methods with their JVM descriptors
lodestone.exe trainer --fly --tp 100 80 100     headless engine driver

methods is what makes the client maintainable: every JNI signature it binds was read out of the running game rather than guessed.

Path syntax is <Class> <staticField> [field…], with [n] to index an array:

lodestone.exe get net/minecraft/client/Minecraft \
    instance singleplayerServer playerList players elementData [0] position y

Insert opens the menu. Every module has its own bindable key, plus a panic key that switches everything off; settings save to C:\lodestone\config.txt.

Layout

crates/core     external engine: RPM/WPM, VMStructs, classes, fields, methods
crates/cli      the explorer built on it
crates/inject   the loader
crates/client   the injected client:
                  hook.rs     inline hook with instruction relocation
                  jni.rs      the JVM's own API, with local-frame discipline
                  mc.rs       Minecraft's classes, resolved once at startup
                  cheats.rs   the modules
                  overlay.rs  egui inside the game's GL context
                  input.rs    window-procedure hook, keybinds
                  state.rs    the module registry
                  config.rs   settings on disk

Three bugs worth knowing about

Drawing inside someone else's render loop means inheriting their GL state, and each of these took a while to find:

  • A sampler object bound to texture unit 0 overrides the font atlas's own parameters and renders the entire menu flat black.
  • GL_UNPACK_ROW_LENGTH left at 32 scrambles every font-atlas upload into noise. A bound GL_PIXEL_UNPACK_BUFFER does the same thing, worse.
  • Unmapping the DLL on unload crashes the game: the window still points at our window procedure. Unload now unhooks and goes inert, and leaves the module resident — a restart is what clears it.

Cross-compiled from Linux: cargo build --release --target x86_64-pc-windows-gnu