Skip to content

Commit bf30dba

Browse files
committed
ChunkSearcher Fix
1 parent 17ecc39 commit bf30dba

File tree

2 files changed

+115
-15
lines changed

2 files changed

+115
-15
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,7 @@ Examples:
575575
- Fixed crashes on empty/zero-size shapes.
576576
- Search tracers now use block centers as fallback.
577577
- SignESP skips zero-size entries safely.
578+
- ChunkSearcher now snapshots each chunk’s block-state palettes on the client thread and lets the async scan read from that immutable copy, preventing the off-thread palette races that causes rare crashes.
578579

579580
### Notes
580581
- Scanning only includes server-loaded chunks. Larger radii work best in single-player or on high view distance servers.

src/main/java/net/wurstclient/util/chunk/ChunkSearcher.java

Lines changed: 114 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,30 @@
1111
import java.util.Collections;
1212
import java.util.List;
1313
import java.util.concurrent.CompletableFuture;
14-
import java.util.concurrent.ExecutorService;
1514
import java.util.function.BiPredicate;
1615
import java.util.stream.Stream;
16+
import org.slf4j.Logger;
17+
import com.mojang.logging.LogUtils;
1718
import net.minecraft.core.BlockPos;
19+
import net.minecraft.core.SectionPos;
1820
import net.minecraft.world.level.ChunkPos;
21+
import net.minecraft.world.level.block.Blocks;
1922
import net.minecraft.world.level.block.state.BlockState;
2023
import net.minecraft.world.level.chunk.ChunkAccess;
24+
import net.minecraft.world.level.chunk.LevelChunkSection;
25+
import net.minecraft.world.level.chunk.MissingPaletteEntryException;
26+
import net.minecraft.world.level.chunk.PalettedContainer;
2127
import net.minecraft.world.level.dimension.DimensionType;
28+
import net.wurstclient.WurstClient;
2229
import net.wurstclient.util.MinPriorityThreadFactory;
2330

