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
373
374
375
376
377
378
379
380
381
//! Finding blocks worth seeing.
//!
//! Two very different problems, so two mechanisms:
//!
//! * **Ores** are ordinary blocks with no index anywhere, so they have to be
//! read one position at a time. Far too slow to do in one frame for any
//! useful radius, so the volume is swept a slice per frame on a fixed budget
//! and the finished pass swapped in.
//! * **Containers, shulkers, ender chests, beacons** are *block entities*, and
//! the client already keeps a map of those per chunk. Walking the loaded
//! chunks reads every one of them across the whole render distance in a
//! single pass — no sweeping, no budget, no waiting.
//!
//! Identity for ores: `Block` objects are singletons that do not override
//! `hashCode`, so `System.identityHashCode` is a stable key — one int compare
//! per position instead of a chain of reference tests.
use std::collections::HashMap;
use jni_sys::{jclass, jmethodID, jobject, jvalue};
use crate::jni::Jni;
use crate::state::{BaseHit, BlockKind};
/// Ore groups offered in the X-Ray list. One entry can cover several blocks,
/// because nobody wants to tick "diamond" and "deepslate diamond" separately.
pub const ORE_GROUPS: &[(&str, &[&str])] = &[
("Diamond", &["DIAMOND_ORE", "DEEPSLATE_DIAMOND_ORE"]),
("Ancient Debris", &["ANCIENT_DEBRIS"]),
("Emerald", &["EMERALD_ORE", "DEEPSLATE_EMERALD_ORE"]),
("Gold", &["GOLD_ORE", "DEEPSLATE_GOLD_ORE", "NETHER_GOLD_ORE"]),
("Iron", &["IRON_ORE", "DEEPSLATE_IRON_ORE"]),
("Redstone", &["REDSTONE_ORE", "DEEPSLATE_REDSTONE_ORE"]),
("Lapis", &["LAPIS_ORE", "DEEPSLATE_LAPIS_ORE"]),
("Copper", &["COPPER_ORE", "DEEPSLATE_COPPER_ORE"]),
("Coal", &["COAL_ORE", "DEEPSLATE_COAL_ORE"]),
("Quartz", &["NETHER_QUARTZ_ORE"]),
("Spawner", &["SPAWNER", "TRIAL_SPAWNER"]),
("Portal", &["END_PORTAL_FRAME", "NETHER_PORTAL"]),
("Chest", &["CHEST", "TRAPPED_CHEST", "BARREL"]),
];
/// Block-entity classes, and what they mean. The ones near the top are what
/// actually give a player base away.
const BLOCK_ENTITIES: &[(&str, &str, bool)] = &[
// class, label, counts as a base marker
("net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity", "Shulker Box", true),
("net/minecraft/world/level/block/entity/EnderChestBlockEntity", "Ender Chest", true),
("net/minecraft/world/level/block/entity/BeaconBlockEntity", "Beacon", true),
("net/minecraft/world/level/block/entity/BrewingStandBlockEntity", "Brewing Stand", true),
("net/minecraft/world/level/block/entity/EnchantingTableBlockEntity", "Enchanting Table", true),
("net/minecraft/world/level/block/entity/ChestBlockEntity", "Chest", false),
("net/minecraft/world/level/block/entity/BarrelBlockEntity", "Barrel", false),
("net/minecraft/world/level/block/entity/HopperBlockEntity", "Hopper", false),
("net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity", "Furnace", false),
];
pub struct Blocks {
block_pos_class: jclass,
m_block_pos_init: jmethodID,
m_get_block_state: jmethodID,
m_get_block: jmethodID,
m_identity_hash: jmethodID,
system_class: jclass,
/// identityHashCode -> which ore group it belongs to.
ore_kinds: HashMap<i32, usize>,
// Block entities.
m_get_chunk: Option<jmethodID>,
m_chunk_block_entities: Option<jmethodID>,
m_map_values: Option<jmethodID>,
m_iterator: Option<jmethodID>,
m_has_next: Option<jmethodID>,
m_next: Option<jmethodID>,
m_get_block_pos: Option<jmethodID>,
m_pos_x: Option<jmethodID>,
m_pos_y: Option<jmethodID>,
m_pos_z: Option<jmethodID>,
entity_classes: Vec<(jclass, &'static str, bool)>,
// Ore sweep progress.
origin: (i32, i32, i32),
cursor: usize,
radius: i32,
partial: Vec<(i32, i32, i32, BlockKind)>,
pub ores: Vec<(i32, i32, i32, BlockKind)>,
/// Block entities from the most recent pass — rebuilt whole, every time.
pub entities: Vec<BaseHit>,
}
impl Blocks {
pub fn resolve(j: &Jni, missing: &mut Vec<String>) -> Option<Blocks> {
let block_pos_class = global_class(j, "net/minecraft/core/BlockPos")?;
let level = global_class(j, "net/minecraft/world/level/Level")?;
let level_reader = global_class(j, "net/minecraft/world/level/LevelReader");
let level_chunk = global_class(j, "net/minecraft/world/level/chunk/LevelChunk");
let block_entity = global_class(j, "net/minecraft/world/level/block/entity/BlockEntity");
let vec3i = global_class(j, "net/minecraft/core/Vec3i");
let map = global_class(j, "java/util/Map");
let collection = global_class(j, "java/util/Collection");
let iterator = global_class(j, "java/util/Iterator");
let state_base = global_class(
j,
"net/minecraft/world/level/block/state/BlockBehaviour$BlockStateBase",
)?;
let system_class = global_class(j, "java/lang/System")?;
let blocks_class = global_class(j, "net/minecraft/world/level/block/Blocks")?;
let m_block_pos_init = need(j.method(block_pos_class, "<init>", "(III)V"), "BlockPos.<init>(III)", missing)?;
let m_get_block_state = need(
j.method(
level,
"getBlockState",
"(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;",
),
"Level.getBlockState(BlockPos)",
missing,
)?;
let m_get_block = need(
j.method(state_base, "getBlock", "()Lnet/minecraft/world/level/block/Block;"),
"BlockStateBase.getBlock()",
missing,
)?;
let m_identity_hash = need(
j.static_method(system_class, "identityHashCode", "(Ljava/lang/Object;)I"),
"System.identityHashCode(Object)",
missing,
)?;
// Every ore group's blocks, keyed by identity.
let mut ore_kinds = HashMap::new();
for (index, (_, names)) in ORE_GROUPS.iter().enumerate() {
for name in *names {
let Some(field) =
j.static_field(blocks_class, name, "Lnet/minecraft/world/level/block/Block;")
else {
continue;
};
let Some(block) = j.static_obj_field(blocks_class, field) else {
continue;
};
if let Some(hash) =
j.call_static_int(system_class, m_identity_hash, &[jvalue { l: block }])
{
ore_kinds.insert(hash, index);
}
j.delete_local(block);
}
}
let entity_classes = BLOCK_ENTITIES
.iter()
.filter_map(|(path, label, base)| {
global_class(j, path).map(|c| (c, *label, *base))
})
.collect();
Some(Blocks {
block_pos_class,
m_block_pos_init,
m_get_block_state,
m_get_block,
m_identity_hash,
system_class,
ore_kinds,
m_get_chunk: level_reader.and_then(|c| {
j.method(c, "getChunk", "(II)Lnet/minecraft/world/level/chunk/ChunkAccess;")
}),
m_chunk_block_entities: level_chunk
.and_then(|c| j.method(c, "getBlockEntities", "()Ljava/util/Map;")),
m_map_values: map.and_then(|c| j.method(c, "values", "()Ljava/util/Collection;")),
m_iterator: collection
.and_then(|c| j.method(c, "iterator", "()Ljava/util/Iterator;")),
m_has_next: iterator.and_then(|c| j.method(c, "hasNext", "()Z")),
m_next: iterator.and_then(|c| j.method(c, "next", "()Ljava/lang/Object;")),
m_get_block_pos: block_entity
.and_then(|c| j.method(c, "getBlockPos", "()Lnet/minecraft/core/BlockPos;")),
m_pos_x: vec3i.and_then(|c| j.method(c, "getX", "()I")),
m_pos_y: vec3i.and_then(|c| j.method(c, "getY", "()I")),
m_pos_z: vec3i.and_then(|c| j.method(c, "getZ", "()I")),
entity_classes,
origin: (0, 0, 0),
cursor: 0,
radius: 0,
partial: Vec::new(),
ores: Vec::new(),
entities: Vec::new(),
})
}
// ---- block entities: one cheap pass over the loaded chunks ------------
/// Read every block entity in the loaded chunks around the player. This is
/// what container ESP and the base finder both run on.
pub fn scan_block_entities(
&mut self,
j: &Jni,
level: jobject,
player: (f64, f64, f64),
chunk_radius: i32,
base_only: bool,
) {
self.entities.clear();
let (Some(get_chunk), Some(block_entities), Some(values), Some(iter), Some(has_next), Some(next)) = (
self.m_get_chunk,
self.m_chunk_block_entities,
self.m_map_values,
self.m_iterator,
self.m_has_next,
self.m_next,
) else {
return;
};
let centre = (
(player.0.floor() as i32) >> 4,
(player.2.floor() as i32) >> 4,
);
for cx in (centre.0 - chunk_radius)..=(centre.0 + chunk_radius) {
for cz in (centre.1 - chunk_radius)..=(centre.1 + chunk_radius) {
let Some(chunk) =
j.call_obj(level, get_chunk, &[jvalue { i: cx }, jvalue { i: cz }])
else {
continue;
};
let map = j.call_obj(chunk, block_entities, &[]);
j.delete_local(chunk);
let Some(map) = map else { continue };
let collection = j.call_obj(map, values, &[]);
j.delete_local(map);
let Some(collection) = collection else { continue };
let it = j.call_obj(collection, iter, &[]);
j.delete_local(collection);
let Some(it) = it else { continue };
let mut guard = 0;
while guard < 4096 {
guard += 1;
match j.call_bool(it, has_next, &[]) {
Some(true) => {}
_ => break,
}
let Some(be) = j.call_obj(it, next, &[]) else { break };
self.record(j, be, player, base_only);
j.delete_local(be);
}
j.delete_local(it);
}
}
self.entities
.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap_or(std::cmp::Ordering::Equal));
}
fn record(&mut self, j: &Jni, be: jobject, player: (f64, f64, f64), base_only: bool) {
let Some((label, is_base)) = self.classify(j, be) else {
return;
};
if base_only && !is_base {
return;
}
let (Some(get_pos), Some(gx), Some(gy), Some(gz)) =
(self.m_get_block_pos, self.m_pos_x, self.m_pos_y, self.m_pos_z)
else {
return;
};
let Some(pos) = j.call_obj(be, get_pos, &[]) else {
return;
};
let x = j.call_int(pos, gx, &[]).unwrap_or(0);
let y = j.call_int(pos, gy, &[]).unwrap_or(0);
let z = j.call_int(pos, gz, &[]).unwrap_or(0);
j.delete_local(pos);
let dx = x as f64 + 0.5 - player.0;
let dy = y as f64 + 0.5 - player.1;
let dz = z as f64 + 0.5 - player.2;
self.entities.push(BaseHit {
x,
y,
z,
label,
is_base,
distance: (dx * dx + dy * dy + dz * dz).sqrt() as f32,
});
}
fn classify(&self, j: &Jni, be: jobject) -> Option<(&'static str, bool)> {
for (class, label, is_base) in &self.entity_classes {
if j.is_instance(be, *class) {
return Some((label, *is_base));
}
}
None
}
// ---- ores: a slice of the volume per frame ----------------------------
pub fn step_ores(
&mut self,
j: &Jni,
level: jobject,
player_pos: (f64, f64, f64),
radius: i32,
selected: &[bool],
budget: usize,
) {
if !selected.iter().any(|s| *s) {
self.ores.clear();
self.partial.clear();
self.cursor = 0;
return;
}
let here = (
player_pos.0.floor() as i32,
player_pos.1.floor() as i32,
player_pos.2.floor() as i32,
);
let moved = (here.0 - self.origin.0).abs()
+ (here.1 - self.origin.1).abs()
+ (here.2 - self.origin.2).abs();
let side = (radius * 2 + 1) as usize;
let total = side * side * side;
if self.cursor >= total || moved > radius / 2 || self.radius != radius {
if self.cursor >= total {
std::mem::swap(&mut self.ores, &mut self.partial);
}
self.partial.clear();
self.cursor = 0;
self.origin = here;
self.radius = radius;
}
let side_i = side as i32;
let end = (self.cursor + budget).min(total);
for index in self.cursor..end {
let i = index as i32;
let x = self.origin.0 - radius + (i % side_i);
let y = self.origin.1 - radius + ((i / side_i) % side_i);
let z = self.origin.2 - radius + (i / (side_i * side_i));
if let Some(group) = self.group_at(j, level, x, y, z) {
if selected.get(group).copied().unwrap_or(false) {
self.partial.push((x, y, z, BlockKind::Ore(group)));
}
}
}
self.cursor = end;
}
fn group_at(&self, j: &Jni, level: jobject, x: i32, y: i32, z: i32) -> Option<usize> {
let pos = j.new_object(
self.block_pos_class,
self.m_block_pos_init,
&[jvalue { i: x }, jvalue { i: y }, jvalue { i: z }],
)?;
let state = j.call_obj(level, self.m_get_block_state, &[jvalue { l: pos }]);
j.delete_local(pos);
let state = state?;
let block = j.call_obj(state, self.m_get_block, &[]);
j.delete_local(state);
let block = block?;
let hash =
j.call_static_int(self.system_class, self.m_identity_hash, &[jvalue { l: block }]);
j.delete_local(block);
self.ore_kinds.get(&hash?).copied()
}
}
fn need<T>(value: Option<T>, what: &str, missing: &mut Vec<String>) -> Option<T> {
if value.is_none() {
missing.push(what.to_string());
}
value
}
fn global_class(j: &Jni, name: &str) -> Option<jclass> {
let local = j.find_class(name)?;
j.global(local).map(|g| g as jclass)
}