@@ -89,6 +89,7 @@ interface LifecycleDispatcher<T> {
8989 private static final Map <ChunkPosition , Set <Block >> byChunk = new HashMap <>();
9090 private static final Map <Block , TileEntityType > lastActiveTypes = new HashMap <>();
9191 private static final Map <UUID , Set <ChunkPosition >> watchedChunksByPlayer = new HashMap <>();
92+ private static final Map <UUID , WatchedChunkCenter > watchedChunkCentersByPlayer = new HashMap <>();
9293 private static final Map <ChunkPosition , Integer > watcherCounts = new HashMap <>();
9394 private static final Set <ChunkPosition > dirtyWatcherChunks = new LinkedHashSet <>();
9495 private static final Set <ChunkPosition > unloadingChunks = new LinkedHashSet <>();
@@ -106,16 +107,20 @@ public synchronized static void _init_() {
106107 }
107108 Scheduler .runTaskTimer (plugin , () -> {
108109 if (InteractionVisualizer .eventDrivenBlockUpdates ) {
109- Set <UUID > online = new LinkedHashSet <>();
110110 for (Player player : Bukkit .getOnlinePlayers ()) {
111- online .add (player .getUniqueId ());
112- updateWatchedChunks (player .getUniqueId (), getAllChunks (player .getLocation ()));
111+ updateWatchedChunks (player .getUniqueId (), player .getLocation ());
113112 }
114- for (UUID playerId : new LinkedHashSet <>(watchedChunksByPlayer .keySet ())) {
115- if (!online .contains (playerId )) {
116- clearWatchedChunks (playerId );
113+ Iterator <Map .Entry <UUID , Set <ChunkPosition >>> iterator = watchedChunksByPlayer .entrySet ().iterator ();
114+ while (iterator .hasNext ()) {
115+ Map .Entry <UUID , Set <ChunkPosition >> entry = iterator .next ();
116+ Player player = Bukkit .getPlayer (entry .getKey ());
117+ if (player == null || !player .isOnline ()) {
118+ iterator .remove ();
119+ watchedChunkCentersByPlayer .remove (entry .getKey ());
120+ removeWatcherCounts (entry .getValue ());
117121 }
118122 }
123+ drainWatcherChanges ();
119124 return ;
120125 }
121126 for (TileEntityType type : tileEntityTypes ) {
@@ -125,7 +130,7 @@ public synchronized static void _init_() {
125130 }, 0 , InteractionVisualizerAPI .getGCPeriod ());
126131 for (Player player : Bukkit .getOnlinePlayers ()) {
127132 if (InteractionVisualizer .eventDrivenBlockUpdates ) {
128- updateWatchedChunks (player .getUniqueId (), getAllChunks ( player .getLocation () ));
133+ updateWatchedChunks (player .getUniqueId (), player .getLocation ());
129134 } else {
130135 addTileEntities (getAllChunks (player .getLocation ()));
131136 }
@@ -274,6 +279,7 @@ private static void clearRuntimeState() {
274279 byChunk .clear ();
275280 lastActiveTypes .clear ();
276281 watchedChunksByPlayer .clear ();
282+ watchedChunkCentersByPlayer .clear ();
277283 watcherCounts .clear ();
278284 dirtyWatcherChunks .clear ();
279285 unloadingChunks .clear ();
@@ -283,7 +289,7 @@ private static void clearRuntimeState() {
283289 /** Number of block, chunk or viewer-index roots retained by this lifecycle. */
284290 public synchronized static int retainedStateCount () {
285291 return active .size () + byChunk .size () + lastActiveTypes .size ()
286- + watchedChunksByPlayer .size () + watcherCounts .size ()
292+ + watchedChunksByPlayer .size () + watchedChunkCentersByPlayer . size () + watcherCounts .size ()
287293 + dirtyWatcherChunks .size () + unloadingChunks .size ();
288294 }
289295
@@ -430,18 +436,162 @@ private synchronized static void finishChunkUnload(ChunkPosition chunk) {
430436 }
431437 }
432438
433- private synchronized static void updateWatchedChunks (UUID playerId , Set <ChunkPosition > nextChunks ) {
439+ private synchronized static void updateWatchedChunks (UUID playerId , Location location ) {
440+ World world = location .getWorld ();
441+ if (world == null ) {
442+ clearWatchedChunks (playerId );
443+ return ;
444+ }
445+ int chunkX = location .getBlockX () >> 4 ;
446+ int chunkZ = location .getBlockZ () >> 4 ;
447+ WatchedChunkCenter previous = watchedChunkCentersByPlayer .get (playerId );
448+ int range = InteractionVisualizer .tileEntityCheckingRange ;
449+ if (previous != null && previous .matches (world , chunkX , chunkZ , range )) {
450+ return ;
451+ }
452+
453+ Set <ChunkPosition > watched = watchedChunksByPlayer .get (playerId );
454+ if (previous != null && watched != null && range >= 0 && previous .range () == range
455+ && previous .worldId ().equals (world .getUID ())
456+ && Math .abs (chunkX - previous .chunkX ()) <= 1
457+ && Math .abs (chunkZ - previous .chunkZ ()) <= 1 ) {
458+ shiftWatchedWindow (watchedChunksByPlayer , watcherCounts , dirtyWatcherChunks ,
459+ playerId , world , previous .chunkX (), previous .chunkZ (), chunkX , chunkZ , range );
460+ watchedChunkCentersByPlayer .put (playerId ,
461+ new WatchedChunkCenter (world .getUID (), chunkX , chunkZ , range ));
462+ drainWatcherChanges ();
463+ return ;
464+ }
465+
466+ watchedChunkCentersByPlayer .put (playerId ,
467+ new WatchedChunkCenter (world .getUID (), chunkX , chunkZ , range ));
468+ replaceWatchedChunks (playerId , getAllChunks (location ));
469+ }
470+
471+ private synchronized static void replaceWatchedChunks (UUID playerId , Set <ChunkPosition > nextChunks ) {
434472 commitWatcherUpdate (
435473 watchedChunksByPlayer , watcherCounts , dirtyWatcherChunks ,
436474 playerId , nextChunks );
437475 drainWatcherChanges ();
438476 }
439477
478+ private synchronized static void extendWatchedChunks (UUID playerId , ChunkPosition chunk ) {
479+ Set <ChunkPosition > watched = watchedChunksByPlayer .get (playerId );
480+ if (watched == null || !watched .add (chunk )) {
481+ return ;
482+ }
483+ watchedChunkCentersByPlayer .remove (playerId );
484+ incrementWatcherCount (watcherCounts , dirtyWatcherChunks , chunk );
485+ drainWatcherChanges ();
486+ }
487+
440488 private synchronized static void clearWatchedChunks (UUID playerId ) {
489+ watchedChunkCentersByPlayer .remove (playerId );
441490 if (!watchedChunksByPlayer .containsKey (playerId )) {
442491 return ;
443492 }
444- updateWatchedChunks (playerId , Set .of ());
493+ replaceWatchedChunks (playerId , Set .of ());
494+ }
495+
496+ static <P > int shiftWatchedWindow (
497+ Map <P , Set <ChunkPosition >> watchedByPlayer ,
498+ Map <ChunkPosition , Integer > counts , Set <ChunkPosition > dirty ,
499+ P playerId , World world ,
500+ int previousChunkX , int previousChunkZ ,
501+ int nextChunkX , int nextChunkZ , int range ) {
502+ Set <ChunkPosition > watched = watchedByPlayer .get (playerId );
503+ if (watched == null ) {
504+ return 0 ;
505+ }
506+
507+ UUID worldId = world .getUID ();
508+ int minX = nextChunkX - range ;
509+ int maxX = nextChunkX + range ;
510+ int minZ = nextChunkZ - range ;
511+ int maxZ = nextChunkZ + range ;
512+ Iterator <ChunkPosition > iterator = watched .iterator ();
513+ while (iterator .hasNext ()) {
514+ ChunkPosition chunk = iterator .next ();
515+ if (!worldId .equals (chunk .getWorldUID ()) || chunk .getChunkX () < minX || chunk .getChunkX () > maxX
516+ || chunk .getChunkZ () < minZ || chunk .getChunkZ () > maxZ ) {
517+ iterator .remove ();
518+ decrementWatcherCount (counts , dirty , chunk );
519+ }
520+ }
521+
522+ int candidates = 0 ;
523+ if (nextChunkX > previousChunkX ) {
524+ candidates += addWatcherStripe (watched , counts , dirty , world ,
525+ previousChunkX + range + 1 , nextChunkX + range , minZ , maxZ , true );
526+ } else if (nextChunkX < previousChunkX ) {
527+ candidates += addWatcherStripe (watched , counts , dirty , world ,
528+ nextChunkX - range , previousChunkX - range - 1 , minZ , maxZ , true );
529+ }
530+ int zCrossStart = minX ;
531+ int zCrossEnd = maxX ;
532+ if (nextChunkX > previousChunkX ) {
533+ zCrossEnd --;
534+ } else if (nextChunkX < previousChunkX ) {
535+ zCrossStart ++;
536+ }
537+ if (nextChunkZ > previousChunkZ ) {
538+ candidates += addWatcherStripe (watched , counts , dirty , world ,
539+ previousChunkZ + range + 1 , nextChunkZ + range ,
540+ zCrossStart , zCrossEnd , false );
541+ } else if (nextChunkZ < previousChunkZ ) {
542+ candidates += addWatcherStripe (watched , counts , dirty , world ,
543+ nextChunkZ - range , previousChunkZ - range - 1 ,
544+ zCrossStart , zCrossEnd , false );
545+ }
546+ return candidates ;
547+ }
548+
549+ private static int addWatcherStripe (
550+ Set <ChunkPosition > watched , Map <ChunkPosition , Integer > counts ,
551+ Set <ChunkPosition > dirty , World world ,
552+ int stripeStart , int stripeEnd , int crossStart , int crossEnd ,
553+ boolean xStripe ) {
554+ int candidates = 0 ;
555+ for (int stripe = stripeStart ; stripe <= stripeEnd ; stripe ++) {
556+ for (int cross = crossStart ; cross <= crossEnd ; cross ++) {
557+ candidates ++;
558+ ChunkPosition chunk = xStripe
559+ ? new ChunkPosition (world , stripe , cross )
560+ : new ChunkPosition (world , cross , stripe );
561+ if (watched .add (chunk )) {
562+ incrementWatcherCount (counts , dirty , chunk );
563+ }
564+ }
565+ }
566+ return candidates ;
567+ }
568+
569+ private static void removeWatcherCounts (Collection <ChunkPosition > chunks ) {
570+ for (ChunkPosition chunk : chunks ) {
571+ decrementWatcherCount (watcherCounts , dirtyWatcherChunks , chunk );
572+ }
573+ }
574+
575+ private static void incrementWatcherCount (
576+ Map <ChunkPosition , Integer > counts , Set <ChunkPosition > dirty ,
577+ ChunkPosition chunk ) {
578+ int count = counts .getOrDefault (chunk , 0 );
579+ counts .put (chunk , count + 1 );
580+ if (count == 0 ) {
581+ dirty .add (chunk );
582+ }
583+ }
584+
585+ private static void decrementWatcherCount (
586+ Map <ChunkPosition , Integer > counts , Set <ChunkPosition > dirty ,
587+ ChunkPosition chunk ) {
588+ int count = counts .getOrDefault (chunk , 0 );
589+ if (count <= 1 ) {
590+ counts .remove (chunk );
591+ dirty .add (chunk );
592+ } else {
593+ counts .put (chunk , count - 1 );
594+ }
445595 }
446596
447597 static <P , K > void commitWatcherUpdate (
@@ -524,7 +674,7 @@ private static void callDeactivatedEvent(Block block, TileEntityType type) {
524674 @ EventHandler (priority = EventPriority .MONITOR , ignoreCancelled = true )
525675 public void onJoin (PlayerJoinEvent event ) {
526676 if (InteractionVisualizer .eventDrivenBlockUpdates ) {
527- updateWatchedChunks (event .getPlayer ().getUniqueId (), getAllChunks ( event .getPlayer ().getLocation () ));
677+ updateWatchedChunks (event .getPlayer ().getUniqueId (), event .getPlayer ().getLocation ());
528678 } else {
529679 addTileEntities (getAllChunks (event .getPlayer ().getLocation ()));
530680 }
@@ -538,13 +688,13 @@ public void onQuit(PlayerQuitEvent event) {
538688
539689 public void onRespawn (PlayerRespawnEvent event ) {
540690 if (InteractionVisualizer .eventDrivenBlockUpdates ) {
541- updateWatchedChunks (event .getPlayer ().getUniqueId (), getAllChunks ( event .getRespawnLocation () ));
691+ updateWatchedChunks (event .getPlayer ().getUniqueId (), event .getRespawnLocation ());
542692 }
543693 }
544694
545695 public void onChangedWorld (PlayerChangedWorldEvent event ) {
546696 if (InteractionVisualizer .eventDrivenBlockUpdates ) {
547- updateWatchedChunks (event .getPlayer ().getUniqueId (), getAllChunks ( event .getPlayer ().getLocation () ));
697+ updateWatchedChunks (event .getPlayer ().getUniqueId (), event .getPlayer ().getLocation ());
548698 }
549699 }
550700
@@ -555,9 +705,9 @@ public void onInteract(PlayerInteractEvent event) {
555705 TileEntityType type = TileEntity .getTileEntityType (block .getType ());
556706 if (type != null ) {
557707 if (InteractionVisualizer .eventDrivenBlockUpdates ) {
558- Set < ChunkPosition > chunks = getAllChunks ( event .getPlayer ().getLocation () );
559- chunks . add ( getChunk ( block .getLocation () ));
560- updateWatchedChunks ( event . getPlayer (). getUniqueId (), chunks );
708+ UUID playerId = event .getPlayer ().getUniqueId ( );
709+ updateWatchedChunks ( playerId , event . getPlayer () .getLocation ());
710+ extendWatchedChunks ( playerId , getChunk ( block . getLocation ()) );
561711 }
562712 if (!active .get (type ).contains (block )) {
563713 addTileEntities (getChunk (block .getLocation ()));
@@ -572,7 +722,7 @@ public void onTeleport(PlayerTeleportEvent event) {
572722 Location to = event .getTo ();
573723 if (!from .getWorld ().equals (to .getWorld ()) || from .getBlockX () >> 4 != to .getBlockX () >> 4 || from .getBlockZ () >> 4 != to .getBlockZ () >> 4 ) {
574724 if (InteractionVisualizer .eventDrivenBlockUpdates ) {
575- updateWatchedChunks (event .getPlayer ().getUniqueId (), getAllChunks ( to ) );
725+ updateWatchedChunks (event .getPlayer ().getUniqueId (), to );
576726 } else {
577727 addTileEntities (getAllChunks (to ));
578728 }
@@ -585,14 +735,14 @@ public void onPlayerMove(PlayerMoveEvent event) {
585735 Location to = event .getTo ();
586736 if (!from .getWorld ().equals (to .getWorld ())) {
587737 if (InteractionVisualizer .eventDrivenBlockUpdates ) {
588- updateWatchedChunks (event .getPlayer ().getUniqueId (), getAllChunks ( to ) );
738+ updateWatchedChunks (event .getPlayer ().getUniqueId (), to );
589739 } else {
590740 addTileEntities (getAllChunks (to ));
591741 }
592742 } else if (from .getBlockX () >> 4 != to .getBlockX () >> 4 || from .getBlockZ () >> 4 != to .getBlockZ () >> 4 ) {
593743 if (!isMovingTooFast (event .getPlayer (), from , to )) {
594744 if (InteractionVisualizer .eventDrivenBlockUpdates ) {
595- updateWatchedChunks (event .getPlayer ().getUniqueId (), getAllChunks ( to ) );
745+ updateWatchedChunks (event .getPlayer ().getUniqueId (), to );
596746 } else {
597747 addTileEntities (getAllChunks (to ));
598748 }
@@ -611,10 +761,9 @@ public void onVehicleMove(VehicleMoveEvent event) {
611761 if (InteractionVisualizer .eventDrivenBlockUpdates ) {
612762 boolean movingTooFast = !changedWorld && isMovingTooFast (null , from , to );
613763 if (!movingTooFast ) {
614- Set <ChunkPosition > chunks = getAllChunks (to );
615764 for (org .bukkit .entity .Entity passenger : event .getVehicle ().getPassengers ()) {
616765 if (passenger instanceof Player player ) {
617- updateWatchedChunks (player .getUniqueId (), chunks );
766+ updateWatchedChunks (player .getUniqueId (), to );
618767 }
619768 }
620769 }
@@ -644,6 +793,14 @@ public void onChunkUnload(ChunkUnloadEvent event) {
644793 Scheduler .runTask (plugin , () -> finishChunkUnload (chunk ));
645794 }
646795
796+ private record WatchedChunkCenter (UUID worldId , int chunkX , int chunkZ , int range ) {
797+
798+ private boolean matches (World world , int x , int z , int expectedRange ) {
799+ return worldId .equals (world .getUID ()) && chunkX == x && chunkZ == z
800+ && range == expectedRange ;
801+ }
802+ }
803+
647804 @ EventHandler (priority = EventPriority .MONITOR , ignoreCancelled = true )
648805 public void onBreakBlock (BlockBreakEvent event ) {
649806 if (TileEntity .isTileEntityType (event .getBlock ().getType ())) {
0 commit comments