no async world access #496

Merged
YoyoNow merged 7 commits from Hut512/SteamWar:fix/thread-safe-world-access into main 2026-11-06 09:02:52 +01:00
15 changed files with 782 additions and 600 deletions
@@ -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);
@@ -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 {
YoyoNow marked this conversation as resolved
Review

Den Diff der Klasse durchblicke ich aktuell noch nicht. Das muss ich mir glaube mal genauer anschauen.

Den Diff der Klasse durchblicke ich aktuell noch nicht. Das muss ich mir glaube mal genauer anschauen.
Review

Habe aus dem PacketFilter einen EventHandler gemacht und die Lookups (CONTAINERS + IGNORED) vereinfacht.

Habe aus dem PacketFilter einen EventHandler gemacht und die Lookups (CONTAINERS + IGNORED) vereinfacht.
Review

Alles klar ok.

Alles klar ok.
private static final Set<Material> CONTAINERS = new HashSet<>();
private static final Set<Material> IGNORED = new HashSet<>();
private static final Set<Material> 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<UUID> 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<Player> SMART_PLACING = new HashSet<>();
private static final Set<Player> 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);
}
@@ -108,7 +108,7 @@ public class SpectatorListener implements Listener {
}
public void addHidingRules(Player player, List<HidingRule> 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
@@ -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<Player, ServerboundMovePlayerPacket, Object> 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
@@ -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();
@@ -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
+14 -8
View File
@@ -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<IntVector> uncoveredSurface = new HashSet<>();
private final Viewers viewers;
private final HashSet<Entity> entities = new HashSet<>();
private final HashSet<REntity> rentities = new HashSet<>();
// Concurrent as it is read on netty threads, keyed by entity id
private final Map<Integer, Entity> entities = new ConcurrentHashMap<>();
private final Set<REntity> 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);
}
}
@@ -146,6 +146,16 @@ public class HullHider<K> 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) {
@@ -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();
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 {
Outdated
Review

Warum entfernst du diese Klasse?

Warum entfernst du diese Klasse?
Outdated
Review
PaperMC hat bereits einen robusteren Fix: https://github.com/PaperMC/Paper/blob/4d854e66b80dad9898d71a0f0b1f83cd39b97119/paper-server/patches/sources/net/minecraft/server/network/ServerGamePacketListenerImpl.java.patch#L1155-L1160
Outdated
Review

Gefühlt würde ich das nochmal drin lassen, weil der Path in paper ist zwar von vor 8 Jahren aber trotzdem ist dieser Bug aufgekommen und war exploitable.

Gefühlt würde ich das nochmal drin lassen, weil der Path in paper ist zwar von vor 8 Jahren aber trotzdem ist dieser Bug aufgekommen und war exploitable.
private final Map<Player, Integer> 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;
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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.
* <p>
* 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<IChunkGet> 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));
}
}
}
@@ -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() {
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Location> 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<Location> 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<Location> selectFrontier(Integer x, Integer y, Integer z) {
Stream<Location> 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;
}
}
@@ -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 {
3
@@ -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<UUID> 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<BlockVector3> 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<BlockVector3> 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);
}
}
@@ -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);
YoyoNow marked this conversation as resolved Outdated
Outdated
Review

Das hier war schon absichtlich so gebaut, sodass der Cursor snappier sich anfühlt. Paper Events sind nicht immer die schnellsten insbesondere beim Movement Packet

Das hier war schon absichtlich so gebaut, sodass der Cursor snappier sich anfühlt. Paper Events sind nicht immer die schnellsten insbesondere beim Movement Packet
Outdated
Review

Der Cursor wird doch bereits 20 Mal pro Sekunde aktualisiert. Ich habe beide Varianten (mit und ohne PacketFilter) direkt nacheinander getestet und konnte keinen Unterschied feststellen. Außerdem sollte man Cursor::render nicht von Netty aufrufen, weil es sehr rechenintensiv ist und auf die Welt zugreift (z.B. Raytracing).

Der Cursor wird doch bereits 20 Mal pro Sekunde aktualisiert. Ich habe beide Varianten (mit und ohne PacketFilter) direkt nacheinander getestet und konnte keinen Unterschied feststellen. Außerdem sollte man Cursor::render nicht von Netty aufrufen, weil es sehr rechenintensiv ist und auf die Welt zugreift (z.B. Raytracing).
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<Cursor> 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 -> {