Skip to content

Fix runtime packet handling and swimming state#37

Open
llucasandersen wants to merge 9 commits into
TuffNetwork:mainfrom
llucasandersen:fix/runtime-issues
Open

Fix runtime packet handling and swimming state#37
llucasandersen wants to merge 9 commits into
TuffNetwork:mainfrom
llucasandersen:fix/runtime-issues

Conversation

@llucasandersen

@llucasandersen llucasandersen commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

This fixes the async block read path, the broken section block update decoding below Y0, the unreliable Viablocks chunk send path, and the swim state regression. It also cuts cache churn when the relevant features are off or no Tuff players are active.

Closes #21
Closes #14
Closes #8
Closes #5
Refs #17
Refs #30

Summary by CodeRabbit

  • New Features
    • Added Folia-aware SchedulerCompat utilities, including safe scheduling and plugin-message sending helpers.
  • Bug Fixes
    • Improved Via/ViaBlocks/Y0 chunk handling by gating requests to active/ready states and standardizing cross-thread delivery.
    • Refined ViaBlocks caching and refresh behavior when disabled or unavailable.
    • Updated swimming enforcement to better reconcile client/server state and clean up on quit/disable.
    • Hardened PacketEvents bootstrapping and related plugin-message paths.
  • Tests
    • Added ChunkHandlerTest for packed position decoding (including negative coordinates) and updated JUnit 5 test setup.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces SchedulerCompat, a Folia-aware scheduling facade, and migrates all direct Bukkit scheduler and sendPluginMessage calls across ViaBlocks, ViaEntities, Y0, Swimming, and ServerRegistry to use it. ChunkHandler gains Via-active gating, BlockChangePosition decoding, and region-thread buffer resolution. Swimming adds per-player maintenance task management. Chunk caching now gates on enabled state and world player presence. JUnit 5 test infrastructure is wired with ChunkHandler decoding tests for negative coordinates.

Changes

Folia Scheduling, State Guards, and Chunk Decoding

