diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java index c524c4d0..2e4d2b6f 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/BauSystem.java @@ -30,6 +30,7 @@ import de.steamwar.bausystem.features.tracer.TraceManager; import de.steamwar.bausystem.features.tracer.TraceRecorder; import de.steamwar.bausystem.features.world.BauScoreboard; import de.steamwar.bausystem.linkage.BauGuiItem; +import de.steamwar.bausystem.region.Point; import de.steamwar.bausystem.region.Region; import de.steamwar.bausystem.region.RegionSystem; import de.steamwar.bausystem.utils.ScoreboardElement; @@ -47,7 +48,6 @@ import de.steamwar.providers.BauServerInfo; import lombok.Getter; import org.bukkit.Bukkit; import org.bukkit.GameRule; -import org.bukkit.Location; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -136,7 +136,9 @@ public class BauSystem extends JavaPlugin implements Listener { } else { return SteamEdit.AdjacentFilter.ALWAYS; } - return (x, y, z) -> area.inRegion(new Location(null, x, y, z), true); + Point min = area.getMinPoint(true); + Point max = area.getMaxPoint(true); + return new SteamEdit.BoxFilter(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ()); }; Bukkit.getWorlds().get(0).setGameRule(GameRule.SEND_COMMAND_FEEDBACK, false); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceListener.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceListener.java index 3554a7e5..bc1636cd 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceListener.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/smartplace/SmartPlaceListener.java @@ -19,13 +19,10 @@ package de.steamwar.bausystem.features.smartplace; -import com.comphenix.tinyprotocol.TinyProtocol; import de.steamwar.bausystem.BauSystem; import de.steamwar.bausystem.Permission; import de.steamwar.bausystem.configplayer.Config; import de.steamwar.linkage.Linked; -import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket; -import net.minecraft.world.InteractionHand; import org.bukkit.*; import org.bukkit.block.Block; import org.bukkit.block.BlockFace; @@ -35,106 +32,99 @@ import org.bukkit.block.data.*; import org.bukkit.block.data.type.*; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.block.Action; import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.inventory.ItemStack; -import org.bukkit.util.RayTraceResult; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; @Linked public class SmartPlaceListener implements Listener { - private static final Set CONTAINERS = new HashSet<>(); - private static final Set IGNORED = new HashSet<>(); + private static final Set IGNORED = Set.of(Material.TNT, Material.REDSTONE_ORE, Material.BEEHIVE, Material.SEA_PICKLE); - static { - World world = Bukkit.getWorlds().get(0); - Block block = world.getBlockAt(0, 0, 0); - block.setType(Material.AIR); - BlockState state = block.getState(); - for (Material material : Material.values()) { - if (material.isLegacy()) continue; - if (!material.isInteractable() && !material.isBlock()) continue; - BlockData blockData = material.createBlockData(); - block.setBlockData(blockData); - if (block.getState() instanceof TileState) { - CONTAINERS.add(material); - } else if (blockData instanceof Stairs) { - CONTAINERS.add(material); - } - state.update(true, false); - } - CONTAINERS.add(Material.GRINDSTONE); - CONTAINERS.remove(Material.COMPARATOR); - state.update(true, false); + private static final Set FAKED_SNEAK = new HashSet<>(); - IGNORED.add(Material.TNT); - IGNORED.add(Material.REDSTONE_ORE); - IGNORED.add(Material.BEEHIVE); - IGNORED.add(Material.SEA_PICKLE); - IGNORED.remove(Material.STONE); - IGNORED.remove(Material.BARRIER); + private static boolean isContainer(Block block) { + if (block.getType() == Material.COMPARATOR) return false; + if (block.getType() == Material.GRINDSTONE) return true; + return block.getState() instanceof TileState || block.getBlockData() instanceof Stairs; } - private static final Set SMART_PLACING = new HashSet<>(); - private static final Set WAS_EXECUTED = new HashSet<>(); + private static boolean isReallySneaking(Player player) { + return player.isSneaking() && !FAKED_SNEAK.contains(player.getUniqueId()); + } - public SmartPlaceListener() { - TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, (player, packet) -> { - if (!Permission.BUILD.hasPermission(player)) return packet; - if (!Config.STORE.get(player).isSmartPlace()) return packet; - ItemStack usedItem = packet.getHand() == InteractionHand.MAIN_HAND ? player.getInventory().getItemInMainHand() : player.getInventory().getItemInOffHand(); - if (usedItem.getType() == Material.DEBUG_STICK) return packet; - RayTraceResult rayTraceResult = player.rayTraceBlocks(6); - Block block = rayTraceResult != null ? rayTraceResult.getHitBlock() : null; - BlockFace blockFace = rayTraceResult != null ? rayTraceResult.getHitBlockFace() : null; - boolean shouldSneak = false; - if (block != null) { - ItemStack itemStack = player.getInventory().getItemInMainHand(); - if (block.getType().isInteractable() || block.getType() == Material.NOTE_BLOCK) { - shouldSneak = true; - } - if (CONTAINERS.contains(block.getType())) { - if (itemStack.getType() == Material.TNT) { - if (block.getType() == Material.CHEST || block.getType() == Material.BARREL || block.getType().name().endsWith("SHULKER_BOX")) { - shouldSneak = false; - } - } - } else { - shouldSneak = false; - } - if (IGNORED.contains(block.getType())) { - shouldSneak = false; - } - if (blockFace == BlockFace.DOWN && (block.getType() == Material.REPEATER || block.getType() == Material.COMPARATOR) && itemStack.getType().isSolid()) { - shouldSneak = true; + private static boolean shouldSmartPlace(Block block, BlockFace blockFace, Material heldType) { + boolean smartPlace = false; + if (block.getType().isInteractable() || block.getType() == Material.NOTE_BLOCK) { + smartPlace = true; + } + if (isContainer(block)) { + if (heldType == Material.TNT) { + if (block.getType() == Material.CHEST || block.getType() == Material.BARREL || block.getType().name().endsWith("SHULKER_BOX")) { + smartPlace = false; } } - boolean sneaking = player.isSneaking(); - if (sneaking) SMART_PLACING.add(player); - player.setSneaking(shouldSneak || sneaking); - Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { - SMART_PLACING.remove(player); - player.setSneaking(sneaking); - }, 0); - return packet; + } else { + smartPlace = false; + } + if (IGNORED.contains(block.getType())) { + smartPlace = false; + } + if (blockFace == BlockFace.DOWN && (block.getType() == Material.REPEATER || block.getType() == Material.COMPARATOR) && heldType.isSolid()) { + smartPlace = true; + } + return smartPlace; + } + + private static boolean smartPlaceEnabled(Player player) { + return Config.STORE.get(player).isSmartPlace(); + } + + @EventHandler(priority = EventPriority.LOW) + public void onSmartPlace(PlayerInteractEvent event) { + if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return; + Player player = event.getPlayer(); + if (player.getGameMode() == GameMode.SPECTATOR) return; + if (!Permission.BUILD.hasPermission(player)) return; + if (!smartPlaceEnabled(player)) return; + if (player.isSneaking()) return; + + ItemStack itemStack = event.getItem(); + if (itemStack == null || itemStack.getType().isAir()) return; + if (itemStack.getType() == Material.DEBUG_STICK) return; + if (!shouldSmartPlace(event.getClickedBlock(), event.getBlockFace(), itemStack.getType())) return; + + player.setSneaking(true); + FAKED_SNEAK.add(player.getUniqueId()); + Bukkit.getScheduler().runTask(BauSystem.getInstance(), () -> { + if (FAKED_SNEAK.remove(player.getUniqueId())) { + player.setSneaking(false); + } }); } + @EventHandler + public void onQuit(PlayerQuitEvent event) { + FAKED_SNEAK.remove(event.getPlayer().getUniqueId()); + } + @EventHandler public void onPlayerInteract(PlayerInteractEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; - if (!Config.STORE.get(event.getPlayer()).isSmartPlace()) return; - WAS_EXECUTED.add(event.getPlayer()); + if (!smartPlaceEnabled(event.getPlayer())) return; if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return; if (event.getPlayer().getGameMode() == GameMode.SPECTATOR) return; - if (!event.getPlayer().isSneaking()) return; + if (!isReallySneaking(event.getPlayer())) return; if (event.getClickedBlock().getType() != Material.REPEATER) return; if ((event.getItem() == null || event.getItem().getType() == Material.REPEATER)) { Repeater repeater = (Repeater) event.getClickedBlock().getBlockData(); @@ -149,9 +139,9 @@ public class SmartPlaceListener implements Listener { @EventHandler public void onBlockPlace(BlockPlaceEvent event) { if (!Permission.BUILD.hasPermission(event.getPlayer())) return; - if (!Config.STORE.get(event.getPlayer()).isSmartPlace()) return; - if (!SMART_PLACING.contains(event.getPlayer())) { - if (CONTAINERS.contains(event.getBlockAgainst().getType())) { + if (!smartPlaceEnabled(event.getPlayer())) return; + if (!isReallySneaking(event.getPlayer())) { + if (isContainer(event.getBlockAgainst())) { SoundGroup soundGroup = event.getBlockPlaced().getBlockData().getSoundGroup(); event.getPlayer().playSound(event.getBlockPlaced().getLocation(), soundGroup.getPlaceSound(), soundGroup.getVolume() * 0.8F, soundGroup.getPitch() * 0.8F); } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/SpectatorListener.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/SpectatorListener.java index ae32e0c1..a046ad22 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/SpectatorListener.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/SpectatorListener.java @@ -108,7 +108,7 @@ public class SpectatorListener implements Listener { } public void addHidingRules(Player player, List rules) { - if (!(Permission.BUILD.hasPermission(player) || NO_TECHHIDER.contains(player))) { + if (!NO_TECHHIDER.contains(player)) { rules.add(SPECTATOR_RULE); } } @@ -138,6 +138,9 @@ public class SpectatorListener implements Listener { @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { + if (Permission.BUILD.hasPermission(event.getPlayer())) { + NO_TECHHIDER.add(event.getPlayer()); + } if (BauSystem.DEV_SERVER) return; if (event.getPlayer().getUniqueId().equals(BauServer.getInstance().getOwner())) { return; @@ -159,17 +162,20 @@ public class SpectatorListener implements Listener { @EventHandler public void onBauMemberUpdate(BauMemberUpdateEvent event) { + event.getChanged().forEach(player -> { + if (Permission.BUILD.hasPermission(player)) { + NO_TECHHIDER.add(player); + } else { + NO_TECHHIDER.remove(player); + } + resendChunks(player); + }); if (!anySupervisorOnline(null) && !BauSystem.DEV_SERVER) { Bukkit.getOnlinePlayers().forEach(player -> { player.kickPlayer(""); }); return; } - - event.getChanged().forEach(player -> { - NO_TECHHIDER.remove(player); - resendChunks(player); - }); } @EventHandler diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/xray/XrayCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/xray/XrayCommand.java index c8f01679..09001a89 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/xray/XrayCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/xray/XrayCommand.java @@ -19,7 +19,6 @@ package de.steamwar.bausystem.features.xray; -import com.comphenix.tinyprotocol.TinyProtocol; import de.steamwar.bausystem.BauSystem; import de.steamwar.bausystem.features.hullhider.HullHiderCommand; import de.steamwar.bausystem.features.techhider.HidingRule; @@ -30,12 +29,10 @@ import de.steamwar.core.CraftbukkitWrapper; import de.steamwar.linkage.Linked; import de.steamwar.linkage.LinkedInstance; import de.steamwar.techhider.ObfuscationConfig; +import io.papermc.paper.event.player.PlayerFailMoveEvent; +import io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason; import net.md_5.bungee.api.ChatMessageType; -import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; -import net.minecraft.server.level.ServerPlayer; -import org.bukkit.Bukkit; import org.bukkit.Material; -import org.bukkit.craftbukkit.entity.CraftPlayer; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -45,7 +42,6 @@ import org.bukkit.event.player.PlayerQuitEvent; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiFunction; @Linked public class XrayCommand extends SWCommand implements Listener { @@ -103,27 +99,12 @@ public class XrayCommand extends SWCommand implements Listener { }); } - { - BiFunction positionSetter = (player, packet) -> { - Region region = Region.getRegion(player.getLocation()); - if (hidden.containsKey(region) && hidden.get(region).contains(player)) { - Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), () -> { - ServerPlayer serverPlayer = ((CraftPlayer) player).getHandle(); - if (packet.hasPos) { - serverPlayer.setPosRaw(packet.x, packet.y, packet.z); - } - if (packet.hasRot) { - serverPlayer.setXRot(packet.xRot); - serverPlayer.setYRot(packet.yRot); - } - }, 0); - return null; - } - return packet; - }; - - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Pos.class, positionSetter); - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.PosRot.class, positionSetter); + @EventHandler + public void onFailMove(PlayerFailMoveEvent event) { + if (event.getFailReason() != FailReason.CLIPPED_INTO_BLOCK) return; + Region region = Region.getRegion(event.getTo()); + if (!hidden.containsKey(region) || !hidden.get(region).contains(event.getPlayer())) return; + event.setAllowed(true); } @EventHandler diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java index fd961d96..ed56d1b9 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java @@ -132,6 +132,10 @@ public class FightHullHider { return hullHider.isBlockHidden(player, x, y, z); } + public boolean isEntityHidden(Player player, int entityId) { + return hullHider.isEntityHidden(player, entityId); + } + private void onTick() { Recording.iterateOverEntities(Objects::nonNull, hullHider::checkEntity); hullHider.sendUncoveredBlocks(); diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java index 534e7cee..58cd1a45 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java @@ -39,7 +39,6 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import org.bukkit.GameMode; -import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -133,16 +132,9 @@ public class TechHiderWrapper extends StateDependent implements Listener { return !isHidden(blockX, blockY, blockZ) || !obfuscation.hidesBlockState(blockState); } - // TODO will require entity tracking on the netty thread to prevent future race conditions @Override public boolean canAccessEntity(int entityId) { - net.minecraft.world.entity.Entity nmsEntity = ((CraftWorld) player.getWorld()).getHandle().moonrise$getEntityLookup().get(entityId); - - if (nmsEntity != null) { - return !hullHider.isBlockHidden(player, nmsEntity.getBlockX(), nmsEntity.getBlockY(), nmsEntity.getBlockZ()); - } else { - return true; - } + return !hullHider.isEntityHidden(player, entityId); } @Override diff --git a/HiderCore/src/de/steamwar/hullhider/Hull.java b/HiderCore/src/de/steamwar/hullhider/Hull.java index 02f8c56c..a05765f9 100644 --- a/HiderCore/src/de/steamwar/hullhider/Hull.java +++ b/HiderCore/src/de/steamwar/hullhider/Hull.java @@ -53,6 +53,7 @@ import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.logging.Level; @@ -81,8 +82,9 @@ public class Hull { private final HashSet uncoveredSurface = new HashSet<>(); private final Viewers viewers; - private final HashSet entities = new HashSet<>(); - private final HashSet rentities = new HashSet<>(); + // Concurrent as it is read on netty threads, keyed by entity id + private final Map entities = new ConcurrentHashMap<>(); + private final Set rentities = ConcurrentHashMap.newKeySet(); /** * @param groundVisible whether the bottom layer of the region is uncovered (if the region does not reach the ground) @@ -162,6 +164,10 @@ public class Hull { return region.contains(x, y, z) && !isViewer(player) && !visibility.get(region.toIndex(x, y, z)); } + public boolean isEntityHidden(Player player, int entityId) { + return player.getEntityId() != entityId && entities.containsKey(entityId) && !isViewer(player); + } + public boolean isViewer(Player player) { return player != null && viewers.isViewer(player); } @@ -187,13 +193,13 @@ public class Hull { private void updateEntities(Player player, boolean wasViewer) { if (isViewer(player)) { if (!wasViewer) { - for (Entity entity : entities) { + for (Entity entity : entities.values()) { trackEntity(player, entity); } } } else { // Also hides the entities from players that joined after they were hidden - for (Entity entity : entities) { + for (Entity entity : entities.values()) { if (!player.equals(entity)) untrackEntity(player, entity); } } @@ -210,13 +216,13 @@ public class Hull { public void checkEntity(Entity entity) { Location location = entity.getLocation(); if (region.contains(location) && !visibility.get(new IntVector(location).toId(region))) { - if (entities.add(entity)) { + if (entities.put(entity.getEntityId(), entity) == null) { for (Player player : Bukkit.getOnlinePlayers()) { if (!isViewer(player) && !player.equals(entity)) untrackEntity(player, entity); } } } else { - if (entities.remove(entity)) { + if (entities.remove(entity.getEntityId()) != null) { for (Player player : Bukkit.getOnlinePlayers()) { if (!isViewer(player) && !player.equals(entity)) trackEntity(player, entity); } @@ -241,7 +247,7 @@ public class Hull { } public void removeEntity(Entity entity) { - entities.remove(entity); + entities.remove(entity.getEntityId()); } public void checkREntity(REntity entity) { @@ -264,7 +270,7 @@ public class Hull { for (Player player : Bukkit.getOnlinePlayers()) { if (isViewer(player)) continue; - for (Entity entity : entities) { + for (Entity entity : entities.values()) { if (!player.equals(entity)) trackEntity(player, entity); } } diff --git a/HiderCore/src/de/steamwar/hullhider/HullHider.java b/HiderCore/src/de/steamwar/hullhider/HullHider.java index c2a5d1b7..3793fd0e 100644 --- a/HiderCore/src/de/steamwar/hullhider/HullHider.java +++ b/HiderCore/src/de/steamwar/hullhider/HullHider.java @@ -146,6 +146,16 @@ public class HullHider implements Listener { return false; } + public boolean isEntityHidden(Player player, int entityId) { + for (Hull hull : hulls) { + if (hull.isEntityHidden(player, entityId)) { + return true; + } + } + + return false; + } + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlockPhysic(BlockPhysicsEvent e) { if (e.getBlock() == e.getSourceBlock() || e.getChangedType() == Material.AIR) { diff --git a/HiderCore/src/de/steamwar/techhider/ChunkHider.java b/HiderCore/src/de/steamwar/techhider/ChunkHider.java index ab4eb45e..77bd672b 100644 --- a/HiderCore/src/de/steamwar/techhider/ChunkHider.java +++ b/HiderCore/src/de/steamwar/techhider/ChunkHider.java @@ -30,7 +30,8 @@ import net.minecraft.util.SimpleBitStorage; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; -import org.bukkit.entity.Player; +import org.bukkit.Bukkit; +import org.bukkit.World; import java.util.ArrayList; import java.util.Arrays; @@ -48,6 +49,15 @@ public class ChunkHider { private final int BLOCKS_PER_SECTION = 4096; private final int BIOMES_PER_SECTION = 64; + private final int worldMinHeight; + private final int worldMaxHeight; + + public ChunkHider() { + World world = Bukkit.getWorlds().getFirst(); + worldMinHeight = world.getMinHeight(); + worldMaxHeight = world.getMaxHeight(); + } + private int getLongsRequiredToEncodeEntries(int bitsPerEntry, int entryCount) { int entriesPerLong = Long.SIZE / bitsPerEntry; int dataLengthAsLongCount = (entryCount + entriesPerLong - 1) / entriesPerLong; @@ -67,7 +77,6 @@ public class ChunkHider { } public ClientboundLevelChunkWithLightPacket processLevelChunkWithLightPacket(ForPlayer access, ClientboundLevelChunkWithLightPacket packet) { - Player player = access.getPlayer(); int chunkX = packet.getX(); int chunkZ = packet.getZ(); ClientboundLevelChunkPacketData chunkData = packet.getChunkData(); @@ -75,9 +84,6 @@ public class ChunkHider { ByteBuf in = Unpooled.wrappedBuffer(chunkData.getReadBuffer()); ByteBuf out = Unpooled.buffer(in.readableBytes() + 64); - int worldMinHeight = player.getWorld().getMinHeight(); - int worldMaxHeight = player.getWorld().getMaxHeight(); - for (int yOffset = worldMinHeight; yOffset < worldMaxHeight; yOffset += SECTION_SPAN_SIZE) { short blockCount = in.readShort(); diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/AntiNocom.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/AntiNocom.java deleted file mode 100644 index 6bce3aa4..00000000 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/AntiNocom.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2025 SteamWar.de-Serverteam - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package de.steamwar.core.events; - -import com.comphenix.tinyprotocol.TinyProtocol; -import de.steamwar.core.Core; -import de.steamwar.core.ProtocolUtils; -import de.steamwar.linkage.Linked; -import de.steamwar.sql.SWException; -import net.minecraft.core.BlockPos; -import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; -import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerQuitEvent; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -@Linked -public class AntiNocom implements Listener { - - private final Map flags = new ConcurrentHashMap<>(); - - public AntiNocom() { - TinyProtocol.instance.addFilter(ServerboundPlayerActionPacket.class, this::onDig); - registerUseItem(); - } - - @EventHandler - public void onQuit(PlayerQuitEvent e) { - flags.remove(e.getPlayer()); - } - - private void registerUseItem() { - TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, (player, packet) -> { - BlockPos pos = packet.getHitResult().getBlockPos(); - return isValid(player, "UseItem", pos.getX(), pos.getZ()) ? packet : null; - }); - } - - private ServerboundPlayerActionPacket onDig(Player player, ServerboundPlayerActionPacket packet) { - BlockPos pos = packet.getPos(); - return isValid(player, "Dig", pos.getX(), pos.getZ()) ? packet : null; - } - - private boolean isValid(Player player, String type, int x, int z) { - if ((x == 0 && z == 0) || player.getWorld().isChunkLoaded(ProtocolUtils.posToChunk(x), ProtocolUtils.posToChunk(z))) { - return true; - } - - int amount = flags.compute(player, (p, a) -> a == null ? 1 : a + 1); - if (amount % 8 == 0) { // Only after 8 and every 8 to reduce false flags and spam - if (amount == 8) { - // Schedule player kick only once - Bukkit.getScheduler().runTask(Core.getInstance(), () -> player.kickPlayer(null)); - } - - SWException.log(player.getName() + " kicked for potential NoCom-DOS attack", x + " " + z + " " + type + " " + amount); - } - return false; - } -} diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/ConnectedSearch.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/ConnectedSearch.java new file mode 100644 index 00000000..8dd76d30 --- /dev/null +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/ConnectedSearch.java @@ -0,0 +1,475 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar.core.worldedit; + +import com.fastasyncworldedit.core.queue.IChunkGet; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.World; +import com.sk89q.worldedit.world.block.BlockState; +import com.sk89q.worldedit.world.block.BlockType; +import com.sk89q.worldedit.world.block.BlockTypes; +import com.sk89q.worldedit.world.block.BlockTypesCache; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.server.level.ServerLevel; + +/** + * Bounds of the blocks face-connected to a start block, meant to run off the main thread. + *

+ * Works on 16³ chunk sections as bitsets of 64 longs, indexed like FAWE's section arrays + * (bit y << 8 | z << 4 | x, so a long holds four 16 block x-rows of one y layer). + * Each section is classified once from a FAWE copy of its blocks, then filled with bit operations: + * whole x-rows at once, z and y one layer per pass, repeated until nothing changes, and + * handed over to the neighbouring sections through their faces. + * Unloaded chunks count as not connected, so a search never loads chunks. + * A search must only be used by one thread at a time. + */ +final class ConnectedSearch { + + // Horizontal search box around the start block, larger than every schematic type + static final int RADIUS = 255; + + private static final int WORDS = 4096 / 64; + // Lowest and highest x of each 16 bit x-row in a word + private static final long ROW_FIRST = 0x0001_0001_0001_0001L; + private static final long ROW_LAST = 0x8000_8000_8000_8000L; + private static final long ROW = 0xFFFFL; + + /** + * Where a search reads its blocks from. + */ + @FunctionalInterface + interface SectionSource { + /** + * @return the blocks of a section as FAWE block state ordinals, null if it is not loaded or empty + */ + char[] blocks(int sectionX, int sectionY, int sectionZ); + + /** + * Reads loaded chunks through FAWE, which copies the sections thread safely. + * Caches the chunks, so it belongs to one search. + */ + static SectionSource loaded(World world, ServerLevel level) { + Long2ObjectOpenHashMap chunks = new Long2ObjectOpenHashMap<>(); + return (sectionX, sectionY, sectionZ) -> { + long key = (long) sectionX << 32 | (sectionZ & 0xFFFFFFFFL); + IChunkGet chunk; + if (chunks.containsKey(key)) { + chunk = chunks.get(key); + } else { + // Paper's lookup for any thread, FAWE itself would load missing chunks + chunk = level.getChunkSource().getChunkAtIfLoadedImmediately(sectionX, sectionZ) == null ? null : world.get(sectionX, sectionZ); + chunks.put(key, chunk); + } + return chunk == null || !chunk.hasSection(sectionY) ? null : chunk.load(sectionY); + }; + } + } + + private final SectionSource source; + private final SteamEdit.AdjacentFilter filter; + + // Search box in block coordinates, inclusive + private final int minX; + private final int minY; + private final int minZ; + private final int maxX; + private final int maxY; + private final int maxZ; + + private final int minSectionX; + private final int minSectionY; + private final int minSectionZ; + private final int sectionsX; + private final int sectionsY; + private final int sectionsZ; + + // Per section: null until classified, EMPTY if nothing in it can be connected + private final Section[] sections; + private int[] queue = new int[256]; + private int queueSize = 0; + + private boolean complete = true; + + ConnectedSearch(World world, ServerLevel level, int x, int y, int z, SteamEdit.AdjacentFilter filter) { + this(SectionSource.loaded(world, level), world.getMinY(), world.getMaxY(), x, y, z, filter); + } + + /** + * @param worldMinY lowest block y of the world + * @param worldMaxY highest block y of the world + */ + ConnectedSearch(SectionSource source, int worldMinY, int worldMaxY, int x, int y, int z, SteamEdit.AdjacentFilter filter) { + this.source = source; + this.filter = filter; + + int boxMinX = x - RADIUS, boxMinY = worldMinY, boxMinZ = z - RADIUS; + int boxMaxX = x + RADIUS, boxMaxY = worldMaxY, boxMaxZ = z + RADIUS; + if (filter instanceof SteamEdit.BoxFilter box) { + boxMinX = Math.max(boxMinX, box.minX()); + boxMinY = Math.max(boxMinY, box.minY()); + boxMinZ = Math.max(boxMinZ, box.minZ()); + boxMaxX = Math.min(boxMaxX, box.maxX()); + boxMaxY = Math.min(boxMaxY, box.maxY()); + boxMaxZ = Math.min(boxMaxZ, box.maxZ()); + } + minX = boxMinX; + minY = boxMinY; + minZ = boxMinZ; + maxX = boxMaxX; + maxY = boxMaxY; + maxZ = boxMaxZ; + + minSectionX = minX >> 4; + minSectionY = minY >> 4; + minSectionZ = minZ >> 4; + sectionsX = (maxX >> 4) - minSectionX + 1; + sectionsY = (maxY >> 4) - minSectionY + 1; + sectionsZ = (maxZ >> 4) - minSectionZ + 1; + sections = new Section[sectionsX * sectionsY * sectionsZ]; + + // The start block is part of the result regardless of its type, like WorldEdit's RecursiveVisitor + int start = sectionIndex(x >> 4, y >> 4, z >> 4); + Section section = section(start); + if (section == Section.EMPTY) { + section = new Section(x >> 4, y >> 4, z >> 4); + sections[start] = section; + } + int bit = bit(x & 15, y & 15, z & 15); + section.reached[bit >>> 6] |= 1L << bit; + enqueue(start, section); + } + + /** + * @param deadline {@link System#nanoTime()} after which the search stops with the bounds found so far + */ + ConnectedSearch run(long deadline) { + while (queueSize > 0) { + if (System.nanoTime() > deadline) { + complete = false; + break; + } + + int index = queue[--queueSize]; + Section section = sections[index]; + section.queued = false; + section.fill(); + spread(section); + } + return this; + } + + BlockVector3[] bounds() { + int[] min = {Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE}; + int[] max = {Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE}; + for (Section section : sections) { + if (section != null && section != Section.EMPTY) section.bounds(min, max); + } + return new BlockVector3[]{BlockVector3.at(min[0], min[1], min[2]), BlockVector3.at(max[0], max[1], max[2])}; + } + + boolean isComplete() { + return complete; + } + + private static int bit(int x, int y, int z) { + return y << 8 | z << 4 | x; + } + + private int sectionIndex(int sectionX, int sectionY, int sectionZ) { + return ((sectionY - minSectionY) * sectionsZ + (sectionZ - minSectionZ)) * sectionsX + (sectionX - minSectionX); + } + + private void enqueue(int index, Section section) { + if (section.queued) return; + section.queued = true; + if (queueSize == queue.length) { + int[] grown = new int[queue.length * 2]; + System.arraycopy(queue, 0, grown, 0, queueSize); + queue = grown; + } + queue[queueSize++] = index; + } + + /** + * Hands the reached blocks on the faces of a section over to its neighbours. + */ + private void spread(Section from) { + long[] reached = from.reached; + long[] face = new long[WORDS]; + + if (from.x > minSectionX) { + for (int w = 0; w < WORDS; w++) face[w] = (reached[w] & ROW_FIRST) << 15; + handOver(from.x - 1, from.y, from.z, face); + } + if (from.x < minSectionX + sectionsX - 1) { + for (int w = 0; w < WORDS; w++) face[w] = (reached[w] & ROW_LAST) >>> 15; + handOver(from.x + 1, from.y, from.z, face); + } + + if (from.z > minSectionZ) { + java.util.Arrays.fill(face, 0); + for (int y = 0; y < 16; y++) face[y << 2 | 3] = (reached[y << 2] & ROW) << 48; + handOver(from.x, from.y, from.z - 1, face); + } + if (from.z < minSectionZ + sectionsZ - 1) { + java.util.Arrays.fill(face, 0); + for (int y = 0; y < 16; y++) face[y << 2] = reached[y << 2 | 3] >>> 48; + handOver(from.x, from.y, from.z + 1, face); + } + + if (from.y > minSectionY) { + java.util.Arrays.fill(face, 0); + System.arraycopy(reached, 0, face, WORDS - 4, 4); + handOver(from.x, from.y - 1, from.z, face); + } + if (from.y < minSectionY + sectionsY - 1) { + java.util.Arrays.fill(face, 0); + System.arraycopy(reached, WORDS - 4, face, 0, 4); + handOver(from.x, from.y + 1, from.z, face); + } + } + + private void handOver(int sectionX, int sectionY, int sectionZ, long[] face) { + boolean any = false; + for (long word : face) { + if (word != 0) { + any = true; + break; + } + } + if (!any) return; + + int index = sectionIndex(sectionX, sectionY, sectionZ); + Section section = section(index); + if (section == Section.EMPTY) return; + + boolean changed = false; + for (int w = 0; w < WORDS; w++) { + long added = face[w] & section.solid[w] & ~section.reached[w]; + if (added != 0) { + section.reached[w] |= added; + changed = true; + } + } + if (changed) enqueue(index, section); + } + + /** + * @return the classified section, {@link Section#EMPTY} if it is unloaded, outside the search box or has nothing to connect + */ + private Section section(int index) { + Section section = sections[index]; + if (section != null) return section; + + int sectionX = index % sectionsX + minSectionX; + int sectionZ = index / sectionsX % sectionsZ + minSectionZ; + int sectionY = index / sectionsX / sectionsZ + minSectionY; + section = classify(sectionX, sectionY, sectionZ); + sections[index] = section; + return section; + } + + private Section classify(int sectionX, int sectionY, int sectionZ) { + char[] blocks = source.blocks(sectionX, sectionY, sectionZ); + if (blocks == null) return Section.EMPTY; + + Section section = new Section(sectionX, sectionY, sectionZ); + if (!classify(blocks, section.solid)) return Section.EMPTY; + restrict(section); + return section; + } + + /** + * Sets the connectable blocks of a section. + * + * @return whether the section has any connectable block + */ + private static boolean classify(char[] blocks, long[] solid) { + boolean[] connectable = Connectable.ORDINALS; + long any = 0; + for (int w = 0; w < WORDS; w++) { + int base = w << 6; + long word = 0; + for (int i = 0; i < 64; i++) { + if (connectable[blocks[base + i]]) word |= 1L << i; + } + solid[w] = word; + any |= word; + } + return any != 0; + } + + /** + * Removes the blocks outside the search box and the filter from a section. + */ + private void restrict(Section section) { + int baseX = section.x << 4, baseY = section.y << 4, baseZ = section.z << 4; + boolean inside = baseX >= minX && baseX + 15 <= maxX + && baseY >= minY && baseY + 15 <= maxY + && baseZ >= minZ && baseZ + 15 <= maxZ; + + if (!inside) { + long row = 0; + for (int x = 0; x < 16; x++) { + if (baseX + x >= minX && baseX + x <= maxX) row |= 1L << x; + } + for (int y = 0; y < 16; y++) { + boolean yInside = baseY + y >= minY && baseY + y <= maxY; + for (int zq = 0; zq < 4; zq++) { + long mask = 0; + for (int lane = 0; lane < 4; lane++) { + int z = baseZ + (zq << 2 | lane); + if (yInside && z >= minZ && z <= maxZ) mask |= row << (lane << 4); + } + section.solid[y << 2 | zq] &= mask; + } + } + } + + // Boxes are applied above, other filters have to be asked per block + if (filter != SteamEdit.AdjacentFilter.ALWAYS && !(filter instanceof SteamEdit.BoxFilter)) { + for (int bit = 0; bit < 4096; bit++) { + long flag = 1L << bit; + if ((section.solid[bit >>> 6] & flag) != 0 && !filter.contains(baseX + (bit & 15), baseY + (bit >>> 8), baseZ + (bit >>> 4 & 15))) { + section.solid[bit >>> 6] &= ~flag; + } + } + } + } + + // Initialized on the first search, once WorldEdit has loaded its block registry + private static final class Connectable { + // Same as Bukkit's !isEmpty() && !isLiquid(), indexed by FAWE's block state ordinal + static final boolean[] ORDINALS = new boolean[Character.MAX_VALUE + 1]; + + static { + BlockState[] states = BlockTypesCache.states; + // Ordinal 0 is FAWE's reserved "no block" + for (int ordinal = 1; ordinal < states.length; ordinal++) { + BlockType type = states[ordinal].getBlockType(); + ORDINALS[ordinal] = !type.getMaterial().isAir() && type != BlockTypes.WATER && type != BlockTypes.LAVA; + } + } + } + + private static final class Section { + static final Section EMPTY = new Section(0, 0, 0); + + final int x; + final int y; + final int z; + final long[] solid = new long[WORDS]; + final long[] reached = new long[WORDS]; + boolean queued = false; + + Section(int x, int y, int z) { + this.x = x; + this.y = y; + this.z = z; + } + + /** + * Extends the reached blocks over all connected solid blocks of this section. + * Ascending passes carry +y/+z within the pass, descending passes -y/-z. + */ + void fill() { + boolean changed = true; + boolean ascending = true; + while (changed) { + changed = false; + for (int i = 0; i < WORDS; i++) { + int w = ascending ? i : WORDS - 1 - i; + long s = solid[w]; + long r = reached[w]; + + long in = r + | (r << 1 & ~ROW_FIRST) | (r >>> 1 & ~ROW_LAST) + | r << 16 | r >>> 16; + if ((w & 3) != 0) in |= reached[w - 1] >>> 48; + if ((w & 3) != 3) in |= reached[w + 1] << 48; + if (w >= 4) in |= reached[w - 4]; + if (w < WORDS - 4) in |= reached[w + 4]; + + long next = r | fillRows(in & s, s); + if (next != r) { + reached[w] = next; + changed = true; + } + } + ascending = !ascending; + } + } + + /** + * Extends the seeds along the x-rows over contiguous solid blocks, in both directions. + */ + private static long fillRows(long seeds, long solid) { + long up = seeds; + long open = solid & ~ROW_FIRST; + up |= open & (up << 1); + open &= open << 1; + up |= open & (up << 2); + open &= open << 2; + up |= open & (up << 4); + open &= open << 4; + up |= open & (up << 8); + + long down = seeds; + open = solid & ~ROW_LAST; + down |= open & (down >>> 1); + open &= open >>> 1; + down |= open & (down >>> 2); + open &= open >>> 2; + down |= open & (down >>> 4); + open &= open >>> 4; + down |= open & (down >>> 8); + + return up | down; + } + + void bounds(int[] min, int[] max) { + long rows = 0; + int zMask = 0; + int lowWord = -1, highWord = -1; + for (int w = 0; w < WORDS; w++) { + long r = reached[w]; + if (r == 0) continue; + if (lowWord < 0) lowWord = w; + highWord = w; + for (int lane = 0; lane < 4; lane++) { + long row = r >>> (lane << 4) & ROW; + if (row != 0) { + rows |= row; + zMask |= 1 << ((w & 3) << 2 | lane); + } + } + } + if (lowWord < 0) return; + + int baseX = x << 4, baseY = y << 4, baseZ = z << 4; + min[0] = Math.min(min[0], baseX + Long.numberOfTrailingZeros(rows)); + max[0] = Math.max(max[0], baseX + 63 - Long.numberOfLeadingZeros(rows)); + min[1] = Math.min(min[1], baseY + (lowWord >>> 2)); + max[1] = Math.max(max[1], baseY + (highWord >>> 2)); + min[2] = Math.min(min[2], baseZ + Integer.numberOfTrailingZeros(zMask)); + max[2] = Math.max(max[2], baseZ + 31 - Integer.numberOfLeadingZeros(zMask)); + } + } +} diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEdit.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEdit.java index 03c7922e..616f7638 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEdit.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEdit.java @@ -52,6 +52,13 @@ public class SteamEdit { boolean contains(int x, int y, int z); } + public record BoxFilter(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) implements AdjacentFilter { + @Override + public boolean contains(int x, int y, int z) { + return x >= minX && x <= maxX && y >= minY && y <= maxY && z >= minZ && z <= maxZ; + } + } + private static final class Command extends SWCommand { public Command() { diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditSelectAdjacent.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditSelectAdjacent.java deleted file mode 100644 index fd6d0713..00000000 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditSelectAdjacent.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * This file is a part of the SteamWar software. - * - * Copyright (C) 2026 SteamWar.de-Serverteam - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package de.steamwar.core.worldedit; - -import com.sk89q.worldedit.math.BlockVector3; -import de.steamwar.core.Core; -import org.bukkit.Location; -import org.bukkit.block.BlockFace; -import org.bukkit.util.Vector; - -import java.util.LinkedHashSet; -import java.util.List; -import java.util.stream.Stream; - -public class SteamEditSelectAdjacent { - - private static final BlockFace[] FACES = { - BlockFace.NORTH, - BlockFace.SOUTH, - BlockFace.EAST, - BlockFace.WEST, - BlockFace.UP, - BlockFace.DOWN, - }; - - private Location min; - private Location max; - - private SteamEdit.AdjacentFilter filter; - - private LinkedHashSet frontier = new LinkedHashSet<>(); - - public SteamEditSelectAdjacent(Location origin) { - // TODO: Fix the flood fill algorithm! - min = origin.clone(); - max = origin.clone(); - frontier.add(origin); - filter = SteamEdit.ADJACENT_FILTERS.apply(origin); - - long time = System.currentTimeMillis(); - for (int i = 0; i < 500_000 && !frontier.isEmpty(); i++) { - if (System.currentTimeMillis() - time > 1000) break; - expandNext(); - } - - Core.getInstance().getLogger().finer(System.currentTimeMillis() - time + "ms " + frontier.size() + " " + min + " to " + max); - } - - public BlockVector3 pos1() { - return BlockVector3.at(min.getBlockX(), min.getBlockY(), min.getBlockZ()); - } - - public BlockVector3 pos2() { - return BlockVector3.at(max.getBlockX(), max.getBlockY(), max.getBlockZ()); - } - - private boolean inside(Location loc, int offset) { - return loc.getBlockX() >= min.getBlockX() + offset && loc.getBlockX() <= max.getBlockX() - offset && - loc.getBlockY() >= min.getBlockY() + offset && loc.getBlockY() <= max.getBlockY() - offset && - loc.getBlockZ() >= min.getBlockZ() + offset && loc.getBlockZ() <= max.getBlockZ() - offset; - } - - public void expandNext() { - Location location = frontier.removeFirst(); - expand(location); - } - - private void expand(Location loc) { - for (BlockFace face : FACES) { - Vector direction = face.getDirection(); - Location location = loc.clone().add(direction); - if (!filter.contains(location.getBlockX(), location.getBlockY(), location.getBlockZ())) continue; - if (inside(location, 0)) continue; - List layer = Stream.concat(selectFrontier(direction.getBlockX() == 0 ? null : loc.getBlockX(), direction.getBlockY() == 0 ? null : loc.getBlockY(), direction.getBlockZ() == 0 ? null : loc.getBlockZ()) - .map(Location::clone) - .map(l -> l.add(direction)), Stream.of(location)) - .filter(l -> !l.getBlock().isEmpty()) - .filter(l -> !l.getBlock().isLiquid()) - .toList(); - if (layer.isEmpty()) continue; - - if (direction.getBlockX() < 0 || direction.getBlockY() < 0 || direction.getBlockZ() < 0) { - min.add(direction); - } else { - max.add(direction); - } - layer.stream().filter(l -> !inside(l, 1)) - .filter(l -> filter.contains(l.getBlockX(), l.getBlockY(), l.getBlockZ())) - .forEach(frontier::add); - } - } - - private Stream selectFrontier(Integer x, Integer y, Integer z) { - Stream stream = frontier.stream(); - if (x != null) stream = stream.filter(loc -> loc.getBlockX() == x); - if (y != null) stream = stream.filter(loc -> loc.getBlockY() == y); - if (z != null) stream = stream.filter(loc -> loc.getBlockZ() == z); - return stream; - } -} diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java index eb1f180a..d9793d73 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/SteamEditWand.java @@ -19,36 +19,39 @@ package de.steamwar.core.worldedit; -import com.comphenix.tinyprotocol.TinyProtocol; -import com.sk89q.worldedit.IncompleteRegionException; +import com.fastasyncworldedit.core.util.TaskManager; import com.sk89q.worldedit.LocalSession; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.bukkit.WorldEditPlugin; +import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.regions.RegionSelector; +import com.sk89q.worldedit.regions.selector.CuboidRegionSelector; import com.sk89q.worldedit.regions.selector.limit.PermissiveSelectorLimits; -import com.sk89q.worldedit.world.World; import de.steamwar.core.Core; -import de.steamwar.core.SWPlayer; import de.steamwar.inventory.SWItem; -import net.minecraft.core.BlockPos; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.game.*; +import io.papermc.paper.event.player.PlayerPickBlockEvent; +import net.minecraft.server.level.ServerLevel; import org.bukkit.*; import org.bukkit.attribute.Attribute; import org.bukkit.attribute.AttributeModifier; import org.bukkit.block.Block; +import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerSwapHandItemsEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; -import org.bukkit.scheduler.BukkitTask; -import org.bukkit.util.RayTraceResult; +import java.util.HashSet; import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; public class SteamEditWand implements Listener { @@ -73,287 +76,205 @@ public class SteamEditWand implements Listener { ITEM.setItemMeta(itemMeta); } - private boolean noWand(Player player) { - return player.getInventory().getItemInMainHand().getType() != SteamEditWand.MATERIAL; + private static final long MAX_CONNECTED_NANOS = TimeUnit.MILLISECONDS.toNanos(500); + + private boolean checkWand(Player player) { + ItemStack item = player.getInventory().getItemInMainHand(); + if (item.getType() != MATERIAL) return false; + if (!ITEM.equals(item)) player.getInventory().setItemInMainHand(ITEM); + return true; } private final WorldEditPlugin we; + private final Set selectingConnected = new HashSet<>(); SteamEditWand() { we = WorldEditWrapper.getWorldEditPlugin(); Bukkit.getPluginManager().registerEvents(this, Core.getInstance()); - - TinyProtocol.instance.addFilter(ServerboundSwingPacket.class, this::leftClick); - TinyProtocol.instance.addFilter(ServerboundInteractPacket.class, (player, packet) -> { - if (packet.isAttack()) { - return leftClick(player, packet); - } else { - return rightClick(player, packet); - } - }); - TinyProtocol.instance.addFilter(ServerboundUseItemPacket.class, this::rightClick); - TinyProtocol.instance.addFilter(ServerboundUseItemOnPacket.class, this::rightClick); - TinyProtocol.instance.addFilter(ServerboundPickItemFromBlockPacket.class, this::middleClick); - TinyProtocol.instance.addFilter(ServerboundPlayerActionPacket.class, this::swapHands); } @EventHandler public void onPlayerInteract(PlayerInteractEvent event) { - if (noWand(event.getPlayer())) return; + Player player = event.getPlayer(); + if (!checkWand(player)) return; + Block block = event.getClickedBlock(); + if (block == null) return; + BlockVector3 clicked = BukkitAdapter.asBlockVector(block.getLocation()); + switch (event.getAction()) { + case LEFT_CLICK_BLOCK -> { + setCorner(player, Corner.PRIMARY, clicked); + } + case RIGHT_CLICK_BLOCK -> { + if (player.isSneaking()) { + setPos2OrExpand(player, clicked); + } else { + setCorner(player, Corner.SECONDARY, clicked); + } + } + default -> { + return; + } + } event.setCancelled(true); } - private Packet leftClick(Player player, Packet packet) { - if (noWand(player)) return packet; - if (player.getOpenInventory().getType() != InventoryType.CREATIVE) return packet; - double distance = WorldEditRendererCUIEditor.Type.CURSOR.getWidth(player).blocks; - RayTraceResult result = player.rayTraceBlocks(distance, FluidCollisionMode.NEVER); - if (result == null) return packet; - Block block = result.getHitBlock(); - if (block == null) return packet; - BlockVector3 pos1 = BlockVector3.at(block.getX(), block.getY(), block.getZ()); - setPos1(player, pos1); - return null; - } - - private void setPos1(Player player, BlockVector3 pos1) { + private void setCorner(Player player, Corner corner, BlockVector3 position) { LocalSession session = we.getSession(player); - World world = session.getSelectionWorld(); - if (world == null) return; - selectPos1(session.getRegionSelector(world), player, session, pos1); + select(corner, regionSelector(player, session), player, session, position); } - private Packet rightClick(Player player, Packet packet) { - if (noWand(player)) return packet; - double distance = WorldEditRendererCUIEditor.Type.CURSOR.getWidth(player).blocks; - RayTraceResult result = player.rayTraceBlocks(distance, FluidCollisionMode.NEVER); - if (result == null) return packet; - Block block = result.getHitBlock(); - if (block == null) return packet; - BlockVector3 pos2 = BlockVector3.at(block.getX(), block.getY(), block.getZ()); - if (!player.isSneaking()) { - setPos2(player, pos2); - } else { - setPos2OrExpand(player, pos2); - } - return null; - } - - private void setPos2(Player player, BlockVector3 pos2) { - LocalSession session = we.getSession(player); - World world = session.getSelectionWorld(); - if (world == null) return; - selectPos2(session.getRegionSelector(world), player, session, pos2); + private static RegionSelector regionSelector(Player player, LocalSession session) { + return session.getRegionSelector(BukkitAdapter.adapt(player.getWorld())); } private void setPos2OrExpand(Player player, BlockVector3 expand) { LocalSession session = we.getSession(player); - World world = session.getSelectionWorld(); - if (world == null) return; - RegionSelector regionSelector = session.getRegionSelector(world); + RegionSelector regionSelector = regionSelector(player, session); - // Only primary set for now - if (!regionSelector.isDefined()) { - selectPos2(session.getRegionSelector(world), player, session, expand); - return; - } - - List vertices = regionSelector.getVertices(); - // Probably not a cuboid selection! - if (vertices.size() > 2) { - selectPos2(session.getRegionSelector(world), player, session, expand); + if (!(regionSelector instanceof CuboidRegionSelector cuboidSelector) || !cuboidSelector.isDefined()) { + select(Corner.SECONDARY, regionSelector, player, session, expand); return; } + List vertices = cuboidSelector.getVertices(); BlockVector3 pos1 = vertices.get(0); BlockVector3 pos2 = vertices.get(1); + if (expand.containedWithin(pos1.getMinimum(pos2), pos1.getMaximum(pos2))) return; - BlockVector3 min = pos1.getMinimum(pos2); - BlockVector3 max = pos1.getMaximum(pos2); - if (expand.containedWithin(pos1, pos2)) { - return; + int[][] corners = {toArray(pos1), toArray(pos2)}; + int[] target = toArray(expand); + + Corner[] closer = new Corner[3]; + for (int axis = 0; axis < 3; axis++) { + closer[axis] = Corner.closer(corners[0][axis], corners[1][axis], target[axis]); } - Closer x = Closer.UNKNOWN; - Closer y = Closer.UNKNOWN; - Closer z = Closer.UNKNOWN; - - boolean insideX = min.x() < expand.x() && max.x() > expand.x(); - boolean insideY = min.y() < expand.y() && max.y() > expand.y(); - boolean insideZ = min.z() < expand.z() && max.z() > expand.z(); - - if (insideX) { - // No Change needed - } else if (Math.abs(pos1.x() - expand.x()) > Math.abs(pos2.x() - expand.x())) { - x = Closer.POS2; - } else if (Math.abs(pos1.x() - expand.x()) < Math.abs(pos2.x() - expand.x())) { - x = Closer.POS1; + for (int axis = 0; axis < 3; axis++) { + Corner corner = closer[axis]; + // Both corners share this coordinate, move the one the other axes agree on + if (corners[0][axis] == corners[1][axis] && corners[0][axis] != target[axis]) { + corner = Objects.requireNonNullElse(Corner.agree(closer[(axis + 1) % 3], closer[(axis + 2) % 3]), Corner.PRIMARY); + } + if (corner != null) corners[corner.ordinal()][axis] = target[axis]; } - if (insideY) { - // No Change needed - } else if (Math.abs(pos1.y() - expand.y()) > Math.abs(pos2.y() - expand.y())) { - y = Closer.POS2; - } else if (Math.abs(pos1.y() - expand.y()) < Math.abs(pos2.y() - expand.y())) { - y = Closer.POS1; - } - - if (insideZ) { - // No Change needed - } else if (Math.abs(pos1.z() - expand.z()) > Math.abs(pos2.z() - expand.z())) { - z = Closer.POS2; - } else if (Math.abs(pos1.z() - expand.z()) < Math.abs(pos2.z() - expand.z())) { - z = Closer.POS1; - } - - Closer x1 = Closer.UNKNOWN; - Closer y1 = Closer.UNKNOWN; - Closer z1 = Closer.UNKNOWN; - - if (!insideX && pos1.x() != expand.x() && pos2.x() != expand.x() && x == Closer.UNKNOWN) { - x1 = y.and(z).or(Closer.POS1); - } - if (!insideY && pos1.y() != expand.y() && pos2.y() != expand.y() && y == Closer.UNKNOWN) { - y1 = x.and(z).or(Closer.POS1); - } - if (!insideZ && pos1.z() != expand.z() && pos2.z() != expand.z() && z == Closer.UNKNOWN) { - z1 = x.and(y).or(Closer.POS1); - } - - if (x1 != Closer.UNKNOWN) x = x1; - if (y1 != Closer.UNKNOWN) y = y1; - if (z1 != Closer.UNKNOWN) z = z1; - - boolean p1updated = false; - boolean p2updated = false; - - if (x == Closer.POS1) { - pos1 = pos1.withX(expand.x()); - p1updated = true; - } else if (x == Closer.POS2) { - pos2 = pos2.withX(expand.x()); - p2updated = true; - } - - if (y == Closer.POS1) { - pos1 = pos1.withY(expand.y()); - p1updated = true; - } else if (y == Closer.POS2) { - pos2 = pos2.withY(expand.y()); - p2updated = true; - } - - if (z == Closer.POS1) { - pos1 = pos1.withZ(expand.z()); - p1updated = true; - } else if (z == Closer.POS2) { - pos2 = pos2.withZ(expand.z()); - p2updated = true; - } - - if (p1updated) selectPos1(session.getRegionSelector(world), player, session, pos1); - if (p2updated) selectPos2(session.getRegionSelector(world), player, session, pos2); + BlockVector3 newPos1 = BlockVector3.at(corners[0][0], corners[0][1], corners[0][2]); + BlockVector3 newPos2 = BlockVector3.at(corners[1][0], corners[1][1], corners[1][2]); + if (!newPos1.equals(pos1)) select(Corner.PRIMARY, regionSelector, player, session, newPos1); + if (!newPos2.equals(pos2)) select(Corner.SECONDARY, regionSelector, player, session, newPos2); } - private enum Closer { - POS1, - POS2, - UNKNOWN, + private static int[] toArray(BlockVector3 vector) { + return new int[]{vector.x(), vector.y(), vector.z()}; + } + + @EventHandler + public void onPickBlock(PlayerPickBlockEvent event) { + Player player = event.getPlayer(); + if (!checkWand(player)) return; + event.setCancelled(true); + if (!selectingConnected.add(player.getUniqueId())) return; + + Block origin = event.getBlock(); + World world = origin.getWorld(); + SteamEdit.AdjacentFilter filter = SteamEdit.ADJACENT_FILTERS.apply(origin.getLocation()); + com.sk89q.worldedit.world.World weWorld = BukkitAdapter.adapt(world); + ServerLevel level = ((CraftWorld) world).getHandle(); + int x = origin.getX(), y = origin.getY(), z = origin.getZ(); + + TaskManager.taskManager().async(() -> { + BlockVector3[] bounds = null; + try { + bounds = new ConnectedSearch(weWorld, level, x, y, z, filter) + .run(System.nanoTime() + MAX_CONNECTED_NANOS) + .bounds(); + } catch (Exception e) { + Core.getInstance().getLogger().log(Level.SEVERE, "Could not select connected blocks", e); + } + + BlockVector3[] result = bounds; + TaskManager.taskManager().task(() -> { + selectingConnected.remove(player.getUniqueId()); + if (result != null && player.isOnline()) selectConnected(player, result); + }); + }); + } + + private void selectConnected(Player player, BlockVector3[] bounds) { + LocalSession session = we.getSession(player); + RegionSelector regionSelector = regionSelector(player, session); + + boolean primaryChanged = Corner.PRIMARY.changed(regionSelector, bounds[0]); + boolean secondaryChanged = Corner.SECONDARY.changed(regionSelector, bounds[1]); + Corner.PRIMARY.select(regionSelector, bounds[0]); + Corner.SECONDARY.select(regionSelector, bounds[1]); + Actor actor = BukkitAdapter.adapt(player); + if (primaryChanged) Corner.PRIMARY.explain(regionSelector, actor, session, bounds[0]); + if (secondaryChanged) Corner.SECONDARY.explain(regionSelector, actor, session, bounds[1]); + WorldEditRenderer.renderPlayer(player); + } + + private enum Corner { + PRIMARY { + @Override + void select(RegionSelector regionSelector, BlockVector3 position) { + regionSelector.selectPrimary(position, PermissiveSelectorLimits.getInstance()); + } + + @Override + void explain(RegionSelector regionSelector, Actor actor, LocalSession session, BlockVector3 position) { + regionSelector.explainPrimarySelection(actor, session, position); + } + }, + SECONDARY { + @Override + void select(RegionSelector regionSelector, BlockVector3 position) { + regionSelector.selectSecondary(position, PermissiveSelectorLimits.getInstance()); + } + + @Override + void explain(RegionSelector regionSelector, Actor actor, LocalSession session, BlockVector3 position) { + regionSelector.explainSecondarySelection(actor, session, position); + } + }, ; - public Closer and(Closer closer) { - if (this == closer) { - return this; - } else if (this == UNKNOWN) { - return closer; - } else if (closer == UNKNOWN) { - return this; - } else { - return UNKNOWN; - } + static Corner closer(int primary, int secondary, int target) { + if (Math.min(primary, secondary) < target && target < Math.max(primary, secondary)) return null; + int difference = Math.abs(primary - target) - Math.abs(secondary - target); + if (difference < 0) return PRIMARY; + if (difference > 0) return SECONDARY; + return null; } - public Closer or(Closer closer) { - if (this == UNKNOWN) return closer; - return this; + static Corner agree(Corner a, Corner b) { + if (a == null) return b; + if (b == null || a == b) return a; + return null; } + + boolean changed(RegionSelector regionSelector, BlockVector3 position) { + BlockVector3 current = regionSelector instanceof CuboidRegionSelector cuboidSelector ? cuboidSelector.getVertices().get(ordinal()) : null; + return !position.equals(current); + } + + abstract void select(RegionSelector regionSelector, BlockVector3 position); + + abstract void explain(RegionSelector regionSelector, Actor actor, LocalSession session, BlockVector3 position); } - private ServerboundPickItemFromBlockPacket middleClick(Player player, ServerboundPickItemFromBlockPacket packet) { - if (noWand(player)) return packet; - LocalSession session = we.getSession(player); - World world = session.getSelectionWorld(); - if (world == null) return packet; - RegionSelector regionSelector = session.getRegionSelector(world); - BlockPos pos = packet.pos(); - SteamEditSelectAdjacent selector = new SteamEditSelectAdjacent(new Location(player.getWorld(), pos.getX(), pos.getY(), pos.getZ())); - selectPos1(regionSelector, player, session, selector.pos1()); - selectPos2(regionSelector, player, session, selector.pos2()); - return null; - } - - private static class Pos1Message implements SWPlayer.Component { - private BukkitTask task; - - public Pos1Message(RegionSelector regionSelector, Player player, LocalSession session, BlockVector3 pos1) { - task = Bukkit.getScheduler().runTaskLater(Core.getInstance(), () -> { - regionSelector.explainPrimarySelection(BukkitAdapter.adapt(player), session, pos1); - SWPlayer.of(player).removeComponent(Pos1Message.class); - }, 1); - } - - @Override - public void onUnmount(SWPlayer player) { - task.cancel(); - } - } - - private void selectPos1(RegionSelector regionSelector, Player player, LocalSession session, BlockVector3 pos1) { - try { - if (!pos1.equals(regionSelector.getPrimaryPosition())) { - SWPlayer.of(player).setComponent(new Pos1Message(regionSelector, player, session, pos1)); - } - } catch (IncompleteRegionException e) { - SWPlayer.of(player).setComponent(new Pos1Message(regionSelector, player, session, pos1)); - } - regionSelector.selectPrimary(pos1, PermissiveSelectorLimits.getInstance()); + private void select(Corner corner, RegionSelector regionSelector, Player player, LocalSession session, BlockVector3 position) { + boolean changed = corner.changed(regionSelector, position); + corner.select(regionSelector, position); + if (changed) corner.explain(regionSelector, BukkitAdapter.adapt(player), session, position); WorldEditRenderer.renderPlayer(player); } - private static class Pos2Message implements SWPlayer.Component { - private BukkitTask task; - - public Pos2Message(RegionSelector regionSelector, Player player, LocalSession session, BlockVector3 pos2) { - task = Bukkit.getScheduler().runTaskLater(Core.getInstance(), () -> { - regionSelector.explainSecondarySelection(BukkitAdapter.adapt(player), session, pos2); - SWPlayer.of(player).removeComponent(Pos1Message.class); - }, 1); - } - - @Override - public void onUnmount(SWPlayer player) { - task.cancel(); - } - } - - private void selectPos2(RegionSelector regionSelector, Player player, LocalSession session, BlockVector3 pos2) { - try { - if (regionSelector.getVertices().size() == 2 && !pos2.equals(regionSelector.getVertices().get(1))) { - SWPlayer.of(player).setComponent(new Pos2Message(regionSelector, player, session, pos2)); - } - } catch (IncompleteRegionException e) { - SWPlayer.of(player).setComponent(new Pos2Message(regionSelector, player, session, pos2)); - } - regionSelector.selectSecondary(pos2, PermissiveSelectorLimits.getInstance()); - WorldEditRenderer.renderPlayer(player); - } - - private ServerboundPlayerActionPacket swapHands(Player player, ServerboundPlayerActionPacket packet) { - if (noWand(player)) return packet; - if (packet.getAction() != ServerboundPlayerActionPacket.Action.SWAP_ITEM_WITH_OFFHAND) return packet; - Bukkit.getScheduler().runTask(Core.getInstance(), () -> { - WorldEditRendererCUIEditor.open(player, true); - }); - return null; + @EventHandler + public void onSwapHands(PlayerSwapHandItemsEvent event) { + Player player = event.getPlayer(); + if (!checkWand(player)) return; + event.setCancelled(true); + WorldEditRendererCUIEditor.open(player, true); } } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/cursor/CursorListener.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/cursor/CursorListener.java index 0b919898..0c94d05a 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/cursor/CursorListener.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/cursor/CursorListener.java @@ -1,19 +1,13 @@ package de.steamwar.cursor; -import com.comphenix.tinyprotocol.TinyProtocol; import de.steamwar.core.SWPlayer; import de.steamwar.linkage.Linked; import de.steamwar.linkage.LinkedTimer; import lombok.Getter; -import net.minecraft.network.protocol.Packet; -import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; -import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerInteractEvent; -import java.util.Optional; - @Linked public class CursorListener implements Listener { @@ -24,10 +18,6 @@ public class CursorListener implements Listener { if (instance == null) { instance = this; } - - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Pos.class, this::updateCursorFromPacket); - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Rot.class, this::updateCursorFromPacket); - TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.PosRot.class, this::updateCursorFromPacket); } @LinkedTimer(delay = 1, period = 1) @@ -37,15 +27,6 @@ public class CursorListener implements Listener { .forEach(Cursor::renderDeduplicated); } - public Packet updateCursorFromPacket(Player player, Packet packet) { - SWPlayer swPlayer = SWPlayer.of(player); - Optional activeCursor = swPlayer.getComponent(Cursor.class); - - activeCursor.ifPresent(Cursor::renderDeduplicated); - - return packet; - } - @EventHandler public void onPlayerInteract(PlayerInteractEvent event) { SWPlayer.of(event.getPlayer()).getComponent(Cursor.class).ifPresent(cursor -> {