diff --git a/build.gradle b/build.gradle index 22db1c1..cda6a81 100644 --- a/build.gradle +++ b/build.gradle @@ -46,13 +46,15 @@ dependencies { annotationProcessor 'org.projectlombok:lombok:1.18.30' // Tests - testImplementation 'com.viaversion:viaversion:5.9.1' - testImplementation 'com.viaversion:viabackwards:5.3.2' - testImplementation 'io.netty:netty-all:4.1.97.Final' + testImplementation 'com.github.seeseemelk:MockBukkit-v1.18:2.85.2' testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.11.0' testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.0' - testImplementation 'com.github.seeseemelk:MockBukkit-v1.18:2.85.2' + testRuntimeOnly 'org.spigotmc:spigot-api:1.18.2-R0.1-SNAPSHOT' + testRuntimeOnly 'io.netty:netty-all:4.1.97.Final' + testRuntimeOnly 'it.unimi.dsi:fastutil:8.5.16' + testRuntimeOnly 'com.viaversion:viaversion:5.9.1' + testRuntimeOnly 'com.viaversion:viabackwards:5.3.2' } processResources { @@ -96,4 +98,4 @@ test { events "passed", "skipped", "failed" exceptionFormat = "full" } -} \ No newline at end of file +} diff --git a/builds/TuffXPlus-1.0.1-beta.jar b/builds/TuffXPlus-1.0.1-beta.jar index 03c29f9..bf8606b 100644 Binary files a/builds/TuffXPlus-1.0.1-beta.jar and b/builds/TuffXPlus-1.0.1-beta.jar differ diff --git a/src/main/java/tf/tuff/ServerRegistry.java b/src/main/java/tf/tuff/ServerRegistry.java index a561a59..91b841b 100644 --- a/src/main/java/tf/tuff/ServerRegistry.java +++ b/src/main/java/tf/tuff/ServerRegistry.java @@ -5,12 +5,14 @@ import org.java_websocket.handshake.ServerHandshake; import java.net.URI; +import tf.tuff.util.SchedulerCompat; + public class ServerRegistry { private final JavaPlugin p; private final String wsUrl; private final String server; private WebSocketClient client; - private boolean running = true; + private volatile boolean running = true; public ServerRegistry(JavaPlugin pl, String registryUrl, String serverAddr) { p = pl; @@ -19,7 +21,7 @@ public ServerRegistry(JavaPlugin pl, String registryUrl, String serverAddr) { } public void connect() { - p.getServer().getScheduler().runTaskAsynchronously(p, this::doConnect); + SchedulerCompat.runAsync(p, this::doConnect); } private void doConnect() { @@ -40,7 +42,7 @@ public void onMessage(String msg) { @Override public void onClose(int code, String reason, boolean remote) { if (running) { - p.getServer().getScheduler().runTaskLaterAsynchronously(p, () -> doConnect(), 100L); + SchedulerCompat.runAsyncLater(p, ServerRegistry.this::doConnect, 100L); } } @@ -52,7 +54,7 @@ public void onError(Exception e) { client.connect(); } catch (Exception e) { if (running) { - p.getServer().getScheduler().runTaskLaterAsynchronously(p, () -> doConnect(), 100L); + SchedulerCompat.runAsyncLater(p, this::doConnect, 100L); } } } diff --git a/src/main/java/tf/tuff/TuffX.java b/src/main/java/tf/tuff/TuffX.java index 8f59f2e..7a23231 100644 --- a/src/main/java/tf/tuff/TuffX.java +++ b/src/main/java/tf/tuff/TuffX.java @@ -26,6 +26,7 @@ import tf.tuff.netty.ChunkInjector; import tf.tuff.tuffactions.TuffActions; +import tf.tuff.util.SchedulerCompat; import tf.tuff.viablocks.ViaBlocksPlugin; import tf.tuff.viaentities.ViaEntitiesPlugin; import tf.tuff.y0.Y0Plugin; @@ -39,11 +40,13 @@ public class TuffX extends JavaPlugin implements Listener, PluginMessageListener public TuffActions tuffActions; public ViaEntitiesPlugin viaEntitiesPlugin; private ChunkInjector chunkInjector; + private boolean packetEventsEnabled; // required by MockBukkit public TuffX(JavaPluginLoader loader, PluginDescriptionFile description, File dataFolder, File file) { super(loader, description, dataFolder, file); } + public TuffX() { super(); } @Override public void onLoad() { @@ -52,18 +55,29 @@ public void onLoad() { this.tuffActions = new TuffActions(this); this.viaEntitiesPlugin = new ViaEntitiesPlugin(this); - PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this)); - PacketEvents.getAPI().getSettings().reEncodeByDefault(false) - .checkForUpdates(false); - PacketEvents.getAPI().load(); + if (shouldBootstrapPacketEvents()) { + PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this)); + PacketEvents.getAPI().getSettings().reEncodeByDefault(false) + .checkForUpdates(false); + PacketEvents.getAPI().load(); + packetEventsEnabled = true; + } } @Override public void onEnable() { - PacketEvents.getAPI().init(); + if (packetEventsEnabled && PacketEvents.getAPI() != null) { + PacketEvents.getAPI().init(); + } else { + packetEventsEnabled = false; + } saveDefaultConfig(); + getLogger().info(SchedulerCompat.isFolia() + ? "Folia detected. Using region and entity schedulers." + : "Using standard Bukkit-compatible schedulers."); + y0Plugin.onTuffXEnable(); tuffActions.onTuffXEnable(); viaBlocksPlugin.onTuffXEnable(); @@ -76,9 +90,11 @@ public void onEnable() { getConfig().options().copyDefaults(true); saveConfig(); - PacketEvents.getAPI().getEventManager().registerListener( - new NetworkListener(this), PacketListenerPriority.NORMAL - ); + if (packetEventsEnabled) { + PacketEvents.getAPI().getEventManager().registerListener( + new NetworkListener(this), PacketListenerPriority.NORMAL + ); + } getServer().getPluginManager().registerEvents(this, this); @@ -109,12 +125,20 @@ public void onDisable() { serverRegistry = null; } - PacketEvents.getAPI().terminate(); + if (packetEventsEnabled && PacketEvents.getAPI() != null) { + PacketEvents.getAPI().terminate(); + } + packetEventsEnabled = false; getServer().getMessenger().unregisterIncomingPluginChannel(this); getServer().getMessenger().unregisterOutgoingPluginChannel(this); } + private boolean shouldBootstrapPacketEvents() { + return getServer() == null + || !getServer().getClass().getName().startsWith("be.seeseemelk.mockbukkit"); + } + public void reloadTuffX(){ saveDefaultConfig(); reloadConfig(); diff --git a/src/main/java/tf/tuff/netty/ChunkHandler.java b/src/main/java/tf/tuff/netty/ChunkHandler.java index c10003b..062e290 100644 --- a/src/main/java/tf/tuff/netty/ChunkHandler.java +++ b/src/main/java/tf/tuff/netty/ChunkHandler.java @@ -10,6 +10,7 @@ import org.bukkit.entity.Player; import tf.tuff.viablocks.CustomBlockListener; import tf.tuff.y0.Y0Plugin; +import tf.tuff.util.SchedulerCompat; import java.util.Map; import java.util.UUID; @@ -27,6 +28,8 @@ public class ChunkHandler extends ChannelOutboundHandlerAdapter { private static final long TIMEOUT_MS = 500; + static record BlockChangePosition(int x, int y, int z) {} + public ChunkHandler(CustomBlockListener viaBlocks, Y0Plugin y0, Player player) { this.viaBlocks = viaBlocks; this.y0 = y0; @@ -77,12 +80,12 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) return; } - if (packetId == 0x0B && viaBlocks != null) { + if (packetId == 0x0B && isViaActive()) { handleBlockChange(ctx, buf, promise); return; } - if (packetId == 0x10 && viaBlocks != null) { + if (packetId == 0x10 && isViaActive()) { handleMultiBlockChange(ctx, buf, promise); return; } @@ -97,18 +100,24 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) super.write(ctx, msg, promise); } + private boolean isViaActive() { + return viaBlocks != null + && viaBlocks.plugin.isEnabled() + && viaBlocks.plugin.isPlayerEnabled(player); + } + private void handleChunkPacket(ChannelHandlerContext ctx, ByteBuf buf, ChannelPromise promise) throws Exception { int chunkX = buf.readInt(); int chunkZ = buf.readInt(); buf.resetReaderIndex(); - byte[] viaData = viaBlocks != null ? viaBlocks.getExtraDataForChunk(player.getWorld().getName(), chunkX, chunkZ) : null; + boolean viaActive = isViaActive(); + byte[] viaData = viaActive ? viaBlocks.getExtraDataForChunk(player.getWorld().getName(), chunkX, chunkZ) : null; byte[] y0Data = y0 != null ? y0.getY0DataForChunk(player, chunkX, chunkZ) : null; - boolean needVia = viaBlocks != null; boolean needY0 = y0 != null && y0.isPlayerReady(player); - boolean viaReady = !needVia || viaData != null; + boolean viaReady = !viaActive || viaData != null; boolean y0Ready = !needY0 || y0Data != null; if (viaReady && y0Ready) { @@ -135,67 +144,68 @@ private void handleChunkPacket(ChannelHandlerContext ctx, ByteBuf buf, ChannelPr } private void handleBlockChange(ChannelHandlerContext ctx, ByteBuf buf, ChannelPromise promise) throws Exception { - int idx = buf.readerIndex(); - long val = buf.getLong(idx); - int x = (int) (val >> 38); - int z = (int) ((val >> 12) & 0x3FFFFFF); - int y = (int) (val & 0xFFF); - if (x >= 0x2000000) x -= 0x4000000; - if (z >= 0x2000000) z -= 0x4000000; - if (y >= 0x800) y -= 0x1000; - - World world = player.getWorld(); - - // end this call if the chunk called upon is not loaded - // it seems like it should never happen, but it does, and it causes crashes. - if (!world.isChunkLoaded(x >> 4, z >> 4)) { - buf.resetReaderIndex(); - super.write(ctx, buf, promise); - return; - } - - byte[] data = viaBlocks.getExtraDataForSingleBlock(world, x, y, z); - if (data != null && data.length > 0) { - buf.resetReaderIndex(); - writeWithViaOnly(ctx, buf, promise, data); - return; - } - buf.resetReaderIndex(); - super.write(ctx, buf, promise); + BlockChangePosition position = decodeSingleBlockChangePosition(buf.getLong(buf.readerIndex())); + resolveViaDataOnRegionThread(ctx, buf, promise, player.getWorld(), position.x >> 4, position.z >> 4, () -> { + World world = player.getWorld(); + if (!world.isChunkLoaded(position.x >> 4, position.z >> 4)) { + return null; + } + return viaBlocks.getExtraDataForSingleBlock(world, position.x, position.y, position.z); + }); } private void handleMultiBlockChange(ChannelHandlerContext ctx, ByteBuf buf, ChannelPromise promise) throws Exception { buf.resetReaderIndex(); buf.skipBytes(varIntLen(buf)); long chunkSectionPos = buf.readLong(); - int cx = (int)(chunkSectionPos >> 42); - int cz = (int)((chunkSectionPos << 44) >> 44); - - if (Math.abs(cx) < 2000000 && Math.abs(cz) < 2000000) { - buf.readBoolean(); - int count = readVarInt(buf); - java.util.List locs = new java.util.ArrayList<>(); - for (int i = 0; i < count; i++) { - short h = buf.readUnsignedByte(); - int by = buf.readUnsignedByte(); - readVarInt(buf); - int bx = (h >> 4 & 15) + (cx * 16); - int bz = (h & 15) + (cz * 16); - locs.add(viaBlocks.packLocation(bx, by, bz)); - } - byte[] data = viaBlocks.getExtraDataForMultiBlock(player.getWorld(), locs); - if (data != null && data.length > 0) { - buf.resetReaderIndex(); - writeWithViaOnly(ctx, buf, promise, data); - return; - } + int cx = decodeSectionCoordX(chunkSectionPos); + int cz = decodeSectionCoordZ(chunkSectionPos); + buf.readBoolean(); + int count = readVarInt(buf); + java.util.List locs = new java.util.ArrayList<>(count); + for (int i = 0; i < count; i++) { + BlockChangePosition position = decodeMultiBlockChangePosition(chunkSectionPos, readVarLong(buf)); + locs.add(viaBlocks.packLocation(position.x, position.y, position.z)); } - buf.resetReaderIndex(); - super.write(ctx, buf, promise); + + resolveViaDataOnRegionThread(ctx, buf, promise, player.getWorld(), cx, cz, () -> viaBlocks.getExtraDataForMultiBlock(player.getWorld(), locs)); + } + + private void resolveViaDataOnRegionThread(ChannelHandlerContext ctx, ByteBuf buf, ChannelPromise promise, + World world, int chunkX, int chunkZ, + java.util.concurrent.Callable supplier) { + ByteBuf retained = buf.retain(); + SchedulerCompat.runRegion(viaBlocks.plugin.plugin, world, chunkX, chunkZ, () -> { + 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(); + } + } + }); + }); } private void requestViaCache(int cx, int cz, long key) { - Bukkit.getScheduler().runTask(viaBlocks.plugin.plugin, () -> { + SchedulerCompat.runRegion(viaBlocks.plugin.plugin, player.getWorld(), cx, cz, () -> { if (!player.isOnline()) { release(key); return; @@ -212,7 +222,7 @@ private void requestViaCache(int cx, int cz, long key) { } private void requestY0Cache(int cx, int cz, long key) { - Bukkit.getScheduler().runTask(viaBlocks.plugin.plugin, () -> { + SchedulerCompat.runRegion(viaBlocks.plugin.plugin, player.getWorld(), cx, cz, () -> { if (!player.isOnline()) { release(key); return; @@ -304,7 +314,7 @@ private void writeWithViaOnly(ChannelHandlerContext ctx, ByteBuf buf, ChannelPro tail.writeBytes(data); CompositeByteBuf composite = ctx.alloc().compositeBuffer(); - composite.addComponents(true, buf.retain(), tail); + composite.addComponents(true, buf, tail); ctx.write(composite, promise); } @@ -325,6 +335,58 @@ private int readVarInt(ByteBuf buf) { return r; } + private long readVarLong(ByteBuf buf) { + long value = 0L; + int position = 0; + byte currentByte; + do { + currentByte = buf.readByte(); + value |= (long) (currentByte & 0x7F) << position; + position += 7; + if (position > 70) { + throw new RuntimeException("VarLong too big"); + } + } while ((currentByte & 0x80) != 0); + return value; + } + + static BlockChangePosition decodeSingleBlockChangePosition(long value) { + int x = decodeSigned((int) (value >> 38), 26); + int z = decodeSigned((int) ((value >> 12) & 0x3FFFFFFL), 26); + int y = decodeSigned((int) (value & 0xFFFL), 12); + return new BlockChangePosition(x, y, z); + } + + static BlockChangePosition decodeMultiBlockChangePosition(long sectionPosition, long entry) { + int sectionX = decodeSectionCoordX(sectionPosition); + int sectionY = decodeSectionCoordY(sectionPosition); + int sectionZ = decodeSectionCoordZ(sectionPosition); + int localPosition = (int) (entry & 0xFFFL); + int x = (sectionX << 4) | ((localPosition >> 8) & 0xF); + int z = (sectionZ << 4) | ((localPosition >> 4) & 0xF); + int y = (sectionY << 4) | (localPosition & 0xF); + return new BlockChangePosition(x, y, z); + } + + private static int decodeSectionCoordX(long sectionPosition) { + return decodeSigned((int) (sectionPosition >> 42), 22); + } + + private static int decodeSectionCoordY(long sectionPosition) { + return decodeSigned((int) (sectionPosition & 0xFFFFFL), 20); + } + + private static int decodeSectionCoordZ(long sectionPosition) { + return decodeSigned((int) ((sectionPosition >> 20) & 0x3FFFFFL), 22); + } + + private static int decodeSigned(int value, int bits) { + int signBit = 1 << (bits - 1); + int fullMask = (1 << bits) - 1; + value &= fullMask; + return (value ^ signBit) - signBit; + } + private int varIntLen(ByteBuf buf) { int s = buf.readerIndex(); readVarInt(buf); diff --git a/src/main/java/tf/tuff/tuffactions/TuffActions.java b/src/main/java/tf/tuff/tuffactions/TuffActions.java index f2564c2..6955dc3 100644 --- a/src/main/java/tf/tuff/tuffactions/TuffActions.java +++ b/src/main/java/tf/tuff/tuffactions/TuffActions.java @@ -129,7 +129,7 @@ public void sendPluginMessage(Player player, byte[] payload) { } public void sendPluginMessage(Player player, String channel, byte[] payload) { - if (player == null || payload == null || !player.isOnline() || !PacketEvents.getAPI().isInitialized()) return; + if (player == null || payload == null || !player.isOnline() || PacketEvents.getAPI() == null || !PacketEvents.getAPI().isInitialized()) return; WrapperPlayServerPluginMessage packet = new WrapperPlayServerPluginMessage(channel, payload); PacketEvents.getAPI().getPlayerManager().sendPacket(player, packet); } diff --git a/src/main/java/tf/tuff/tuffactions/creative/CreativeMenu.java b/src/main/java/tf/tuff/tuffactions/creative/CreativeMenu.java index 665398b..0a79488 100644 --- a/src/main/java/tf/tuff/tuffactions/creative/CreativeMenu.java +++ b/src/main/java/tf/tuff/tuffactions/creative/CreativeMenu.java @@ -19,6 +19,8 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import tf.tuff.util.SchedulerCompat; + public class CreativeMenu extends TuffActionBase { private final Set itemMapping = ConcurrentHashMap.newKeySet(); private final Map playerHoldingPlaceholder = new ConcurrentHashMap<>(); @@ -99,7 +101,7 @@ public void onPlayerInventoryClick(InventoryClickEvent event) { InventoryAction action = event.getAction(); if (action == InventoryAction.PLACE_ALL || action == InventoryAction.PLACE_ONE || action == InventoryAction.SWAP_WITH_CURSOR) { - Bukkit.getScheduler().runTaskLater(plugin, () -> { + SchedulerCompat.runEntityLater(player, plugin, () -> { ItemStack realItemStack = playerHoldingPlaceholder.get(playerUUID); if (realItemStack != null && event.getClickedInventory() != null) { diff --git a/src/main/java/tf/tuff/tuffactions/swimming/Swimming.java b/src/main/java/tf/tuff/tuffactions/swimming/Swimming.java index 84c7538..0272aaf 100644 --- a/src/main/java/tf/tuff/tuffactions/swimming/Swimming.java +++ b/src/main/java/tf/tuff/tuffactions/swimming/Swimming.java @@ -1,99 +1,121 @@ package tf.tuff.tuffactions.swimming; -import java.io.ByteArrayOutputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; - -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.event.entity.EntityToggleSwimEvent; -import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.inventory.ItemStack; - -import tf.tuff.tuffactions.TuffActionBase; -import tf.tuff.tuffactions.TuffActions; - -public class Swimming extends TuffActionBase { - - private final Set swimmingPlayers = ConcurrentHashMap.newKeySet(); +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.entity.EntityToggleSwimEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.inventory.ItemStack; + +import io.github.retrooper.packetevents.util.folia.TaskWrapper; +import tf.tuff.tuffactions.TuffActionBase; +import tf.tuff.tuffactions.TuffActions; +import tf.tuff.util.SchedulerCompat; + +public class Swimming extends TuffActionBase { + + private final Set swimmingPlayers = ConcurrentHashMap.newKeySet(); + private final Map swimStateTasks = new ConcurrentHashMap<>(); public Swimming(TuffActions plugin) { super(plugin, "Swimming", "swimming", true); } - @Override - protected void disable() { - swimmingPlayers.clear(); - super.disable(); - } - - /*** CUSTOM, SERVER-BOUND PACKETS ***/ - public void handleSwimReady(Player player) { - if (!isEnabled()) return; - plugin.getServer().getScheduler().runTaskLater(plugin, () -> { - for (UUID swimmingPlayerId : swimmingPlayers) { - Player swimmingPlayer = Bukkit.getPlayer(swimmingPlayerId); - if (swimmingPlayer != null && swimmingPlayer.isOnline() && player.canSee(swimmingPlayer)) { - sendSwimState(player, swimmingPlayer, true); - } - } - }, 20L); - } - - public void handleSwimState(Player player, boolean isSwimming) { - if (!isEnabled()) return; - if (isSwimming) { - swimmingPlayers.add(player.getUniqueId()); - } else { - swimmingPlayers.remove(player.getUniqueId()); - } - player.setSwimming(isSwimming); - broadcastSwimState(player, isSwimming); - } - - public void handleElytraState(Player player, boolean isGliding) { - if (!isEnabled()) return; - ItemStack chest = player.getInventory().getChestplate(); - if (chest != null && chest.getType() == Material.ELYTRA) player.setGliding(isGliding); - } + @Override + protected void disable() { + for (TaskWrapper task : swimStateTasks.values()) { + task.cancel(); + } + swimStateTasks.clear(); + swimmingPlayers.clear(); + super.disable(); + } + + /*** CUSTOM, SERVER-BOUND PACKETS ***/ + public void handleSwimReady(Player player) { + if (!isEnabled()) return; + SchedulerCompat.runEntityLater(player, plugin, () -> { + for (UUID swimmingPlayerId : swimmingPlayers) { + Player swimmingPlayer = Bukkit.getPlayer(swimmingPlayerId); + if (swimmingPlayer != null && swimmingPlayer.isOnline() && player.canSee(swimmingPlayer)) { + sendSwimState(player, swimmingPlayer, true); + } + } + }, 20L); + } + + public void handleSwimState(Player player, boolean isSwimming) { + if (!isEnabled()) return; + if (isSwimming) { + swimmingPlayers.add(player.getUniqueId()); + startSwimMaintenance(player); + } else { + swimmingPlayers.remove(player.getUniqueId()); + stopSwimMaintenance(player.getUniqueId()); + } + SchedulerCompat.runEntity(player, plugin, () -> applySwimmingState(player, isSwimming)); + broadcastSwimState(player, isSwimming); + } + + public void handleElytraState(Player player, boolean isGliding) { + if (!isEnabled()) return; + SchedulerCompat.runEntity(player, plugin, () -> { + ItemStack chest = player.getInventory().getChestplate(); + if (chest != null && chest.getType() == Material.ELYTRA) player.setGliding(isGliding); + }); + } /*** EVENT HANDLERS ***/ - 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())) { - // event.setCancelled(true); - } - } - - public void handlePlayerQuit(PlayerQuitEvent event) { - if (!isEnabled()) return; - Player player = event.getPlayer(); - if (swimmingPlayers.remove(player.getUniqueId())) { - broadcastSwimState(player, false); - } + 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())) { + event.setCancelled(true); + SchedulerCompat.runEntity(player, plugin, () -> { + if (swimmingPlayers.contains(player.getUniqueId()) && player.isOnline()) { + applySwimmingState(player, true); + } + }); + } + } + + public void handlePlayerQuit(PlayerQuitEvent event) { + if (!isEnabled()) return; + Player player = event.getPlayer(); + stopSwimMaintenance(player.getUniqueId()); + if (swimmingPlayers.remove(player.getUniqueId())) { + broadcastSwimState(player, false); + } } /*** CUSTOM CLIENT-BOUND PACKETS ***/ - private void broadcastSwimState(Player subject, boolean isSwimming) { - for (UUID otherUUID : TuffActions.tuffPlayers) { - if (!otherUUID.equals(subject.getUniqueId())) { - Player recipient = Bukkit.getPlayer(otherUUID); - if (recipient != null && recipient.isOnline() && recipient.canSee(subject)) { - sendSwimState(recipient, subject, isSwimming); - } - } - } + private void broadcastSwimState(Player subject, boolean isSwimming) { + for (UUID otherUUID : TuffActions.tuffPlayers) { + if (!otherUUID.equals(subject.getUniqueId())) { + Player recipient = Bukkit.getPlayer(otherUUID); + if (recipient != null && recipient.isOnline()) { + SchedulerCompat.runEntity(recipient, plugin, () -> { + if (recipient.isOnline() && recipient.canSee(subject)) { + sendSwimState(recipient, subject, isSwimming); + } + }); + } + } + } } - private void sendSwimState(Player recipient, Player subject, boolean isSwimming) { - if (recipient == null || !recipient.isOnline()) return; - try (ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(bout)) { + private void sendSwimState(Player recipient, Player subject, boolean isSwimming) { + if (recipient == null || !recipient.isOnline()) return; + try (ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream out = new DataOutputStream(bout)) { out.writeUTF("update_other_swim"); out.writeLong(subject.getUniqueId().getMostSignificantBits()); @@ -102,7 +124,43 @@ private void sendSwimState(Player recipient, Player subject, boolean isSwimming) actsPlugin.sendPluginMessage(recipient, bout.toByteArray()); } catch (IOException e) { - debug("Failed to send swim state to " + recipient.getName(), e); - } - } -} + debug("Failed to send swim state to " + recipient.getName(), e); + } + } + + private void maintainSwimmingState(Player player) { + if (!player.isOnline()) { + stopSwimMaintenance(player.getUniqueId()); + swimmingPlayers.remove(player.getUniqueId()); + return; + } + if (!player.isInWater()) { + stopSwimMaintenance(player.getUniqueId()); + swimmingPlayers.remove(player.getUniqueId()); + applySwimmingState(player, false); + broadcastSwimState(player, false); + return; + } + applySwimmingState(player, true); + } + + private void applySwimmingState(Player player, boolean swimming) { + if (player == null || !player.isOnline()) return; + if (swimming && !player.isInWater()) return; + if (player.isSwimming() != swimming) { + player.setSwimming(swimming); + } + } + + private void startSwimMaintenance(Player player) { + swimStateTasks.computeIfAbsent(player.getUniqueId(), + ignored -> SchedulerCompat.runEntityTimer(player, plugin, () -> maintainSwimmingState(player), 1L, 1L)); + } + + private void stopSwimMaintenance(UUID playerId) { + TaskWrapper task = swimStateTasks.remove(playerId); + if (task != null) { + task.cancel(); + } + } +} diff --git a/src/main/java/tf/tuff/util/SchedulerCompat.java b/src/main/java/tf/tuff/util/SchedulerCompat.java new file mode 100644 index 0000000..a663cee --- /dev/null +++ b/src/main/java/tf/tuff/util/SchedulerCompat.java @@ -0,0 +1,82 @@ +package tf.tuff.util; + +import java.util.concurrent.TimeUnit; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import io.github.retrooper.packetevents.util.folia.FoliaScheduler; +import io.github.retrooper.packetevents.util.folia.TaskWrapper; + +public final class SchedulerCompat { + + private SchedulerCompat() { + } + + public static boolean isFolia() { + return FoliaScheduler.isFolia(); + } + + public static void runGlobal(Plugin plugin, Runnable task) { + FoliaScheduler.getGlobalRegionScheduler().execute(plugin, task); + } + + public static TaskWrapper runGlobalLater(Plugin plugin, Runnable task, long delayTicks) { + return FoliaScheduler.getGlobalRegionScheduler().runDelayed(plugin, scheduledTask -> task.run(), delayTicks); + } + + public static TaskWrapper runGlobalTimer(Plugin plugin, Runnable task, long delayTicks, long periodTicks) { + return FoliaScheduler.getGlobalRegionScheduler().runAtFixedRate(plugin, scheduledTask -> task.run(), delayTicks, periodTicks); + } + + public static void runAsync(Plugin plugin, Runnable task) { + FoliaScheduler.getAsyncScheduler().runNow(plugin, scheduledTask -> task.run()); + } + + public static TaskWrapper runAsyncLater(Plugin plugin, Runnable task, long delayTicks) { + return FoliaScheduler.getAsyncScheduler().runDelayed(plugin, scheduledTask -> task.run(), delayTicks * 50L, TimeUnit.MILLISECONDS); + } + + public static void runRegion(Plugin plugin, World world, int chunkX, int chunkZ, Runnable task) { + FoliaScheduler.getRegionScheduler().execute(plugin, world, chunkX, chunkZ, task); + } + + public static TaskWrapper runRegionLater(Plugin plugin, World world, int chunkX, int chunkZ, Runnable task, long delayTicks) { + return FoliaScheduler.getRegionScheduler().runDelayed(plugin, world, chunkX, chunkZ, scheduledTask -> task.run(), delayTicks); + } + + public static void runRegion(Plugin plugin, Location location, Runnable task) { + FoliaScheduler.getRegionScheduler().execute(plugin, location, task); + } + + public static TaskWrapper runRegionLater(Plugin plugin, Location location, Runnable task, long delayTicks) { + return FoliaScheduler.getRegionScheduler().runDelayed(plugin, location, scheduledTask -> task.run(), delayTicks); + } + + public static void runEntity(Entity entity, Plugin plugin, Runnable task) { + FoliaScheduler.getEntityScheduler().execute(entity, plugin, task, () -> { + }, 0L); + } + + public static TaskWrapper runEntityLater(Entity entity, Plugin plugin, Runnable task, long delayTicks) { + return FoliaScheduler.getEntityScheduler().runDelayed(entity, plugin, scheduledTask -> task.run(), () -> { + }, delayTicks); + } + + public static TaskWrapper runEntityTimer(Entity entity, Plugin plugin, Runnable task, long delayTicks, long periodTicks) { + return FoliaScheduler.getEntityScheduler().runAtFixedRate(entity, plugin, scheduledTask -> task.run(), () -> { + }, delayTicks, periodTicks); + } + + public static void sendPluginMessage(Plugin plugin, Player player, String channel, byte[] payload) { + if (player == null || channel == null || payload == null || !player.isOnline()) return; + runEntity(player, plugin, () -> { + if (player.isOnline()) { + player.sendPluginMessage(plugin, channel, payload); + } + }); + } +} diff --git a/src/main/java/tf/tuff/viablocks/CustomBlockListener.java b/src/main/java/tf/tuff/viablocks/CustomBlockListener.java index edfec8e..3a79fe6 100644 --- a/src/main/java/tf/tuff/viablocks/CustomBlockListener.java +++ b/src/main/java/tf/tuff/viablocks/CustomBlockListener.java @@ -12,6 +12,7 @@ import java.util.function.Consumer; import javax.annotation.Nonnull; +import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.ChunkSnapshot; import org.bukkit.Location; @@ -32,6 +33,7 @@ import com.google.common.io.ByteStreams; import tf.tuff.netty.ChunkInjector; +import tf.tuff.util.SchedulerCompat; import tf.tuff.viablocks.version.VersionAdapter; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; @@ -157,15 +159,23 @@ private void sendChunksBatched(Player player, String worldName, List chun int endIndex = Math.min(startIndex + CHUNKS_PER_TICK, chunks.size()); for (int i = startIndex; i < endIndex; i++) { int[] chunk = chunks.get(i); - byte[] data = getExtraDataForChunk(worldName, chunk[0], chunk[1]); - if (data != null && data.length > 0) { - player.sendPluginMessage(plugin.plugin, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, data); + World world = player.getWorld(); + if (!world.getName().equals(worldName)) { + return; } + cacheChunkWithCallback(world, chunk[0], chunk[1], data -> { + if (!player.isOnline() || !plugin.isPlayerEnabled(player)) return; + if (data != null && data.length > 0) { + SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, data); + } + }); } if (endIndex < chunks.size()) { final int nextStart = endIndex; - runSyncLater(() -> sendChunksBatched(player, worldName, chunks, nextStart), 1); + if (player.isOnline()) { + SchedulerCompat.runEntityLater(player, plugin.plugin, () -> sendChunksBatched(player, worldName, chunks, nextStart), 1L); + } } } @@ -182,11 +192,13 @@ public void handlePlayerQuit(PlayerQuitEvent event) { } public void handleChunkLoad(ChunkLoadEvent event) { + if (!plugin.isEnabled() || !hasViaBlocksPlayersInWorld(event.getWorld())) return; prepareChunkCache(event.getChunk()); } public void prepareChunkCache(Chunk chunk) { - if (!chunk.isLoaded() || modernMaterials.isEmpty()) return; + if (!plugin.isEnabled() || !chunk.isLoaded() || modernMaterials.isEmpty()) return; + if (plugin.chunkExecutor == null || plugin.chunkExecutor.isShutdown()) return; String key = chunkKey(chunk.getWorld().getName(), chunk.getX(), chunk.getZ()); @@ -214,23 +226,34 @@ public byte[] getExtraDataForChunk(String worldName, int x, int z) { } public void cacheChunkWithCallback(World world, int x, int z, Consumer callback) { + if (!plugin.isEnabled()) { + deliverCallback(callback, null); + return; + } + String key = chunkKey(world.getName(), x, z); byte[] existing = chunkPacketCache.getIfPresent(key); if (existing != null) { - callback.accept(existing.length > 0 ? existing : null); + deliverCallback(callback, existing.length > 0 ? existing : null); return; } if (!world.isChunkLoaded(x, z)) { chunkPacketCache.put(key, EMPTY_PACKET); - callback.accept(null); + deliverCallback(callback, null); return; } Chunk chunk = world.getChunkAt(x, z); if (!chunk.isLoaded() || modernMaterials.isEmpty()) { chunkPacketCache.put(key, EMPTY_PACKET); - callback.accept(null); + deliverCallback(callback, null); + return; + } + + if (plugin.chunkExecutor == null || plugin.chunkExecutor.isShutdown()) { + chunkPacketCache.put(key, EMPTY_PACKET); + deliverCallback(callback, null); return; } @@ -242,21 +265,21 @@ public void cacheChunkWithCallback(World world, int x, int z, Consumer c try { byte[] cached = chunkPacketCache.getIfPresent(key); if (cached != null) { - callback.accept(cached.length > 0 ? cached : null); + deliverCallback(callback, cached.length > 0 ? cached : null); return; } Map> foundBlocks = findModernBlocksInChunk(snapshot, minHeight, maxHeight); if (foundBlocks.isEmpty()) { chunkPacketCache.put(key, EMPTY_PACKET); - callback.accept(null); + deliverCallback(callback, null); } else { @SuppressWarnings("null") @Nonnull byte[] data = buildChunkPacket(foundBlocks); chunkPacketCache.put(key, data); - callback.accept(data); + deliverCallback(callback, data); } } catch (Exception e) { - callback.accept(null); + deliverCallback(callback, null); } }); } @@ -266,24 +289,22 @@ private Map> findModernBlocksInChunk(ChunkSnapshot chunkSnap int chunkX = chunkSnapshot.getX() << 4; int chunkZ = chunkSnapshot.getZ() << 4; - + for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { for (int y = minHeight; y < maxHeight; y++) { - // Check material FIRST — getBlockType() returns an enum, no allocation Material blockType = chunkSnapshot.getBlockType(x, y, z); - if (blockType == Material.AIR || blockType == Material.CAVE_AIR || blockType == Material.VOID_AIR || !this.modernMaterials.contains(blockType)) { continue; } - + // Only allocate BlockData for confirmed modern blocks BlockData data = chunkSnapshot.getBlockData(x, y, z); - + Integer cachedId = blockDataIdCache.getIfPresent(data); int materialId; if (cachedId != null) { @@ -292,7 +313,7 @@ private Map> findModernBlocksInChunk(ChunkSnapshot chunkSnap materialId = this.paletteManager.getOrCreateId(data.getAsString()); blockDataIdCache.put(data, materialId); } - + if (materialId != -1) { long packedLocation = packLocation(chunkX + x, y, chunkZ + z); LongList locs = foundBlocks.get(materialId); @@ -305,10 +326,9 @@ private Map> findModernBlocksInChunk(ChunkSnapshot chunkSnap } } } - return (Map>) (Map) foundBlocks; - } - + } + public byte[] getExtraDataForMultiBlock(World world, List locations) { Map> foundBlocks = new HashMap<>(); @@ -510,24 +530,14 @@ private void sendBlockStateUpdateToNearbyPlayers(Location location, BlockData da } if (stateId == -1) return; - World world = location.getWorld(); - for (Player player : world.getPlayers()) { - if (plugin.isPlayerEnabled(player) && player.getLocation().distanceSquared(location) < UPDATE_RADIUS_SQUARED) { - sendPacket(player, stateId, location); - } - } + scheduleNearbyEnabledPlayers(location, player -> sendPacket(player, stateId, location)); } private void sendClearUpdateToNearbyPlayers(Location location) { if (!plugin.isEnabled() || plugin.viaBlocksEnabledPlayers.isEmpty() || location.getWorld() == null) return; final int AIR_ID = 0; - World world = location.getWorld(); - - for (Player player : world.getPlayers()) { - if (plugin.isPlayerEnabled(player) && player.getLocation().distanceSquared(location) < UPDATE_RADIUS_SQUARED) { - sendPacket(player, AIR_ID, location); - } - } + + scheduleNearbyEnabledPlayers(location, player -> sendPacket(player, AIR_ID, location)); } private void invalidateChunkCache(Chunk chunk) { @@ -550,7 +560,7 @@ private void sendPacket(Player player, int stateId, Location location) { } stateList.add(packLocation(location)); if (pendingFlush.add(playerId)) { - runSyncLater(() -> flushPendingUpdates(playerId), plugin.getUpdateBatchDelayTicks()); + SchedulerCompat.runEntityLater(player, plugin.plugin, () -> flushPendingUpdates(playerId), plugin.getUpdateBatchDelayTicks()); } } @@ -563,7 +573,7 @@ private void flushPendingUpdates(UUID playerId) { if (player == null || !player.isOnline()) return; byte[] packetData = buildChunkPacket(updateData); - player.sendPluginMessage(plugin.plugin, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, packetData); + SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, packetData); } private int getMaterialId(BlockData data) { @@ -598,7 +608,7 @@ public void sendPaletteToClient(Player player) { for (String state : palette) { out.writeUTF(state); } - player.sendPluginMessage(plugin.plugin, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, out.toByteArray()); + SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, out.toByteArray()); } public boolean isModernMaterial(Material material) { @@ -607,11 +617,12 @@ public boolean isModernMaterial(Material material) { public void processChunkForSinglePlayer(Chunk chunk, Player player) { if (!chunk.isLoaded() || !plugin.isPlayerEnabled(player)) return; - prepareChunkCache(chunk); - byte[] data = getExtraDataForChunk(chunk.getWorld().getName(), chunk.getX(), chunk.getZ()); - if (data != null && data.length > 0) { - player.sendPluginMessage(plugin.plugin, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, data); - } + cacheChunkWithCallback(chunk.getWorld(), chunk.getX(), chunk.getZ(), data -> { + if (!player.isOnline() || !plugin.isPlayerEnabled(player)) return; + if (data != null && data.length > 0) { + SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, data); + } + }); } public void clearCache() { @@ -622,11 +633,37 @@ public void clearCache() { recentModernChanges.invalidateAll(); } - private void runSyncLater(Runnable task, long delay) { - if (!plugin.plugin.isEnabled()) return; - try { - plugin.plugin.getServer().getScheduler().runTaskLater(plugin.plugin, task, delay); - } catch (Exception e) {} + private void deliverCallback(Consumer callback, byte[] data) { + if (callback != null) { + callback.accept(data); + } + } + + private boolean hasViaBlocksPlayersInWorld(World world) { + for (UUID playerId : plugin.viaBlocksEnabledPlayers) { + Player player = Bukkit.getPlayer(playerId); + if (player != null && player.isOnline() && world.equals(player.getWorld())) { + return true; + } + } + return false; + } + + private void scheduleNearbyEnabledPlayers(Location location, Consumer action) { + World world = location.getWorld(); + if (world == null) return; + + SchedulerCompat.runGlobal(plugin.plugin, () -> { + for (Player player : Bukkit.getOnlinePlayers()) { + if (!plugin.isPlayerEnabled(player)) continue; + SchedulerCompat.runEntity(player, plugin.plugin, () -> { + if (!player.isOnline()) return; + if (!world.equals(player.getWorld())) return; + if (player.getLocation().distanceSquared(location) >= UPDATE_RADIUS_SQUARED) return; + action.accept(player); + }); + } + }); } public long packLocation(int x, int y, int z) { diff --git a/src/main/java/tf/tuff/viablocks/PaletteManager.java b/src/main/java/tf/tuff/viablocks/PaletteManager.java index 2e56d0f..450d977 100644 --- a/src/main/java/tf/tuff/viablocks/PaletteManager.java +++ b/src/main/java/tf/tuff/viablocks/PaletteManager.java @@ -10,10 +10,11 @@ import org.bukkit.Material; import org.bukkit.entity.Player; -import com.google.common.io.ByteArrayDataOutput; -import com.google.common.io.ByteStreams; - -import tf.tuff.viablocks.version.VersionAdapter; +import com.google.common.io.ByteArrayDataOutput; +import com.google.common.io.ByteStreams; + +import tf.tuff.util.SchedulerCompat; +import tf.tuff.viablocks.version.VersionAdapter; public class PaletteManager { @@ -111,14 +112,14 @@ private void broadcastNewPaletteEntry(String state) { out.writeUTF(state); byte[] data = out.toByteArray(); - Bukkit.getScheduler().runTask(plugin.plugin, () -> { - if (!plugin.plugin.isEnabled() || !plugin.isEnabled()) return; - for (Player player : Bukkit.getOnlinePlayers()) { - if (plugin.isPlayerEnabled(player)) { - player.sendPluginMessage(plugin.plugin, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, data); - } - } - }); + SchedulerCompat.runGlobal(plugin.plugin, () -> { + if (!plugin.plugin.isEnabled() || !plugin.isEnabled()) return; + for (Player player : Bukkit.getOnlinePlayers()) { + if (plugin.isPlayerEnabled(player)) { + SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaBlocksPlugin.CLIENTBOUND_CHANNEL, data); + } + } + }); } public synchronized int getId(String state) { diff --git a/src/main/java/tf/tuff/viablocks/ViaBlocksPlugin.java b/src/main/java/tf/tuff/viablocks/ViaBlocksPlugin.java index 0f76a1a..d0e1a62 100644 --- a/src/main/java/tf/tuff/viablocks/ViaBlocksPlugin.java +++ b/src/main/java/tf/tuff/viablocks/ViaBlocksPlugin.java @@ -29,6 +29,7 @@ import tf.tuff.viablocks.version.VersionAdapter; import tf.tuff.viablocks.version.modern.ModernAdapter; import tf.tuff.TuffX; +import tf.tuff.util.SchedulerCompat; public final class ViaBlocksPlugin { @@ -52,8 +53,6 @@ public final class ViaBlocksPlugin { public PaletteManager paletteManager; private long updateBatchDelayTicks = 1L; public ExecutorService chunkExecutor; - public boolean isPaper = false; - public TuffX plugin; public ViaBlocksPlugin(TuffX plugin){ @@ -61,12 +60,17 @@ public ViaBlocksPlugin(TuffX plugin){ } public void onTuffXReload() { + Set previouslyEnabledPlayers = ConcurrentHashMap.newKeySet(); + previouslyEnabledPlayers.addAll(viaBlocksEnabledPlayers); + loadSyncSettings(); if (chunkExecutor != null) { chunkExecutor.shutdownNow(); } - this.chunkExecutor = Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors())); + this.chunkExecutor = enabled + ? Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors())) + : null; if (playerDataFile == null) { playerDataFile = new File(plugin.getDataFolder(), "players.yml"); @@ -76,6 +80,15 @@ public void onTuffXReload() { if (blockListener != null) { blockListener.clearCache(); } + + viaBlocksEnabledPlayers.clear(); + if (enabled && blockListener != null) { + for (Player player : plugin.getServer().getOnlinePlayers()) { + if (!previouslyEnabledPlayers.contains(player.getUniqueId())) continue; + setPlayerEnabled(player, true); + blockListener.onViaBlocksPlayerJoin(player); + } + } info("ViaBlocks reloaded."); } @@ -83,23 +96,15 @@ public void onTuffXReload() { public void onTuffXEnable() { instance = this; - this.chunkExecutor = Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors())); - this.versionAdapter = new ModernAdapter(); this.paletteManager = new PaletteManager(this.versionAdapter); - try { - Class.forName("io.papermc.paper.threadedregions.scheduler.AsyncScheduler"); - this.isPaper = true; - info("Paper detected. Enabling optimized asynchronous scheduling."); - } catch (ClassNotFoundException e) { - this.isPaper = false; - info("Running on Spigot/Bukkit. Using standard scheduler."); - } - plugin.saveDefaultConfig(); loadSyncSettings(); + this.chunkExecutor = enabled + ? Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors())) + : null; setupPlayerData(); @@ -109,10 +114,15 @@ public void onTuffXEnable() { this.blockListener = new CustomBlockListener(this, this.versionAdapter, this.paletteManager); plugin.getCommand("viablocks").setExecutor(plugin); - info("ViaBlocks has been enabled successfully and is listening for client handshakes."); + if (enabled) { + info("ViaBlocks has been enabled successfully and is listening for client handshakes."); + } else { + info("ViaBlocks is disabled in config."); + } } public void handlePacket(Player player, byte[] message) { + if (!isEnabled() || blockListener == null) return; if (!isPlayerEnabled(player) && isEnabled()) { debug("Received ViaBlocks handshake from player: " + player.getName() + ". Enabling custom blocks."); setPlayerEnabled(player, true); @@ -150,6 +160,8 @@ public void onTuffXDisable(){ chunkExecutor = null; } + viaBlocksEnabledPlayers.clear(); + info("ViaBlocks has been disabled."); } private void setupPlayerData() { @@ -213,7 +225,7 @@ public void sendWelcomeGui(Player player) { disclaimer.setItalic(true); meta.spigot().addPage(new ComponentBuilder("").append(welcome).append(body).append(link).append(new TextComponent(".")).append(disclaimer).create()); book.setItemMeta(meta); - plugin.getServer().getScheduler().runTask(plugin, () -> player.openBook(book)); + SchedulerCompat.runEntity(player, plugin, () -> player.openBook(book)); } public boolean isEnabled() { @@ -257,6 +269,10 @@ public boolean onTuffXCommand(CommandSender sender, Command command, String labe player.sendMessage("\u00A7cYou do not have permission to use this command."); return true; } + if (!isEnabled() || blockListener == null) { + player.sendMessage("\u00A7cViaBlocks is disabled."); + return true; + } player.sendMessage("\u00A7aRefreshing modern blocks in your view distance..."); World world = player.getWorld(); int viewDistance = this.versionAdapter.getClientViewDistance(player); diff --git a/src/main/java/tf/tuff/viaentities/EntityDataHandler.java b/src/main/java/tf/tuff/viaentities/EntityDataHandler.java index b8f90d2..5b3c07b 100644 --- a/src/main/java/tf/tuff/viaentities/EntityDataHandler.java +++ b/src/main/java/tf/tuff/viaentities/EntityDataHandler.java @@ -8,6 +8,8 @@ import com.google.common.io.ByteArrayDataOutput; import com.google.common.io.ByteStreams; +import tf.tuff.util.SchedulerCompat; + public class EntityDataHandler extends ChannelOutboundHandlerAdapter { private final ViaEntitiesPlugin plugin; @@ -321,7 +323,7 @@ private void sendEntitySpawn(int entityId, String entityType, double x, double y out.writeFloat(pitch); byte[] data = out.toByteArray(); - player.sendPluginMessage(plugin.plugin, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, data); + SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, data); } private void sendEntityMetadata(int entityId, Object packedItems) { @@ -368,7 +370,7 @@ private void sendEntityMetadata(int entityId, Object packedItems) { } byte[] data = out.toByteArray(); - player.sendPluginMessage(plugin.plugin, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, data); + SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, data); } catch (Exception e) { } } @@ -382,7 +384,7 @@ private void sendEntityAnimation(int entityId, int animationType) { out.writeInt(animationType); byte[] data = out.toByteArray(); - player.sendPluginMessage(plugin.plugin, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, data); + SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, data); } private void sendEntityDestroy(int entityId) { @@ -393,6 +395,6 @@ private void sendEntityDestroy(int entityId) { out.writeInt(entityId); byte[] data = out.toByteArray(); - player.sendPluginMessage(plugin.plugin, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, data); + SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, data); } } diff --git a/src/main/java/tf/tuff/viaentities/EntityInjector.java b/src/main/java/tf/tuff/viaentities/EntityInjector.java index b1c1424..066bbfa 100644 --- a/src/main/java/tf/tuff/viaentities/EntityInjector.java +++ b/src/main/java/tf/tuff/viaentities/EntityInjector.java @@ -4,6 +4,7 @@ import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import tf.tuff.netty.BaseInjector; +import tf.tuff.util.SchedulerCompat; public class EntityInjector extends BaseInjector { @@ -21,15 +22,13 @@ protected ChannelHandler createHandler(Player player) { @Override protected void onPostInject(Player player) { - plugin.plugin.getServer().getScheduler().runTask(plugin.plugin, () -> { - sendExistingEntities(player); - }); + SchedulerCompat.runEntity(player, plugin.plugin, () -> sendExistingEntities(player)); } private void sendExistingEntities(Player player) { int viewDistance = player.getWorld().getViewDistance() * 16; - for (Entity entity : player.getWorld().getEntities()) { + for (Entity entity : player.getNearbyEntities(viewDistance, viewDistance, viewDistance)) { if (entity.equals(player)) continue; if (entity instanceof Player) continue; @@ -59,6 +58,6 @@ public void sendEntityData(Player player, int entityId, String entityType, Entit out.writeFloat(entity.getLocation().getYaw()); out.writeFloat(entity.getLocation().getPitch()); - player.sendPluginMessage(plugin.plugin, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, out.toByteArray()); + SchedulerCompat.sendPluginMessage(plugin.plugin, player, ViaEntitiesPlugin.CLIENTBOUND_CHANNEL, out.toByteArray()); } } diff --git a/src/main/java/tf/tuff/viaentities/ViaEntitiesPlugin.java b/src/main/java/tf/tuff/viaentities/ViaEntitiesPlugin.java index 5a714cd..17abecc 100644 --- a/src/main/java/tf/tuff/viaentities/ViaEntitiesPlugin.java +++ b/src/main/java/tf/tuff/viaentities/ViaEntitiesPlugin.java @@ -8,6 +8,8 @@ import java.util.UUID; import java.util.logging.Level; +import tf.tuff.util.SchedulerCompat; + public final class ViaEntitiesPlugin { public static final String CLIENTBOUND_CHANNEL = "viaentities:data"; @@ -105,7 +107,7 @@ private void sendPaletteToClient(Player player) { out.writeUTF(entityType); } - player.sendPluginMessage(plugin, CLIENTBOUND_CHANNEL, out.toByteArray()); + SchedulerCompat.sendPluginMessage(plugin, player, CLIENTBOUND_CHANNEL, out.toByteArray()); } public void handlePlayerQuit(org.bukkit.event.player.PlayerQuitEvent event) { diff --git a/src/main/java/tf/tuff/y0/ChunkPacketListener.java b/src/main/java/tf/tuff/y0/ChunkPacketListener.java index 045f7b4..1c263d4 100644 --- a/src/main/java/tf/tuff/y0/ChunkPacketListener.java +++ b/src/main/java/tf/tuff/y0/ChunkPacketListener.java @@ -6,6 +6,8 @@ import org.bukkit.World; import org.bukkit.entity.Player; +import tf.tuff.util.SchedulerCompat; + public class ChunkPacketListener { public final Y0Plugin plugin; @@ -17,11 +19,11 @@ public ChunkPacketListener(Y0Plugin plugin) { public void handleChunk(TuffX plugin, Player player, World world, int chunkX, int chunkZ){ if (!this.plugin.isPlayerReady(player)) return; - plugin.getServer().getScheduler().runTask(plugin, () -> { + SchedulerCompat.runRegion(plugin, world, chunkX, chunkZ, () -> { if (player.isOnline() && world.isChunkLoaded(chunkX, chunkZ)) { Chunk chunk = world.getChunkAt(chunkX, chunkZ); this.plugin.processAndSendChunk(player, chunk); } }); } -} \ No newline at end of file +} diff --git a/src/main/java/tf/tuff/y0/ViaBlockIds.java b/src/main/java/tf/tuff/y0/ViaBlockIds.java index c3e2a9c..a0832d8 100644 --- a/src/main/java/tf/tuff/y0/ViaBlockIds.java +++ b/src/main/java/tf/tuff/y0/ViaBlockIds.java @@ -21,6 +21,7 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import tf.tuff.TuffX; +import tf.tuff.util.SchedulerCompat; public class ViaBlockIds { private final TuffX p; @@ -37,11 +38,16 @@ public ViaBlockIds(TuffX pl) { plugin.info("Server Minecraft Version: " + serverVersion); - Bukkit.getScheduler().runTaskLater(pl, this::initializeMappings, 1L); + SchedulerCompat.runGlobalLater(pl, this::initializeMappings, 1L); } private void initializeMappings() { - if (Via.getAPI() == null) { + try { + if (Via.getAPI() == null) { + plugin.severe("ViaVersion API not found! Is ViaVersion installed?"); + return; + } + } catch (IllegalArgumentException e) { plugin.severe("ViaVersion API not found! Is ViaVersion installed?"); return; } @@ -272,4 +278,4 @@ public int[] ctl(int mbsi) { int mt = csi & 0xF; return new int[]{bi, mt}; } -} \ No newline at end of file +} diff --git a/src/main/java/tf/tuff/y0/Y0Plugin.java b/src/main/java/tf/tuff/y0/Y0Plugin.java index 5d25a7f..c1a4dda 100644 --- a/src/main/java/tf/tuff/y0/Y0Plugin.java +++ b/src/main/java/tf/tuff/y0/Y0Plugin.java @@ -21,6 +21,7 @@ import java.util.logging.Level; import javax.annotation.Nonnull; +import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.ChunkSnapshot; import org.bukkit.Location; @@ -48,6 +49,7 @@ import tf.tuff.TuffX; import tf.tuff.netty.ChunkInjector; +import tf.tuff.util.SchedulerCompat; public class Y0Plugin { @@ -273,7 +275,7 @@ public void handlePacket(Player player, byte[] data) { private void handlePacket(Player player, String subchannel) { if (!enabledWorlds.contains(player.getWorld().getName()) && !subchannel.equalsIgnoreCase("ready")) { - player.sendPluginMessage(plugin, CHANNEL, y0StatusPkt(false)); + SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, y0StatusPkt(false)); return; } @@ -287,10 +289,10 @@ private void handlePacket(Player player, String subchannel) { if (chunkInjector != null) { chunkInjector.inject(player); } - player.sendPluginMessage(plugin, CHANNEL, y0StatusPkt(true)); + SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, y0StatusPkt(true)); resendChunksInView(player); } else { - player.sendPluginMessage(plugin, CHANNEL, y0StatusPkt(false)); + SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, y0StatusPkt(false)); } break; case "use_on_block": @@ -375,15 +377,16 @@ private void sendY0ChunksBatched(Player player, String worldName, List ch ObjectArrayList cachedData = chunkCache.getIfPresent(k); if (cachedData != null && !cachedData.isEmpty()) { for (byte[] py : cachedData) { - player.sendPluginMessage(plugin, CHANNEL, py); + SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, py); } } } if (endIndex < chunks.size()) { final int nextStart = endIndex; - plugin.getServer().getScheduler().runTaskLater(plugin, () -> - sendY0ChunksBatched(player, worldName, chunks, nextStart), 1); + if (player.isOnline()) { + SchedulerCompat.runEntityLater(player, plugin, () -> sendY0ChunksBatched(player, worldName, chunks, nextStart), 1L); + } } } @@ -406,9 +409,9 @@ private byte[] dimensionChangePkt() { public void handlePlayerChangeWorld(PlayerChangedWorldEvent event) { Player player = event.getPlayer(); - player.sendPluginMessage(plugin, CHANNEL, dimensionChangePkt()); + SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, dimensionChangePkt()); boolean isEnabledWorld = enabledWorlds.contains(player.getWorld().getName()); - player.sendPluginMessage(plugin, CHANNEL, y0StatusPkt(isEnabledWorld)); + SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, y0StatusPkt(isEnabledWorld)); if (isPlayerReady(player) && isEnabledWorld) { resendChunksInView(player); } @@ -416,22 +419,23 @@ public void handlePlayerChangeWorld(PlayerChangedWorldEvent event) { public void handlePlayerJoin(PlayerJoinEvent event) { Player player = event.getPlayer(); - player.sendPluginMessage(plugin, CHANNEL, dimensionChangePkt()); + SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, dimensionChangePkt()); boolean isEnabledWorld = enabledWorlds.contains(player.getWorld().getName()); - player.sendPluginMessage(plugin, CHANNEL, y0StatusPkt(isEnabledWorld)); + SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, y0StatusPkt(isEnabledWorld)); } public void processAndSendChunk(final Player player, final Chunk c) { if (c == null || player == null || !player.isOnline()) return; + if (!c.getWorld().equals(player.getWorld())) return; if (enabledWorlds != null && !enabledWorlds.contains(c.getWorld().getName())) return; final WorldChunk k = new WorldChunk(c.getWorld().getName(), c.getX(), c.getZ()); ObjectArrayList cachedData = chunkCache.getIfPresent(k); if (cachedData != null) { - if (player.isOnline()) { + if (player.isOnline() && c.getWorld().equals(player.getWorld())) { for (byte[] py : cachedData) { - player.sendPluginMessage(plugin, CHANNEL, py); + SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, py); } } return; @@ -467,13 +471,9 @@ private void processSnapshotAsync(final Player player, final ChunkSnapshot snaps chunkCache.put(k, pp); storeCombined(k, pp); if (!pp.isEmpty()) { - plugin.getServer().getScheduler().runTask(plugin, () -> { - if (player.isOnline()) { - for (byte[] py : pp) { - player.sendPluginMessage(plugin, CHANNEL, py); - } - } - }); + for (byte[] py : pp) { + SchedulerCompat.sendPluginMessage(plugin, player, CHANNEL, py); + } } }); } @@ -641,6 +641,7 @@ public void handlePlayerQuit(PlayerQuitEvent event) { public void handleChunkLoad(ChunkLoadEvent event) { if (enabledWorlds == null || !enabledWorlds.contains(event.getWorld().getName())) return; + if (!hasReadyPlayersInWorld(event.getWorld())) return; Chunk chunk = event.getChunk(); int chunkX = chunk.getX(); @@ -727,7 +728,6 @@ private byte[] createSectionPayload(ChunkSnapshot s, int x, int z, int sy, Objec out.writeInt(sy); out.write(bd, 0, idx); } - return bout.toByteArray(); } @@ -750,37 +750,33 @@ public void handleBlockPhysics(BlockPhysicsEvent event) { if (block.getY() < 0) { final Location loc = block.getLocation(); final World world = loc.getWorld(); - plugin.getServer().getScheduler().runTask(plugin, () -> { + SchedulerCompat.runRegionLater(plugin, loc, () -> { BlockData ud = world.getBlockData(loc); sendSingleBlockUpdate(loc, ud); invalidateChunkCache(world, loc.getBlockX(), loc.getBlockZ()); - }); + }, 1L); } } public void handleBlockExplode(BlockExplodeEvent event) { - final ObjectOpenHashSet ac = new ObjectOpenHashSet<>(); final List btu = new ArrayList<>(event.blockList()); - plugin.getServer().getScheduler().runTask(plugin, () -> { - for (Block block : btu) { - if (block.getY() < 0) { - sendSingleBlockUpdate(block.getLocation(), Material.AIR.createBlockData()); - ac.add(new WorldChunk(block.getWorld().getName(), block.getX() >> 4, block.getZ() >> 4)); - } - } - if (!ac.isEmpty()) { - ac.forEach(chunkCache::invalidate); - } - }); + for (Block block : btu) { + if (block.getY() >= 0) continue; + final Location loc = block.getLocation(); + SchedulerCompat.runRegionLater(plugin, loc, () -> { + sendSingleBlockUpdate(loc, Material.AIR.createBlockData()); + invalidateChunkCache(loc.getWorld(), loc.getBlockX(), loc.getBlockZ()); + }, 1L); + } } public void handleBlockFromTo(BlockFromToEvent event) { final Block block = event.getToBlock(); if (block.getY() < 0) { - plugin.getServer().getScheduler().runTask(plugin, () -> { + SchedulerCompat.runRegionLater(plugin, block.getLocation(), () -> { sendSingleBlockUpdate(block.getLocation(), block.getBlockData()); invalidateChunkCache(block.getWorld(), block.getX(), block.getZ()); - }); + }, 1L); } } @@ -797,13 +793,7 @@ private void sendSingleBlockUpdate(Location loc, BlockData data) { out.writeShort((short) ((ld[1] << 12) | (ld[0] & 0xFFF))); byte[] py = bout.toByteArray(); - plugin.getServer().getScheduler().runTask(plugin, () -> { - for (Player player : loc.getWorld().getPlayers()) { - if (player.getLocation().distanceSquared(loc) < 4096) { - player.sendPluginMessage(plugin, CHANNEL, py); - } - } - }); + sendToNearbyPlayers(loc, py); } catch (IOException e) { severe("Failed to create single block update payload: " + e.getMessage()); @@ -858,13 +848,7 @@ private void sendLightUpdate(Location loc) { chunkProcessor.submit(() -> { try { byte[] py = createLightPayload(s, sc); - plugin.getServer().getScheduler().runTask(plugin, () -> { - for (Player player : w.getPlayers()) { - if (player.isOnline() && player.getLocation().distanceSquared(loc) < 4096) { - player.sendPluginMessage(plugin, CHANNEL, py); - } - } - }); + sendToNearbyPlayers(loc, py); } catch (IOException e) { severe("Failed to create lighting payload: " + e.getMessage()); } @@ -873,6 +857,26 @@ private void sendLightUpdate(Location loc) { } } + private void sendToNearbyPlayers(Location loc, byte[] payload) { + if (payload == null || loc.getWorld() == null || aib.isEmpty()) return; + + final World world = loc.getWorld(); + + SchedulerCompat.runGlobal(plugin, () -> { + for (Player player : Bukkit.getOnlinePlayers()) { + if (!player.isOnline() || !isPlayerReady(player)) continue; + if (!world.equals(player.getWorld())) continue; + if (player.getLocation().distanceSquared(loc) >= 4096) continue; + SchedulerCompat.runEntity(player, plugin, () -> { + if (!player.isOnline() || !isPlayerReady(player)) return; + if (!world.equals(player.getWorld())) return; + if (player.getLocation().distanceSquared(loc) >= 4096) return; + player.sendPluginMessage(plugin, CHANNEL, payload); + }); + } + }); + } + private byte[] createLightPayload(ChunkSnapshot s, Coords sc) throws IOException { try (ByteArrayOutputStream bout = new ByteArrayOutputStream(4120); DataOutputStream out = new DataOutputStream(bout)) { @@ -900,4 +904,14 @@ private byte[] createLightPayload(ChunkSnapshot s, Coords sc) throws IOException } } + private boolean hasReadyPlayersInWorld(World world) { + for (UUID playerId : aib) { + Player player = plugin.getServer().getPlayer(playerId); + if (player != null && player.isOnline() && world.equals(player.getWorld())) { + return true; + } + } + return false; + } + } diff --git a/src/test/java/tf/tuff/TuffXTest.java b/src/test/java/tf/tuff/TuffXTest.java index 521b889..25028bf 100644 --- a/src/test/java/tf/tuff/TuffXTest.java +++ b/src/test/java/tf/tuff/TuffXTest.java @@ -2,7 +2,6 @@ import be.seeseemelk.mockbukkit.MockBukkit; import be.seeseemelk.mockbukkit.ServerMock; -import be.seeseemelk.mockbukkit.entity.PlayerMock; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; @@ -34,4 +33,4 @@ void reloadDoesNotThrow() { assertDoesNotThrow(() -> plugin.reloadTuffX(), "reloadTuffX() should not throw"); } -} \ No newline at end of file +} diff --git a/src/test/java/tf/tuff/netty/ChunkHandlerTest.java b/src/test/java/tf/tuff/netty/ChunkHandlerTest.java new file mode 100644 index 0000000..f91b4b5 --- /dev/null +++ b/src/test/java/tf/tuff/netty/ChunkHandlerTest.java @@ -0,0 +1,48 @@ +package tf.tuff.netty; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class ChunkHandlerTest { + + @Test + void decodesSingleBlockChangePositionWithNegativeCoordinates() { + long packed = packBlockPosition(-21, -64, 37); + + ChunkHandler.BlockChangePosition position = ChunkHandler.decodeSingleBlockChangePosition(packed); + + assertEquals(-21, position.x()); + assertEquals(-64, position.y()); + assertEquals(37, position.z()); + } + + @Test + void decodesSectionBlockChangePositionBelowYZero() { + long sectionPosition = packSectionPosition(12, -5, -9); + long entry = packSectionEntry(3, 11, 7, 8123); + + ChunkHandler.BlockChangePosition position = ChunkHandler.decodeMultiBlockChangePosition(sectionPosition, entry); + + assertEquals((12 << 4) + 3, position.x()); + assertEquals((-5 << 4) + 7, position.y()); + assertEquals((-9 << 4) + 11, position.z()); + } + + private static long packBlockPosition(int x, int y, int z) { + return ((long) x & 0x3FFFFFFL) << 38 + | ((long) z & 0x3FFFFFFL) << 12 + | ((long) y & 0xFFFL); + } + + private static long packSectionPosition(int x, int y, int z) { + return ((long) x & 0x3FFFFFL) << 42 + | ((long) z & 0x3FFFFFL) << 20 + | ((long) y & 0xFFFFFL); + } + + private static long packSectionEntry(int localX, int localZ, int localY, int blockStateId) { + int localPosition = ((localX & 0xF) << 8) | ((localZ & 0xF) << 4) | (localY & 0xF); + return ((long) blockStateId << 12) | (localPosition & 0xFFFL); + } +}