2431
/**
2532
* Searches the given {@link ChunkAccess} for blocks matching the given query.
2633
*/
2734
public final class ChunkSearcher
2835
{
29-
private static final ExecutorService BACKGROUND_THREAD_POOL =
36+
private static final Logger LOGGER = LogUtils.getLogger();
37+
private static final java.util.concurrent.ExecutorService BACKGROUND_THREAD_POOL =
3038
MinPriorityThreadFactory.newFixedThreadPool();
3139

3240
private final BiPredicate<BlockPos, BlockState> query;
@@ -51,21 +59,31 @@ public void start()
5159
if(future != null || interrupted)
5260
throw new IllegalStateException();
5361

54-
future = CompletableFuture.supplyAsync(this::searchNow,
62+
ChunkSnapshot snapshot = ChunkSnapshot.capture(chunk);
63+
if(snapshot == null)
64+
{
65+
future = CompletableFuture.completedFuture(new ArrayList<>());
66+
return;
67+
}
68+
69+
future = CompletableFuture.supplyAsync(() -> searchNow(snapshot),
5570
BACKGROUND_THREAD_POOL);
5671
}
5772

58-
private ArrayList<Result> searchNow()
73+
private ArrayList<Result> searchNow(ChunkSnapshot snapshot)
5974
{
6075
ArrayList<Result> results = new ArrayList<>();
61-
ChunkPos chunkPos = chunk.getPos();
76+
boolean reportedMissingEntry = false;
77+
ChunkPos chunkPos = snapshot.chunkPos();
6278

63-
int minX = chunkPos.getMinBlockX();
64-
int minY = chunk.getMinY();
65-
int minZ = chunkPos.getMinBlockZ();
66-
int maxX = chunkPos.getMaxBlockX();
67-
int maxY = ChunkUtils.getHighestNonEmptySectionYOffset(chunk) + 16;
68-
int maxZ = chunkPos.getMaxBlockZ();
79+
int minX = snapshot.minX();
80+
int minY = snapshot.minY();
81+
int minZ = snapshot.minZ();
82+
int maxX = snapshot.maxX();
83+
int maxY = snapshot.maxY();
84+
int maxZ = snapshot.maxZ();
85+
86+
BlockPos.MutableBlockPos mutablePos = new BlockPos.MutableBlockPos();
6987

7088
for(int x = minX; x <= maxX; x++)
7189
for(int y = minY; y <= maxY; y++)
@@ -74,12 +92,27 @@ private ArrayList<Result> searchNow()
7492
if(interrupted)
7593
return results;
7694

77-
BlockPos pos = new BlockPos(x, y, z);
78-
BlockState state = chunk.getBlockState(pos);
79-
if(!query.test(pos, state))
95+
mutablePos.set(x, y, z);
96+
BlockState state;
97+
try
98+
{
99+
state = snapshot.getBlockState(mutablePos);
100+
101+
}catch(MissingPaletteEntryException e)
102+
{
103+
if(!reportedMissingEntry)
104+
{
105+
reportedMissingEntry = true;
106+
LOGGER.warn(
107+
"ChunkSearcher skipped palette gap in chunk {}: {}",
108+
chunkPos, e.getMessage());
109+
}
110+
continue;
111+
}
112+
if(!query.test(mutablePos, state))
80113
continue;
81114

82-
results.add(new Result(pos.immutable(), state));
115+
results.add(new Result(mutablePos.immutable(), state));
83116
}
84117

85118
return results;
@@ -115,6 +148,9 @@ public Stream<Result> getMatches()
115148
return Stream.empty();
116149

117150
ensureResultsLoaded();
151+
if(results == null)
152+
return Stream.empty();
153+
118154
ArrayList<Result> snapshot;
119155
synchronized(this)
120156
{
@@ -129,6 +165,9 @@ public List<Result> getMatchesList()
129165
return List.of();
130166

131167
ensureResultsLoaded();
168+
if(results == null)
169+
return List.of();
170+
132171
synchronized(this)
133172
{
134173
return Collections.unmodifiableList(new ArrayList<>(results));
@@ -208,6 +247,9 @@ private void ensureResultsLoaded()
208247
if(results != null || future == null || future.isCancelled())
209248
return;
210249

250+
if(!future.isDone())
251+
return;
252+
211253
ArrayList<Result> computed = future.join();
212254

213255
synchronized(this)
@@ -276,4 +318,61 @@ public record Result(BlockPos pos, BlockState state)
276318

277319
public record BlockUpdate(BlockPos pos, BlockState state)
278320
{}
321+
322+
private record ChunkSnapshot(ChunkPos chunkPos, int minX, int minY,
323+
int minZ, int maxX, int maxY, int maxZ, int minSectionCoord,
324+
PalettedContainer<BlockState>[] sections)
325+
{
326+
static ChunkSnapshot capture(ChunkAccess chunk)
327+
{
328+
if(WurstClient.MC == null || WurstClient.MC.level == null)
329+
return null;
330+
331+
ChunkPos chunkPos = chunk.getPos();
332+
if(!WurstClient.MC.level.hasChunk(chunkPos.x, chunkPos.z))
333+
return null;
334+
335+
LevelChunkSection[] chunkSections = chunk.getSections();
336+
@SuppressWarnings("unchecked")
337+
PalettedContainer<BlockState>[] copies =
338+
new PalettedContainer[chunkSections.length];
339+
340+
for(int i = 0; i < chunkSections.length; i++)
341+
{
342+
LevelChunkSection section = chunkSections[i];
343+
if(section == null || section.hasOnlyAir())
344+
continue;
345+
346+
copies[i] = section.getStates().copy();
347+
}
348+
349+
int minX = chunkPos.getMinBlockX();
350+
int minY = chunk.getMinY();
351+
int minZ = chunkPos.getMinBlockZ();
352+
int maxX = chunkPos.getMaxBlockX();
353+
int maxY = ChunkUtils.getHighestNonEmptySectionYOffset(chunk) + 16;
354+
int maxZ = chunkPos.getMaxBlockZ();
355+
int minSectionCoord = SectionPos.blockToSectionCoord(minY);
356+
357+
return new ChunkSnapshot(chunkPos, minX, minY, minZ, maxX, maxY,
358+
maxZ, minSectionCoord, copies);
359+
}
360+
361+
BlockState getBlockState(BlockPos pos)
362+
{
363+
int ySection = SectionPos.blockToSectionCoord(pos.getY());
364+
int sectionIndex = ySection - minSectionCoord;
365+
366+
PalettedContainer<BlockState> container = null;
367+
368+
if(sectionIndex >= 0 && sectionIndex < sections.length)
369+
container = sections[sectionIndex];
370+
371+
if(container == null)
372+
return Blocks.AIR.defaultBlockState();
373+
374+
return container.get(pos.getX() & 15, pos.getY() & 15,
375+
pos.getZ() & 15);
376+
}
377+
}
279378
}

0 commit comments

Comments
 (0)