2525import net .minecraft .world .level .block .ChestBlock ;
2626import net .minecraft .world .level .block .DoorBlock ;
2727import net .minecraft .world .level .block .HopperBlock ;
28+ import net .minecraft .world .level .block .DispenserBlock ;
2829import net .minecraft .world .level .block .ShulkerBoxBlock ;
2930import net .minecraft .world .level .block .state .BlockState ;
3031import net .minecraft .world .level .block .state .properties .ChestType ;
3132import net .minecraft .world .level .block .entity .BlockEntity ;
3233import net .minecraft .world .level .block .entity .ChestBlockEntity ;
34+ import net .minecraft .world .level .block .entity .BarrelBlockEntity ;
35+ import net .minecraft .world .level .block .entity .HopperBlockEntity ;
36+ import net .minecraft .world .level .block .entity .DispenserBlockEntity ;
3337import net .minecraft .world .level .block .entity .TrialSpawnerBlockEntity ;
3438import net .minecraft .world .phys .AABB ;
3539import net .minecraft .world .phys .Vec3 ;
@@ -81,6 +85,7 @@ public class ChestEspHack extends Hack implements UpdateListener,
8185 "Opacity for tracers (0 = fully transparent, 255 = opaque)." , 128 ,
8286 0 , 255 , 1 , SliderSetting .ValueDisplay .INTEGER );
8387 private int foundCount ;
88+ private boolean preFilteredEnv ;
8489
8590 private final CheckboxSetting onlyAboveGround =
8691 new CheckboxSetting ("Above ground only" ,
@@ -111,7 +116,7 @@ public class ChestEspHack extends Hack implements UpdateListener,
111116
112117 private final CheckboxSetting filterTrialChambers = new CheckboxSetting (
113118 "Filter trial chambers" ,
114- "Hides single chests and barrels that match common trial chamber layouts. Does not affect double chests or shulkers." ,
119+ "Hides single chests, barrels, hoppers, and dispensers that match common trial chamber layouts. Does not affect double chests or shulkers." ,
115120 false );
116121
117122 private final CheckboxSetting doubleChestsOnly =
@@ -175,20 +180,31 @@ protected void onDisable()
175180 cachedTrialSpawners = List .of ();
176181 cachedVillagerPositions = List .of ();
177182 cachedGolemPositions = List .of ();
183+ preFilteredEnv = false ;
178184 }
179185
180186 @ Override
181187 public void onUpdate ()
182188 {
183189 groups .allGroups .forEach (ChestEspGroup ::clear );
184190
191+ // Build environmental caches first so we can pre-filter before adding
192+ refreshEnvironmentalCaches ();
193+ preFilteredEnv = MC .level != null && (filterNearSpawners .isChecked ()
194+ || filterTrialChambers .isChecked () || filterVillages .isChecked ());
195+
185196 double yLimit = aboveGroundY .getValue ();
186197 boolean enforceAboveGround = onlyAboveGround .isChecked ();
187198
188199 ChunkUtils .getLoadedBlockEntities ().forEach (be -> {
189200 if (enforceAboveGround && be .getBlockPos ().getY () < yLimit )
190201 return ;
191202
203+ // Pre-filter by environment to avoid flicker and wasted work
204+ if (preFilteredEnv && shouldFilterBlockEntityByEnvironment (be ))
205+ return ;
206+
207+ // Respect double-chest-only suppression
192208 if (shouldSkipSingleChest (be ))
193209 return ;
194210
@@ -207,8 +223,6 @@ public void onUpdate()
207223 }
208224 }
209225
210- refreshEnvironmentalCaches ();
211-
212226 int total = groups .allGroups .stream ().filter (ChestEspGroup ::isEnabled )
213227 .mapToInt (g -> g .getBoxes ().size ()).sum ();
214228 foundCount = Math .min (total , 999 );
@@ -289,8 +303,8 @@ private void renderBoxes(PoseStack matrixStack)
289303 String curDim = MC .level == null ? "overworld"
290304 : MC .level .dimension ().identifier ().getPath ();
291305
292- boolean applyEnvFilters =
293- MC . level != null && (filterNearSpawners .isChecked ()
306+ boolean applyEnvFilters = MC . level != null && ! preFilteredEnv
307+ && (filterNearSpawners .isChecked ()
294308 || filterTrialChambers .isChecked ()
295309 || filterVillages .isChecked ());
296310
@@ -647,8 +661,8 @@ private void renderTracers(PoseStack matrixStack, float partialTicks)
647661 : MC .level .dimension ().identifier ().getPath ();
648662 }
649663
650- boolean applyEnvFilters =
651- MC . level != null && (filterNearSpawners .isChecked ()
664+ boolean applyEnvFilters = MC . level != null && ! preFilteredEnv
665+ && (filterNearSpawners .isChecked ()
652666 || filterTrialChambers .isChecked ()
653667 || filterVillages .isChecked ());
654668
@@ -802,12 +816,35 @@ private List<AABB> filterBoxesByEnvironment(List<AABB> boxes)
802816 BlockPos barrelPos = getSingleBarrelPosIfApplicable (box );
803817 if (barrelPos == null )
804818 {
819+ // Trial chamber ignore for hoppers
820+ BlockPos hopperPos = getSingleHopperPosIfApplicable (box );
821+ if (hopperPos != null )
822+ {
823+ if (filterTrialChambers .isChecked ()
824+ && isInTrialChamberArea (hopperPos ))
825+ continue ;
826+ out .add (box );
827+ continue ;
828+ }
829+
830+ // Trial chamber ignore for dispensers
831+ BlockPos dispenserPos =
832+ getSingleDispenserPosIfApplicable (box );
833+ if (dispenserPos != null )
834+ {
835+ if (filterTrialChambers .isChecked ()
836+ && isInTrialChamberArea (dispenserPos ))
837+ continue ;
838+ out .add (box );
839+ continue ;
840+ }
841+
805842 out .add (box );
806843 continue ;
807844 }
808845
809846 if (filterTrialChambers .isChecked ()
810- && isTrialChamberChest (barrelPos ))
847+ && isInTrialChamberArea (barrelPos ))
811848 continue ;
812849
813850 out .add (box );
@@ -819,7 +856,7 @@ && isNearSpawner(singleChestPos, 7))
819856 continue ;
820857
821858 if (filterTrialChambers .isChecked ()
822- && isTrialChamberChest (singleChestPos ))
859+ && isInTrialChamberArea (singleChestPos ))
823860 continue ;
824861
825862 if (filterVillages .isChecked ()
@@ -832,6 +869,63 @@ && isLikelyVillageChest(singleChestPos))
832869 return out ;
833870 }
834871
872+ // Fast pre-filter used in onUpdate() to keep filtered blocks from ever
873+ // entering render lists. Mirrors the logic of filterBoxesByEnvironment
874+ // but operates directly on BlockEntity types for speed.
875+ private boolean shouldFilterBlockEntityByEnvironment (BlockEntity be )
876+ {
877+ if (MC .level == null )
878+ return false ;
879+
880+ BlockPos pos = be .getBlockPos ();
881+ BlockState state = be .getBlockState ();
882+
883+ // Chest-specific single/double handling
884+ if (be instanceof ChestBlockEntity )
885+ {
886+ boolean isSingle = true ;
887+ if (state != null && state .hasProperty (ChestBlock .TYPE ))
888+ isSingle = state .getValue (ChestBlock .TYPE ) == ChestType .SINGLE ;
889+
890+ if (isSingle )
891+ {
892+ if (filterNearSpawners .isChecked () && isNearSpawner (pos , 7 ))
893+ return true ;
894+ if (filterTrialChambers .isChecked () && isInTrialChamberArea (pos ))
895+ return true ;
896+ if (filterVillages .isChecked () && isLikelyVillageChest (pos ))
897+ return true ;
898+ }
899+ return false ;
900+ }
901+
902+ // Barrels in Trial Chambers
903+ if (be instanceof BarrelBlockEntity )
904+ {
905+ if (filterTrialChambers .isChecked () && isInTrialChamberArea (pos ))
906+ return true ;
907+ return false ;
908+ }
909+
910+ // Hoppers in Trial Chambers
911+ if (be instanceof HopperBlockEntity )
912+ {
913+ if (filterTrialChambers .isChecked () && isInTrialChamberArea (pos ))
914+ return true ;
915+ return false ;
916+ }
917+
918+ // Dispensers in Trial Chambers
919+ if (be instanceof DispenserBlockEntity )
920+ {
921+ if (filterTrialChambers .isChecked () && isInTrialChamberArea (pos ))
922+ return true ;
923+ return false ;
924+ }
925+
926+ return false ;
927+ }
928+
835929 private BlockPos getSingleChestPosIfApplicable (AABB box )
836930 {
837931 if (MC .level == null || box == null )
@@ -945,16 +1039,121 @@ private boolean isNearSpawner(BlockPos center, int range)
9451039 .anyMatch (pos -> BlockUtils .getBlock (pos ) == Blocks .SPAWNER );
9461040 }
9471041
948- private boolean isTrialChamberChest (BlockPos pos )
1042+ private boolean isInTrialChamberArea (BlockPos pos )
9491043 {
9501044 int y = pos .getY ();
951- if (y < -38 || y > 10 )
1045+ // Trial chambers typically generate underground; keep a broad sanity
1046+ // window
1047+ if (y < -64 || y > 48 )
9521048 return false ;
9531049
954- if (!isNearWaxedCopper (pos , 5 ))
1050+ // Rely on proximity to Trial Spawners when available
1051+ if (isNearTrialSpawner (pos , 128 ))
1052+ return true ;
1053+
1054+ // Fallback heuristic: look for characteristic Trial Chamber blocks
1055+ // nearby
1056+ return isNearLikelyTrialBlocks (pos , 6 );
1057+ }
1058+
1059+ private boolean isNearLikelyTrialBlocks (BlockPos center , int range )
1060+ {
1061+ if (MC .level == null )
9551062 return false ;
9561063
957- return isNearTrialSpawner (pos , 100 );
1064+ return BlockUtils .getAllInBoxStream (center , range ).anyMatch (pos -> {
1065+ Block b = BlockUtils .getBlock (pos );
1066+ if (b == null )
1067+ return false ;
1068+ String path = BuiltInRegistries .BLOCK .getKey (b ).getPath ();
1069+ // Strong signals of Trial Chambers
1070+ if (path .contains ("trial_spawner" ) || path .contains ("vault" ))
1071+ return true ;
1072+ // Common building blocks in Trial Chambers (heuristic)
1073+ return path .contains ("tuff_bricks" )
1074+ || path .contains ("chiseled_tuff" )
1075+ || path .contains ("polished_tuff" )
1076+ || path .contains ("copper_bulb" );
1077+ });
1078+ }
1079+
1080+ // Backward-compatibility alias for any callers still using the old name
1081+ @ Deprecated
1082+ private boolean isTrialChamberChest (BlockPos pos )
1083+ {
1084+ return isInTrialChamberArea (pos );
1085+ }
1086+
1087+ private BlockPos getSingleHopperPosIfApplicable (AABB box )
1088+ {
1089+ if (MC .level == null || box == null )
1090+ return null ;
1091+
1092+ int boxMinX = (int )Math .floor (box .minX + 1e-6 );
1093+ int boxMaxX = (int )Math .floor (box .maxX - 1e-6 );
1094+ int boxMinY = (int )Math .floor (box .minY + 1e-6 );
1095+ int boxMaxY = (int )Math .floor (box .maxY - 1e-6 );
1096+ int boxMinZ = (int )Math .floor (box .minZ + 1e-6 );
1097+ int boxMaxZ = (int )Math .floor (box .maxZ - 1e-6 );
1098+
1099+ BlockPos found = null ;
1100+ int count = 0 ;
1101+
1102+ for (int x = boxMinX ; x <= boxMaxX ; x ++)
1103+ for (int y = boxMinY ; y <= boxMaxY ; y ++)
1104+ for (int z = boxMinZ ; z <= boxMaxZ ; z ++)
1105+ {
1106+ BlockPos pos = new BlockPos (x , y , z );
1107+ BlockState state = MC .level .getBlockState (pos );
1108+ if (state == null )
1109+ continue ;
1110+ if (state .getBlock () instanceof HopperBlock )
1111+ {
1112+ count ++;
1113+ if (found == null )
1114+ found = pos ;
1115+ if (count > 1 )
1116+ return null ;
1117+ }
1118+ }
1119+
1120+ return found ;
1121+ }
1122+
1123+ private BlockPos getSingleDispenserPosIfApplicable (AABB box )
1124+ {
1125+ if (MC .level == null || box == null )
1126+ return null ;
1127+
1128+ int boxMinX = (int )Math .floor (box .minX + 1e-6 );
1129+ int boxMaxX = (int )Math .floor (box .maxX - 1e-6 );
1130+ int boxMinY = (int )Math .floor (box .minY + 1e-6 );
1131+ int boxMaxY = (int )Math .floor (box .maxY - 1e-6 );
1132+ int boxMinZ = (int )Math .floor (box .minZ + 1e-6 );
1133+ int boxMaxZ = (int )Math .floor (box .maxZ - 1e-6 );
1134+
1135+ BlockPos found = null ;
1136+ int count = 0 ;
1137+
1138+ for (int x = boxMinX ; x <= boxMaxX ; x ++)
1139+ for (int y = boxMinY ; y <= boxMaxY ; y ++)
1140+ for (int z = boxMinZ ; z <= boxMaxZ ; z ++)
1141+ {
1142+ BlockPos pos = new BlockPos (x , y , z );
1143+ BlockState state = MC .level .getBlockState (pos );
1144+ if (state == null )
1145+ continue ;
1146+ if (state .getBlock () instanceof DispenserBlock )
1147+ {
1148+ count ++;
1149+ if (found == null )
1150+ found = pos ;
1151+ if (count > 1 )
1152+ return null ;
1153+ }
1154+ }
1155+
1156+ return found ;
9581157 }
9591158
9601159 private boolean isNearWaxedCopper (BlockPos center , int range )
0 commit comments