Layer / File(s) Summary
SchedulerCompat facade and ServerRegistry migration
src/main/java/tf/tuff/util/SchedulerCompat.java, src/main/java/tf/tuff/ServerRegistry.java
Adds the new SchedulerCompat class with static helpers for global/async/region/entity scheduling and guarded sendPluginMessage. Migrates ServerRegistry connection and reconnection scheduling to use SchedulerCompat with running marked volatile.
ChunkHandler Via-active gating, block-change decoding, and region-thread resolution
src/main/java/tf/tuff/netty/ChunkHandler.java
Adds isViaActive() predicate, BlockChangePosition record, and VarLong/packed-coordinate decoding helpers. Refactors single/multi block-change handling to retain the buffer, resolve Via data on a region thread via SchedulerCompat.runRegion, then write Via-only or passthrough. Migrates cache requests to region scheduling and adjusts buffer retain semantics.
JUnit 5 test infrastructure and ChunkHandler decoding tests
build.gradle, src/test/java/tf/tuff/netty/ChunkHandlerTest.java
Adds MockBukkit, JUnit Jupiter, and runtime test dependencies with useJUnitPlatform(). Adds ChunkHandlerTest with two tests validating BlockChangePosition decoding for negative Y and X/Z coordinates.
ViaBlocksPlugin enabled-state lifecycle
src/main/java/tf/tuff/viablocks/ViaBlocksPlugin.java
Guards chunkExecutor creation on the enabled config flag during reload and enable. Clears viaBlocksEnabledPlayers on disable/reload and conditionally re-enables previously-enabled players. Adds early-returns in handlePacket and /viablocks refresh when disabled or blockListener is null. Switches sendWelcomeGui to SchedulerCompat.runEntity.
CustomBlockListener chunk caching, traversal, and scheduling
src/main/java/tf/tuff/viablocks/CustomBlockListener.java, src/main/java/tf/tuff/viablocks/PaletteManager.java
Centralizes chunk retrieval through cacheChunkWithCallback and deliverCallback, handling disabled/shutdown/empty cases. Gates handleChunkLoad on enabled state and world player presence. Fixes findModernBlocksInChunk traversal order and expands air-block skip set (CAVE_AIR, VOID_AIR). Replaces inline player iteration with scheduleNearbyEnabledPlayers. Migrates all message sends and scheduling to SchedulerCompat.
ViaEntities message dispatch and injector scope
src/main/java/tf/tuff/viaentities/EntityDataHandler.java, src/main/java/tf/tuff/viaentities/EntityInjector.java, src/main/java/tf/tuff/viaentities/ViaEntitiesPlugin.java
Routes all outbound plugin messages through SchedulerCompat.sendPluginMessage. Switches EntityInjector post-inject deferral to SchedulerCompat.runEntity and scopes sendExistingEntities to nearby entities instead of all world entities.
Y0 regional scheduling, world-ready guards, and nearby delivery
src/main/java/tf/tuff/y0/Y0Plugin.java, src/main/java/tf/tuff/y0/ChunkPacketListener.java, src/main/java/tf/tuff/y0/ViaBlockIds.java
Adds hasReadyPlayersInWorld guard to skip caching unless ready players exist in world. Migrates message sends and task scheduling to SchedulerCompat. Adds sendToNearbyPlayers helper for block/light update delivery to online ready players within distance. Switches block physics/explode/from-to to SchedulerCompat.runRegionLater.
Swimming per-player maintenance and state reconciliation
src/main/java/tf/tuff/tuffactions/swimming/Swimming.java, src/main/java/tf/tuff/tuffactions/creative/CreativeMenu.java
Adds swimStateTasks map and cancels tasks on disable. Refactors handleSwimState to maintain per-player state and start/stop maintenance. Adds maintainSwimmingState, applySwimmingState, and task lifecycle helpers. Updates sendSwimState to use try-with-resources and pass IOException to debug logging. Migrates CreativeMenu delayed scheduling to SchedulerCompat.runEntityLater.
TuffX PacketEvents conditional bootstrapping
src/main/java/tf/tuff/TuffX.java
Adds packetEventsEnabled flag to conditionally bootstrap PacketEvents only when shouldBootstrapPacketEvents() is true (skips under MockBukkit). Uses SchedulerCompat.isFolia() for scheduler logging. Registers listener and terminates only when enabled.
Null-safety and test cleanup
src/main/java/tf/tuff/tuffactions/TuffActions.java, src/test/java/tf/tuff/TuffXTest.java
Adds explicit PacketEvents API null check in TuffActions.sendPluginMessage before checking initialization. Removes unused test import.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • TuffNetwork/TuffXPlus#18: Refactors CustomBlockListener's findModernBlocksInChunk scanning logic, including coordinate iteration order and air-material skip set (CAVE_AIR, VOID_AIR), which this PR builds upon and extends.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Fix runtime packet handling and swimming state' directly addresses the main changes: fixing asynchronous packet handling (ChunkHandler, ViaBlocks) and swimming state restoration (Swimming.java).
Linked Issues check ✅ Passed All linked issues (#21, #14, #8, #5) are addressed: #21 fixed via SchedulerCompat and ChunkHandler refactoring for thread safety, #14 fixed by BlockChangePosition record for negative Y coordinate decoding, #8 addressed through ViaBlocks listener improvements, and #5 fixed through Swimming state refactoring.
Out of Scope Changes check ✅ Passed All changes are in-scope: SchedulerCompat is a foundation utility enabling proper scheduling across the codebase, test additions validate the fixes, and all modifications directly support resolving the four linked issues.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (4)
src/main/java/tf/tuff/netty/ChunkHandler.java (1)

160-160: 📐 Maintainability & Code Quality | 💤 Low value

Unnecessary decode call.

decodeSectionCoordX(chunkSectionPos) is called but its result is discarded. The comment says it "validates bit layout," but no validation exception is thrown, and the coordinates are decoded again inside decodeMultiBlockChangePosition. This call can be removed.

Suggested fix
-        decodeSectionCoordX(chunkSectionPos); // validates bit layout alongside the packed entries below
         buf.readBoolean();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/tf/tuff/netty/ChunkHandler.java` at line 160, Remove the
redundant call to decodeSectionCoordX(chunkSectionPos) — it returns a value that
is discarded and the coordinates are already decoded inside
decodeMultiBlockChangePosition; delete the standalone call and any comment that
implies validation occurs there, leaving decodeMultiBlockChangePosition to
perform the decoding/validation for chunkSectionPos.
src/main/java/tf/tuff/TuffX.java (1)

93-105: 📐 Maintainability & Code Quality | ⚡ Quick win

Consider also unregistering outgoing plugin channels for completeness.

The incoming channel cleanup is correct, but sub-plugins like ViaEntitiesPlugin register outgoing channels with TuffX as the plugin owner. For defensive cleanup, consider also calling getServer().getMessenger().unregisterOutgoingPluginChannel(this); to ensure all registered channels are cleaned up during shutdown.

♻️ Proposed enhancement
     PacketEvents.getAPI().terminate();
     getServer().getMessenger().unregisterIncomingPluginChannel(this);
+    getServer().getMessenger().unregisterOutgoingPluginChannel(this);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/tf/tuff/TuffX.java` around lines 93 - 105, The onDisable method
currently unregisters incoming plugin channels but misses outgoing channels;
update onDisable in TuffX.java (method onDisable) to also call
getServer().getMessenger().unregisterOutgoingPluginChannel(this) (in addition to
the existing unregisterIncomingPluginChannel(this)) so any outgoing channels
registered by sub-plugins (e.g., ViaEntitiesPlugin) are cleaned up; ensure this
call is placed alongside the existing messenger cleanup and executed before
returning from onDisable.
src/main/java/tf/tuff/tuffactions/swimming/Swimming.java (2)

127-142: 📐 Maintainability & Code Quality | 💤 Low value

Use explicit Iterator for cleaner removal during iteration.

While ConcurrentHashMap.newKeySet() won't throw ConcurrentModificationException, removing via the set reference (rather than iterator.remove()) during iteration can lead to skipped elements in edge cases. Using an explicit Iterator provides more predictable behavior.

♻️ Suggested refactor using Iterator
 private void maintainSwimmingState() {
-    for (UUID playerId : swimmingPlayers) {
+    var iterator = swimmingPlayers.iterator();
+    while (iterator.hasNext()) {
+        UUID playerId = iterator.next();
         Player player = Bukkit.getPlayer(playerId);
         if (player == null || !player.isOnline()) {
-            swimmingPlayers.remove(playerId);
+            iterator.remove();
             continue;
         }
         if (!player.isInWater()) {
-            swimmingPlayers.remove(playerId);
+            iterator.remove();
             applySwimmingState(player, false);
             broadcastSwimState(player, false);
             continue;
         }
         applySwimmingState(player, true);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/tf/tuff/tuffactions/swimming/Swimming.java` around lines 127 -
142, The loop in maintainSwimmingState removes from swimmingPlayers directly
while iterating, which can skip elements; change it to use an explicit Iterator
over swimmingPlayers (Iterator<UUID> it = swimmingPlayers.iterator()) and call
it.remove() when you need to remove the current UUID; keep the same logic
calling Bukkit.getPlayer(playerId), applySwimmingState(player, false/true) and
broadcastSwimState(player, false) but ensure removals use iterator.remove() so
iteration is predictable.

79-90: 🎯 Functional Correctness | 💤 Low value

Consider cancelling the event to prevent state flicker.

The current approach lets the event proceed (setting swimming to false), then schedules a task to re-apply swimming. This creates a brief one-tick window where the player's swimming state is false. Cancelling the event directly would be more responsive and prevent the intermediate state.

♻️ Alternative approach using event cancellation
 public void handleToggleSwim(EntityToggleSwimEvent event) {
     if (!isEnabled()) return;
     if (!(event.getEntity() instanceof Player)) return;
     Player player = (Player) event.getEntity();
     if (!event.isSwimming() && swimmingPlayers.contains(player.getUniqueId())) {
-        plugin.getServer().getScheduler().runTask(plugin, () -> {
-            if (swimmingPlayers.contains(player.getUniqueId()) && player.isOnline()) {
-                applySwimmingState(player, true);
-            }
-        });
+        event.setCancelled(true);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/tf/tuff/tuffactions/swimming/Swimming.java` around lines 79 -
90, The handleToggleSwim method presently lets EntityToggleSwimEvent run then
re-applies swimming on the next tick causing a flicker; change the logic so when
the event is coming from a Player and you detect that swimming should remain
true (i.e., !event.isSwimming() &&
swimmingPlayers.contains(player.getUniqueId())), call event.setCancelled(true)
on that EntityToggleSwimEvent to prevent the state flip and then ensure you call
applySwimmingState(player, true) (immediately or via the existing scheduler if
needed) so the player stays in the expected swimming state without the one-tick
false window; update references in handleToggleSwim and keep applySwimmingState
usage consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/tf/tuff/netty/ChunkHandler.java`:
- Around line 172-202: The leak is caused by double-retaining the ByteBuf:
resolveViaDataOnMainThread retains buf and then writeWithViaOnly retains it
again for the composite; remove the extra retain in writeWithViaOnly so the
composite takes the caller's ownership, and update resolveViaDataOnMainThread so
you do not release the retained buffer in the finally block when you call
writeWithViaOnly (ensure only the code path that does currentCtx.write(...)
releases the retained buffer). Touch the methods resolveViaDataOnMainThread and
writeWithViaOnly to transfer ownership consistently (caller retains once, callee
must not retain) and ensure all other paths still release appropriately.

In `@src/main/java/tf/tuff/tuffactions/swimming/Swimming.java`:
- Around line 40-46: The enable method's null check on swimStateTask can miss
cases where the Task object exists but the scheduled task was cancelled; update
the scheduling guard in enable(...) to schedule a new task when swimStateTask is
null OR swimStateTask.isCancelled() is true, using the same call to
plugin.getServer().getScheduler().runTaskTimer(...) to assign swimStateTask;
reference swimStateTask, enable, maintainSwimmingState and the scheduler
runTaskTimer call when making the change.

In `@src/main/java/tf/tuff/viablocks/ViaBlocksPlugin.java`:
- Around line 82-83: Reload currently clears the viaBlocksEnabledPlayers set in
onTuffXReload which causes online players to lose ViaBlocks-enabled state and
stop receiving chunk/block packets until they re-handshake; instead preserve or
rebuild enabled-player state across reloads by iterating online players after
reload and repopulating viaBlocksEnabledPlayers (or trigger a re-handshake) for
those where CustomBlockListener.hasViaBlocksPlayersInWorld(...) /
plugin.isPlayerEnabled(...) previously returned true; update onTuffXReload to
either restore the set from a persisted snapshot or call the same logic used in
handlePacket -> setPlayerEnabled(player, true) for each online player so
CustomBlockListener.handleChunkLoad continues to prepare caches without
requiring clients to resend viablocks:handshake.

---

Nitpick comments:
In `@src/main/java/tf/tuff/netty/ChunkHandler.java`:
- Line 160: Remove the redundant call to decodeSectionCoordX(chunkSectionPos) —
it returns a value that is discarded and the coordinates are already decoded
inside decodeMultiBlockChangePosition; delete the standalone call and any
comment that implies validation occurs there, leaving
decodeMultiBlockChangePosition to perform the decoding/validation for
chunkSectionPos.

In `@src/main/java/tf/tuff/tuffactions/swimming/Swimming.java`:
- Around line 127-142: The loop in maintainSwimmingState removes from
swimmingPlayers directly while iterating, which can skip elements; change it to
use an explicit Iterator over swimmingPlayers (Iterator<UUID> it =
swimmingPlayers.iterator()) and call it.remove() when you need to remove the
current UUID; keep the same logic calling Bukkit.getPlayer(playerId),
applySwimmingState(player, false/true) and broadcastSwimState(player, false) but
ensure removals use iterator.remove() so iteration is predictable.
- Around line 79-90: The handleToggleSwim method presently lets
EntityToggleSwimEvent run then re-applies swimming on the next tick causing a
flicker; change the logic so when the event is coming from a Player and you
detect that swimming should remain true (i.e., !event.isSwimming() &&
swimmingPlayers.contains(player.getUniqueId())), call event.setCancelled(true)
on that EntityToggleSwimEvent to prevent the state flip and then ensure you call
applySwimmingState(player, true) (immediately or via the existing scheduler if
needed) so the player stays in the expected swimming state without the one-tick
false window; update references in handleToggleSwim and keep applySwimmingState
usage consistent.

In `@src/main/java/tf/tuff/TuffX.java`:
- Around line 93-105: The onDisable method currently unregisters incoming plugin
channels but misses outgoing channels; update onDisable in TuffX.java (method
onDisable) to also call
getServer().getMessenger().unregisterOutgoingPluginChannel(this) (in addition to
the existing unregisterIncomingPluginChannel(this)) so any outgoing channels
registered by sub-plugins (e.g., ViaEntitiesPlugin) are cleaned up; ensure this
call is placed alongside the existing messenger cleanup and executed before
returning from onDisable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 80cf6668-62b1-44ac-b6d7-b087a2f792bc

📥 Commits

Reviewing files that changed from the base of the PR and between 65120c3 and 19197db.

⛔ Files ignored due to path filters (1)
  • builds/TuffXPlus-1.0.1-beta.jar is excluded by !**/*.jar
📒 Files selected for processing (8)
  • build.gradle
  • src/main/java/tf/tuff/TuffX.java
  • src/main/java/tf/tuff/netty/ChunkHandler.java
  • src/main/java/tf/tuff/tuffactions/swimming/Swimming.java
  • src/main/java/tf/tuff/viablocks/CustomBlockListener.java
  • src/main/java/tf/tuff/viablocks/ViaBlocksPlugin.java
  • src/main/java/tf/tuff/y0/Y0Plugin.java
  • src/test/java/tf/tuff/netty/ChunkHandlerTest.java

Comment on lines 172 to 202
private void resolveViaDataOnMainThread(ChannelHandlerContext ctx, ByteBuf buf, ChannelPromise promise,
java.util.concurrent.Callable<byte[]> supplier) {
ByteBuf retained = buf.retain();
Bukkit.getScheduler().runTask(viaBlocks.plugin.plugin, () -> {
byte[] data = null;
try {
if (player.isOnline() && isViaActive()) {
data = supplier.call();
}
} catch (Exception ignored) {
}
final byte[] resolvedData = data;

ChannelHandlerContext currentCtx = this.ctx != null ? this.ctx : ctx;
currentCtx.channel().eventLoop().execute(() -> {
try {
retained.resetReaderIndex();
if (resolvedData != null && resolvedData.length > 0) {
writeWithViaOnly(currentCtx, retained, promise, resolvedData);
} else {
currentCtx.write(retained, promise);
}
} catch (Exception ignored) {
} finally {
if (retained.refCnt() > 0) {
retained.release();
}
}
});
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Memory leak in writeWithViaOnly path.

When resolveViaDataOnMainThread takes the writeWithViaOnly path, there's one unreleased buffer reference:

  1. buf starts with refCnt=1
  2. Line 174: buf.retain()refCnt=2
  3. Line 314 (writeWithViaOnly): buf.retain()refCnt=3, composite takes ownership of this reference
  4. Line 197: retained.release()refCnt=2
  5. After ctx.write(composite) completes, composite releases its component → refCnt=1

The original reference is never released, causing a memory leak.

In writeWithViaOnly, you're retaining the buffer to add it to the composite, but the caller (resolveViaDataOnMainThread) also holds a retained reference. You should not retain again in writeWithViaOnly since the caller is passing ownership of its retained reference.

Proposed fix
 private void writeWithViaOnly(ChannelHandlerContext ctx, ByteBuf buf, ChannelPromise promise,
                               byte[] data) throws Exception {
     ByteBuf tail = ctx.alloc().buffer();
     tail.writeBytes(data);

     CompositeByteBuf composite = ctx.alloc().compositeBuffer();
-    composite.addComponents(true, buf.retain(), tail);
+    composite.addComponents(true, buf, tail);

     ctx.write(composite, promise);
 }

And adjust resolveViaDataOnMainThread to not release when writing via writeWithViaOnly (since composite takes ownership), or remove the finally release entirely and let the write operations handle it:

             try {
                 retained.resetReaderIndex();
                 if (resolvedData != null && resolvedData.length > 0) {
                     writeWithViaOnly(currentCtx, retained, promise, resolvedData);
                 } else {
                     currentCtx.write(retained, promise);
                 }
             } catch (Exception ignored) {
-            } finally {
-                if (retained.refCnt() > 0) {
-                    retained.release();
-                }
+            } catch (Exception e) {
+                if (retained.refCnt() > 0) {
+                    retained.release();
+                }
             }

Also applies to: 308-317

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/tf/tuff/netty/ChunkHandler.java` around lines 172 - 202, The
leak is caused by double-retaining the ByteBuf: resolveViaDataOnMainThread
retains buf and then writeWithViaOnly retains it again for the composite; remove
the extra retain in writeWithViaOnly so the composite takes the caller's
ownership, and update resolveViaDataOnMainThread so you do not release the
retained buffer in the finally block when you call writeWithViaOnly (ensure only
the code path that does currentCtx.write(...) releases the retained buffer).
Touch the methods resolveViaDataOnMainThread and writeWithViaOnly to transfer
ownership consistently (caller retains once, callee must not retain) and ensure
all other paths still release appropriately.

Comment thread src/main/java/tf/tuff/tuffactions/swimming/Swimming.java Outdated
Comment thread src/main/java/tf/tuff/viablocks/ViaBlocksPlugin.java
@llucasandersen

Copy link
Copy Markdown
Contributor Author

Fixed the current review items on the branch: the ChunkHandler Via-only path now uses consistent buffer ownership, TuffX unregisters outgoing plugin channels on disable, ViaBlocks preserves enabled players across reload, and Swimming now cancels the false toggle while cleaning up iteration/removal behavior.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/tf/tuff/y0/Y0Plugin.java (1)

425-436: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent stale cross-world chunk payload delivery.

Line 425 currently doesn’t verify that player is still in c.getWorld(). If the player changes worlds before this call runs, old-world chunk payloads can still be sent.

Suggested fix
     public void processAndSendChunk(final Player player, final Chunk c) {
         if (c == null || player == null || !player.isOnline()) return;
+        if (!player.getWorld().equals(c.getWorld())) return;
 
         if (enabledWorlds != null && !enabledWorlds.contains(c.getWorld().getName())) return;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/tf/tuff/y0/Y0Plugin.java` around lines 425 - 436, The
processAndSendChunk method checks if the player is online at the start but does
not verify the player remains in the chunk's world before sending cached chunk
payloads. If a player switches worlds between the initial validation and the
actual packet sending, stale chunk data from the old world could be delivered.
Add an additional check right before the loop that sends payloads (where
SchedulerCompat.sendPluginMessage is called) to verify that player.getWorld()
equals c.getWorld() to ensure the player is still in the correct world, and only
proceed with sending the chunk data if this condition is true.
🧹 Nitpick comments (2)
src/main/java/tf/tuff/tuffactions/swimming/Swimming.java (1)

43-46: 💤 Low value

Empty override can be removed.

This method only delegates to super.enable(wasEnabled) without adding any behavior. Unless it's intentionally kept as a placeholder for future functionality, it can be removed.

Note: The past review comment about checking isCancelled() no longer applies since the global swimStateTask was replaced with per-player tasks in swimStateTasks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/tf/tuff/tuffactions/swimming/Swimming.java` around lines 43 -
46, The enable method in the Swimming class is an empty override that only
delegates to the parent class implementation without adding any custom behavior.
Remove the entire enable method override from the Swimming class, as the parent
class implementation will be invoked automatically when the method is not
overridden.
src/main/java/tf/tuff/util/SchedulerCompat.java (1)

75-82: 💤 Low value

Missing null check for channel parameter.

The sendPluginMessage method validates player and payload for null but not channel. If a caller passes a null channel, player.sendPluginMessage will throw an NPE inside the scheduled task.

🛡️ Suggested fix
 public static void sendPluginMessage(Plugin plugin, Player player, String channel, byte[] payload) {
-    if (player == null || payload == null || !player.isOnline()) return;
+    if (player == null || channel == null || payload == null || !player.isOnline()) return;
     runEntity(player, plugin, () -> {
         if (player.isOnline()) {
             player.sendPluginMessage(plugin, channel, payload);
         }
     });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/tf/tuff/util/SchedulerCompat.java` around lines 75 - 82, The
sendPluginMessage method validates player and payload parameters for null in the
initial guard clause but is missing a null check for the channel parameter. Add
channel to the existing null validation in the early return statement (alongside
the existing checks for player and payload) to prevent a potential NPE when
player.sendPluginMessage is called inside the runEntity task.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/tf/tuff/ServerRegistry.java`:
- Around line 24-25: The `running` field in the ServerRegistry class is accessed
from multiple async contexts (in the async reconnect paths at lines 24, 45, and
57) and mutated in the `disconnect()` method on different threads, but it is not
declared as volatile. This allows the field's value to become stale across
threads, potentially permitting reconnect attempts after shutdown. Declare the
`running` field as volatile to ensure thread-safe visibility of all mutations
across the async reconnect code paths and the disconnect method.

In `@src/main/java/tf/tuff/tuffactions/swimming/Swimming.java`:
- Around line 133-145: The no-argument maintainSwimmingState() method is unused
and can be safely removed. Delete the entire method definition (including the
method signature, iterator loop, and all logic within it) since all swimming
state maintenance is handled by the overloaded maintainSwimmingState(Player)
variant that is called within the SchedulerCompat.runEntity task in the
per-player maintenance logic.

In `@src/main/java/tf/tuff/y0/Y0Plugin.java`:
- Around line 847-860: In the sendToNearbyPlayers method, the current
implementation schedules an entity task for every single online player, then
performs distance/world/online checks inside those tasks. Move the distance and
world filtering checks into the global SchedulerCompat.runGlobal task (before
the inner entity scheduling loop) to filter out players that are not nearby or
in the same world without creating unnecessary scheduled tasks. Additionally,
add a readiness check before scheduling each per-player entity task to avoid
scheduling tasks for players that are not ready to receive plugin messages,
thereby reducing scheduler overhead and network churn during frequent
block/light updates.

---

Outside diff comments:
In `@src/main/java/tf/tuff/y0/Y0Plugin.java`:
- Around line 425-436: The processAndSendChunk method checks if the player is
online at the start but does not verify the player remains in the chunk's world
before sending cached chunk payloads. If a player switches worlds between the
initial validation and the actual packet sending, stale chunk data from the old
world could be delivered. Add an additional check right before the loop that
sends payloads (where SchedulerCompat.sendPluginMessage is called) to verify
that player.getWorld() equals c.getWorld() to ensure the player is still in the
correct world, and only proceed with sending the chunk data if this condition is
true.

---

Nitpick comments:
In `@src/main/java/tf/tuff/tuffactions/swimming/Swimming.java`:
- Around line 43-46: The enable method in the Swimming class is an empty
override that only delegates to the parent class implementation without adding
any custom behavior. Remove the entire enable method override from the Swimming
class, as the parent class implementation will be invoked automatically when the
method is not overridden.

In `@src/main/java/tf/tuff/util/SchedulerCompat.java`:
- Around line 75-82: The sendPluginMessage method validates player and payload
parameters for null in the initial guard clause but is missing a null check for
the channel parameter. Add channel to the existing null validation in the early
return statement (alongside the existing checks for player and payload) to
prevent a potential NPE when player.sendPluginMessage is called inside the
runEntity task.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f8367aec-3a04-465c-8bf9-cd3da05302a0

📥 Commits

Reviewing files that changed from the base of the PR and between 19197db and 58fe1b5.

⛔ Files ignored due to path filters (1)
  • builds/TuffXPlus-1.0.1-beta.jar is excluded by !**/*.jar
📒 Files selected for processing (15)
  • src/main/java/tf/tuff/ServerRegistry.java
  • src/main/java/tf/tuff/TuffX.java
  • src/main/java/tf/tuff/netty/ChunkHandler.java
  • src/main/java/tf/tuff/tuffactions/creative/CreativeMenu.java
  • src/main/java/tf/tuff/tuffactions/swimming/Swimming.java
  • src/main/java/tf/tuff/util/SchedulerCompat.java
  • src/main/java/tf/tuff/viablocks/CustomBlockListener.java
  • src/main/java/tf/tuff/viablocks/PaletteManager.java
  • src/main/java/tf/tuff/viablocks/ViaBlocksPlugin.java
  • src/main/java/tf/tuff/viaentities/EntityDataHandler.java
  • src/main/java/tf/tuff/viaentities/EntityInjector.java
  • src/main/java/tf/tuff/viaentities/ViaEntitiesPlugin.java
  • src/main/java/tf/tuff/y0/ChunkPacketListener.java
  • src/main/java/tf/tuff/y0/ViaBlockIds.java
  • src/main/java/tf/tuff/y0/Y0Plugin.java
✅ Files skipped from review due to trivial changes (1)
  • src/main/java/tf/tuff/y0/ViaBlockIds.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/main/java/tf/tuff/TuffX.java
  • src/main/java/tf/tuff/viablocks/ViaBlocksPlugin.java
  • src/main/java/tf/tuff/netty/ChunkHandler.java

Comment thread src/main/java/tf/tuff/ServerRegistry.java
Comment thread src/main/java/tf/tuff/tuffactions/swimming/Swimming.java Outdated
Comment thread src/main/java/tf/tuff/y0/Y0Plugin.java
Comment thread src/main/java/tf/tuff/netty/ChunkHandler.java Outdated
Comment thread src/main/java/tf/tuff/tuffactions/swimming/Swimming.java Outdated
Comment thread src/main/java/tf/tuff/viablocks/ViaBlocksPlugin.java Outdated
Comment thread src/main/java/tf/tuff/ServerRegistry.java Outdated
@llucasandersen

Copy link
Copy Markdown
Contributor Author

Fixed the remaining review items, tightened the CI test bootstrap for MockBukkit and missing ViaVersion state, cleaned up the Y0 and scheduler guards, and ./gradlew test build passes on this branch now.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/tf/tuff/tuffactions/swimming/Swimming.java (1)

101-123: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move viewer-side swim updates onto the viewer scheduler.

broadcastSwimState() is reached from handleSwimState(), handlePlayerQuit(), and the maintenance timer, but the recipient.canSee(subject) check and actsPlugin.sendPluginMessage(...) still run on that caller's thread. In this Folia-focused PR, that means swimming updates can still touch arbitrary viewers from the wrong execution context even though the rest of the stack was migrated to SchedulerCompat. Please route each recipient branch through SchedulerCompat.runEntity(...) / SchedulerCompat.sendPluginMessage(...) so the visibility check and plugin-message send both happen on the recipient's scheduler.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/tf/tuff/tuffactions/swimming/Swimming.java` around lines 101 -
123, The recipient visibility check and plugin message sending in
broadcastSwimState() and sendSwimState() currently execute on the caller's
thread, which is incompatible with Folia's scheduler requirements. Wrap the
visibility check (recipient.canSee(subject)) and the sendSwimState() call within
broadcastSwimState() using SchedulerCompat.runEntity() for each recipient
player, ensuring both the visibility determination and the plugin message
transmission in sendSwimState() happen on the recipient's scheduler context
rather than the caller's thread.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/tf/tuff/TuffX.java`:
- Around line 68-70: The issue is that PacketEvents.getAPI() is called without
null validation in the onEnable method at line 68, and the packetEventsEnabled
flag is only reset conditionally inside a null check in the onDisable method at
line 125. This creates a race condition where if PacketEvents.getAPI() becomes
null between cycles, the flag won't reset, causing a potential NPE on the next
enable cycle. Add a defensive null check for PacketEvents.getAPI() before
calling init() in the onEnable method around line 68-70. Additionally, move the
packetEventsEnabled flag reset statement outside of the null check conditional
in the onDisable method around line 125, so the flag is always reset regardless
of whether the API is null.

---

Outside diff comments:
In `@src/main/java/tf/tuff/tuffactions/swimming/Swimming.java`:
- Around line 101-123: The recipient visibility check and plugin message sending
in broadcastSwimState() and sendSwimState() currently execute on the caller's
thread, which is incompatible with Folia's scheduler requirements. Wrap the
visibility check (recipient.canSee(subject)) and the sendSwimState() call within
broadcastSwimState() using SchedulerCompat.runEntity() for each recipient
player, ensuring both the visibility determination and the plugin message
transmission in sendSwimState() happen on the recipient's scheduler context
rather than the caller's thread.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4101cc4d-55d9-4684-8264-56b328a9af64

📥 Commits

Reviewing files that changed from the base of the PR and between 4884c25 and 0b6eaba.

📒 Files selected for processing (10)
  • src/main/java/tf/tuff/ServerRegistry.java
  • src/main/java/tf/tuff/TuffX.java
  • src/main/java/tf/tuff/netty/ChunkHandler.java
  • src/main/java/tf/tuff/tuffactions/TuffActions.java
  • src/main/java/tf/tuff/tuffactions/swimming/Swimming.java
  • src/main/java/tf/tuff/util/SchedulerCompat.java
  • src/main/java/tf/tuff/viablocks/ViaBlocksPlugin.java
  • src/main/java/tf/tuff/y0/ViaBlockIds.java
  • src/main/java/tf/tuff/y0/Y0Plugin.java
  • src/test/java/tf/tuff/TuffXTest.java
💤 Files with no reviewable changes (1)
  • src/main/java/tf/tuff/viablocks/ViaBlocksPlugin.java
✅ Files skipped from review due to trivial changes (1)
  • src/test/java/tf/tuff/TuffXTest.java
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/java/tf/tuff/y0/ViaBlockIds.java
  • src/main/java/tf/tuff/util/SchedulerCompat.java
  • src/main/java/tf/tuff/netty/ChunkHandler.java
  • src/main/java/tf/tuff/y0/Y0Plugin.java

Comment thread src/main/java/tf/tuff/TuffX.java Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Thread failed main thread check: Cannot read world asynchronously [Bug] Viablocks under y0 appear exploded by wind burst Viablocks Swimming

2 participants