3737import java .util .concurrent .atomic .AtomicInteger ;
3838import java .util .concurrent .ConcurrentLinkedQueue ;
3939
40+ import com .google .common .cache .Cache ;
41+ import com .google .common .cache .CacheBuilder ;
42+
4043import java .util .logging .Level ;
4144
4245public class TuffX extends JavaPlugin implements Listener , PluginMessageListener {
@@ -54,6 +57,8 @@ public class TuffX extends JavaPlugin implements Listener, PluginMessageListener
5457
5558 private BukkitTask processorTask ;
5659
60+ private Cache <WorldChunkKey , List <byte []>> chunkPayloadCache ;
61+
5762 private boolean debug ;
5863
5964 private ExecutorService chunkProcessorPool ;
@@ -62,6 +67,8 @@ private void logDebug(String message) {
6267 if (debug ) getLogger ().log (Level .INFO , "[TuffX-Debug] " + message );
6368 }
6469
70+ public record WorldChunkKey (String worldName , int x , int z ) {}
71+
6572 @ Override
6673 public void onEnable () {
6774 saveDefaultConfig ();
@@ -71,6 +78,11 @@ public void onEnable() {
7178
7279 logDebug ("TuffX will be active in the following worlds: " + String .join (", " , this .enabledWorlds ));
7380
81+ this .chunkPayloadCache = CacheBuilder .newBuilder ()
82+ .maximumSize (getConfig ().getInt ("cache-size" , 512 ))
83+ .expireAfterAccess (getConfig ().getInt ("cache-expiration" , 5 ), TimeUnit .MINUTES )
84+ .build ();
85+
7486 getServer ().getMessenger ().registerOutgoingPluginChannel (this , CHANNEL );
7587 getServer ().getMessenger ().registerIncomingPluginChannel (this , CHANNEL , this );
7688 getServer ().getPluginManager ().registerEvents (this , this );
@@ -223,17 +235,33 @@ public void run() {
223235 }
224236
225237 Queue <Vector > queue = requestQueue .get (playerUUID );
226- if (queue != null && !queue .isEmpty ()) {
227- for (int i = 0 ; i < CHUNKS_PER_TICK && !queue .isEmpty (); i ++) {
228- Vector vec = queue .poll ();
229- if (vec != null ) {
230- World world = player .getWorld ();
231- if (world .isChunkLoaded (vec .getBlockX (), vec .getBlockZ ())) {
232- processAndSendChunk (player , world .getChunkAt (vec .getBlockX (), vec .getBlockZ ()));
233- } else {
234- world .loadChunk (vec .getBlockX (), vec .getBlockZ (), true );
235- queue .add (vec );
236- }
238+ if (queue == null ) continue ;
239+
240+ for (int i = 0 ; i < CHUNKS_PER_TICK && !queue .isEmpty (); i ++) {
241+ Vector vec = queue .poll ();
242+ if (vec != null ) {
243+ World world = player .getWorld ();
244+ WorldChunkKey key = new WorldChunkKey (world .getName (), vec .getBlockX (), vec .getBlockZ ());
245+
246+ List <byte []> cachedData = chunkPayloadCache .getIfPresent (key );
247+
248+ if (cachedData != null ) {
249+ logDebug ("Cache HIT for chunk: " + key );
250+ sendPayloadsToPlayer (player , cachedData );
251+ checkIfInitialLoadComplete (player );
252+ } else {
253+ logDebug ("Cache MISS for chunk: " + key + ". Generating..." );
254+ new BukkitRunnable () {
255+ @ Override
256+ public void run () {
257+ if (world .isChunkLoaded (key .x (), key .z ())) {
258+ processAndSendChunk (player , world .getChunkAt (key .x (), key .z ()));
259+ } else {
260+ world .loadChunk (key .x (), key .z (), true );
261+ processAndSendChunk (player , world .getChunkAt (key .x (), key .z ()));
262+ }
263+ }
264+ }.runTaskAsynchronously (TuffX .this );
237265 }
238266 }
239267 }
@@ -242,33 +270,48 @@ public void run() {
242270 }.runTaskTimer (this , 0L , 1L );
243271 }
244272
273+ private void sendPayloadsToPlayer (Player player , List <byte []> payloads ) {
274+ new BukkitRunnable () {
275+ @ Override
276+ public void run () {
277+ if (player .isOnline ()) {
278+ for (byte [] payload : payloads ) {
279+ player .sendPluginMessage (TuffX .this , CHANNEL , payload );
280+ }
281+ }
282+ }
283+ }.runTask (this );
284+ }
285+
245286 @ EventHandler (priority = EventPriority .MONITOR )
246287 public void onPlayerChangeWorld (PlayerChangedWorldEvent event ) {
247288 Player player = event .getPlayer ();
248289 UUID playerId = player .getUniqueId ();
249290
250291 Queue <Vector > playerQueue = requestQueue .get (playerId );
251292 if (playerQueue != null && !playerQueue .isEmpty ()) {
252- logDebug ("Player " + player .getName () + " changed worlds. Clearing " + playerQueue .size () + " pending chunk requests." );
253- playerQueue .clear ();
254- }
255-
256- if (initialChunksToProcess .remove (playerId ) != null ) {
257- logDebug ("Player " + player .getName () + " was in the middle of an initial chunk load. The process has been cancelled." );
258- awaitingInitialBatch .remove (playerId );
259- player .sendPluginMessage (this , CHANNEL , createLoadFinishedPayload ());
260- }
293+ logDebug ("Player " + player .getName () + " changed worlds. Clearing " + playerQueue .size () + " pending chunk requests." );
294+ playerQueue .clear ();
295+ }
296+
297+ if (initialChunksToProcess .remove (playerId ) != null ) {
298+ logDebug ("Player " + player .getName () + " was in the middle of an initial chunk load. The process has been cancelled." );
299+ awaitingInitialBatch .remove (playerId );
300+ player .sendPluginMessage (this , CHANNEL , createLoadFinishedPayload ());
301+ }
261302
262- player .sendPluginMessage (this , CHANNEL , createDimensionPayload ());
303+ player .sendPluginMessage (this , CHANNEL , createDimensionPayload ());
263304
264- player .sendPluginMessage (this , CHANNEL , createBelowY0StatusPayload (enabledWorlds .contains (player .getWorld ().getName ())));
265- }
305+ player .sendPluginMessage (this , CHANNEL , createBelowY0StatusPayload (enabledWorlds .contains (player .getWorld ().getName ())));
306+ }
266307
267- private void processAndSendChunk (final Player player , final Chunk chunk ) {
308+ private void processAndSendChunk (final Player player , final Chunk chunk ) {
268309 if (chunk == null || !player .isOnline () || chunkProcessorPool .isShutdown ()) {
269310 return ;
270311 }
271312
313+ final WorldChunkKey key = new WorldChunkKey (chunk .getWorld ().getName (), chunk .getX (), chunk .getZ ());
314+
272315 chunkProcessorPool .submit (() -> {
273316 final List <byte []> processedPayloads = new ArrayList <>();
274317 final ChunkSnapshot snapshot = chunk .getChunkSnapshot (true , false , false );
@@ -288,6 +331,8 @@ private void processAndSendChunk(final Player player, final Chunk chunk) {
288331 }
289332 }
290333
334+ chunkPayloadCache .put (key , processedPayloads );
335+
291336 new BukkitRunnable () {
292337 @ Override
293338 public void run () {
@@ -302,6 +347,12 @@ public void run() {
302347 });
303348 }
304349
350+ private void invalidateChunkCache (World world , int blockX , int blockZ ) {
351+ WorldChunkKey key = new WorldChunkKey (world .getName (), blockX >> 4 , blockZ >> 4 );
352+ chunkPayloadCache .invalidate (key );
353+ logDebug ("Invalidated cache for chunk: " + key );
354+ }
355+
305356 private void checkIfInitialLoadComplete (Player player ) {
306357 UUID playerId = player .getUniqueId ();
307358 AtomicInteger counter = initialChunksToProcess .get (playerId );
@@ -405,11 +456,29 @@ private byte[] createSectionPayload(ChunkSnapshot snapshot, int cx, int cz, int
405456
406457
407458 @ EventHandler (priority = EventPriority .MONITOR , ignoreCancelled = true )
408- public void onBlockBreak (BlockBreakEvent event ) { if (event .getBlock ().getY () < 0 ) handleBlockChange (event .getBlock ().getLocation (), event .getBlock ().getBlockData (), Material .AIR .createBlockData ()); }
459+ public void onBlockBreak (BlockBreakEvent event ) {
460+ if (event .getBlock ().getY () < 0 ) {
461+ handleBlockChange (event .getBlock ().getLocation (), event .getBlock ().getBlockData (), Material .AIR .createBlockData ());
462+ invalidateChunkCache (event .getBlock ().getWorld (), event .getBlock ().getX (), event .getBlock ().getZ ());
463+ }
464+ }
465+
409466 @ EventHandler (priority = EventPriority .MONITOR , ignoreCancelled = true )
410- public void onBlockPlace (BlockPlaceEvent event ) { if (event .getBlock ().getY () < 0 ) handleBlockChange (event .getBlock ().getLocation (), event .getBlockReplacedState ().getBlockData (), event .getBlock ().getBlockData ()); }
467+ public void onBlockPlace (BlockPlaceEvent event ) {
468+ if (event .getBlock ().getY () < 0 ) {
469+ handleBlockChange (event .getBlock ().getLocation (), event .getBlockReplacedState ().getBlockData (), event .getBlock ().getBlockData ());
470+ invalidateChunkCache (event .getBlock ().getWorld (), event .getBlock ().getX (), event .getBlock ().getZ ());
471+ }
472+ }
473+
411474 @ EventHandler (priority = EventPriority .MONITOR , ignoreCancelled = true )
412- public void onBlockPhysics (BlockPhysicsEvent event ) { Block block = event .getBlock (); if (block .getY () < 0 ) sendSingleBlockUpdate (block .getLocation (), block .getBlockData ());sendLightingUpdate (block .getLocation ()); }
475+ public void onBlockPhysics (BlockPhysicsEvent event ) {
476+ Block block = event .getBlock ();
477+ if (block .getY () < 0 ) {
478+ sendSingleBlockUpdate (block .getLocation (), block .getBlockData ());sendLightingUpdate (block .getLocation ());
479+ invalidateChunkCache (block .getWorld (), block .getX (), block .getZ ());
480+ }
481+ }
413482
414483 private void sendSingleBlockUpdate (Location loc , BlockData data ) {
415484 try (ByteArrayOutputStream bout = new ByteArrayOutputStream (64 );
0 commit comments