1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# 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`