@@ -91,6 +91,17 @@ public final class TunnelHoleStairEspHack extends Hack
9191 "Periodically re-queue all chunks in range so stale detections refresh\n "
9292 + "without toggling the hack." ,
9393 10 , 0 , 60 , 1 , ValueDisplay .INTEGER .withSuffix (" s" ));
94+ private final CheckboxSetting adaptiveMovementScan = new CheckboxSetting (
95+ "Adaptive movement scan" ,
96+ "Adjusts scan area and throughput based on your movement speed.\n "
97+ + "Standing still keeps your selected area. Moving/flying tightens area\n "
98+ + "and boosts nearby scan speed." ,
99+ true );
100+ private final SliderSetting nearbyPriorityRadius = new SliderSetting (
101+ "Nearby priority radius" ,
102+ "Always prioritize chunks in this radius around you each tick.\n "
103+ + "Higher values discover nearby tunnels faster, but can cost more CPU." ,
104+ 2 , 0 , 12 , 1 , ValueDisplay .INTEGER .withSuffix (" chunks" ));
94105 private final CheckboxSetting airOnly = new CheckboxSetting ("Air only" ,
95106 "Only treat pure air as passable. Turning this off will also treat\n "
96107 + "other non-solid blocks as passable." ,
@@ -213,6 +224,7 @@ public final class TunnelHoleStairEspHack extends Hack
213224 private int scanConfigHash ;
214225 private int refreshTimerTicks ;
215226 private Level activeLevel ;
227+ private Vec3 lastPlayerPos ;
216228
217229 public TunnelHoleStairEspHack ()
218230 {
@@ -225,6 +237,8 @@ public TunnelHoleStairEspHack()
225237 addSetting (chunksPerTick );
226238 addSetting (scanTimeBudgetMs );
227239 addSetting (refreshInterval );
240+ addSetting (adaptiveMovementScan );
241+ addSetting (nearbyPriorityRadius );
228242 addSetting (airOnly );
229243 addSetting (minYOffset );
230244 addSetting (maxYOffset );
@@ -283,6 +297,7 @@ protected void onEnable()
283297 refreshTimerTicks = 0 ;
284298 lastAreaSelection = area .getSelected ();
285299 activeLevel = null ;
300+ lastPlayerPos = null ;
286301 clearRuntimeState ();
287302 EVENTS .add (UpdateListener .class , this );
288303 EVENTS .add (RenderListener .class , this );
@@ -298,6 +313,7 @@ protected void onDisable()
298313 EVENTS .remove (CameraTransformViewBobbingListener .class , this );
299314 EVENTS .remove (PacketInputListener .class , this );
300315 activeLevel = null ;
316+ lastPlayerPos = null ;
301317 clearRuntimeState ();
302318 }
303319
@@ -324,6 +340,7 @@ public void onUpdate()
324340 refreshTimerTicks = 0 ;
325341 lastAreaSelection = area .getSelected ();
326342 scanConfigHash = getScanConfigHash ();
343+ lastPlayerPos = null ;
327344 clearRuntimeState ();
328345 }
329346
@@ -349,11 +366,14 @@ public void onUpdate()
349366 clearRuntimeState ();
350367 }
351368
352- HashSet <ChunkPos > areaChunks = getAreaChunks ();
369+ ScanProfile profile = getScanProfile ();
370+ HashSet <ChunkPos > areaChunks = getAreaChunks (profile .range );
353371 boolean changed = syncToArea (areaChunks );
354372 enqueueDirtyChunks (areaChunks );
355373 enqueuePeriodicRefresh (areaChunks );
356- changed |= processQueuedScans (areaChunks );
374+ promoteNearbyChunks (areaChunks , profile .nearbyRadius );
375+ changed |=
376+ processQueuedScans (areaChunks , profile .scans , profile .budgetNs );
357377
358378 if (changed )
359379 rebuildRenderCache ();
@@ -518,7 +538,7 @@ private boolean isEnabledInCurrentDimension()
518538 return true ;
519539 }
520540
521- private HashSet <ChunkPos > getAreaChunks ()
541+ private HashSet <ChunkPos > getAreaChunks (int chunkRange )
522542 {
523543 ChunkPos center = getAreaCenterChunk ();
524544 ChunkAreaSetting .ChunkArea selection = area .getSelected ();
@@ -528,7 +548,6 @@ private HashSet<ChunkPos> getAreaChunks()
528548 // requiring
529549 // a hack toggle or area change.
530550 areaChunkCache .clear ();
531- int chunkRange = getChunkRange (selection );
532551 for (int x = center .x () - chunkRange ; x <= center .x () + chunkRange ; x ++)
533552 for (int z = center .z () - chunkRange ; z <= center .z ()
534553 + chunkRange ; z ++)
@@ -592,15 +611,44 @@ private void enqueueDirtyChunks(HashSet<ChunkPos> areaChunks)
592611
593612 if (queuedChunks .add (pos ))
594613 scanQueue .addFirst (pos );
614+ else if (scanQueue .remove (pos ))
615+ scanQueue .addFirst (pos );
595616
596617 promoted ++;
597618 }
598619 }
599620
600- private boolean processQueuedScans (HashSet <ChunkPos > areaChunks )
621+ private void promoteNearbyChunks (HashSet <ChunkPos > areaChunks ,
622+ int nearbyRadius )
623+ {
624+ ChunkPos center = getAreaCenterChunk ();
625+ ArrayList <ChunkPos > nearby = new ArrayList <>();
626+ int radius = Math .min (getChunkRange (area .getSelected ()), nearbyRadius );
627+
628+ for (int dx = -radius ; dx <= radius ; dx ++)
629+ for (int dz = -radius ; dz <= radius ; dz ++)
630+ {
631+ ChunkPos pos = new ChunkPos (center .x () + dx , center .z () + dz );
632+ if (areaChunks .contains (pos ))
633+ nearby .add (pos );
634+ }
635+
636+ nearby .sort (
637+ Comparator .comparingInt (pos -> getChunkDistance (pos , center )));
638+ // Push nearest chunks to the front so "standing on it" updates fast.
639+ for (int i = nearby .size () - 1 ; i >= 0 ; i --)
640+ {
641+ ChunkPos pos = nearby .get (i );
642+ if (queuedChunks .add (pos ))
643+ scanQueue .addFirst (pos );
644+ else if (scanQueue .remove (pos ))
645+ scanQueue .addFirst (pos );
646+ }
647+ }
648+
649+ private boolean processQueuedScans (HashSet <ChunkPos > areaChunks , int scans ,
650+ long budgetNs )
601651 {
602- int scans = Math .max (1 , chunksPerTick .getValueI ());
603- long budgetNs = Math .max (1L , scanTimeBudgetMs .getValueI ()) * 1_000_000L ;
604652 long startNs = System .nanoTime ();
605653 boolean changed = false ;
606654
@@ -623,6 +671,52 @@ private boolean processQueuedScans(HashSet<ChunkPos> areaChunks)
623671 return changed ;
624672 }
625673
674+ private ScanProfile getScanProfile ()
675+ {
676+ int baseRange = getChunkRange (area .getSelected ());
677+ int baseScans = Math .max (1 , chunksPerTick .getValueI ());
678+ long baseBudgetNs =
679+ Math .max (1L , scanTimeBudgetMs .getValueI ()) * 1_000_000L ;
680+ int baseNearby = nearbyPriorityRadius .getValueI ();
681+
682+ if (MC .player == null )
683+ return new ScanProfile (baseRange , baseScans , baseBudgetNs ,
684+ baseNearby );
685+
686+ Vec3 currentPos = MC .player .position ();
687+ double speed = 0 ;
688+ if (lastPlayerPos != null )
689+ speed = currentPos .subtract (lastPlayerPos ).horizontalDistance ();
690+ lastPlayerPos = currentPos ;
691+
692+ if (!adaptiveMovementScan .isChecked ())
693+ return new ScanProfile (baseRange , baseScans , baseBudgetNs ,
694+ baseNearby );
695+
696+ boolean flying =
697+ MC .player .getAbilities ().flying || MC .player .isFallFlying ();
698+ if (flying || speed >= 0.9 )
699+ {
700+ int range = Math .min (baseRange , 6 );
701+ int scans = Math .max (baseScans , 16 );
702+ long budgetNs = Math .max (baseBudgetNs , 16_000_000L );
703+ int nearby = Math .min (Math .max (baseNearby , 3 ), 6 );
704+ return new ScanProfile (range , scans , budgetNs , nearby );
705+ }
706+
707+ if (speed >= 0.25 )
708+ {
709+ int range = Math .min (baseRange , 12 );
710+ int scans = Math .max (baseScans , 10 );
711+ long budgetNs = Math .max (baseBudgetNs , 10_000_000L );
712+ int nearby = Math .min (Math .max (baseNearby , 3 ), 8 );
713+ return new ScanProfile (range , scans , budgetNs , nearby );
714+ }
715+
716+ // Standing/slow movement: keep full selected area and user throughput.
717+ return new ScanProfile (baseRange , baseScans , baseBudgetNs , baseNearby );
718+ }
719+
626720 private ChunkDetections scanChunk (ChunkPos chunkPos )
627721 {
628722 ChunkDetections result = new ChunkDetections ();
@@ -1373,7 +1467,8 @@ private int getScanConfigHash()
13731467 minLadderHeight .getValueI (), detectBubbleColumns .isChecked (),
13741468 minBubbleColumnHeight .getValueI (), detectWaterColumns .isChecked (),
13751469 minWaterColumnHeight .getValueI (), maxPerChunk .getValueI (),
1376- refreshInterval .getValueI (), naturalWallsOnly .isChecked (),
1470+ refreshInterval .getValueI (), adaptiveMovementScan .isChecked (),
1471+ nearbyPriorityRadius .getValueI (), naturalWallsOnly .isChecked (),
13771472 naturalWallRatio .getValue (), overworld .isChecked (),
13781473 nether .isChecked (), end .isChecked ());
13791474 }
@@ -1421,6 +1516,23 @@ private static final class ChunkDetections
14211516 private final ArrayList <AABB > waterColumns = new ArrayList <>();
14221517 }
14231518
1519+ private static final class ScanProfile
1520+ {
1521+ private final int range ;
1522+ private final int scans ;
1523+ private final long budgetNs ;
1524+ private final int nearbyRadius ;
1525+
1526+ private ScanProfile (int range , int scans , long budgetNs ,
1527+ int nearbyRadius )
1528+ {
1529+ this .range = range ;
1530+ this .scans = scans ;
1531+ this .budgetNs = budgetNs ;
1532+ this .nearbyRadius = nearbyRadius ;
1533+ }
1534+ }
1535+
14241536 private enum DetectionMode
14251537 {
14261538 ALL ("All" ),
0 commit comments