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
//! Block scanning: X-Ray and container highlighting.
//!
//! Entity ESP can walk a list the game already keeps. Blocks have no such list,
//! so this reads them one position at a time — which is far too slow to do in a
//! single frame for any useful radius. Instead the volume is walked a slice at
//! a time with a fixed budget per frame, and the completed result is swapped in
//! when a pass finishes. A pass restarts when the player has moved far enough
//! that the old result is stale.
//!
//! Identity: `Block` objects are singletons and do not override `hashCode`, so
//! `System.identityHashCode` is a stable key for "which block is this" — one
//! int to compare instead of a chain of reference comparisons.
use std::collections::HashMap;
use jni_sys::{jclass, jmethodID, jobject, jvalue};
use crate::jni::Jni;
use crate::state::BlockKind;
/// Ores worth seeing through stone.
const ORES: &[&str] = &[
"COAL_ORE",
"DEEPSLATE_COAL_ORE",
"IRON_ORE",
"DEEPSLATE_IRON_ORE",
"COPPER_ORE",
"DEEPSLATE_COPPER_ORE",
"GOLD_ORE",
"DEEPSLATE_GOLD_ORE",
"REDSTONE_ORE",
"DEEPSLATE_REDSTONE_ORE",
"LAPIS_ORE",
"DEEPSLATE_LAPIS_ORE",
"EMERALD_ORE",
"DEEPSLATE_EMERALD_ORE",
"DIAMOND_ORE",
"DEEPSLATE_DIAMOND_ORE",
"NETHER_GOLD_ORE",
"NETHER_QUARTZ_ORE",
"ANCIENT_DEBRIS",
];
/// Things that hold loot.
const CONTAINERS: &[&str] = &[
"CHEST",
"TRAPPED_CHEST",
"ENDER_CHEST",
"BARREL",
"SHULKER_BOX",
"FURNACE",
"BLAST_FURNACE",
"SMOKER",
"HOPPER",
"DISPENSER",
"DROPPER",
"BREWING_STAND",
"BEACON",
];
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 -> what kind of thing it is.
kinds: HashMap<i32, BlockKind>,
/// Where the current pass started, and how far it has got.
origin: (i32, i32, i32),
cursor: usize,
radius: i32,
partial: Vec<(i32, i32, i32, BlockKind)>,
/// The last completed pass, which is what gets drawn.
pub found: Vec<(i32, i32, i32, BlockKind)>,
}
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 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 = j.method(block_pos_class, "<init>", "(III)V").or_else(|| {
missing.push("BlockPos.<init>(III)".into());
None
})?;
let m_get_block_state = j
.method(
level,
"getBlockState",
"(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;",
)
.or_else(|| {
missing.push("Level.getBlockState(BlockPos)".into());
None
})?;
let m_get_block = j
.method(state_base, "getBlock", "()Lnet/minecraft/world/level/block/Block;")
.or_else(|| {
missing.push("BlockStateBase.getBlock()".into());
None
})?;
let m_identity_hash = j
.static_method(system_class, "identityHashCode", "(Ljava/lang/Object;)I")
.or_else(|| {
missing.push("System.identityHashCode(Object)".into());
None
})?;
// Precompute the identity of every block we care about.
let mut kinds = HashMap::new();
for (names, kind) in [(ORES, BlockKind::Ore), (CONTAINERS, BlockKind::Container)] {
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 }])
{
kinds.insert(hash, kind);
}
j.delete_local(block);
}
}
if kinds.is_empty() {
missing.push("Blocks.* constants".into());
return None;
}
Some(Blocks {
block_pos_class,
m_block_pos_init,
m_get_block_state,
m_get_block,
m_identity_hash,
system_class,
kinds,
origin: (0, 0, 0),
cursor: 0,
radius: 0,
partial: Vec::new(),
found: Vec::new(),
})
}
/// Advance the scan. `budget` positions are read per call, so the cost per
/// frame stays flat regardless of how big the search volume is.
pub fn step(
&mut self,
j: &Jni,
level: jobject,
player_pos: (f64, f64, f64),
radius: i32,
want_ores: bool,
want_containers: bool,
budget: usize,
) {
if !want_ores && !want_containers {
self.found.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;
// Start a fresh pass when the volume changed or we drifted out of it.
if self.cursor >= total || moved > radius / 2 || self.radius != radius {
if self.cursor >= total {
std::mem::swap(&mut self.found, &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(kind) = self.kind_at(j, level, x, y, z) {
let wanted = match kind {
BlockKind::Ore => want_ores,
BlockKind::Container => want_containers,
};
if wanted {
self.partial.push((x, y, z, kind));
}
}
}
self.cursor = end;
}
fn kind_at(&self, j: &Jni, level: jobject, x: i32, y: i32, z: i32) -> Option<BlockKind> {
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.kinds.get(&hash?).copied()
}
}
fn global_class(j: &Jni, name: &str) -> Option<jclass> {
let local = j.find_class(name)?;
j.global(local).map(|g| g as jclass)
}