1111import java .util .Arrays ;
1212import java .util .List ;
1313import java .util .function .BiPredicate ;
14+ import java .util .stream .Collectors ;
15+ import java .util .stream .StreamSupport ;
1416
17+ import net .minecraft .block .Block ;
1518import net .minecraft .block .BlockState ;
1619import net .minecraft .block .BedBlock ;
20+ import net .minecraft .block .Blocks ;
21+ import net .minecraft .block .DoorBlock ;
22+ import net .minecraft .block .entity .BlockEntity ;
23+ import net .minecraft .block .entity .TrialSpawnerBlockEntity ;
24+ import net .minecraft .block .enums .BedPart ;
1725import net .minecraft .client .util .math .MatrixStack ;
26+ import net .minecraft .entity .Entity ;
27+ import net .minecraft .entity .passive .IronGolemEntity ;
28+ import net .minecraft .entity .passive .VillagerEntity ;
29+ import net .minecraft .registry .Registries ;
30+ import net .minecraft .registry .RegistryKeys ;
31+ import net .minecraft .registry .tag .TagKey ;
32+ import net .minecraft .util .Identifier ;
1833import net .minecraft .util .math .BlockPos ;
1934import net .minecraft .util .math .Box ;
2035import net .minecraft .util .math .ChunkPos ;
3348import net .wurstclient .settings .EspStyleSetting ;
3449import net .wurstclient .settings .SliderSetting ;
3550import net .wurstclient .util .RenderUtils ;
51+ import net .wurstclient .util .BlockUtils ;
3652import net .wurstclient .util .chunk .ChunkSearcher .Result ;
3753import net .wurstclient .util .chunk .ChunkSearcherCoordinator ;
54+ import net .wurstclient .util .chunk .ChunkUtils ;
3855
3956@ SearchTags ({"BedESP" , "bed esp" })
4057public final class BedEspHack extends Hack implements UpdateListener ,
@@ -68,6 +85,12 @@ public final class BedEspHack extends Hack implements UpdateListener,
6885 "Only show beds at or above the configured Y level." , false );
6986 private final SliderSetting aboveGroundY = new SliderSetting (
7087 "Set ESP Y limit" , 62 , -65 , 255 , 1 , SliderSetting .ValueDisplay .INTEGER );
88+ private final net .wurstclient .settings .CheckboxSetting filterTrialChambers =
89+ new net .wurstclient .settings .CheckboxSetting ("Filter trial chambers" ,
90+ "Hides beds that match common trial chamber layouts." , false );
91+ private final net .wurstclient .settings .CheckboxSetting filterVillageBeds =
92+ new net .wurstclient .settings .CheckboxSetting ("Filter village beds" ,
93+ "Hides beds that appear to belong to villages." , false );
7194
7295 private final BiPredicate <BlockPos , BlockState > query =
7396 (pos , state ) -> state .getBlock () instanceof BedBlock ;
@@ -78,6 +101,13 @@ public final class BedEspHack extends Hack implements UpdateListener,
78101 private boolean groupsUpToDate ;
79102 private ChunkPos lastPlayerChunk ;
80103 private int foundCount ;
104+ private List <BlockPos > cachedTrialSpawners = List .of ();
105+ private List <Vec3d > cachedVillagerPositions = List .of ();
106+ private List <Vec3d > cachedGolemPositions = List .of ();
107+ private static final TagKey <Block > WAXED_COPPER_BLOCKS_TAG = TagKey .of (
108+ RegistryKeys .BLOCK , Identifier .of ("minecraft" , "waxed_copper_blocks" ));
109+ private boolean lastTrialFilterState ;
110+ private boolean lastVillageFilterState ;
81111
82112 public BedEspHack ()
83113 {
@@ -91,6 +121,11 @@ public BedEspHack()
91121 addSetting (stickyArea );
92122 addSetting (onlyAboveGround );
93123 addSetting (aboveGroundY );
124+ addSetting (filterTrialChambers );
125+ addSetting (filterVillageBeds );
126+
127+ lastTrialFilterState = filterTrialChambers .isChecked ();
128+ lastVillageFilterState = filterVillageBeds .isChecked ();
94129 }
95130
96131 @ Override
@@ -102,6 +137,8 @@ protected void onEnable()
102137 EVENTS .add (CameraTransformViewBobbingListener .class , this );
103138 EVENTS .add (RenderListener .class , this );
104139 lastPlayerChunk = new ChunkPos (MC .player .getBlockPos ());
140+ lastTrialFilterState = filterTrialChambers .isChecked ();
141+ lastVillageFilterState = filterVillageBeds .isChecked ();
105142 }
106143
107144 @ Override
@@ -116,6 +153,9 @@ protected void onDisable()
116153 groups .forEach (BedEspBlockGroup ::clear );
117154 // reset count
118155 foundCount = 0 ;
156+ cachedTrialSpawners = List .of ();
157+ cachedVillagerPositions = List .of ();
158+ cachedGolemPositions = List .of ();
119159 }
120160
121161 @ Override
@@ -132,6 +172,10 @@ public void onUpdate()
132172 coordinator .reset ();
133173 groupsUpToDate = false ;
134174 }
175+
176+ if (didFiltersChange ())
177+ groupsUpToDate = false ;
178+
135179 if (!groupsUpToDate && coordinator .isDone ())
136180 updateGroupBoxes ();
137181 }
@@ -191,6 +235,7 @@ private void updateGroupBoxes()
191235 {
192236 groups .forEach (BedEspBlockGroup ::clear );
193237 java .util .List <Result > results = coordinator .getMatches ().toList ();
238+ refreshEnvironmentalCaches ();
194239 results .forEach (this ::addToGroupBoxes );
195240 groupsUpToDate = true ;
196241 // update count for HUD (clamped to 999) based on displayed boxes
@@ -209,13 +254,188 @@ public String getRenderName()
209254
210255 private void addToGroupBoxes (Result result )
211256 {
257+ BlockState state = result .state ();
258+ if (!(state .getBlock () instanceof BedBlock )
259+ || state .get (BedBlock .PART ) == BedPart .FOOT )
260+ return ;
261+
262+ BlockPos headPos = result .pos ();
212263 if (onlyAboveGround .isChecked ()
213- && result .pos ().getY () < aboveGroundY .getValue ())
264+ && headPos .getY () < aboveGroundY .getValue ())
265+ return ;
266+
267+ if (filterTrialChambers .isChecked () && isTrialChamberBed (headPos ))
268+ return ;
269+
270+ if (filterVillageBeds .isChecked () && isLikelyVillageBed (headPos ))
214271 return ;
215272 for (BedEspBlockGroup group : groups )
216273 {
217274 group .add (result );
218275 break ;
219276 }
220277 }
278+
279+ private void refreshEnvironmentalCaches ()
280+ {
281+ if (filterTrialChambers .isChecked ())
282+ cachedTrialSpawners = collectTrialSpawnerPositions ();
283+ else
284+ cachedTrialSpawners = List .of ();
285+
286+ if (filterVillageBeds .isChecked ())
287+ {
288+ cachedVillagerPositions =
289+ collectEntityPositions (VillagerEntity .class );
290+ cachedGolemPositions =
291+ collectEntityPositions (IronGolemEntity .class );
292+ }else
293+ {
294+ cachedVillagerPositions = List .of ();
295+ cachedGolemPositions = List .of ();
296+ }
297+ }
298+
299+ private boolean didFiltersChange ()
300+ {
301+ boolean trial = filterTrialChambers .isChecked ();
302+ boolean village = filterVillageBeds .isChecked ();
303+ if (trial != lastTrialFilterState || village != lastVillageFilterState )
304+ {
305+ lastTrialFilterState = trial ;
306+ lastVillageFilterState = village ;
307+ return true ;
308+ }
309+
310+ return false ;
311+ }
312+
313+ private List <BlockPos > collectTrialSpawnerPositions ()
314+ {
315+ if (MC .world == null )
316+ return List .of ();
317+
318+ return ChunkUtils .getLoadedBlockEntities ()
319+ .filter (be -> be instanceof TrialSpawnerBlockEntity )
320+ .map (BlockEntity ::getPos ).map (BlockPos ::toImmutable )
321+ .collect (Collectors .toList ());
322+ }
323+
324+ private <T extends Entity > List <Vec3d > collectEntityPositions (Class <T > type )
325+ {
326+ if (MC .world == null )
327+ return List .of ();
328+
329+ return StreamSupport .stream (MC .world .getEntities ().spliterator (), false )
330+ .filter (e -> !e .isRemoved ()).filter (type ::isInstance )
331+ .map (entity -> Vec3d .ofCenter (entity .getBlockPos ()))
332+ .collect (Collectors .toList ());
333+ }
334+
335+ private boolean isTrialChamberBed (BlockPos headPos )
336+ {
337+ int y = headPos .getY ();
338+ if (y < -38 || y > 10 )
339+ return false ;
340+
341+ if (!isNearWaxedCopper (headPos , 5 ))
342+ return false ;
343+
344+ return isNearTrialSpawner (headPos , 100 );
345+ }
346+
347+ private boolean isNearWaxedCopper (BlockPos center , int range )
348+ {
349+ if (MC .world == null )
350+ return false ;
351+
352+ return BlockUtils .getAllInBoxStream (center , range )
353+ .anyMatch (pos -> isWaxedCopper (BlockUtils .getState (pos )));
354+ }
355+
356+ private boolean isWaxedCopper (BlockState state )
357+ {
358+ if (state .isIn (WAXED_COPPER_BLOCKS_TAG ))
359+ return true ;
360+
361+ String idPath = Registries .BLOCK .getId (state .getBlock ()).getPath ();
362+ return idPath .contains ("waxed" ) && idPath .contains ("copper" );
363+ }
364+
365+ private boolean isNearTrialSpawner (BlockPos center , int range )
366+ {
367+ if (cachedTrialSpawners .isEmpty ())
368+ return false ;
369+
370+ double rangeSq = range * range ;
371+ Vec3d centerVec = Vec3d .ofCenter (center );
372+ return cachedTrialSpawners .stream ().anyMatch (
373+ pos -> Vec3d .ofCenter (pos ).squaredDistanceTo (centerVec ) <= rangeSq );
374+ }
375+
376+ private boolean isLikelyVillageBed (BlockPos headPos )
377+ {
378+ if (!hasDoorNearby (headPos , 4 ))
379+ return false ;
380+
381+ boolean hasVillageEntity =
382+ isEntityWithinRange (cachedVillagerPositions , headPos , 24 )
383+ || isEntityWithinRange (cachedGolemPositions , headPos , 24 );
384+ boolean hayCluster = hasHayBaleCluster (headPos , 6 );
385+
386+ if (hasVillageEntity || hayCluster )
387+ return true ;
388+
389+ return hasGlassPaneCluster (headPos , 4 , 1 );
390+ }
391+
392+ private boolean isEntityWithinRange (List <Vec3d > positions , BlockPos center ,
393+ double range )
394+ {
395+ if (positions .isEmpty ())
396+ return false ;
397+
398+ double rangeSq = range * range ;
399+ Vec3d centerVec = Vec3d .ofCenter (center );
400+ return positions .stream ()
401+ .anyMatch (pos -> pos .squaredDistanceTo (centerVec ) <= rangeSq );
402+ }
403+
404+ private boolean hasHayBaleCluster (BlockPos center , int range )
405+ {
406+ if (MC .world == null )
407+ return false ;
408+
409+ long count = BlockUtils .getAllInBoxStream (center , range )
410+ .filter (pos -> BlockUtils .getBlock (pos ) == Blocks .HAY_BLOCK )
411+ .limit (16 ).count ();
412+ return count >= 4 ;
413+ }
414+
415+ private boolean hasDoorNearby (BlockPos center , int range )
416+ {
417+ if (MC .world == null )
418+ return false ;
419+
420+ return BlockUtils .getAllInBoxStream (center , range )
421+ .anyMatch (pos -> BlockUtils .getBlock (pos ) instanceof DoorBlock );
422+ }
423+
424+ private boolean hasGlassPaneCluster (BlockPos center , int range ,
425+ int requiredCount )
426+ {
427+ if (MC .world == null )
428+ return false ;
429+
430+ long glassCount = BlockUtils .getAllInBoxStream (center , range )
431+ .filter (pos -> isGlassPane (BlockUtils .getBlock (pos )))
432+ .limit (requiredCount ).count ();
433+ return glassCount >= requiredCount ;
434+ }
435+
436+ private boolean isGlassPane (Block block )
437+ {
438+ String path = Registries .BLOCK .getId (block ).getPath ();
439+ return path .contains ("glass_pane" );
440+ }
221441}
0 commit comments