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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
// Minecraft bindings: the handles the client needs, resolved by name once.
//
// Every id is looked up through `jni::Env`, which clears exceptions, so a rename
// in a future version costs the one feature that used it rather than the whole
// client. Anything optional is reported through `missing` so the menu can say
// what is unavailable instead of silently doing nothing.
//
// Signatures here were checked against the decompiled 26.2 classes rather than
// guessed — 26.2 renamed a good deal (ResourceLocation became Identifier, and
// textures went behind Blaze3D's GpuTexture), so assuming 1.20 shapes would
// quietly resolve nothing.
#pragma once
#include <string>
#include <vector>
#include "jni.hpp"
#include "state.hpp"
namespace lodestone::mc {
using jni::Args;
using jni::Env;
using jni::jv;
/// Core handles: the client singleton, the local player, the world, and the
/// per-entity reads that every feature starts from.
class Mc {
public:
/// Resolve everything. Returns false only when the client class itself is
/// unrecognisable, which means the version is too different to continue.
bool resolve(const Env& j, std::vector<std::string>& missing) {
c_minecraft_ = j.find_class("net/minecraft/client/Minecraft");
c_entity_ = j.find_class("net/minecraft/world/entity/Entity");
c_living_ = j.find_class("net/minecraft/world/entity/LivingEntity");
c_vec3_ = j.find_class("net/minecraft/world/phys/Vec3");
c_player_ = j.find_class("net/minecraft/world/entity/player/Player");
if (!c_minecraft_ || !c_entity_ || !c_vec3_) {
missing.emplace_back("Minecraft/Entity/Vec3 classes");
return false;
}
m_get_instance_ = j.static_method(c_minecraft_, "getInstance",
"()Lnet/minecraft/client/Minecraft;");
f_player_ = j.field(c_minecraft_, "player", "Lnet/minecraft/client/player/LocalPlayer;");
f_level_ = j.field(c_minecraft_, "level", "Lnet/minecraft/client/multiplayer/ClientLevel;");
f_screen_ = j.field(c_minecraft_, "screen", "Lnet/minecraft/client/gui/screens/Screen;");
f_game_mode_ = j.field(c_minecraft_, "gameMode",
"Lnet/minecraft/client/multiplayer/MultiPlayerGameMode;");
// `position` exists as both a private field and a public accessor; the
// accessor is the stable one.
m_position_ = j.method(c_entity_, "position", "()Lnet/minecraft/world/phys/Vec3;");
f_x_ = j.field(c_vec3_, "x", "D");
f_y_ = j.field(c_vec3_, "y", "D");
f_z_ = j.field(c_vec3_, "z", "D");
f_y_rot_ = j.field(c_entity_, "yRot", "F");
f_x_rot_ = j.field(c_entity_, "xRot", "F");
m_on_ground_ = j.method(c_entity_, "onGround", "()Z");
m_is_alive_ = j.method(c_entity_, "isAlive", "()Z");
m_get_id_ = j.method(c_entity_, "getId", "()I");
m_bounding_box_ = j.method(c_entity_, "getBoundingBox", "()Lnet/minecraft/world/phys/AABB;");
if (c_living_) {
m_get_health_ = j.method(c_living_, "getHealth", "()F");
m_get_max_health_ = j.method(c_living_, "getMaxHealth", "()F");
}
// The field of view the world is actually rendered with, which the ESP
// projection has to match. It arrives as a boxed Integer because the
// option type erases.
f_options_ = j.field(c_minecraft_, "options", "Lnet/minecraft/client/Options;");
jclass options = j.find_class("net/minecraft/client/Options");
jclass option_instance = j.find_class("net/minecraft/client/OptionInstance");
jclass integer = j.find_class("java/lang/Integer");
m_fov_ = j.method(options, "fov", "()Lnet/minecraft/client/OptionInstance;");
m_option_get_ = j.method(option_instance, "get", "()Ljava/lang/Object;");
m_int_value_ = j.method(integer, "intValue", "()I");
// The settings a visual module changes are OptionInstance objects on
// Options, so reading or writing one means boxing through java.lang.
m_option_set_ = j.method(option_instance, "set", "(Ljava/lang/Object;)V");
f_opt_fov_ = j.field(options, "fov", "Lnet/minecraft/client/OptionInstance;");
f_opt_gamma_ = j.field(options, "gamma", "Lnet/minecraft/client/OptionInstance;");
f_opt_bob_view_ = j.field(options, "bobView", "Lnet/minecraft/client/OptionInstance;");
f_opt_damage_tilt_ =
j.field(options, "damageTiltStrength", "Lnet/minecraft/client/OptionInstance;");
f_opt_render_distance_ =
j.field(options, "renderDistance", "Lnet/minecraft/client/OptionInstance;");
c_integer_ = integer;
c_double_ = j.find_class("java/lang/Double");
c_boolean_ = j.find_class("java/lang/Boolean");
m_int_value_of_ = j.static_method(integer, "valueOf", "(I)Ljava/lang/Integer;");
m_double_value_ = j.method(c_double_, "doubleValue", "()D");
m_double_value_of_ = j.static_method(c_double_, "valueOf", "(D)Ljava/lang/Double;");
m_bool_value_ = j.method(c_boolean_, "booleanValue", "()Z");
m_bool_value_of_ = j.static_method(c_boolean_, "valueOf", "(Z)Ljava/lang/Boolean;");
// Occlusion culling, the frame timer, and two one-shot actions.
f_smart_cull_ = j.field(c_minecraft_, "smartCull", "Z");
f_delta_tracker_ =
j.field(c_minecraft_, "deltaTracker", "Lnet/minecraft/client/DeltaTracker$Timer;");
if (jclass timer = j.find_class("net/minecraft/client/DeltaTracker$Timer")) {
f_ms_per_tick_ = j.field(timer, "msPerTick", "F");
}
// Client-side use/attack cooldowns the server does not enforce.
f_right_click_delay_ = j.field(c_minecraft_, "rightClickDelay", "I");
f_miss_time_ = j.field(c_minecraft_, "missTime", "I");
if (jclass game_mode = j.find_class("net/minecraft/client/multiplayer/MultiPlayerGameMode")) {
f_destroy_delay_ = j.field(game_mode, "destroyDelay", "I");
}
m_start_attack_ = j.method(c_minecraft_, "startAttack", "()Z");
c_local_player_ = j.find_class("net/minecraft/client/player/LocalPlayer");
m_respawn_ = j.method(c_local_player_, "respawn", "()V");
for (auto& [id, what] : {std::pair{m_get_instance_ ? 1 : 0, "Minecraft.getInstance()"},
std::pair{f_player_ ? 1 : 0, "Minecraft.player"},
std::pair{f_level_ ? 1 : 0, "Minecraft.level"},
std::pair{m_position_ ? 1 : 0, "Entity.position()"}}) {
if (!id) missing.emplace_back(what);
}
return true;
}
// ---- handles ---------------------------------------------------------
jobject instance(const Env& j) const {
return j.call_static_obj(c_minecraft_, m_get_instance_);
}
jobject player(const Env& j, jobject mc) const { return j.obj_field(mc, f_player_); }
jobject level(const Env& j, jobject mc) const { return j.obj_field(mc, f_level_); }
jobject game_mode(const Env& j, jobject mc) const { return j.obj_field(mc, f_game_mode_); }
/// A screen is open (inventory, chat, pause) — most features should hold off.
bool screen_open(const Env& j, jobject mc) const {
jobject s = j.obj_field(mc, f_screen_);
if (!s) return false;
j.delete_local(s);
return true;
}
// ---- per-entity reads ------------------------------------------------
bool position(const Env& j, jobject entity, state::Vec3* out) const {
jobject v = j.call_obj(entity, m_position_);
if (!v) return false;
out->x = j.double_field(v, f_x_);
out->y = j.double_field(v, f_y_);
out->z = j.double_field(v, f_z_);
j.delete_local(v);
return true;
}
float yaw(const Env& j, jobject entity) const { return j.float_field(entity, f_y_rot_); }
float pitch(const Env& j, jobject entity) const { return j.float_field(entity, f_x_rot_); }
bool on_ground(const Env& j, jobject entity) const { return j.call_bool(entity, m_on_ground_); }
bool is_alive(const Env& j, jobject entity) const { return j.call_bool(entity, m_is_alive_); }
int entity_id(const Env& j, jobject entity) const { return j.call_int(entity, m_get_id_); }
float health(const Env& j, jobject entity) const { return j.call_float(entity, m_get_health_); }
float max_health(const Env& j, jobject entity) const {
return j.call_float(entity, m_get_max_health_);
}
bool is_player(const Env& j, jobject entity) const { return j.is_instance(entity, c_player_); }
/// The rendered field of view, or 0 when it could not be read.
float fov(const Env& j, jobject mc) const {
jobject options = j.obj_field(mc, f_options_);
if (!options) return 0.0f;
jobject instance = j.call_obj(options, m_fov_);
if (!instance) return 0.0f;
jobject boxed = j.call_obj(instance, m_option_get_);
float out = boxed ? static_cast<float>(j.call_int(boxed, m_int_value_)) : 0.0f;
j.delete_local(boxed);
j.delete_local(instance);
j.delete_local(options);
return out;
}
// ---- options ---------------------------------------------------------
//
// Each getter reports whether it could actually read, so a module only ever
// remembers a previous value it really saw — and therefore only restores
// one it is sure about.
/// The OptionInstance a field names, or null. The caller owns the ref.
jobject option(const Env& j, jobject mc, jfieldID field) const {
jobject options = j.obj_field(mc, f_options_);
if (!options) return nullptr;
jobject opt = j.obj_field(options, field);
j.delete_local(options);
return opt;
}
bool option_double(const Env& j, jobject mc, jfieldID field, double* out) const {
jobject opt = option(j, mc, field);
if (!opt) return false;
jobject boxed = j.call_obj(opt, m_option_get_);
bool ok = boxed != nullptr;
if (ok) *out = j.call_double(boxed, m_double_value_);
j.delete_local(boxed);
j.delete_local(opt);
return ok;
}
void set_option_double(const Env& j, jobject mc, jfieldID field, double value) const {
jobject opt = option(j, mc, field);
if (!opt) return;
if (m_option_set_ && m_double_value_of_) {
if (jobject boxed = j.call_static_obj(c_double_, m_double_value_of_, {jv(value)})) {
j.call_void(opt, m_option_set_, {jv(boxed)});
j.delete_local(boxed);
}
}
j.delete_local(opt);
}
bool option_int(const Env& j, jobject mc, jfieldID field, int* out) const {
jobject opt = option(j, mc, field);
if (!opt) return false;
jobject boxed = j.call_obj(opt, m_option_get_);
bool ok = boxed != nullptr;
if (ok) *out = j.call_int(boxed, m_int_value_);
j.delete_local(boxed);
j.delete_local(opt);
return ok;
}
void set_option_int(const Env& j, jobject mc, jfieldID field, int value) const {
jobject opt = option(j, mc, field);
if (!opt) return;
if (m_option_set_ && m_int_value_of_) {
jobject boxed =
j.call_static_obj(c_integer_, m_int_value_of_, {jv(static_cast<jint>(value))});
if (boxed) {
j.call_void(opt, m_option_set_, {jv(boxed)});
j.delete_local(boxed);
}
}
j.delete_local(opt);
}
bool option_bool(const Env& j, jobject mc, jfieldID field, bool* out) const {
jobject opt = option(j, mc, field);
if (!opt) return false;
jobject boxed = j.call_obj(opt, m_option_get_);
bool ok = boxed != nullptr;
if (ok) *out = j.call_bool(boxed, m_bool_value_);
j.delete_local(boxed);
j.delete_local(opt);
return ok;
}
void set_option_bool(const Env& j, jobject mc, jfieldID field, bool value) const {
jobject opt = option(j, mc, field);
if (!opt) return;
if (m_option_set_ && m_bool_value_of_) {
if (jobject boxed = j.call_static_obj(c_boolean_, m_bool_value_of_, {jv(value)})) {
j.call_void(opt, m_option_set_, {jv(boxed)});
j.delete_local(boxed);
}
}
j.delete_local(opt);
}
bool gamma(const Env& j, jobject mc, double* out) const {
return option_double(j, mc, f_opt_gamma_, out);
}
/// The option is clamped in the settings screen but not on the way in.
void set_gamma(const Env& j, jobject mc, double v) const {
set_option_double(j, mc, f_opt_gamma_, v);
}
bool damage_tilt(const Env& j, jobject mc, double* out) const {
return option_double(j, mc, f_opt_damage_tilt_, out);
}
void set_damage_tilt(const Env& j, jobject mc, double v) const {
set_option_double(j, mc, f_opt_damage_tilt_, v);
}
bool bob_view(const Env& j, jobject mc, bool* out) const {
return option_bool(j, mc, f_opt_bob_view_, out);
}
void set_bob_view(const Env& j, jobject mc, bool v) const {
set_option_bool(j, mc, f_opt_bob_view_, v);
}
bool fov_option(const Env& j, jobject mc, int* out) const {
return option_int(j, mc, f_opt_fov_, out);
}
void set_fov_option(const Env& j, jobject mc, int v) const {
set_option_int(j, mc, f_opt_fov_, v);
}
bool render_distance(const Env& j, jobject mc, int* out) const {
return option_int(j, mc, f_opt_render_distance_, out);
}
/// Beyond vanilla's own maximum the renderer is off its designed range, so
/// this asks for no more than that however the slider is set.
void set_render_distance(const Env& j, jobject mc, int chunks) const {
int want = chunks < 2 ? 2 : (chunks > 32 ? 32 : chunks);
set_option_int(j, mc, f_opt_render_distance_, want);
}
bool smart_cull(const Env& j, jobject mc, bool* out) const {
if (!f_smart_cull_) return false;
*out = j.bool_field(mc, f_smart_cull_);
return true;
}
void set_smart_cull(const Env& j, jobject mc, bool on) const {
if (f_smart_cull_) j.set_bool(mc, f_smart_cull_, on);
}
/// The game runs physics from the timer's msPerTick, so a smaller value is
/// more ticks per second — everything, including what you send, runs
/// proportionally faster. 50 ms/tick is normal, i.e. 20 TPS.
void set_timer(const Env& j, jobject mc, float multiplier) const {
if (!f_delta_tracker_ || !f_ms_per_tick_) return;
float m = multiplier < 0.1f ? 0.1f : (multiplier > 10.0f ? 10.0f : multiplier);
jobject timer = j.obj_field(mc, f_delta_tracker_);
if (!timer) return;
j.set_float(timer, f_ms_per_tick_, 50.0f / m);
j.delete_local(timer);
}
/// Clear the client-side gaps between using and between missing. Neither is
/// enforced by the server, and both refill on their own, so there is
/// nothing to restore — only a value to keep knocking back down.
void clear_use_cooldowns(const Env& j, jobject mc) const {
if (f_right_click_delay_ && j.int_field(mc, f_right_click_delay_) != 0) {
j.set_int(mc, f_right_click_delay_, 0);
}
if (f_miss_time_ && j.int_field(mc, f_miss_time_) != 0) {
j.set_int(mc, f_miss_time_, 0);
}
}
void set_destroy_delay(const Env& j, jobject game_mode, int v) const {
if (f_destroy_delay_) j.set_int(game_mode, f_destroy_delay_, v);
}
/// One swing, through the game's own path — the same call the mouse makes.
void start_attack(const Env& j, jobject mc) const {
if (m_start_attack_) j.call_bool(mc, m_start_attack_);
}
void respawn(const Env& j, jobject player) const {
if (m_respawn_) j.call_void(player, m_respawn_);
}
jclass entity_class() const { return c_entity_; }
jclass living_class() const { return c_living_; }
jclass minecraft_class() const { return c_minecraft_; }
jclass player_class() const { return c_player_; }
jmethodID bounding_box_method() const { return m_bounding_box_; }
private:
jclass c_minecraft_ = nullptr, c_entity_ = nullptr, c_living_ = nullptr;
jclass c_vec3_ = nullptr, c_player_ = nullptr;
jmethodID m_get_instance_ = nullptr, m_position_ = nullptr, m_on_ground_ = nullptr;
jmethodID m_is_alive_ = nullptr, m_get_id_ = nullptr, m_bounding_box_ = nullptr;
jmethodID m_get_health_ = nullptr, m_get_max_health_ = nullptr;
jmethodID m_fov_ = nullptr, m_option_get_ = nullptr, m_int_value_ = nullptr;
jfieldID f_player_ = nullptr, f_level_ = nullptr, f_screen_ = nullptr, f_game_mode_ = nullptr;
jfieldID f_options_ = nullptr;
jfieldID f_x_ = nullptr, f_y_ = nullptr, f_z_ = nullptr;
jfieldID f_y_rot_ = nullptr, f_x_rot_ = nullptr;
jclass c_integer_ = nullptr, c_double_ = nullptr, c_boolean_ = nullptr;
jclass c_local_player_ = nullptr;
jmethodID m_option_set_ = nullptr, m_int_value_of_ = nullptr;
jmethodID m_double_value_ = nullptr, m_double_value_of_ = nullptr;
jmethodID m_bool_value_ = nullptr, m_bool_value_of_ = nullptr;
jmethodID m_start_attack_ = nullptr, m_respawn_ = nullptr;
jfieldID f_opt_fov_ = nullptr, f_opt_gamma_ = nullptr, f_opt_bob_view_ = nullptr;
jfieldID f_opt_damage_tilt_ = nullptr, f_opt_render_distance_ = nullptr;
jfieldID f_smart_cull_ = nullptr, f_delta_tracker_ = nullptr, f_ms_per_tick_ = nullptr;
jfieldID f_right_click_delay_ = nullptr, f_miss_time_ = nullptr, f_destroy_delay_ = nullptr;
};
} // namespace lodestone::mc