From a3a97ee102af7ec7621ac49a26f5e15318d73e60 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 14:44:55 +0100 Subject: [PATCH 01/19] Move TechHider into new HiderCore module HiderCore holds the TechHider, ChunkHider and AccessPrivilegeProvider so that FightSystem and BauSystem can both shade it. BlockIds and ProtocolUtils stay in SpigotCore (used by AntiNocom and RFallingBlockEntity) and move to de.steamwar.core to avoid a split package. The access widener entries the TechHider needs are duplicated into hidercore.accesswidener; the legacy TechHider still needs its own copies in SpigotCore. --- FightSystem/FightSystem_Core/build.gradle.kts | 1 + .../fightsystem/record/PacketProcessor.java | 2 +- .../de/steamwar/fightsystem/utils/Region.java | 2 +- HiderCore/build.gradle.kts | 41 +++++++++++++++++++ .../techhider/AccessPrivilegeProvider.java | 0 .../src/de/steamwar/techhider/ChunkHider.java | 1 + .../src/de/steamwar/techhider/TechHider.java | 0 HiderCore/src/hidercore.accesswidener | 27 ++++++++++++ .../{techhider => core}/BlockIds.java | 2 +- .../{techhider => core}/ProtocolUtils.java | 2 +- .../de/steamwar/core/events/AntiNocom.java | 2 +- .../steamwar/entity/RFallingBlockEntity.java | 2 +- .../steamwar/techhider/legacy/ChunkHider.java | 2 +- .../steamwar/techhider/legacy/TechHider.java | 4 +- .../src/spigotcore.accesswidener | 7 +--- settings.gradle.kts | 2 + 16 files changed, 83 insertions(+), 14 deletions(-) create mode 100644 HiderCore/build.gradle.kts rename {SpigotCore/SpigotCore_Main => HiderCore}/src/de/steamwar/techhider/AccessPrivilegeProvider.java (100%) rename {SpigotCore/SpigotCore_Main => HiderCore}/src/de/steamwar/techhider/ChunkHider.java (99%) rename {SpigotCore/SpigotCore_Main => HiderCore}/src/de/steamwar/techhider/TechHider.java (100%) create mode 100644 HiderCore/src/hidercore.accesswidener rename SpigotCore/SpigotCore_Main/src/de/steamwar/{techhider => core}/BlockIds.java (98%) rename SpigotCore/SpigotCore_Main/src/de/steamwar/{techhider => core}/ProtocolUtils.java (99%) diff --git a/FightSystem/FightSystem_Core/build.gradle.kts b/FightSystem/FightSystem_Core/build.gradle.kts index 9b2f746b..dfe91b4f 100644 --- a/FightSystem/FightSystem_Core/build.gradle.kts +++ b/FightSystem/FightSystem_Core/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { compileOnly(libs.classindex) annotationProcessor(libs.classindex) compileOnly(project(":SpigotCore", "default")) + implementation(project(":HiderCore")) compileOnly(libs.netty) compileOnly(libs.paperapi) diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/record/PacketProcessor.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/record/PacketProcessor.java index 72d728c6..c0b6738f 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/record/PacketProcessor.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/record/PacketProcessor.java @@ -20,6 +20,7 @@ package de.steamwar.fightsystem.record; import com.sk89q.worldedit.extent.clipboard.Clipboard; +import de.steamwar.core.BlockIds; import de.steamwar.core.worldedit.WorldEditWrapper; import de.steamwar.entity.REntity; import de.steamwar.entity.REntityServer; @@ -40,7 +41,6 @@ import de.steamwar.fightsystem.utils.TechHiderWrapper; import de.steamwar.sql.SchematicNode; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.Team; -import de.steamwar.techhider.BlockIds; import net.md_5.bungee.api.ChatMessageType; import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.TextComponent; diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/Region.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/Region.java index 7d75e02a..c28257ef 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/Region.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/Region.java @@ -19,7 +19,7 @@ package de.steamwar.fightsystem.utils; -import de.steamwar.techhider.ProtocolUtils; +import de.steamwar.core.ProtocolUtils; import lombok.AllArgsConstructor; import lombok.Getter; import org.bukkit.Location; diff --git a/HiderCore/build.gradle.kts b/HiderCore/build.gradle.kts new file mode 100644 index 00000000..086bf56c --- /dev/null +++ b/HiderCore/build.gradle.kts @@ -0,0 +1,41 @@ +/* + * 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 . + */ + +plugins { + steamwar.java + widener +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +dependencies { + compileOnly(project(":SpigotCore", "default")) + + compileOnly(libs.paperapi) + compileOnly(libs.nms) + compileOnly(libs.netty) + compileOnly(libs.fastutil) +} + +widener { + fromCatalog(libs.nms) +} diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/AccessPrivilegeProvider.java b/HiderCore/src/de/steamwar/techhider/AccessPrivilegeProvider.java similarity index 100% rename from SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/AccessPrivilegeProvider.java rename to HiderCore/src/de/steamwar/techhider/AccessPrivilegeProvider.java diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/ChunkHider.java b/HiderCore/src/de/steamwar/techhider/ChunkHider.java similarity index 99% rename from SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/ChunkHider.java rename to HiderCore/src/de/steamwar/techhider/ChunkHider.java index caa9c96b..9ad25a4e 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/ChunkHider.java +++ b/HiderCore/src/de/steamwar/techhider/ChunkHider.java @@ -19,6 +19,7 @@ package de.steamwar.techhider; +import de.steamwar.core.ProtocolUtils; import de.steamwar.techhider.AccessPrivilegeProvider.ForPlayer; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/TechHider.java b/HiderCore/src/de/steamwar/techhider/TechHider.java similarity index 100% rename from SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/TechHider.java rename to HiderCore/src/de/steamwar/techhider/TechHider.java diff --git a/HiderCore/src/hidercore.accesswidener b/HiderCore/src/hidercore.accesswidener new file mode 100644 index 00000000..347067a1 --- /dev/null +++ b/HiderCore/src/hidercore.accesswidener @@ -0,0 +1,27 @@ +accessWidener v2 named + +# For TechHider +accessible field net/minecraft/network/protocol/game/ClientboundEntityEventPacket entityId I +accessible field net/minecraft/network/protocol/game/ClientboundMoveEntityPacket entityId I +accessible field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket entityId I +accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket sectionPos Lnet/minecraft/core/SectionPos; +accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket positions [S +accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket states [Lnet/minecraft/world/level/block/state/BlockState; + +# For ChunkHider +## transitive-extendable means that a public no args constructor is added without any super initialization +transitive-extendable class net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket +accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket chunkData Lnet/minecraft/network/protocol/game/ClientboundLevelChunkPacketData; +mutable field net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket chunkData Lnet/minecraft/network/protocol/game/ClientboundLevelChunkPacketData; + +## transitive-extendable means that a public no args constructor is added without any super initialization +transitive-extendable class net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData +accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData buffer [B +mutable field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData buffer [B +accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData blockEntitiesData Ljava/util/List; +mutable field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData blockEntitiesData Ljava/util/List; + +accessible class net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo +accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo type Lnet/minecraft/world/level/block/entity/BlockEntityType; +accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo packedXZ I +accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo y I diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/BlockIds.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/BlockIds.java similarity index 98% rename from SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/BlockIds.java rename to SpigotCore/SpigotCore_Main/src/de/steamwar/core/BlockIds.java index b13295bf..c9b41de9 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/BlockIds.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/BlockIds.java @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -package de.steamwar.techhider; +package de.steamwar.core; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.world.level.block.Block; diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/ProtocolUtils.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/ProtocolUtils.java similarity index 99% rename from SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/ProtocolUtils.java rename to SpigotCore/SpigotCore_Main/src/de/steamwar/core/ProtocolUtils.java index 70d86008..58e8145e 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/ProtocolUtils.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/ProtocolUtils.java @@ -17,7 +17,7 @@ * along with this program. If not, see . */ -package de.steamwar.techhider; +package de.steamwar.core; import io.netty.buffer.ByteBuf; diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/AntiNocom.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/AntiNocom.java index d3625026..6bce3aa4 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/AntiNocom.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/events/AntiNocom.java @@ -21,9 +21,9 @@ 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 de.steamwar.techhider.ProtocolUtils; import net.minecraft.core.BlockPos; import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket; diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RFallingBlockEntity.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RFallingBlockEntity.java index 50226dfa..b52187f2 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RFallingBlockEntity.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/entity/RFallingBlockEntity.java @@ -19,7 +19,7 @@ package de.steamwar.entity; -import de.steamwar.techhider.BlockIds; +import de.steamwar.core.BlockIds; import lombok.Getter; import lombok.Setter; import org.bukkit.Location; diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ChunkHider.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ChunkHider.java index 9aaa9b40..bca91a11 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ChunkHider.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ChunkHider.java @@ -19,7 +19,7 @@ package de.steamwar.techhider.legacy; -import de.steamwar.techhider.ProtocolUtils; +import de.steamwar.core.ProtocolUtils; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import lombok.Getter; diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/TechHider.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/TechHider.java index dfa0a5f3..5c9cd169 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/TechHider.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/TechHider.java @@ -20,8 +20,8 @@ package de.steamwar.techhider.legacy; import com.comphenix.tinyprotocol.TinyProtocol; -import de.steamwar.techhider.BlockIds; -import de.steamwar.techhider.ProtocolUtils; +import de.steamwar.core.BlockIds; +import de.steamwar.core.ProtocolUtils; import lombok.Getter; import net.minecraft.core.BlockPos; import net.minecraft.core.Vec3i; diff --git a/SpigotCore/SpigotCore_Main/src/spigotcore.accesswidener b/SpigotCore/SpigotCore_Main/src/spigotcore.accesswidener index 68d0aa82..99170241 100644 --- a/SpigotCore/SpigotCore_Main/src/spigotcore.accesswidener +++ b/SpigotCore/SpigotCore_Main/src/spigotcore.accesswidener @@ -33,11 +33,10 @@ mutable field net/minecraft/network/protocol/game/ClientboundEntityEventPacket e transitive-extendable class net/minecraft/network/protocol/game/ClientboundRotateHeadPacket accessible field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket yHeadRot B mutable field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket yHeadRot B -## + TechHider accessible field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket entityId I mutable field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket entityId I -# For ChunkHider +# For legacy/ChunkHider ## transitive-extendable means that a public no args constructor is added without any super initialization transitive-extendable class net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket chunkData Lnet/minecraft/network/protocol/game/ClientboundLevelChunkPacketData; @@ -55,8 +54,7 @@ accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacket accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo packedXZ I accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo y I -# For TechHider -accessible field net/minecraft/network/protocol/game/ClientboundMoveEntityPacket entityId I +# For legacy/TechHider ## transitive-extendable means that a public no args constructor is added without any super initialization transitive-extendable class net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket sectionPos Lnet/minecraft/core/SectionPos; @@ -65,7 +63,6 @@ mutable field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdate accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket states [Lnet/minecraft/world/level/block/state/BlockState; mutable field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket states [Lnet/minecraft/world/level/block/state/BlockState; -# For legacy/TechHider ## transitive-extendable means that a public no args constructor is added without any super initialization transitive-extendable class net/minecraft/network/protocol/game/ClientboundBlockUpdatePacket mutable field net/minecraft/network/protocol/game/ClientboundBlockUpdatePacket blockState Lnet/minecraft/world/level/block/state/BlockState; \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 63ec80e5..1b360abf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -196,6 +196,8 @@ include( "FightSystem:FightSystem_Standalone" ) +include("HiderCore") + include("KotlinCore") include("LobbySystem") From da4b684a4b21d9315ed07d1de3bc7353970d6e01 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 14:49:00 +0100 Subject: [PATCH 02/19] Move Hull and generic HullHider into HiderCore Hull no longer depends on FightSystem: plugin, world, region, ground visibility and hidden blocks are passed in. HullRegion is the common block box hulls work on. The generic HullHider manages one hull per key and does the block, entity and REntity tracking; FightHullHider keeps the fight specific parts (team hulls, viewer rules, state dependent registration). Entities are now checked against the hull with block coordinates, so entities in the bottom layer and at the max z edge are handled correctly. --- .../de/steamwar/fightsystem/FightSystem.java | 10 +- .../commands/TechhiderbugCommand.java | 2 +- .../fightsystem/fight/FightWorld.java | 2 +- .../{HullHider.java => FightHullHider.java} | 119 ++++---------- .../de/steamwar/fightsystem/utils/Region.java | 5 + .../fightsystem/utils/TechHiderWrapper.java | 4 +- HiderCore/build.gradle.kts | 1 + .../src/de/steamwar/hullhider}/Hull.java | 59 ++++--- .../src/de/steamwar/hullhider/HullHider.java | 154 ++++++++++++++++++ .../src/de/steamwar/hullhider/HullRegion.java | 97 +++++++++++ 10 files changed, 334 insertions(+), 119 deletions(-) rename FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/{HullHider.java => FightHullHider.java} (59%) rename {FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils => HiderCore/src/de/steamwar/hullhider}/Hull.java (87%) create mode 100644 HiderCore/src/de/steamwar/hullhider/HullHider.java create mode 100644 HiderCore/src/de/steamwar/hullhider/HullRegion.java diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.java index c676308e..be367444 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.java @@ -32,8 +32,8 @@ import de.steamwar.fightsystem.record.GlobalRecorder; import de.steamwar.fightsystem.states.FightState; import de.steamwar.fightsystem.states.OneShotStateDependent; import de.steamwar.fightsystem.states.StateDependentListener; +import de.steamwar.fightsystem.utils.FightHullHider; import de.steamwar.fightsystem.utils.FightUI; -import de.steamwar.fightsystem.utils.HullHider; import de.steamwar.fightsystem.utils.SWSound; import de.steamwar.fightsystem.utils.TechHiderWrapper; import de.steamwar.linkage.AbstractLinker; @@ -58,7 +58,7 @@ public class FightSystem extends JavaPlugin { private FightTeam lastWinner; private String lastWinreason; private TechHiderWrapper techHider; - private HullHider hullHider; + private FightHullHider hullHider; private SpigotLinker spigotLinker; @@ -111,9 +111,9 @@ public class FightSystem extends JavaPlugin { new OneShotStateDependent(ArenaMode.Test, FightState.All, SteamEdit::init); Config.world.setGameRule(GameRule.REDUCED_DEBUG_INFO, ArenaMode.AntiTest.contains(Config.mode)); - hullHider = new HullHider(); + hullHider = new FightHullHider(); techHider = new TechHiderWrapper(hullHider); - FightSystem.getHullHider().getHullMap().values().forEach(hull -> hull.fill(true)); + hullHider.fillAll(true); FileSource.startReplay(); @@ -165,7 +165,7 @@ public class FightSystem extends JavaPlugin { return plugin.techHider; } - public static HullHider getHullHider() { + public static FightHullHider getHullHider() { return plugin.hullHider; } diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/TechhiderbugCommand.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/TechhiderbugCommand.java index f4ae7c73..09eb3e44 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/TechhiderbugCommand.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/commands/TechhiderbugCommand.java @@ -56,7 +56,7 @@ public class TechhiderbugCommand extends FightSWCommand { writer.append('\n'); writer.append("HullHider regions: "); - FightSystem.getHullHider().getHullMap().forEach((t, h) -> writer.append(t.getName()).append(':').append(h.getRegion().toString()).append(' ')); + FightSystem.getHullHider().forEachHull((t, h) -> writer.append(t.getName()).append(':').append(h.getRegion().toString()).append(' ')); writer.append('\n'); writer.append("Hidden regions: "); diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightWorld.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightWorld.java index 83c15170..4c05125e 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightWorld.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/fight/FightWorld.java @@ -84,7 +84,7 @@ public class FightWorld extends StateDependent { entities.forEach(Entity::remove); entities.clear(); - FightSystem.getHullHider().getHullMap().values().forEach(hull -> hull.fill(true)); + FightSystem.getHullHider().fillAll(true); Config.ArenaRegion.forEachChunk((x, z) -> { for (Player p : Bukkit.getOnlinePlayers()) { diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/HullHider.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java similarity index 59% rename from FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/HullHider.java rename to FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java index fe41631e..4175655b 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/HullHider.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java @@ -30,7 +30,8 @@ import de.steamwar.fightsystem.listener.Recording; import de.steamwar.fightsystem.states.FightState; import de.steamwar.fightsystem.states.StateDependentListener; import de.steamwar.fightsystem.states.StateDependentTask; -import lombok.Getter; +import de.steamwar.hullhider.Hull; +import de.steamwar.hullhider.HullHider; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.block.Block; @@ -38,43 +39,38 @@ 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.BlockPhysicsEvent; -import org.bukkit.event.entity.EntityDeathEvent; -import org.bukkit.event.entity.EntitySpawnEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; -import java.util.HashMap; -import java.util.Map; import java.util.Objects; +import java.util.function.BiConsumer; -public class HullHider implements Listener { +/** + * Fight specific part of the hull hider: one hull per team schematic region, and who may view which hull. + */ +public class FightHullHider { - @Getter - private final Map hullMap = new HashMap<>(); - private final Hull[] hulls; + private final HullHider hullHider = new HullHider<>(); - public HullHider() { - if (!TechHiderWrapper.ENABLED) { - hulls = new Hull[0]; - return; - } - - Fight.teams().forEach(team -> hullMap.put(team, new Hull(team))); - hulls = hullMap.values().toArray(new Hull[0]); + public FightHullHider() { + if (!TechHiderWrapper.ENABLED) return; + Fight.teams().forEach(team -> { + Region region = team.getSchemRegion(); + hullHider.addHull(team, new Hull(FightSystem.getPlugin(), Config.world, region.toHullRegion(), region.getMinY() != Config.PlayerRegion.getMinY(), Config.GameModeConfig.Techhider.HiddenBlocks)); + }); new StateDependentListener(TechHiderWrapper.ENABLED, FightState.All, new MembershipListener()); - new StateDependentListener(TechHiderWrapper.ENABLED, FightState.Schem, this); + new StateDependentListener(TechHiderWrapper.ENABLED, FightState.Schem, hullHider); new StateDependentTask(TechHiderWrapper.ENABLED, FightState.Schem, this::onTick, 0, 1); } public void initialize(FightTeam team) { if (!TechHiderWrapper.ENABLED) return; - Hull hull = hullMap.get(team); - hull.initialize(); + hullHider.initialize(team); // Chunks encoded while the hull was filled as hidden contain obfuscated blocks that are visible now, but no block update is generated for them + Hull hull = hullHider.getHull(team); for (Player player : Bukkit.getOnlinePlayers()) { if (!hull.isViewer(player)) { FightSystem.getTechHider().reloadChunks(player, team.getExtendRegion(), Region.EMPTY); @@ -84,7 +80,15 @@ public class HullHider implements Listener { public void fill(FightTeam team, boolean visible) { if (!TechHiderWrapper.ENABLED) return; - hullMap.get(team).fill(visible); + hullHider.fill(team, visible); + } + + public void fillAll(boolean visible) { + hullHider.fillAll(visible); + } + + public void forEachHull(BiConsumer consumer) { + hullHider.forEach(consumer); } // Registered in every state: players must be hidden from the hulls by default, no matter when they join. @@ -96,7 +100,7 @@ public class HullHider implements Listener { @EventHandler public void onLeave(PlayerQuitEvent e) { - forgetViewer(e.getPlayer()); + hullHider.forgetViewer(e.getPlayer()); } // A dead player loses the right to see the enemy hull again @@ -114,82 +118,27 @@ public class HullHider implements Listener { FightTeam team = Fight.getPlayerTeam(player); FightPlayer fp = Fight.getFightPlayer(player); - for (Map.Entry hull : hullMap.entrySet()) { - if (Config.isReferee(player) || hull.getKey() == team || (fp != null && fp.canEntern())) { - hull.getValue().addViewer(player); - } else { - hull.getValue().removeViewer(player); - } - } - } - - private void forgetViewer(Player player) { - for (Hull hull : hulls) { - hull.forgetViewer(player); - } - } - - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onBlockPhysic(BlockPhysicsEvent e) { - if (e.getBlock() == e.getSourceBlock() || e.getChangedType() == Material.AIR) { - blockUpdate(e.getBlock(), e.getChangedType()); - } + hullHider.forEach((hullTeam, hull) -> hullHider.setViewer(player, hullTeam, Config.isReferee(player) || hullTeam == team || (fp != null && fp.canEntern()))); } public void blockUpdate(Block block, Material changedType) { - for (Hull hull : hulls) { - hull.updateBlockVisibility(block, changedType); - } + hullHider.blockUpdate(block, changedType); } public boolean isBlockHidden(Player player, int x, int y, int z) { - if (!TechHiderWrapper.ENABLED) return false; - - for (Hull hull : hulls) { - if (hull.isBlockHidden(player, x, y, z)) { - return true; - } - } - - return false; - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onSpawn(EntitySpawnEvent e) { - for (Hull hull : hulls) { - hull.checkEntity(e.getEntity()); - } + return hullHider.isBlockHidden(player, x, y, z); } private void onTick() { - Recording.iterateOverEntities(Objects::nonNull, entity -> { - for (Hull hull : hulls) { - hull.checkEntity(entity); - } - }); - - for (Hull hull : hulls) { - hull.sendUncoveredBlocks(); - } - } - - @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) - public void onDeath(EntityDeathEvent e) { - for (Hull hull : hulls) { - hull.removeEntity(e.getEntity()); - } + Recording.iterateOverEntities(Objects::nonNull, hullHider::checkEntity); + hullHider.sendUncoveredBlocks(); } public void updateREntity(REntity e) { - for (Hull hull : hulls) { - hull.checkREntity(e); - } + hullHider.updateREntity(e); } public void despawnREntity(REntity e) { - for (Hull hull : hulls) { - hull.removeREntity(e); - } + hullHider.despawnREntity(e); } } diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/Region.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/Region.java index c28257ef..d5de73be 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/Region.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/Region.java @@ -20,6 +20,7 @@ package de.steamwar.fightsystem.utils; import de.steamwar.core.ProtocolUtils; +import de.steamwar.hullhider.HullRegion; import lombok.AllArgsConstructor; import lombok.Getter; import org.bukkit.Location; @@ -155,6 +156,10 @@ public class Region { return in2dRegion(x, z) && minY <= y && y < maxY; } + public HullRegion toHullRegion() { + return new HullRegion(minX, minY, minZ, maxX, maxY, maxZ); + } + public Region intersection(Region other) { int x = Math.max(minX, other.minX); int y = Math.max(minY, other.minY); 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 abc5dd52..469159c3 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java @@ -67,9 +67,9 @@ public class TechHiderWrapper extends StateDependent implements Listener { @Getter private final ConcurrentHashMap hiddenRegion = new ConcurrentHashMap<>(); - private final HullHider hullHider; + private final FightHullHider hullHider; - public TechHiderWrapper(HullHider hullHider) { + public TechHiderWrapper(FightHullHider hullHider) { super(ENABLED, FightState.All); this.hullHider = hullHider; diff --git a/HiderCore/build.gradle.kts b/HiderCore/build.gradle.kts index 086bf56c..283cd19e 100644 --- a/HiderCore/build.gradle.kts +++ b/HiderCore/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { compileOnly(libs.nms) compileOnly(libs.netty) compileOnly(libs.fastutil) + compileOnly(libs.authlib) } widener { diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/Hull.java b/HiderCore/src/de/steamwar/hullhider/Hull.java similarity index 87% rename from FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/Hull.java rename to HiderCore/src/de/steamwar/hullhider/Hull.java index 16af9f4c..7344af14 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/Hull.java +++ b/HiderCore/src/de/steamwar/hullhider/Hull.java @@ -17,15 +17,12 @@ * along with this program. If not, see . */ -package de.steamwar.fightsystem.utils; +package de.steamwar.hullhider; import com.comphenix.tinyprotocol.TinyProtocol; import com.mojang.authlib.GameProfile; import de.steamwar.core.ProtocolWrapper; import de.steamwar.entity.REntity; -import de.steamwar.fightsystem.Config; -import de.steamwar.fightsystem.FightSystem; -import de.steamwar.fightsystem.fight.FightTeam; import it.unimi.dsi.fastutil.shorts.Short2ObjectArrayMap; import lombok.AllArgsConstructor; import lombok.Getter; @@ -38,10 +35,12 @@ import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.craftbukkit.block.data.CraftBlockData; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -53,13 +52,12 @@ public class Hull { private static final IntVector[] NO_BRANCHES = new IntVector[0]; - private static boolean isOccluding(Material material) { - return material.isOccluding() || Config.GameModeConfig.Techhider.HiddenBlocks.contains(material); - } - + private final Plugin plugin; + private final World world; @Getter - private final Region region; + private final HullRegion region; private final boolean groundVisible; + private final Set hiddenBlocks; private final IntVector[] directions; private final BitSet occluding; @@ -72,9 +70,16 @@ public class Hull { private final HashSet entities = new HashSet<>(); private final HashSet rentities = new HashSet<>(); - public Hull(FightTeam team) { - this.region = team.getSchemRegion(); - this.groundVisible = region.getMinY() != Config.PlayerRegion.getMinY(); + /** + * @param groundVisible whether the bottom layer of the region is uncovered (if the region does not reach the ground) + * @param hiddenBlocks blocks that occlude like solid blocks, as the techhider replaces them by an occluding block + */ + public Hull(Plugin plugin, World world, HullRegion region, boolean groundVisible, Set hiddenBlocks) { + this.plugin = plugin; + this.world = world; + this.region = region; + this.groundVisible = groundVisible; + this.hiddenBlocks = hiddenBlocks; this.occluding = new BitSet(region.volume()); this.visibility = new BitSet(region.volume()); @@ -87,8 +92,12 @@ public class Hull { } } + private boolean isOccluding(Material material) { + return material.isOccluding() || hiddenBlocks.contains(material); + } + public boolean isBlockHidden(Player player, int x, int y, int z) { - return region.inRegion(x, y, z) && !isViewer(player) && !visibility.get(((y - region.getMinY()) * region.getSizeZ() + (z - region.getMinZ())) * region.getSizeX() + (x - region.getMinX())); + return region.contains(x, y, z) && !isViewer(player) && !visibility.get(region.toIndex(x, y, z)); } public boolean isViewer(Player player) { @@ -123,7 +132,7 @@ public class Hull { public void checkEntity(Entity entity) { Location location = entity.getLocation(); - if (region.inRegion(location) && !visibility.get(new IntVector(location).toId(region))) { + if (region.contains(location) && !visibility.get(new IntVector(location).toId(region))) { if (entities.add(entity)) { for (Player player : Bukkit.getOnlinePlayers()) { if (!isViewer(player) && !player.equals(entity)) untrackEntity(player, entity); @@ -143,11 +152,11 @@ public class Hull { TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.REMOVE, new GameProfile(entity.getUniqueId(), entity.getName()), GameMode.CREATIVE)); } - player.showEntity(FightSystem.getPlugin(), entity); + player.showEntity(plugin, entity); } public void untrackEntity(Player player, Entity entity) { - player.hideEntity(FightSystem.getPlugin(), entity); + player.hideEntity(plugin, entity); if (entity instanceof Player) { TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.ADD, new GameProfile(entity.getUniqueId(), entity.getName()), GameMode.CREATIVE)); @@ -159,8 +168,8 @@ public class Hull { } public void checkREntity(REntity entity) { - Location location = new Location(Config.world, entity.getX(), entity.getY(), entity.getZ()); - if (region.inRegion(location) && !visibility.get(new IntVector(location).toId(region))) { + Location location = new Location(world, entity.getX(), entity.getY(), entity.getZ()); + if (region.contains(location) && !visibility.get(new IntVector(location).toId(region))) { if (rentities.add(entity)) entity.hide(true); } else { if (rentities.remove(entity)) entity.hide(false); @@ -191,12 +200,12 @@ public class Hull { long start = System.currentTimeMillis(); region.forEach((x, y, z) -> { IntVector block = new IntVector(x, y, z); - if (isOccluding(Config.world.getBlockAt(x, y, z).getType())) { + if (isOccluding(world.getBlockAt(x, y, z).getType())) { occluding.set(block.toId(region)); } }); forEachBorder((root, direction) -> uncoverBlocks(new NullList<>(), root, direction)); - FightSystem.getPlugin().getLogger().log(Level.INFO, () -> "[HullHider] initialisation finished: " + (System.currentTimeMillis() - start) + " ms, visible blocks: " + visibility.cardinality()); + plugin.getLogger().log(Level.INFO, () -> "[HullHider] initialisation finished: " + (System.currentTimeMillis() - start) + " ms, visible blocks: " + visibility.cardinality()); } public void updateBlockVisibility(Block b, Material changedType) { @@ -235,7 +244,7 @@ public class Hull { Object[] blockdata = new Object[changes.size()]; for (int i = 0; i < blockdata.length; i++) { IntVector change = changes.get(i); - blockdata[i] = ((CraftBlockData) Config.world.getBlockData(change.getX(), change.getY(), change.getZ())).getState(); + blockdata[i] = ((CraftBlockData) world.getBlockData(change.getX(), change.getY(), change.getZ())).getState(); } if (changes.size() > 1) { @@ -332,12 +341,12 @@ public class Hull { this.z = location.getBlockZ(); } - public boolean notInRegion(Region region) { - return !region.inRegion(x, y, z); + public boolean notInRegion(HullRegion region) { + return !region.contains(x, y, z); } - public int toId(Region region) { - return ((y - region.getMinY()) * region.getSizeZ() + (z - region.getMinZ())) * region.getSizeX() + (x - region.getMinX()); + public int toId(HullRegion region) { + return region.toIndex(x, y, z); } public IntVector add(int x, int y, int z) { diff --git a/HiderCore/src/de/steamwar/hullhider/HullHider.java b/HiderCore/src/de/steamwar/hullhider/HullHider.java new file mode 100644 index 00000000..9bbc846f --- /dev/null +++ b/HiderCore/src/de/steamwar/hullhider/HullHider.java @@ -0,0 +1,154 @@ +/* + * 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.hullhider; + +import de.steamwar.entity.REntity; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +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.BlockPhysicsEvent; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.event.entity.EntitySpawnEvent; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +/** + * Manages one {@link Hull} per key (e.g. a team or a region). + * Which players may view which hull, when hulls are initialised and when this listener is registered is up to the user. + */ +public class HullHider implements Listener { + + private final Map hullMap = new LinkedHashMap<>(); + // Read on netty threads, replaced on every change + private volatile Hull[] hulls = new Hull[0]; + + public void addHull(K key, Hull hull) { + hullMap.put(key, hull); + hulls = hullMap.values().toArray(new Hull[0]); + } + + public Hull getHull(K key) { + return hullMap.get(key); + } + + public void forEach(BiConsumer consumer) { + hullMap.forEach(consumer); + } + + public void initialize(K key) { + hullMap.get(key).initialize(); + } + + public void fill(K key, boolean visible) { + hullMap.get(key).fill(visible); + } + + public void fillAll(boolean visible) { + for (Hull hull : hulls) { + hull.fill(visible); + } + } + + public void setViewer(Player player, K key, boolean viewer) { + Hull hull = hullMap.get(key); + if (viewer) { + hull.addViewer(player); + } else { + hull.removeViewer(player); + } + } + + public void forgetViewer(Player player) { + for (Hull hull : hulls) { + hull.forgetViewer(player); + } + } + + public boolean isBlockHidden(Player player, int x, int y, int z) { + for (Hull hull : hulls) { + if (hull.isBlockHidden(player, x, y, z)) { + return true; + } + } + + return false; + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onBlockPhysic(BlockPhysicsEvent e) { + if (e.getBlock() == e.getSourceBlock() || e.getChangedType() == Material.AIR) { + blockUpdate(e.getBlock(), e.getChangedType()); + } + } + + public void blockUpdate(Block block, Material changedType) { + for (Hull hull : hulls) { + hull.updateBlockVisibility(block, changedType); + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onSpawn(EntitySpawnEvent e) { + checkEntity(e.getEntity()); + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onDeath(EntityDeathEvent e) { + for (Hull hull : hulls) { + hull.removeEntity(e.getEntity()); + } + } + + /** + * Has to be called every tick for every entity, as entities can move into or out of the hidden part of a hull. + */ + public void checkEntity(Entity entity) { + for (Hull hull : hulls) { + hull.checkEntity(entity); + } + } + + /** + * Has to be called every tick after the entities have been checked. + */ + public void sendUncoveredBlocks() { + for (Hull hull : hulls) { + hull.sendUncoveredBlocks(); + } + } + + public void updateREntity(REntity e) { + for (Hull hull : hulls) { + hull.checkREntity(e); + } + } + + public void despawnREntity(REntity e) { + for (Hull hull : hulls) { + hull.removeREntity(e); + } + } +} diff --git a/HiderCore/src/de/steamwar/hullhider/HullRegion.java b/HiderCore/src/de/steamwar/hullhider/HullRegion.java new file mode 100644 index 00000000..b8aacb0b --- /dev/null +++ b/HiderCore/src/de/steamwar/hullhider/HullRegion.java @@ -0,0 +1,97 @@ +/* + * 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.hullhider; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.bukkit.Location; + +/** + * Immutable block box a {@link Hull} covers. The minimum is inclusive, the maximum exclusive. + */ +@Getter +@AllArgsConstructor +public final class HullRegion { + + public static HullRegion fromSize(int minX, int minY, int minZ, int sizeX, int sizeY, int sizeZ) { + return new HullRegion(minX, minY, minZ, minX + sizeX, minY + sizeY, minZ + sizeZ); + } + + public static HullRegion fromInclusive(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + return new HullRegion(minX, minY, minZ, maxX + 1, maxY + 1, maxZ + 1); + } + + private final int minX; + private final int minY; + private final int minZ; + private final int maxX; + private final int maxY; + private final int maxZ; + + public int getSizeX() { + return maxX - minX; + } + + public int getSizeY() { + return maxY - minY; + } + + public int getSizeZ() { + return maxZ - minZ; + } + + public int volume() { + return getSizeX() * getSizeY() * getSizeZ(); + } + + public boolean contains(int x, int y, int z) { + return minX <= x && x < maxX && minY <= y && y < maxY && minZ <= z && z < maxZ; + } + + public boolean contains(Location location) { + return contains(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + } + + /** + * @return the index of the block within a bitset of {@link #volume()} bits, only valid for contained blocks + */ + public int toIndex(int x, int y, int z) { + return ((y - minY) * getSizeZ() + (z - minZ)) * getSizeX() + (x - minX); + } + + public void forEach(BlockConsumer executor) { + for (int x = minX; x < maxX; x++) { + for (int y = minY; y < maxY; y++) { + for (int z = minZ; z < maxZ; z++) { + executor.accept(x, y, z); + } + } + } + } + + public interface BlockConsumer { + void accept(int x, int y, int z); + } + + @Override + public String toString() { + return minX + "," + minY + "," + minZ + "->" + maxX + "," + maxY + "," + maxZ; + } +} From 7d4252325a21ef59ffa2fda4a65866a1e77d9db1 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 14:50:32 +0100 Subject: [PATCH 03/19] Extract obfuscation config into HiderCore ObfuscationConfig resolves the hidden blocks, block states (including all waterlogged states when water is hidden) and block entity types plus the obfuscation block, either from a gamemode's techhider config or from plain materials. TechHiderWrapper now only contains the fight specific access rules. --- .../fightsystem/utils/TechHiderWrapper.java | 74 ++---------- HiderCore/build.gradle.kts | 1 + .../steamwar/techhider/ObfuscationConfig.java | 113 ++++++++++++++++++ 3 files changed, 125 insertions(+), 63 deletions(-) create mode 100644 HiderCore/src/de/steamwar/techhider/ObfuscationConfig.java 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 469159c3..1ccaad8e 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java @@ -32,32 +32,21 @@ import de.steamwar.fightsystem.states.StateDependent; import de.steamwar.fightsystem.states.StateDependentListener; import de.steamwar.techhider.AccessPrivilegeProvider; import de.steamwar.techhider.AccessPrivilegeProvider.ForPlayer; +import de.steamwar.techhider.ObfuscationConfig; import de.steamwar.techhider.TechHider; import lombok.Getter; -import net.minecraft.core.Holder; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.material.FluidState; -import net.minecraft.world.level.material.Fluids; import org.bukkit.GameMode; -import org.bukkit.Material; import org.bukkit.craftbukkit.CraftWorld; -import org.bukkit.craftbukkit.util.CraftMagicNumbers; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerQuitEvent; import java.util.Arrays; -import java.util.Collection; -import java.util.Objects; -import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; -import java.util.stream.Stream; public class TechHiderWrapper extends StateDependent implements Listener { @@ -80,29 +69,15 @@ public class TechHiderWrapper extends StateDependent implements Listener { @Override public void enable() { - Set blockStatesToObfuscate = getBlockStatesToHideFromMaterials(Config.GameModeConfig.Techhider.HiddenBlocks); - - Set blocksToObfuscate = Config.GameModeConfig.Techhider.HiddenBlocks.stream() - .map(CraftMagicNumbers::getBlock) - .collect(Collectors.toUnmodifiableSet()); - - Set> blockEntityTypeToObfuscate = Config.GameModeConfig.Techhider.HiddenBlockEntities.stream() - .map(id -> { - ResourceLocation loc = ResourceLocation.parse(id); - return BuiltInRegistries.BLOCK_ENTITY_TYPE.get(loc).orElse(null); - }) - .filter(Objects::nonNull) - .map(Holder.Reference::value) - .collect(Collectors.toUnmodifiableSet()); - - new TechHider(CraftMagicNumbers.getBlock(Config.GameModeConfig.Techhider.ObfuscateWith), newProvider(blockStatesToObfuscate, blocksToObfuscate, blockEntityTypeToObfuscate)); + ObfuscationConfig obfuscation = ObfuscationConfig.of(Config.GameModeConfig.Techhider); + new TechHider(obfuscation.getObfuscateWith(), newProvider(obfuscation)); } - private AccessPrivilegeProvider newProvider(Set blockStatesToObfuscate, Set blocksToObfuscate, Set> blockEntityTypeToObfuscate) { + private AccessPrivilegeProvider newProvider(ObfuscationConfig obfuscation) { return new AccessPrivilegeProvider() { @Override public ForPlayer forPlayer(Player p) { - return new FightPlayerAccess(p, getHiddenRegions(p), blockStatesToObfuscate, blocksToObfuscate, blockEntityTypeToObfuscate); + return new FightPlayerAccess(p, getHiddenRegions(p), obfuscation); } @Override @@ -118,16 +93,12 @@ public class TechHiderWrapper extends StateDependent implements Listener { private class FightPlayerAccess implements ForPlayer { private final Player player; private final Region[] hiddenRegions; - private final Set blockStatesToObfuscate; - private final Set blocksToObfuscate; - private final Set> blockEntityTypeToObfuscate; + private final ObfuscationConfig obfuscation; - private FightPlayerAccess(Player player, Region[] hiddenRegions, Set blockStatesToObfuscate, Set blocksToObfuscate, Set> blockEntityTypeToObfuscate) { + private FightPlayerAccess(Player player, Region[] hiddenRegions, ObfuscationConfig obfuscation) { this.player = player; this.hiddenRegions = hiddenRegions; - this.blockStatesToObfuscate = blockStatesToObfuscate; - this.blocksToObfuscate = blocksToObfuscate; - this.blockEntityTypeToObfuscate = blockEntityTypeToObfuscate; + this.obfuscation = obfuscation; } private boolean isHidden(int blockX, int blockY, int blockZ) { @@ -155,12 +126,12 @@ public class TechHiderWrapper extends StateDependent implements Listener { @Override public boolean canAccessBlock(int blockX, int blockY, int blockZ, Block block) { - return !isHidden(blockX, blockY, blockZ) || !blocksToObfuscate.contains(block); + return !isHidden(blockX, blockY, blockZ) || !obfuscation.hidesBlock(block); } @Override public boolean canAccessBlockState(int blockX, int blockY, int blockZ, BlockState blockState) { - return !isHidden(blockX, blockY, blockZ) || !blockStatesToObfuscate.contains(blockState); + return !isHidden(blockX, blockY, blockZ) || !obfuscation.hidesBlockState(blockState); } // TODO will require entity tracking on the netty thread to prevent future race conditions @@ -177,7 +148,7 @@ public class TechHiderWrapper extends StateDependent implements Listener { @Override public boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType type) { - return !isHidden(blockX, blockY, blockZ) || !blockEntityTypeToObfuscate.contains(type); + return !isHidden(blockX, blockY, blockZ) || !obfuscation.hidesBlockEntity(type); } } @@ -235,27 +206,4 @@ public class TechHiderWrapper extends StateDependent implements Listener { return new Region[]{Fight.getOpposite(fightPlayer.getTeam()).getExtendRegion()}; } - - private Stream getWaterloggedBlockStates() { - FluidState waterFluidState = Fluids.WATER.getSource(false); - - return BuiltInRegistries.BLOCK.stream() - .map((block) -> block.getStateDefinition().getPossibleStates()) - .flatMap(Collection::stream) - .filter((blockState -> blockState.getFluidState() == waterFluidState)); - } - - private Set getBlockStatesToHideFromMaterials(Set materials) { - Stream allStatesFromMaterials = materials.stream() - .map(CraftMagicNumbers::getBlock) - .map((block) -> block.getStateDefinition().getPossibleStates()) - .flatMap(Collection::stream); - - if(materials.contains(Material.WATER)) { - return Stream.concat(allStatesFromMaterials, getWaterloggedBlockStates()).collect(Collectors.toUnmodifiableSet()); - } - else { - return allStatesFromMaterials.collect(Collectors.toUnmodifiableSet()); - } - } } diff --git a/HiderCore/build.gradle.kts b/HiderCore/build.gradle.kts index 283cd19e..a0b27617 100644 --- a/HiderCore/build.gradle.kts +++ b/HiderCore/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { compileOnly(libs.netty) compileOnly(libs.fastutil) compileOnly(libs.authlib) + compileOnly(libs.datafixer) } widener { diff --git a/HiderCore/src/de/steamwar/techhider/ObfuscationConfig.java b/HiderCore/src/de/steamwar/techhider/ObfuscationConfig.java new file mode 100644 index 00000000..cc8310a5 --- /dev/null +++ b/HiderCore/src/de/steamwar/techhider/ObfuscationConfig.java @@ -0,0 +1,113 @@ +/* + * 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.techhider; + +import de.steamwar.sql.GameModeConfig; +import lombok.Getter; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import org.bukkit.Material; +import org.bukkit.craftbukkit.util.CraftMagicNumbers; + +import java.util.Collection; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Which blocks, block states and block entities to hide, and the block to replace them with. + */ +@Getter +public final class ObfuscationConfig { + + public static ObfuscationConfig of(GameModeConfig.TechhiderConfig config) { + return new ObfuscationConfig(config.ObfuscateWith, config.HiddenBlocks, config.HiddenBlockEntities); + } + + private final Block obfuscateWith; + private final Set blocks; + private final Set blockStates; + private final Set> blockEntityTypes; + + /** + * @param hiddenBlocks "water" also hides all waterlogged block states + * @param hiddenBlockEntities block entity type ids, e.g. "minecraft:sign" + */ + public ObfuscationConfig(Material obfuscateWith, Set hiddenBlocks, Set hiddenBlockEntities) { + this.obfuscateWith = CraftMagicNumbers.getBlock(obfuscateWith); + + this.blocks = hiddenBlocks.stream() + .map(CraftMagicNumbers::getBlock) + .collect(Collectors.toUnmodifiableSet()); + + this.blockStates = getBlockStatesToHideFromMaterials(hiddenBlocks); + + this.blockEntityTypes = hiddenBlockEntities.stream() + .map(id -> { + ResourceLocation loc = ResourceLocation.parse(id); + return BuiltInRegistries.BLOCK_ENTITY_TYPE.get(loc).orElse(null); + }) + .filter(Objects::nonNull) + .map(Holder.Reference::value) + .collect(Collectors.toUnmodifiableSet()); + } + + public boolean hidesBlock(Block block) { + return blocks.contains(block); + } + + public boolean hidesBlockState(BlockState blockState) { + return blockStates.contains(blockState); + } + + public boolean hidesBlockEntity(BlockEntityType type) { + return blockEntityTypes.contains(type); + } + + private static Stream getWaterloggedBlockStates() { + FluidState waterFluidState = Fluids.WATER.getSource(false); + + return BuiltInRegistries.BLOCK.stream() + .map((block) -> block.getStateDefinition().getPossibleStates()) + .flatMap(Collection::stream) + .filter((blockState -> blockState.getFluidState() == waterFluidState)); + } + + private static Set getBlockStatesToHideFromMaterials(Set materials) { + Stream allStatesFromMaterials = materials.stream() + .map(CraftMagicNumbers::getBlock) + .map((block) -> block.getStateDefinition().getPossibleStates()) + .flatMap(Collection::stream); + + if(materials.contains(Material.WATER)) { + return Stream.concat(allStatesFromMaterials, getWaterloggedBlockStates()).collect(Collectors.toUnmodifiableSet()); + } + else { + return allStatesFromMaterials.collect(Collectors.toUnmodifiableSet()); + } + } +} From ce2a23f4b850fd0b9cfc82a5a2a9c3df1799898b Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 15:00:37 +0100 Subject: [PATCH 04/19] Migrate BauSystem to the new TechHider and remove the legacy TechHider The new TechHider registers a permanent global filter, so BauSystem now uses a single BauTechHider that combines the hiding rules of the spectator techhider, /techhider and /xray per player. To support the different obfuscation blocks of these rules, ForPlayer now provides the obfuscation block per position. The ChunkHider now also checks single valued chunk sections, which were sent unfiltered before, and recalculates their block count if they change. Unhandled packet types are logged once; statistics, debug samples and ping responses are passed through. The legacy TechHider and its access widener entries are removed. --- BauSystem/BauSystem_Main/build.gradle.kts | 1 + .../features/techhider/BauTechHider.java | 137 ++++++++ .../features/techhider/HidingRule.java | 52 +++ .../features/techhider/TechHiderCommand.java | 31 +- .../features/world/SpectatorListener.java | 30 +- .../bausystem/features/xray/XrayCommand.java | 30 +- .../fightsystem/utils/TechHiderWrapper.java | 7 +- .../src/de/steamwar/hullhider/HullRegion.java | 4 + .../techhider/AccessPrivilegeProvider.java | 5 + .../src/de/steamwar/techhider/ChunkHider.java | 38 ++- .../steamwar/techhider/ObfuscationConfig.java | 2 + .../src/de/steamwar/techhider/TechHider.java | 22 +- .../steamwar/techhider/legacy/ChunkHider.java | 301 ------------------ .../techhider/legacy/ProtocolWrapper.java | 79 ----- .../steamwar/techhider/legacy/TechHider.java | 169 ---------- .../src/spigotcore.accesswidener | 31 -- 16 files changed, 295 insertions(+), 644 deletions(-) create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/HidingRule.java delete mode 100644 SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ChunkHider.java delete mode 100644 SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ProtocolWrapper.java delete mode 100644 SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/TechHider.java diff --git a/BauSystem/BauSystem_Main/build.gradle.kts b/BauSystem/BauSystem_Main/build.gradle.kts index c387bc36..c320e674 100644 --- a/BauSystem/BauSystem_Main/build.gradle.kts +++ b/BauSystem/BauSystem_Main/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { annotationProcessor(libs.classindex) compileOnly(project(":SpigotCore", "default")) compileOnly(project(":KotlinCore", "default")) + implementation(project(":HiderCore")) compileOnly(libs.axiom) compileOnly(libs.authlib) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java new file mode 100644 index 00000000..a4fd51bf --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java @@ -0,0 +1,137 @@ +/* + * 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.bausystem.features.techhider; + +import de.steamwar.bausystem.features.world.SpectatorListener; +import de.steamwar.bausystem.features.xray.XrayCommand; +import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedInstance; +import de.steamwar.techhider.AccessPrivilegeProvider; +import de.steamwar.techhider.TechHider; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.List; + +/** + * The single TechHider of the server. It combines the hiding rules of the spectator techhider, /techhider and /xray per player. + */ +@Linked +public class BauTechHider implements AccessPrivilegeProvider { + + private static final HidingRule[] NO_RULES = new HidingRule[0]; + + // Injected after construction, until then nothing is hidden + @LinkedInstance + public XrayCommand xrayCommand; + @LinkedInstance + public TechHiderCommand techHiderCommand; + @LinkedInstance + public SpectatorListener spectatorListener; + + public BauTechHider() { + new TechHider(this); + } + + @Override + public ForPlayer forPlayer(Player p) { + if (xrayCommand == null || techHiderCommand == null || spectatorListener == null) { + return new BauPlayerAccess(p, NO_RULES); + } + + // Region rules first: where rules overlap, the first one decides the obfuscation block + List rules = new ArrayList<>(); + xrayCommand.addHidingRules(p, rules); + techHiderCommand.addHidingRules(p, rules); + spectatorListener.addHidingRules(p, rules); + return new BauPlayerAccess(p, rules.toArray(NO_RULES)); + } + + @Override + public boolean isPlayerPrivilegedToPerformAction(Player p) { + return true; + } + + /** + * Snapshot of the hiding rules of one player, taken once per outgoing packet on the netty thread. + */ + private record BauPlayerAccess(Player player, HidingRule[] rules) implements ForPlayer { + + @Override + public Player getPlayer() { + return player; + } + + @Override + public boolean canAccessAllDataWithinChunk(int chunkX, int chunkZ) { + for (HidingRule rule : rules) { + if (rule.coversChunk(chunkX, chunkZ)) return false; + } + return true; + } + + @Override + public boolean canAccessPosition(int blockX, int blockY, int blockZ) { + return true; + } + + @Override + public boolean canAccessBlock(int blockX, int blockY, int blockZ, Block block) { + for (HidingRule rule : rules) { + if (rule.covers(blockX, blockY, blockZ) && rule.obfuscation().hidesBlock(block)) return false; + } + return true; + } + + @Override + public boolean canAccessBlockState(int blockX, int blockY, int blockZ, BlockState blockState) { + for (HidingRule rule : rules) { + if (rule.covers(blockX, blockY, blockZ) && rule.obfuscation().hidesBlockState(blockState)) return false; + } + return true; + } + + @Override + public boolean canAccessEntity(int entityId) { + return true; + } + + @Override + public boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType type) { + for (HidingRule rule : rules) { + if (rule.covers(blockX, blockY, blockZ) && rule.obfuscation().hidesBlockEntity(type)) return false; + } + return true; + } + + @Override + public BlockState getObfuscation(int blockX, int blockY, int blockZ) { + for (HidingRule rule : rules) { + if (rule.covers(blockX, blockY, blockZ)) return rule.obfuscation().getObfuscationState(); + } + // Only asked for positions a rule hides + return Blocks.END_STONE.defaultBlockState(); + } + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/HidingRule.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/HidingRule.java new file mode 100644 index 00000000..a7010e5a --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/HidingRule.java @@ -0,0 +1,52 @@ +/* + * 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.bausystem.features.techhider; + +import de.steamwar.bausystem.region.Point; +import de.steamwar.bausystem.region.Region; +import de.steamwar.hullhider.HullRegion; +import de.steamwar.techhider.ObfuscationConfig; + +/** + * Hides the blocks of an obfuscation config within an area, or everywhere if the area is null. + */ +public record HidingRule(HullRegion area, ObfuscationConfig obfuscation) { + + public static HidingRule everywhere(ObfuscationConfig obfuscation) { + return new HidingRule(null, obfuscation); + } + + /** + * Covers the area including its extension. + */ + public static HidingRule of(Region.Area area, ObfuscationConfig obfuscation) { + Point min = area.getMinPoint(true); + Point max = area.getMaxPoint(true); + return new HidingRule(HullRegion.fromInclusive(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ()), obfuscation); + } + + public boolean covers(int x, int y, int z) { + return area == null || area.contains(x, y, z); + } + + public boolean coversChunk(int chunkX, int chunkZ) { + return area == null || !area.chunkOutside(chunkX, chunkZ); + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java index 2f401a59..b6fd7d8a 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java @@ -31,7 +31,7 @@ import de.steamwar.command.TypeMapper; import de.steamwar.core.CraftbukkitWrapper; import de.steamwar.linkage.Linked; import de.steamwar.linkage.LinkedInstance; -import de.steamwar.techhider.legacy.TechHider; +import de.steamwar.techhider.ObfuscationConfig; import net.md_5.bungee.api.ChatMessageType; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; @@ -41,6 +41,7 @@ import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerQuitEvent; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @Linked @@ -50,8 +51,9 @@ public class TechHiderCommand extends SWCommand implements Listener, ScoreboardE super("techhider", "hider", "obfuscate", "hide", "hide-tech"); } - private Map> techHiders = new HashMap<>(); - private Map> hidden = new HashMap<>(); + // Read on netty threads by the BauTechHider + private Map> hidingRules = new ConcurrentHashMap<>(); + private Map> hidden = new ConcurrentHashMap<>(); @LinkedInstance public XrayCommand xrayCommand; @@ -64,22 +66,15 @@ public class TechHiderCommand extends SWCommand implements Listener, ScoreboardE return; } - Optional techHider = techHiders.computeIfAbsent(region, rg -> { + Optional hidingRule = hidingRules.computeIfAbsent(region, rg -> { if (!region.getGameModeConfig().Techhider.Active) { return Optional.empty(); } - hidden.put(rg, new HashSet<>()); - - TechHider current = new TechHider((p, cX, cY) -> { - if (rg.getBuildArea().isChunkOutside(cX, cY)) return true; - return !hidden.get(rg).contains(p); - }, region.getGameModeConfig().Techhider.ObfuscateWith, region.getGameModeConfig().Techhider.HiddenBlocks, region.getGameModeConfig().Techhider.HiddenBlockEntities); - current.enable(); - - return Optional.of(current); + hidden.put(rg, ConcurrentHashMap.newKeySet()); + return Optional.of(HidingRule.of(rg.getBuildArea(), ObfuscationConfig.of(rg.getGameModeConfig().Techhider))); }); - if (!techHider.isPresent()) { + if (!hidingRule.isPresent()) { return; } @@ -128,6 +123,14 @@ public class TechHiderCommand extends SWCommand implements Listener, ScoreboardE hidden.values().forEach(set -> set.remove(event.getPlayer())); } + public void addHidingRules(Player player, List rules) { + hidden.forEach((region, players) -> { + if (players.contains(player)) { + hidingRules.getOrDefault(region, Optional.empty()).ifPresent(rules::add); + } + }); + } + public void disable(Region region, Player player) { hidden.computeIfPresent(region, (rg, set) -> { set.remove(player); 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 645600cd..ae32e0c1 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 @@ -22,10 +22,11 @@ package de.steamwar.bausystem.features.world; import de.steamwar.bausystem.BauSystem; import de.steamwar.bausystem.Permission; import de.steamwar.bausystem.config.BauServer; +import de.steamwar.bausystem.features.techhider.HidingRule; import de.steamwar.bausystem.utils.BauMemberUpdateEvent; import de.steamwar.linkage.Linked; import de.steamwar.sql.BauweltMember; -import de.steamwar.techhider.legacy.TechHider; +import de.steamwar.techhider.ObfuscationConfig; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; @@ -41,14 +42,18 @@ import org.bukkit.event.entity.EntityPickupItemEvent; import org.bukkit.event.player.*; import org.bukkit.util.Vector; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; @Linked public class SpectatorListener implements Listener { - private static final TechHider techHider; - private static final Set NO_TECHHIDER = new HashSet<>(); + private static final HidingRule SPECTATOR_RULE; + // Read on netty threads by the BauTechHider + private static final Set NO_TECHHIDER = ConcurrentHashMap.newKeySet(); static { Set materials = new HashSet<>(); @@ -99,19 +104,12 @@ public class SpectatorListener implements Listener { materials.add(Material.WATER); materials.remove(Material.BARRIER); materials.remove(Material.STONE); - techHider = new TechHider((TechHider.LocationEvaluator) (player, i, i1) -> { - return Permission.BUILD.hasPermission(player) || NO_TECHHIDER.contains(player); - }, Material.END_STONE, materials, new HashSet<>()); + SPECTATOR_RULE = HidingRule.everywhere(new ObfuscationConfig(Material.END_STONE, materials, Collections.emptySet())); } - private static void enableOrDisableTechhider() { - boolean enable = Bukkit.getOnlinePlayers() - .stream() - .anyMatch(player -> !(Permission.BUILD.hasPermission(player) || NO_TECHHIDER.contains(player))); - if (enable) { - techHider.enable(); - } else { - techHider.disable(); + public void addHidingRules(Player player, List rules) { + if (!(Permission.BUILD.hasPermission(player) || NO_TECHHIDER.contains(player))) { + rules.add(SPECTATOR_RULE); } } @@ -122,7 +120,6 @@ public class SpectatorListener implements Listener { NO_TECHHIDER.add(player); } resendChunks(player); - enableOrDisableTechhider(); } private static void resendChunks(Player player) { @@ -141,7 +138,6 @@ public class SpectatorListener implements Listener { @EventHandler public void onPlayerJoin(PlayerJoinEvent event) { - enableOrDisableTechhider(); if (BauSystem.DEV_SERVER) return; if (event.getPlayer().getUniqueId().equals(BauServer.getInstance().getOwner())) { return; @@ -163,7 +159,6 @@ public class SpectatorListener implements Listener { @EventHandler public void onBauMemberUpdate(BauMemberUpdateEvent event) { - enableOrDisableTechhider(); if (!anySupervisorOnline(null) && !BauSystem.DEV_SERVER) { Bukkit.getOnlinePlayers().forEach(player -> { player.kickPlayer(""); @@ -180,7 +175,6 @@ public class SpectatorListener implements Listener { @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { NO_TECHHIDER.remove(event.getPlayer()); - Bukkit.getScheduler().runTaskLater(BauSystem.getInstance(), SpectatorListener::enableOrDisableTechhider, 1); if (BauSystem.DEV_SERVER) return; if (!anySupervisorOnline(event.getPlayer())) { Bukkit.getOnlinePlayers().forEach(player -> { 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 aa90081c..0b334429 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 @@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.xray; import com.comphenix.tinyprotocol.TinyProtocol; import de.steamwar.bausystem.BauSystem; +import de.steamwar.bausystem.features.techhider.HidingRule; import de.steamwar.bausystem.features.techhider.TechHiderCommand; import de.steamwar.bausystem.region.Region; import de.steamwar.bausystem.utils.ScoreboardElement; @@ -28,7 +29,7 @@ import de.steamwar.command.SWCommand; import de.steamwar.core.CraftbukkitWrapper; import de.steamwar.linkage.Linked; import de.steamwar.linkage.LinkedInstance; -import de.steamwar.techhider.legacy.TechHider; +import de.steamwar.techhider.ObfuscationConfig; import net.md_5.bungee.api.ChatMessageType; import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; import net.minecraft.server.level.ServerPlayer; @@ -43,6 +44,7 @@ import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerQuitEvent; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; @Linked @@ -52,9 +54,10 @@ public class XrayCommand extends SWCommand implements Listener, ScoreboardElemen super("xray"); } - private Map> techHiders = new HashMap<>(); + // Read on netty threads by the BauTechHider and the movement filter + private Map> hidingRules = new ConcurrentHashMap<>(); private Map> xrayedBlocks = new HashMap<>(); - private Map> hidden = new HashMap<>(); + private Map> hidden = new ConcurrentHashMap<>(); @LinkedInstance public TechHiderCommand techHiderCommand; @@ -67,19 +70,19 @@ public class XrayCommand extends SWCommand implements Listener, ScoreboardElemen return; } - Optional techHider = techHiders.computeIfAbsent(region, rg -> { + Optional hidingRule = hidingRules.computeIfAbsent(region, rg -> { if (!region.getGameModeConfig().Techhider.Active) { return Optional.empty(); } - hidden.put(rg, new HashSet<>()); + hidden.put(rg, ConcurrentHashMap.newKeySet()); Set blocks = new HashSet<>(Arrays.asList(region.getGameModeConfig().Techhider.ObfuscateWith)); if (blocks.contains(Material.END_STONE)) blocks.add(Material.END_STONE_BRICKS); xrayedBlocks.put(region, blocks); - return Optional.of(createXray(rg, blocks)); + return Optional.of(HidingRule.of(rg.getBuildArea(), new ObfuscationConfig(Material.STRUCTURE_VOID, blocks, Collections.emptySet()))); }); - if (!techHider.isPresent()) { + if (!hidingRule.isPresent()) { return; } @@ -139,13 +142,12 @@ public class XrayCommand extends SWCommand implements Listener, ScoreboardElemen } } - private TechHider createXray(Region rg, Set obfuscate) { - TechHider current = new TechHider((TechHider.LocationEvaluator) (p, cX, cY) -> { - if (rg.getBuildArea().isChunkOutside(cX, cY)) return true; - return !hidden.get(rg).contains(p); - }, Material.STRUCTURE_VOID, obfuscate, new HashSet<>()); - current.enable(); - return current; + public void addHidingRules(Player player, List rules) { + hidden.forEach((region, players) -> { + if (players.contains(player)) { + hidingRules.getOrDefault(region, Optional.empty()).ifPresent(rules::add); + } + }); } @EventHandler 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 1ccaad8e..6f0b1290 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java @@ -70,7 +70,7 @@ public class TechHiderWrapper extends StateDependent implements Listener { @Override public void enable() { ObfuscationConfig obfuscation = ObfuscationConfig.of(Config.GameModeConfig.Techhider); - new TechHider(obfuscation.getObfuscateWith(), newProvider(obfuscation)); + new TechHider(newProvider(obfuscation)); } private AccessPrivilegeProvider newProvider(ObfuscationConfig obfuscation) { @@ -150,6 +150,11 @@ public class TechHiderWrapper extends StateDependent implements Listener { public boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType type) { return !isHidden(blockX, blockY, blockZ) || !obfuscation.hidesBlockEntity(type); } + + @Override + public BlockState getObfuscation(int blockX, int blockY, int blockZ) { + return obfuscation.getObfuscationState(); + } } @Override diff --git a/HiderCore/src/de/steamwar/hullhider/HullRegion.java b/HiderCore/src/de/steamwar/hullhider/HullRegion.java index b8aacb0b..0006a2af 100644 --- a/HiderCore/src/de/steamwar/hullhider/HullRegion.java +++ b/HiderCore/src/de/steamwar/hullhider/HullRegion.java @@ -69,6 +69,10 @@ public final class HullRegion { return contains(location.getBlockX(), location.getBlockY(), location.getBlockZ()); } + public boolean chunkOutside(int chunkX, int chunkZ) { + return chunkX < (minX >> 4) || chunkX > ((maxX - 1) >> 4) || chunkZ < (minZ >> 4) || chunkZ > ((maxZ - 1) >> 4); + } + /** * @return the index of the block within a bitset of {@link #volume()} bits, only valid for contained blocks */ diff --git a/HiderCore/src/de/steamwar/techhider/AccessPrivilegeProvider.java b/HiderCore/src/de/steamwar/techhider/AccessPrivilegeProvider.java index 38b9b463..be0599cb 100644 --- a/HiderCore/src/de/steamwar/techhider/AccessPrivilegeProvider.java +++ b/HiderCore/src/de/steamwar/techhider/AccessPrivilegeProvider.java @@ -53,5 +53,10 @@ public interface AccessPrivilegeProvider { boolean canAccessEntity(int entityId); boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType type); + + /** + * @return the block state sent instead of a block this player may not access at this position + */ + BlockState getObfuscation(int blockX, int blockY, int blockZ); } } diff --git a/HiderCore/src/de/steamwar/techhider/ChunkHider.java b/HiderCore/src/de/steamwar/techhider/ChunkHider.java index 9ad25a4e..6cabee7b 100644 --- a/HiderCore/src/de/steamwar/techhider/ChunkHider.java +++ b/HiderCore/src/de/steamwar/techhider/ChunkHider.java @@ -31,6 +31,7 @@ import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import org.bukkit.entity.Player; +import java.util.Arrays; import java.util.List; import java.util.function.UnaryOperator; @@ -45,12 +46,6 @@ public class ChunkHider { private final int BLOCKS_PER_SECTION = 4096; private final int BIOMES_PER_SECTION = 64; - private final int blockIdUsedForHiding; - - public ChunkHider(Block blockUsedForObfuscation) { - blockIdUsedForHiding = Block.BLOCK_STATE_REGISTRY.getId(blockUsedForObfuscation.defaultBlockState()); - } - private int getLongsRequiredToEncodeEntries(int bitsPerEntry, int entryCount) { int entriesPerLong = Long.SIZE / bitsPerEntry; int dataLengthAsLongCount = (entryCount + entriesPerLong - 1) / entriesPerLong; @@ -89,9 +84,22 @@ public class ChunkHider { if (bitsPerBlock == 0) { int sectionBlockId = ProtocolUtils.readVarInt(in); - out.writeShort(blockCount); - out.writeByte(bitsPerBlock); - ProtocolUtils.writeVarInt(out, sectionBlockId); + int[] blockData = new int[BLOCKS_PER_SECTION]; + Arrays.fill(blockData, sectionBlockId); + int[] obfuscatedBlockData = obfuscateBlockDataArray(access, chunkX, chunkZ, yOffset, blockData); + + if (Arrays.equals(blockData, obfuscatedBlockData)) { + out.writeShort(blockCount); + out.writeByte(bitsPerBlock); + ProtocolUtils.writeVarInt(out, sectionBlockId); + } else { + // The client skips sections without non-air blocks, so the count has to include the obfuscation blocks + out.writeShort(countNonAirBlocks(obfuscatedBlockData)); + out.writeByte(15); + for (long rawDataSegment : encodeDirectBlockDataArray(obfuscatedBlockData)) { + out.writeLong(rawDataSegment); + } + } } else if (bitsPerBlock <= BIT_PER_BLOCK_INDIRECTION_LIMIT) { int palletLength = ProtocolUtils.readVarInt(in); int[] pallet = ProtocolUtils.readVarIntArray(in, palletLength); @@ -198,7 +206,7 @@ public class ChunkHider { if (access.canAccessPosition(worldX, worldY, worldZ) && access.canAccessBlock(worldX, worldY, worldZ, block) && access.canAccessBlockState(worldX, worldY, worldZ, blockState)) { obfuscatedData[blockDataIndex] = blockId; } else { - obfuscatedData[blockDataIndex] = blockIdUsedForHiding; + obfuscatedData[blockDataIndex] = Block.getId(access.getObfuscation(worldX, worldY, worldZ)); } } } @@ -207,6 +215,16 @@ public class ChunkHider { return obfuscatedData; } + private int countNonAirBlocks(int[] blockDataArray) { + int count = 0; + for (int blockId : blockDataArray) { + if (!Block.BLOCK_STATE_REGISTRY.byId(blockId).isAir()) { + count++; + } + } + return count; + } + private int[] resolveDirectBlockDataArray(long[] rawDataArray, byte bitsPerBlock) { SimpleBitStorage data = new SimpleBitStorage(bitsPerBlock, BLOCKS_PER_SECTION, rawDataArray); diff --git a/HiderCore/src/de/steamwar/techhider/ObfuscationConfig.java b/HiderCore/src/de/steamwar/techhider/ObfuscationConfig.java index cc8310a5..554abaa3 100644 --- a/HiderCore/src/de/steamwar/techhider/ObfuscationConfig.java +++ b/HiderCore/src/de/steamwar/techhider/ObfuscationConfig.java @@ -49,6 +49,7 @@ public final class ObfuscationConfig { } private final Block obfuscateWith; + private final BlockState obfuscationState; private final Set blocks; private final Set blockStates; private final Set> blockEntityTypes; @@ -59,6 +60,7 @@ public final class ObfuscationConfig { */ public ObfuscationConfig(Material obfuscateWith, Set hiddenBlocks, Set hiddenBlockEntities) { this.obfuscateWith = CraftMagicNumbers.getBlock(obfuscateWith); + this.obfuscationState = this.obfuscateWith.defaultBlockState(); this.blocks = hiddenBlocks.stream() .map(CraftMagicNumbers::getBlock) diff --git a/HiderCore/src/de/steamwar/techhider/TechHider.java b/HiderCore/src/de/steamwar/techhider/TechHider.java index eee198e6..af640d18 100644 --- a/HiderCore/src/de/steamwar/techhider/TechHider.java +++ b/HiderCore/src/de/steamwar/techhider/TechHider.java @@ -36,16 +36,20 @@ import net.minecraft.network.protocol.configuration.ClientboundUpdateEnabledFeat import net.minecraft.network.protocol.cookie.ClientboundCookieRequestPacket; import net.minecraft.network.protocol.game.*; import net.minecraft.network.protocol.login.*; +import net.minecraft.network.protocol.ping.ClientboundPongResponsePacket; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.Vec3; +import org.bukkit.Bukkit; import org.bukkit.entity.Player; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.ToIntFunction; +import java.util.logging.Level; /** * The TechHider follows the default-deny security principle, @@ -56,16 +60,14 @@ public class TechHider { private final Set> bypassingPackets; private final Map>, BiFunction, Packet>> packetProcessors; - private final BlockState blockStateUsedForObfuscation; + private final Set> loggedUnhandledPackets = ConcurrentHashMap.newKeySet(); private final ChunkHider chunkHider; private final AccessPrivilegeProvider privilegeProvider; // TODO handle packet bundle - public TechHider(Block blockUsedForObfuscation, AccessPrivilegeProvider privilegeProvider) { - this.blockStateUsedForObfuscation = blockUsedForObfuscation.defaultBlockState(); - - this.chunkHider = new ChunkHider(blockUsedForObfuscation); + public TechHider(AccessPrivilegeProvider privilegeProvider) { + this.chunkHider = new ChunkHider(); this.privilegeProvider = privilegeProvider; this.bypassingPackets = new HashSet<>(List.of( @@ -176,6 +178,9 @@ public class TechHider { ClientboundBlockChangedAckPacket.class, // 7.1.5 Acknowledge Block Change ClientboundChunkBatchFinishedPacket.class, // 7.1.12 Chunk Batch Finished (Delimiter) ClientboundChunkBatchStartPacket.class, // 7.1.13 Chunk Batch Start (Delimiter) + ClientboundAwardStatsPacket.class, // 7.1.4 Award Statistics: the player's own statistics + ClientboundDebugSamplePacket.class, // 7.1.28 Debug Sample: server tick timings + ClientboundPongResponsePacket.class, // Ping Response: answer to the client's own ping ClientboundChunksBiomesPacket.class, // 7.1.14 Chunk Biomes ClientboundContainerClosePacket.class, // 7.1.18 Close Container ClientboundSetChunkCacheCenterPacket.class, // 7.1.93 Set Center Chunk @@ -359,6 +364,9 @@ public class TechHider { if (processor != null) { return processor.apply(access.get(), packet); } else { + if (loggedUnhandledPackets.add(packet.getClass())) { + Bukkit.getLogger().log(Level.WARNING, "[TechHider] Dropping unhandled packet type " + packet.getClass().getName()); + } return null; } } @@ -438,7 +446,7 @@ public class TechHider { if (access.canAccessPosition(blockX, blockY, blockZ) && access.canAccessBlock(blockX, blockY, blockZ, block) && access.canAccessBlockState(blockX, blockY, blockZ, blockState)) { return packet; } else if (access.canAccessPosition(blockX, blockY, blockZ)) { - return new ClientboundBlockUpdatePacket(blockPos, blockStateUsedForObfuscation); + return new ClientboundBlockUpdatePacket(blockPos, access.getObfuscation(blockX, blockY, blockZ)); } else { return null; } @@ -491,7 +499,7 @@ public class TechHider { filteredStates.add(state); } else if (access.canAccessPosition(worldX, worldY, worldZ)) { filteredPos.add(posShort); - filteredStates.add(blockStateUsedForObfuscation); + filteredStates.add(access.getObfuscation(worldX, worldY, worldZ)); } } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ChunkHider.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ChunkHider.java deleted file mode 100644 index bca91a11..00000000 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ChunkHider.java +++ /dev/null @@ -1,301 +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.techhider.legacy; - -import de.steamwar.core.ProtocolUtils; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import lombok.Getter; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData; -import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; -import net.minecraft.util.SimpleBitStorage; -import net.minecraft.world.level.block.entity.BlockEntityType; -import org.bukkit.entity.Player; - -import java.util.Collections; -import java.util.Set; -import java.util.function.BiFunction; -import java.util.function.UnaryOperator; -import java.util.stream.Collectors; - -@Deprecated -public class ChunkHider { - public static final ChunkHider impl = new ChunkHider(); - - private static final UnaryOperator chunkPacketCloner = ProtocolUtils.shallowCloneGenerator(ClientboundLevelChunkWithLightPacket.class, ClientboundLevelChunkWithLightPacket::new); - private static final UnaryOperator chunkDataCloner = ProtocolUtils.shallowCloneGenerator(ClientboundLevelChunkPacketData.class, ClientboundLevelChunkPacketData::new); - - public BiFunction chunkHiderGenerator(TechHider techHider) { - return (p, packet) -> { - int chunkX = packet.getX(); - int chunkZ = packet.getZ(); - if (techHider.getLocationEvaluator().skipChunk(p, chunkX, chunkZ)) { - return packet; - } - - packet = chunkPacketCloner.apply(packet); - ClientboundLevelChunkPacketData dataWrapper = chunkDataCloner.apply(packet.getChunkData()); - - Set hiddenBlockEntities = techHider.getHiddenBlockEntities(); - dataWrapper.blockEntitiesData = dataWrapper.blockEntitiesData.stream() - .filter(te -> tileEntityVisible(hiddenBlockEntities, te)) - .collect(Collectors.toList()); - - ByteBuf in = Unpooled.wrappedBuffer(dataWrapper.buffer); - ByteBuf out = Unpooled.buffer(in.readableBytes() + 64); - for (int yOffset = p.getWorld().getMinHeight(); yOffset < p.getWorld().getMaxHeight(); yOffset += 16) { - SectionHider section = new SectionHider(p, techHider, in, out, chunkX, yOffset / 16, chunkZ); - section.copyBlockCount(); - - blocks(section); - biomes(section); - } - - if (in.readableBytes() != 0) { - throw new IllegalStateException("ChunkHider21: Incomplete chunk data, " + in.readableBytes() + " bytes left"); - } - - byte[] data = new byte[out.readableBytes()]; - out.readBytes(data); - dataWrapper.buffer = data; - - packet.chunkData = dataWrapper; - return packet; - }; - } - - protected boolean tileEntityVisible(Set hiddenBlockEntities, ClientboundLevelChunkPacketData.BlockEntityInfo tile) { - BlockEntityType type = tile.type; - String path = BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(type).getPath(); - return !hiddenBlockEntities.contains(path); - } - - private void blocks(SectionHider section) { - section.copyBitsPerBlock(); - - boolean singleValued = section.getBitsPerBlock() == 0; - if (singleValued) { - int value = ProtocolUtils.readVarInt(section.getIn()); - ProtocolUtils.writeVarInt(section.getOut(), !section.isSkipSection() && section.getObfuscate().contains(value) ? section.getTarget() : value); - return; - } else if (section.getBitsPerBlock() < 9) { - // Indirect (paletted) storage – only present when bitsPerBlock < 9 in 1.21+ - section.processPalette(); - } - - if (section.isSkipSection() || (!section.blockPrecise() && section.isPaletted())) { - section.skipNewDataArray(4096); - return; - } - - SimpleBitStorage values = new SimpleBitStorage(section.getBitsPerBlock(), 4096, section.readNewDataArray(4096)); - - for (int y = 0; y < 16; y++) { - for (int z = 0; z < 16; z++) { - for (int x = 0; x < 16; x++) { - int pos = (((y * 16) + z) * 16) + x; - - TechHider.State test = section.test(x, y, z); - - switch (test) { - case SKIP: - break; - case CHECK: - if (!section.getObfuscate().contains(values.get(pos))) { - break; - } - case HIDE: - values.set(pos, section.getTarget()); - break; - case HIDE_AIR: - default: - values.set(pos, section.getAir()); - } - } - } - } - - section.writeDataArray(values.getRaw()); - } - - private void biomes(SectionHider section) { - section.copyBitsPerBlock(); - if (section.getBitsPerBlock() == 0) { - section.copyVarInt(); - } else if (section.getBitsPerBlock() < 6) { - section.skipPalette(); - section.skipNewDataArray(64); - } else { - // Direct (global) biome IDs – no palette present - section.skipNewDataArray(64); - } - } - - @Getter - class SectionHider { - private final Player player; - private final TechHider techHider; - private final ByteBuf in; - private final ByteBuf out; - - private final int chunkX; - private final int chunkY; - private final int chunkZ; - private final int offsetX; - private final int offsetY; - private final int offsetZ; - - private final boolean skipSection; - - private boolean paletted; - private int bitsPerBlock; - private int blockCount; - private int air; - private int target; - private Set obfuscate; - - public SectionHider(Player player, TechHider techHider, ByteBuf in, ByteBuf out, int chunkX, int chunkY, int chunkZ) { - this.player = player; - this.techHider = techHider; - this.in = in; - this.out = out; - this.chunkX = chunkX; - this.chunkY = chunkY; - this.chunkZ = chunkZ; - this.offsetX = 16 * chunkX; - this.offsetY = 16 * chunkY; - this.offsetZ = 16 * chunkZ; - this.skipSection = techHider.getLocationEvaluator().skipChunkSection(player, chunkX, chunkY, chunkZ); - - this.paletted = false; - this.bitsPerBlock = 0; - this.air = TechHider.AIR_ID; - this.target = techHider.getObfuscationTargetId(); - this.obfuscate = techHider.getObfuscateIds(); - } - - public boolean blockPrecise() { - return techHider.getLocationEvaluator().blockPrecise(player, chunkX, chunkY, chunkZ); - } - - public TechHider.State test(int x, int y, int z) { - return techHider.getLocationEvaluator().check(player, offsetX + x, offsetY + y, offsetZ + z); - } - - public void copyBlockCount() { - this.blockCount = in.readShort(); - out.writeShort(blockCount); - } - - public void copyBitsPerBlock() { - bitsPerBlock = in.readByte(); - out.writeByte(bitsPerBlock); - } - - public int copyVarInt() { - int value = ProtocolUtils.readVarInt(in); - ProtocolUtils.writeVarInt(out, value); - return value; - } - - public void skipPalette() { - int paletteLength = copyVarInt(); - for (int i = 0; i < paletteLength; i++) { - copyVarInt(); - } - } - - public void processPalette() { - if (skipSection) { - skipPalette(); - return; - } - - int paletteLength = copyVarInt(); - if (paletteLength == 0) return; - - paletted = true; - air = 0; - target = 0; - - for (int i = 0; i < paletteLength; i++) { - int entry = ProtocolUtils.readVarInt(in); - if (obfuscate.contains(entry)) { - entry = techHider.getObfuscationTargetId(); - } - - if (entry == TechHider.AIR_ID) { - air = i; - } else if (entry == techHider.getObfuscationTargetId()) { - target = i; - } - - ProtocolUtils.writeVarInt(out, entry); - } - obfuscate = Collections.emptySet(); - } - - public void skipDataArray() { - int dataArrayLength = copyVarInt(); - out.writeBytes(in, dataArrayLength * 8); - } - - public void skipNewDataArray(int entries) { - if (bitsPerBlock == 0) { - return; - } - - char valuesPerLong = (char) (64 / bitsPerBlock); - int i1 = (entries + valuesPerLong - 1) / valuesPerLong; - out.writeBytes(in, i1 * Long.BYTES); - } - - public long[] readDataArray() { - long[] array = new long[copyVarInt()]; - for (int i = 0; i < array.length; i++) { - array[i] = in.readLong(); - } - - return array; - } - - public long[] readNewDataArray(int entries) { - if (bitsPerBlock == 0) { - return new long[entries]; - } - - char valuesPerLong = (char) (64 / bitsPerBlock); - int i1 = (entries + valuesPerLong - 1) / valuesPerLong; - long[] array = new long[i1]; - for (int i = 0; i < i1; i++) { - array[i] = in.readLong(); - } - - return array; - } - - public void writeDataArray(long[] array) { - for (long l : array) { - out.writeLong(l); - } - } - } -} diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ProtocolWrapper.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ProtocolWrapper.java deleted file mode 100644 index cb7b8d05..00000000 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/ProtocolWrapper.java +++ /dev/null @@ -1,79 +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.techhider.legacy; - -import net.minecraft.core.SectionPos; -import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket; -import net.minecraft.world.level.block.state.BlockState; -import org.bukkit.entity.Player; - -import java.util.ArrayList; -import java.util.function.BiFunction; - -@Deprecated -public class ProtocolWrapper { - public static final ProtocolWrapper impl = new ProtocolWrapper(); - - public BiFunction multiBlockChangeGenerator(TechHider techHider) { - return (p, packet) -> { - TechHider.LocationEvaluator locationEvaluator = techHider.getLocationEvaluator(); - SectionPos chunkCoords = packet.sectionPos; - int chunkX = chunkCoords.getX(); - int chunkY = chunkCoords.getY(); - int chunkZ = chunkCoords.getZ(); - if (locationEvaluator.skipChunkSection(p, chunkX, chunkY, chunkZ)) { - return packet; - } - - packet = TechHider.multiBlockChangeCloner.apply(packet); - final short[] oldPos = packet.positions; - final BlockState[] oldBlocks = packet.states; - ArrayList poss = new ArrayList<>(oldPos.length); - ArrayList blocks = new ArrayList<>(oldPos.length); - for (int i = 0; i < oldPos.length; i++) { - short pos = oldPos[i]; - BlockState block = oldBlocks[i]; - switch (locationEvaluator.check(p, 16 * chunkX + (pos >> 8 & 0xF), 16 * chunkY + (pos & 0xF), 16 * chunkZ + (pos >> 4 & 0xF))) { - case SKIP: - poss.add(pos); - blocks.add(block); - break; - case CHECK: - poss.add(pos); - blocks.add(techHider.iBlockDataHidden(block) ? (BlockState) techHider.getObfuscationTarget() : block); - break; - default: - break; - } - } - - if (blocks.isEmpty()) return null; - - short[] newPos = new short[poss.size()]; - for (int i = 0; i < newPos.length; i++) { - newPos[i] = poss.get(i); - } - - packet.positions = newPos; - packet.states = blocks.toArray(BlockState[]::new); - return packet; - }; - } -} diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/TechHider.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/TechHider.java deleted file mode 100644 index 5c9cd169..00000000 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/legacy/TechHider.java +++ /dev/null @@ -1,169 +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.techhider.legacy; - -import com.comphenix.tinyprotocol.TinyProtocol; -import de.steamwar.core.BlockIds; -import de.steamwar.core.ProtocolUtils; -import lombok.Getter; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Vec3i; -import net.minecraft.network.protocol.game.*; -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.Material; -import org.bukkit.craftbukkit.util.CraftMagicNumbers; -import org.bukkit.entity.Player; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.function.BiFunction; -import java.util.function.UnaryOperator; -import java.util.stream.Collectors; - -@Deprecated -public class TechHider { - - public static final Class blockPosition = BlockPos.class; - - public static final Class iBlockData = BlockState.class; - public static final Class block = Block.class; - - public boolean iBlockDataHidden(BlockState iBlockData) { - return obfuscateIds.contains(BlockIds.impl.getCombinedId(iBlockData)); - } - - public static final Object AIR = CraftMagicNumbers.getBlock(Material.AIR).defaultBlockState(); - public static final int AIR_ID = BlockIds.impl.materialToId(Material.AIR); - - private final Map, BiFunction> techhiders = new HashMap<>(); - @Getter - private final LocationEvaluator locationEvaluator; - @Getter - private final Object obfuscationTarget; - @Getter - private final int obfuscationTargetId; - @Getter - private final Set obfuscateIds; - @Getter - private final Set hiddenBlockEntities; - - public TechHider(LocationEvaluator locationEvaluator, Material obfuscationTarget, Set obfuscate, Set hiddenBlockEntities) { - this.locationEvaluator = locationEvaluator; - this.obfuscateIds = obfuscate.stream().flatMap(m -> BlockIds.impl.materialToAllIds(m).stream()).collect(Collectors.toSet()); - this.hiddenBlockEntities = hiddenBlockEntities; - this.obfuscationTarget = CraftMagicNumbers.getBlock(obfuscationTarget).defaultBlockState(); - this.obfuscationTargetId = BlockIds.impl.materialToId(obfuscationTarget); - - techhiders.put(ClientboundBlockEventPacket.class, (player, o) -> this.blockActionHider(player, (ClientboundBlockEventPacket) o)); - techhiders.put(ClientboundBlockUpdatePacket.class, (player, o) -> this.blockChangeHider(player, (ClientboundBlockUpdatePacket) o)); - techhiders.put(ClientboundBlockEntityDataPacket.class, (player, o) -> this.tileEntityDataHider(player, (ClientboundBlockEntityDataPacket) o)); - techhiders.put(ClientboundSectionBlocksUpdatePacket.class, (BiFunction) ProtocolWrapper.impl.multiBlockChangeGenerator(this)); - techhiders.put(ClientboundLevelChunkWithLightPacket.class, (BiFunction) ChunkHider.impl.chunkHiderGenerator(this)); - techhiders.put(ServerboundUseItemOnPacket.class, (p, packet) -> locationEvaluator.suppressInteractions(p) ? null : packet); - techhiders.put(ServerboundInteractPacket.class, (p, packet) -> locationEvaluator.suppressInteractions(p) ? null : packet); - - } - - public void enable() { - techhiders.forEach((type, playerObjectBiFunction) -> TinyProtocol.instance.addFilter((Class) type, playerObjectBiFunction)); - } - - public void disable() { - techhiders.forEach(TinyProtocol.instance::removeFilter); - } - - public static final UnaryOperator multiBlockChangeCloner = ProtocolUtils.shallowCloneGenerator(ClientboundSectionBlocksUpdatePacket.class, ClientboundSectionBlocksUpdatePacket::new); - - private static final UnaryOperator blockChangeCloner = ProtocolUtils.shallowCloneGenerator(ClientboundBlockUpdatePacket.class, ClientboundBlockUpdatePacket::new); - - private ClientboundBlockUpdatePacket blockChangeHider(Player p, ClientboundBlockUpdatePacket packet) { - switch (locationEvaluator.checkBlockPos(p, packet.getPos())) { - case SKIP: - return packet; - case CHECK: - if (!iBlockDataHidden(packet.blockState)) { - return packet; - } - case HIDE: - packet = blockChangeCloner.apply(packet); - packet.blockState = (BlockState) obfuscationTarget; - return packet; - case HIDE_AIR: - default: - packet = blockChangeCloner.apply(packet); - packet.blockState = (BlockState) AIR; - return packet; - } - } - - private Object blockActionHider(Player p, ClientboundBlockEventPacket packet) { - if (locationEvaluator.checkBlockPos(p, packet.getPos()) == State.SKIP) { - return packet; - } - return null; - } - - private ClientboundBlockEntityDataPacket tileEntityDataHider(Player p, ClientboundBlockEntityDataPacket packet) { - switch (locationEvaluator.checkBlockPos(p, packet.getPos())) { - case SKIP: - return packet; - case CHECK: - if (packet.getType() != BlockEntityType.SIGN) { - return packet; - } - default: - return null; - } - } - - public enum State { - SKIP, - CHECK, - HIDE, - HIDE_AIR - } - - public interface LocationEvaluator { - default boolean suppressInteractions(Player player) { - return false; - } - - boolean skipChunk(Player player, int x, int z); - - default boolean skipChunkSection(Player player, int x, int y, int z) { - return skipChunk(player, x, z); - } - - default State check(Player player, int x, int y, int z) { - return skipChunkSection(player, ProtocolUtils.posToChunk(x), ProtocolUtils.posToChunk(y), ProtocolUtils.posToChunk(z)) ? State.SKIP : State.CHECK; - } - - default State checkBlockPos(Player player, Vec3i pos) { - return check(player, pos.getX(), pos.getY(), pos.getZ()); - } - - default boolean blockPrecise(Player player, int x, int y, int z) { - return false; - } - } -} diff --git a/SpigotCore/SpigotCore_Main/src/spigotcore.accesswidener b/SpigotCore/SpigotCore_Main/src/spigotcore.accesswidener index 99170241..210a5faf 100644 --- a/SpigotCore/SpigotCore_Main/src/spigotcore.accesswidener +++ b/SpigotCore/SpigotCore_Main/src/spigotcore.accesswidener @@ -35,34 +35,3 @@ accessible field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket mutable field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket yHeadRot B accessible field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket entityId I mutable field net/minecraft/network/protocol/game/ClientboundRotateHeadPacket entityId I - -# For legacy/ChunkHider -## transitive-extendable means that a public no args constructor is added without any super initialization -transitive-extendable class net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket -accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket chunkData Lnet/minecraft/network/protocol/game/ClientboundLevelChunkPacketData; -mutable field net/minecraft/network/protocol/game/ClientboundLevelChunkWithLightPacket chunkData Lnet/minecraft/network/protocol/game/ClientboundLevelChunkPacketData; - -## transitive-extendable means that a public no args constructor is added without any super initialization -transitive-extendable class net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData -accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData buffer [B -mutable field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData buffer [B -accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData blockEntitiesData Ljava/util/List; -mutable field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData blockEntitiesData Ljava/util/List; - -accessible class net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo -accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo type Lnet/minecraft/world/level/block/entity/BlockEntityType; -accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo packedXZ I -accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo y I - -# For legacy/TechHider -## transitive-extendable means that a public no args constructor is added without any super initialization -transitive-extendable class net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket -accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket sectionPos Lnet/minecraft/core/SectionPos; -accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket positions [S -mutable field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket positions [S -accessible field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket states [Lnet/minecraft/world/level/block/state/BlockState; -mutable field net/minecraft/network/protocol/game/ClientboundSectionBlocksUpdatePacket states [Lnet/minecraft/world/level/block/state/BlockState; - -## transitive-extendable means that a public no args constructor is added without any super initialization -transitive-extendable class net/minecraft/network/protocol/game/ClientboundBlockUpdatePacket -mutable field net/minecraft/network/protocol/game/ClientboundBlockUpdatePacket blockState Lnet/minecraft/world/level/block/state/BlockState; \ No newline at end of file From d1fb4ca67ea81c2f312cf4bafb1f14c734e87be7 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 15:09:04 +0100 Subject: [PATCH 05/19] Allow removing hulls and resend their chunks HullHider#removeHull stops hiding a hull: it shows the entities and REntities the hull hid and resends its chunks to everybody who could not view it. A hull is enabled again by adding and initialising it. Hull#resendChunks only resends chunks the player has loaded. HullHider now resends the chunks to non-viewers after initialising a hull itself, so FightHullHider no longer does it. --- .../fightsystem/utils/FightHullHider.java | 9 ----- HiderCore/src/de/steamwar/hullhider/Hull.java | 34 +++++++++++++++++ .../src/de/steamwar/hullhider/HullHider.java | 37 ++++++++++++++++++- .../src/de/steamwar/hullhider/HullRegion.java | 12 ++++++ 4 files changed, 82 insertions(+), 10 deletions(-) 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 4175655b..6c1d9d1b 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java @@ -32,7 +32,6 @@ import de.steamwar.fightsystem.states.StateDependentListener; import de.steamwar.fightsystem.states.StateDependentTask; import de.steamwar.hullhider.Hull; import de.steamwar.hullhider.HullHider; -import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.entity.Player; @@ -68,14 +67,6 @@ public class FightHullHider { public void initialize(FightTeam team) { if (!TechHiderWrapper.ENABLED) return; hullHider.initialize(team); - - // Chunks encoded while the hull was filled as hidden contain obfuscated blocks that are visible now, but no block update is generated for them - Hull hull = hullHider.getHull(team); - for (Player player : Bukkit.getOnlinePlayers()) { - if (!hull.isViewer(player)) { - FightSystem.getTechHider().reloadChunks(player, team.getExtendRegion(), Region.EMPTY); - } - } } public void fill(FightTeam team, boolean visible) { diff --git a/HiderCore/src/de/steamwar/hullhider/Hull.java b/HiderCore/src/de/steamwar/hullhider/Hull.java index 7344af14..04826401 100644 --- a/HiderCore/src/de/steamwar/hullhider/Hull.java +++ b/HiderCore/src/de/steamwar/hullhider/Hull.java @@ -21,6 +21,7 @@ package de.steamwar.hullhider; import com.comphenix.tinyprotocol.TinyProtocol; import com.mojang.authlib.GameProfile; +import de.steamwar.core.CraftbukkitWrapper; import de.steamwar.core.ProtocolWrapper; import de.steamwar.entity.REntity; import it.unimi.dsi.fastutil.shorts.Short2ObjectArrayMap; @@ -32,6 +33,7 @@ import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket; import net.minecraft.world.level.block.state.BlockState; import org.bukkit.Bukkit; +import org.bukkit.Chunk; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; @@ -180,6 +182,38 @@ public class Hull { rentities.remove(entity); } + /** + * Shows all entities hidden by this hull again, used when the hull is removed. + */ + public void reveal() { + for (Player player : Bukkit.getOnlinePlayers()) { + if (isViewer(player)) continue; + + for (Entity entity : entities) { + if (!player.equals(entity)) trackEntity(player, entity); + } + } + entities.clear(); + + for (REntity entity : rentities) { + entity.hide(false); + } + rentities.clear(); + } + + /** + * Resends the chunks of this hull the player has loaded, so they are encoded with the current visibility. + */ + public void resendChunks(Player player) { + if (player.getWorld() != world) return; + + region.forEachChunk((chunkX, chunkZ) -> { + if (player.isChunkSent(Chunk.getChunkKey(chunkX, chunkZ))) { + CraftbukkitWrapper.sendChunk(player, chunkX, chunkZ); + } + }); + } + public void fill(boolean visible) { visibility.set(0, visibility.size(), visible); occluding.set(0, occluding.size(), !visible); diff --git a/HiderCore/src/de/steamwar/hullhider/HullHider.java b/HiderCore/src/de/steamwar/hullhider/HullHider.java index 9bbc846f..fd37baa8 100644 --- a/HiderCore/src/de/steamwar/hullhider/HullHider.java +++ b/HiderCore/src/de/steamwar/hullhider/HullHider.java @@ -20,6 +20,7 @@ package de.steamwar.hullhider; import de.steamwar.entity.REntity; +import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.entity.Entity; @@ -45,11 +46,30 @@ public class HullHider implements Listener { // Read on netty threads, replaced on every change private volatile Hull[] hulls = new Hull[0]; + /** + * The hull hides everything from non-viewers until it is initialised. + */ public void addHull(K key, Hull hull) { hullMap.put(key, hull); hulls = hullMap.values().toArray(new Hull[0]); } + /** + * Stops hiding the hull: shows the entities it hid and resends its chunks to everybody who could not view it. + */ + public void removeHull(K key) { + Hull hull = hullMap.remove(key); + if (hull == null) return; + hulls = hullMap.values().toArray(new Hull[0]); + + hull.reveal(); + for (Player player : Bukkit.getOnlinePlayers()) { + if (!hull.isViewer(player)) { + hull.resendChunks(player); + } + } + } + public Hull getHull(K key) { return hullMap.get(key); } @@ -59,7 +79,22 @@ public class HullHider implements Listener { } public void initialize(K key) { - hullMap.get(key).initialize(); + Hull hull = hullMap.get(key); + hull.initialize(); + + // Chunks encoded while the hull was filled as hidden contain obfuscated blocks that are visible now, but no block update is generated for them + for (Player player : Bukkit.getOnlinePlayers()) { + if (!hull.isViewer(player)) { + hull.resendChunks(player); + } + } + } + + /** + * Has to be called after the player became a viewer of the hull or stopped being one. + */ + public void resendChunks(Player player, K key) { + hullMap.get(key).resendChunks(player); } public void fill(K key, boolean visible) { diff --git a/HiderCore/src/de/steamwar/hullhider/HullRegion.java b/HiderCore/src/de/steamwar/hullhider/HullRegion.java index 0006a2af..7b5d30ab 100644 --- a/HiderCore/src/de/steamwar/hullhider/HullRegion.java +++ b/HiderCore/src/de/steamwar/hullhider/HullRegion.java @@ -90,10 +90,22 @@ public final class HullRegion { } } + public void forEachChunk(ChunkConsumer executor) { + for (int chunkX = minX >> 4; chunkX <= (maxX - 1) >> 4; chunkX++) { + for (int chunkZ = minZ >> 4; chunkZ <= (maxZ - 1) >> 4; chunkZ++) { + executor.accept(chunkX, chunkZ); + } + } + } + public interface BlockConsumer { void accept(int x, int y, int z); } + public interface ChunkConsumer { + void accept(int chunkX, int chunkZ); + } + @Override public String toString() { return minX + "," + minY + "," + minZ + "->" + maxX + "," + maxY + "," + maxZ; From f02f356b2bf1847157855750a990ad2deb7fd5cb Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 15:32:18 +0100 Subject: [PATCH 06/19] Add /hullhider to BauSystem /hullhider hides the inside of the build area hull of a region from the players who toggle it, like the hull hider of a fight. Each region gets its own hull while somebody uses it; it is rebuilt every time somebody enables it, as WorldEdit and Axiom changes do not update it. /techhider, /xray and /hullhider exclude each other per player and region. Which players may see the hull contents is now decided by Viewers, a sealed class with HiddenByDefault (fight) and VisibleByDefault (bau). Added players are stored in a set; isViewer alone defines what being added means. --- .../BauSystem_Main/src/BauSystem.properties | 6 + .../src/BauSystem_de.properties | 6 + .../features/hullhider/HullHiderCommand.java | 179 ++++++++++++++++++ .../features/techhider/BauTechHider.java | 34 +++- .../features/techhider/HiddenHull.java | 29 +++ .../features/techhider/TechHiderCommand.java | 5 + .../bausystem/features/xray/XrayCommand.java | 5 + .../fightsystem/utils/FightHullHider.java | 11 +- HiderCore/src/de/steamwar/hullhider/Hull.java | 61 +++--- .../src/de/steamwar/hullhider/HullHider.java | 19 +- .../src/de/steamwar/hullhider/Viewers.java | 75 ++++++++ 11 files changed, 391 insertions(+), 39 deletions(-) create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/HiddenHull.java create mode 100644 HiderCore/src/de/steamwar/hullhider/Viewers.java diff --git a/BauSystem/BauSystem_Main/src/BauSystem.properties b/BauSystem/BauSystem_Main/src/BauSystem.properties index c5adfb70..80e591d9 100644 --- a/BauSystem/BauSystem_Main/src/BauSystem.properties +++ b/BauSystem/BauSystem_Main/src/BauSystem.properties @@ -37,6 +37,7 @@ SCOREBOARD_TPS_FROZEN = §eFrozen SCOREBOARD_TRACE_TICKS = Ticks SCOREBOARD_TECHHIDER = TechHider§8: §aOn SCOREBOARD_XRAY = XRay§8: §aOn +SCOREBOARD_HULLHIDER = HullHider§8: §aOn SCOREBOARD_LOCK_TEAM = Bau Lock§8: §eTeam SCOREBOARD_LOCK_SUPERVISOR = Bau Lock§8: §eSupervisor SCOREBOARD_LOCK_TEAM_AND_SERVERTEAM = Bau Lock§8: §e(Server) Team @@ -1033,6 +1034,11 @@ XRAY_HELP = §8/§exray §8- §7Toggle Xray XRAY_GLOBAL = §cNo xray in global region XRAY_ON = §aXray activated XRAY_OFF = §cXray deactivated +# HULLHIDER +HULLHIDER_HELP = §8/§ehullhider §8- §7Toggle Hullhider +HULLHIDER_GLOBAL = §cNo hullhider in global region +HULLHIDER_ON = §aHullhider activated +HULLHIDER_OFF = §cHullhider deactivated # WorldEdit COLORREPLACE_HELP = §8//§ecolorreplace §8[§7color§8] §8[§7color§8] §8- §7Replace all blocks of one color with another TYPEREPLACE_HELP = §8//§etypereplace §8[§7type§8] §8[§7type§8] §8- §7Replace all blocks of one type with another diff --git a/BauSystem/BauSystem_Main/src/BauSystem_de.properties b/BauSystem/BauSystem_Main/src/BauSystem_de.properties index 9bee037c..e08803a9 100644 --- a/BauSystem/BauSystem_Main/src/BauSystem_de.properties +++ b/BauSystem/BauSystem_Main/src/BauSystem_de.properties @@ -37,6 +37,7 @@ SCOREBOARD_TPS_FROZEN = §eEingefroren SCOREBOARD_TRACE_TICKS = Ticks SCOREBOARD_TECHHIDER = TechHider§8: §aAn SCOREBOARD_XRAY = XRay§8: §aAn +SCOREBOARD_HULLHIDER = HullHider§8: §aAn SCOREBOARD_LOCK_TEAM = Bau Lock§8: §eTeam SCOREBOARD_LOCK_TEAM_AND_SERVERTEAM = Bau Lock§8: §e(Server-) Team SCOREBOARD_LOCK_SERVERTEAM = Bau Lock§8: §eServerteam @@ -966,6 +967,11 @@ XRAY_HELP = §8/§exray §8- §7Xray umschalten XRAY_GLOBAL = §cKein Xray in der globalen region XRAY_ON = §aXray aktiviert XRAY_OFF = §cXray deaktiviert +# HULLHIDER +HULLHIDER_HELP = §8/§ehullhider §8- §7Hullhider umschalten +HULLHIDER_GLOBAL = §cKein Hullhider in der globalen region +HULLHIDER_ON = §aHullhider aktiviert +HULLHIDER_OFF = §cHullhider deaktiviert # WorldEdit COLORREPLACE_HELP = §8//§ecolorreplace §8[§7color§8] §8[§7color§8] §8- §7Ersetzt eine Farbe mit einer anderen TYPEREPLACE_HELP = §8//§etyreplace §8[§7type§8] §8[§7type§8] §8- §7Ersetzt einen Blockgruppe mit einer anderen diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java new file mode 100644 index 00000000..b8ad87de --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java @@ -0,0 +1,179 @@ +/* + * 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.bausystem.features.hullhider; + +import de.steamwar.bausystem.BauSystem; +import de.steamwar.bausystem.features.techhider.HiddenHull; +import de.steamwar.bausystem.features.techhider.TechHiderCommand; +import de.steamwar.bausystem.features.xray.XrayCommand; +import de.steamwar.bausystem.region.Point; +import de.steamwar.bausystem.region.Region; +import de.steamwar.bausystem.utils.ScoreboardElement; +import de.steamwar.command.SWCommand; +import de.steamwar.hullhider.Hull; +import de.steamwar.hullhider.HullHider; +import de.steamwar.hullhider.HullRegion; +import de.steamwar.hullhider.Viewers; +import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedInstance; +import de.steamwar.techhider.ObfuscationConfig; +import net.md_5.bungee.api.ChatMessageType; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Hides the inside of the build area hull of a region from the players who toggle it, as the hull hider of a fight would. + */ +@Linked +public class HullHiderCommand extends SWCommand implements Listener, ScoreboardElement { + + public HullHiderCommand() { + super("hullhider"); + + Bukkit.getPluginManager().registerEvents(hullHider, BauSystem.getInstance()); + Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), this::onTick, 1, 1); + } + + private final HullHider hullHider = new HullHider<>(); + // Read on netty threads by the BauTechHider + private final Map hulls = new ConcurrentHashMap<>(); + private final Map> hidden = new ConcurrentHashMap<>(); + + @LinkedInstance + public TechHiderCommand techHiderCommand; + + @LinkedInstance + public XrayCommand xrayCommand; + + @Register(description = "HULLHIDER_HELP") + public void toggleHider(@Validator Player player) { + Region region = Region.getRegion(player.getLocation()); + if (region.getType().isGlobal()) { + BauSystem.MESSAGE.send("HULLHIDER_GLOBAL", player); + return; + } + if (!region.getGameModeConfig().Techhider.Active) { + return; + } + + techHiderCommand.disable(region, player); + xrayCommand.disable(region, player); + if (hidden.getOrDefault(region, Collections.emptySet()).contains(player)) { + disable(region, player); + BauSystem.MESSAGE.sendPrefixless("HULLHIDER_OFF", player, ChatMessageType.ACTION_BAR); + } else { + enable(region, player); + BauSystem.MESSAGE.sendPrefixless("HULLHIDER_ON", player, ChatMessageType.ACTION_BAR); + } + } + + private void enable(Region region, Player player) { + if (!hulls.containsKey(region)) { + Point min = region.getBuildArea().getMinPoint(true); + Point max = region.getBuildArea().getMaxPoint(true); + HullRegion hullRegion = HullRegion.fromInclusive(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ()); + + // Only the hidden blocks of an active techhider occlude, the hull hider is used without one + Hull hull = new Hull(BauSystem.getInstance(), world(), hullRegion, false, Collections.emptySet(), new Viewers.VisibleByDefault()); + hullHider.addHull(region, hull); + hulls.put(region, new HiddenHull(hull, ObfuscationConfig.of(region.getGameModeConfig().Techhider).getObfuscationState())); + } + + hidden.computeIfAbsent(region, rg -> ConcurrentHashMap.newKeySet()).add(player); + // Added players see the hull blocks of a hull visible by default + hullHider.addViewer(player, region); + // Block changes by WorldEdit or Axiom do not update the hull, so it is rebuilt every time somebody enables it. + // This also resends the chunks to the player. + hullHider.initialize(region); + } + + /** + * Stops hiding the hull of the region from the player, removing the hull if nobody uses it anymore. + */ + public void disable(Region region, Player player) { + Set players = hidden.get(region); + if (players == null || !players.remove(player)) return; + + if (players.isEmpty()) { + hidden.remove(region); + hulls.remove(region); + hullHider.removeHull(region); + } else { + hullHider.removeViewer(player, region); + hullHider.resendChunks(player, region); + } + } + + public void addHiddenHulls(Player player, List hiddenHulls) { + hidden.forEach((region, players) -> { + HiddenHull hiddenHull = hulls.get(region); + if (hiddenHull != null && players.contains(player)) { + hiddenHulls.add(hiddenHull); + } + }); + } + + private void onTick() { + if (hidden.isEmpty()) return; + + for (Entity entity : world().getEntities()) { + hullHider.checkEntity(entity); + } + hullHider.sendUncoveredBlocks(); + } + + private World world() { + return Bukkit.getWorlds().get(0); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + for (Region region : hidden.keySet()) { + disable(region, event.getPlayer()); + } + } + + @Override + public ScoreboardGroup getGroup() { + return ScoreboardGroup.OTHER; + } + + @Override + public int order() { + return 0; + } + + @Override + public String get(Region region, Player p) { + if (!hidden.getOrDefault(region, Collections.emptySet()).contains(p)) return null; + return "§e" + BauSystem.MESSAGE.parse("SCOREBOARD_HULLHIDER", p); + } +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java index a4fd51bf..46867bc2 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java @@ -19,6 +19,7 @@ package de.steamwar.bausystem.features.techhider; +import de.steamwar.bausystem.features.hullhider.HullHiderCommand; import de.steamwar.bausystem.features.world.SpectatorListener; import de.steamwar.bausystem.features.xray.XrayCommand; import de.steamwar.linkage.Linked; @@ -29,18 +30,20 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; +import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.entity.Player; import java.util.ArrayList; import java.util.List; /** - * The single TechHider of the server. It combines the hiding rules of the spectator techhider, /techhider and /xray per player. + * The single TechHider of the server. It combines the hiding rules of the spectator techhider, /techhider and /xray and the hulls of /hullhider per player. */ @Linked public class BauTechHider implements AccessPrivilegeProvider { private static final HidingRule[] NO_RULES = new HidingRule[0]; + private static final HiddenHull[] NO_HULLS = new HiddenHull[0]; // Injected after construction, until then nothing is hidden @LinkedInstance @@ -49,6 +52,8 @@ public class BauTechHider implements AccessPrivilegeProvider { public TechHiderCommand techHiderCommand; @LinkedInstance public SpectatorListener spectatorListener; + @LinkedInstance + public HullHiderCommand hullHiderCommand; public BauTechHider() { new TechHider(this); @@ -56,8 +61,8 @@ public class BauTechHider implements AccessPrivilegeProvider { @Override public ForPlayer forPlayer(Player p) { - if (xrayCommand == null || techHiderCommand == null || spectatorListener == null) { - return new BauPlayerAccess(p, NO_RULES); + if (xrayCommand == null || techHiderCommand == null || spectatorListener == null || hullHiderCommand == null) { + return new BauPlayerAccess(p, NO_RULES, NO_HULLS); } // Region rules first: where rules overlap, the first one decides the obfuscation block @@ -65,7 +70,10 @@ public class BauTechHider implements AccessPrivilegeProvider { xrayCommand.addHidingRules(p, rules); techHiderCommand.addHidingRules(p, rules); spectatorListener.addHidingRules(p, rules); - return new BauPlayerAccess(p, rules.toArray(NO_RULES)); + + List hulls = new ArrayList<>(); + hullHiderCommand.addHiddenHulls(p, hulls); + return new BauPlayerAccess(p, rules.toArray(NO_RULES), hulls.toArray(NO_HULLS)); } @Override @@ -76,7 +84,7 @@ public class BauTechHider implements AccessPrivilegeProvider { /** * Snapshot of the hiding rules of one player, taken once per outgoing packet on the netty thread. */ - private record BauPlayerAccess(Player player, HidingRule[] rules) implements ForPlayer { + private record BauPlayerAccess(Player player, HidingRule[] rules, HiddenHull[] hulls) implements ForPlayer { @Override public Player getPlayer() { @@ -88,11 +96,17 @@ public class BauTechHider implements AccessPrivilegeProvider { for (HidingRule rule : rules) { if (rule.coversChunk(chunkX, chunkZ)) return false; } + for (HiddenHull hull : hulls) { + if (!hull.hull().getRegion().chunkOutside(chunkX, chunkZ)) return false; + } return true; } @Override public boolean canAccessPosition(int blockX, int blockY, int blockZ) { + for (HiddenHull hull : hulls) { + if (hull.hull().isBlockHidden(player, blockX, blockY, blockZ)) return false; + } return true; } @@ -114,7 +128,10 @@ public class BauTechHider implements AccessPrivilegeProvider { @Override public boolean canAccessEntity(int entityId) { - return true; + if (hulls.length == 0) return true; + + net.minecraft.world.entity.Entity nmsEntity = ((CraftWorld) player.getWorld()).getHandle().moonrise$getEntityLookup().get(entityId); + return nmsEntity == null || canAccessPosition(nmsEntity.getBlockX(), nmsEntity.getBlockY(), nmsEntity.getBlockZ()); } @Override @@ -130,7 +147,10 @@ public class BauTechHider implements AccessPrivilegeProvider { for (HidingRule rule : rules) { if (rule.covers(blockX, blockY, blockZ)) return rule.obfuscation().getObfuscationState(); } - // Only asked for positions a rule hides + for (HiddenHull hull : hulls) { + if (hull.hull().getRegion().contains(blockX, blockY, blockZ)) return hull.obfuscation(); + } + // Only asked for positions a rule or hull hides return Blocks.END_STONE.defaultBlockState(); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/HiddenHull.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/HiddenHull.java new file mode 100644 index 00000000..e2cff0c8 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/HiddenHull.java @@ -0,0 +1,29 @@ +/* + * 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.bausystem.features.techhider; + +import de.steamwar.hullhider.Hull; +import net.minecraft.world.level.block.state.BlockState; + +/** + * A hull a player may not view, and the block sent instead of its hidden blocks. + */ +public record HiddenHull(Hull hull, BlockState obfuscation) { +} diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java index b6fd7d8a..89956fe2 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java @@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.techhider; import de.steamwar.bausystem.BauSystem; import de.steamwar.bausystem.Permission; +import de.steamwar.bausystem.features.hullhider.HullHiderCommand; import de.steamwar.bausystem.features.world.SpectatorListener; import de.steamwar.bausystem.features.xray.XrayCommand; import de.steamwar.bausystem.region.Region; @@ -58,6 +59,9 @@ public class TechHiderCommand extends SWCommand implements Listener, ScoreboardE @LinkedInstance public XrayCommand xrayCommand; + @LinkedInstance + public HullHiderCommand hullHiderCommand; + @Register(description = "TECHHIDER_HELP") public void toggleHider(@Validator Player player) { Region region = Region.getRegion(player.getLocation()); @@ -79,6 +83,7 @@ public class TechHiderCommand extends SWCommand implements Listener, ScoreboardE } xrayCommand.disable(region, player); + hullHiderCommand.disable(region, player); if (hidden.get(region).contains(player)) { hidden.get(region).remove(player); BauSystem.MESSAGE.sendPrefixless("TECHHIDER_OFF", player, ChatMessageType.ACTION_BAR); 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 0b334429..9e0742d0 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 @@ -21,6 +21,7 @@ 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; import de.steamwar.bausystem.features.techhider.TechHiderCommand; import de.steamwar.bausystem.region.Region; @@ -62,6 +63,9 @@ public class XrayCommand extends SWCommand implements Listener, ScoreboardElemen @LinkedInstance public TechHiderCommand techHiderCommand; + @LinkedInstance + public HullHiderCommand hullHiderCommand; + @Register(description = "XRAY_HELP") public void toggleHider(@Validator Player player) { Region region = Region.getRegion(player.getLocation()); @@ -87,6 +91,7 @@ public class XrayCommand extends SWCommand implements Listener, ScoreboardElemen } techHiderCommand.disable(region, player); + hullHiderCommand.disable(region, player); if (hidden.get(region).contains(player)) { hidden.get(region).remove(player); BauSystem.MESSAGE.sendPrefixless("XRAY_OFF", player, ChatMessageType.ACTION_BAR); 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 6c1d9d1b..cc5074ce 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java @@ -32,6 +32,7 @@ import de.steamwar.fightsystem.states.StateDependentListener; import de.steamwar.fightsystem.states.StateDependentTask; import de.steamwar.hullhider.Hull; import de.steamwar.hullhider.HullHider; +import de.steamwar.hullhider.Viewers; import org.bukkit.Material; import org.bukkit.block.Block; import org.bukkit.entity.Player; @@ -56,7 +57,7 @@ public class FightHullHider { Fight.teams().forEach(team -> { Region region = team.getSchemRegion(); - hullHider.addHull(team, new Hull(FightSystem.getPlugin(), Config.world, region.toHullRegion(), region.getMinY() != Config.PlayerRegion.getMinY(), Config.GameModeConfig.Techhider.HiddenBlocks)); + hullHider.addHull(team, new Hull(FightSystem.getPlugin(), Config.world, region.toHullRegion(), region.getMinY() != Config.PlayerRegion.getMinY(), Config.GameModeConfig.Techhider.HiddenBlocks, new Viewers.HiddenByDefault())); }); new StateDependentListener(TechHiderWrapper.ENABLED, FightState.All, new MembershipListener()); @@ -109,7 +110,13 @@ public class FightHullHider { FightTeam team = Fight.getPlayerTeam(player); FightPlayer fp = Fight.getFightPlayer(player); - hullHider.forEach((hullTeam, hull) -> hullHider.setViewer(player, hullTeam, Config.isReferee(player) || hullTeam == team || (fp != null && fp.canEntern()))); + hullHider.forEach((hullTeam, hull) -> { + if (Config.isReferee(player) || hullTeam == team || (fp != null && fp.canEntern())) { + hullHider.addViewer(player, hullTeam); + } else { + hullHider.removeViewer(player, hullTeam); + } + }); } public void blockUpdate(Block block, Material changedType) { diff --git a/HiderCore/src/de/steamwar/hullhider/Hull.java b/HiderCore/src/de/steamwar/hullhider/Hull.java index 04826401..745d5d88 100644 --- a/HiderCore/src/de/steamwar/hullhider/Hull.java +++ b/HiderCore/src/de/steamwar/hullhider/Hull.java @@ -45,7 +45,6 @@ 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.logging.Level; import java.util.stream.IntStream; @@ -67,21 +66,22 @@ public class Hull { private final HashMap visibilityDirections = new HashMap<>(); // Contains the visible directions of each occluding visible block private final HashSet uncoveredSurface = new HashSet<>(); - // Players that may see the hull contents (viewers). Everybody else is hidden from it (default deny). Concurrent as it is read on netty threads. - private final Set viewers = ConcurrentHashMap.newKeySet(); + private final Viewers viewers; private final HashSet entities = new HashSet<>(); private final HashSet rentities = new HashSet<>(); /** * @param groundVisible whether the bottom layer of the region is uncovered (if the region does not reach the ground) * @param hiddenBlocks blocks that occlude like solid blocks, as the techhider replaces them by an occluding block + * @param viewers which players may see the hull contents, a new instance for every hull */ - public Hull(Plugin plugin, World world, HullRegion region, boolean groundVisible, Set hiddenBlocks) { + public Hull(Plugin plugin, World world, HullRegion region, boolean groundVisible, Set hiddenBlocks, Viewers viewers) { this.plugin = plugin; this.world = world; this.region = region; this.groundVisible = groundVisible; this.hiddenBlocks = hiddenBlocks; + this.viewers = viewers; this.occluding = new BitSet(region.volume()); this.visibility = new BitSet(region.volume()); @@ -103,33 +103,48 @@ public class Hull { } public boolean isViewer(Player player) { - return player != null && viewers.contains(player.getUniqueId()); + return player != null && viewers.isViewer(player); } /** - * Hides the hull from this player (removes the viewer), also hiding all entities currently inside it. - */ - public void removeViewer(Player player) { - viewers.remove(player.getUniqueId()); - for (Entity entity : entities) { - if (!player.equals(entity)) untrackEntity(player, entity); - } - } - - /** - * Allows this player to see the hull (adds a viewer). + * Adds the player to the {@link Viewers}, which decide whether the player may see the hull contents afterwards. */ public void addViewer(Player player) { - if (viewers.add(player.getUniqueId())) { - for (Entity entity : entities) { - trackEntity(player, entity); - } - // techhider triggers block change sending - } + boolean wasViewer = isViewer(player); + viewers.addViewer(player); + updateEntities(player, wasViewer); } + /** + * Removes the player from the {@link Viewers}, which decide whether the player may see the hull contents afterwards. + */ + public void removeViewer(Player player) { + boolean wasViewer = isViewer(player); + viewers.removeViewer(player); + updateEntities(player, wasViewer); + } + + private void updateEntities(Player player, boolean wasViewer) { + if (isViewer(player)) { + if (!wasViewer) { + for (Entity entity : entities) { + trackEntity(player, entity); + } + } + } else { + // Also hides the entities from players that joined after they were hidden + for (Entity entity : entities) { + if (!player.equals(entity)) untrackEntity(player, entity); + } + } + // techhider triggers block change sending + } + + /** + * Resets the player to the default viewer state, e.g. when leaving. + */ public void forgetViewer(Player player) { - viewers.remove(player.getUniqueId()); + viewers.removeViewer(player); } public void checkEntity(Entity entity) { diff --git a/HiderCore/src/de/steamwar/hullhider/HullHider.java b/HiderCore/src/de/steamwar/hullhider/HullHider.java index fd37baa8..92ecb24f 100644 --- a/HiderCore/src/de/steamwar/hullhider/HullHider.java +++ b/HiderCore/src/de/steamwar/hullhider/HullHider.java @@ -107,13 +107,18 @@ public class HullHider implements Listener { } } - public void setViewer(Player player, K key, boolean viewer) { - Hull hull = hullMap.get(key); - if (viewer) { - hull.addViewer(player); - } else { - hull.removeViewer(player); - } + /** + * @see Viewers + */ + public void addViewer(Player player, K key) { + hullMap.get(key).addViewer(player); + } + + /** + * @see Viewers + */ + public void removeViewer(Player player, K key) { + hullMap.get(key).removeViewer(player); } public void forgetViewer(Player player) { diff --git a/HiderCore/src/de/steamwar/hullhider/Viewers.java b/HiderCore/src/de/steamwar/hullhider/Viewers.java new file mode 100644 index 00000000..8467d396 --- /dev/null +++ b/HiderCore/src/de/steamwar/hullhider/Viewers.java @@ -0,0 +1,75 @@ +/* + * 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.hullhider; + +import org.bukkit.entity.Player; + +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * The players of a {@link Hull}. Whether an added player may see the hull contents or sees the hull blocks instead is decided by {@link #isViewer(Player)}. + * Stores the players, so every hull needs its own instance. + */ +public abstract sealed class Viewers { + + // Concurrent as it is read on netty threads + protected final Set players = ConcurrentHashMap.newKeySet(); + + /** + * @return whether the player may see the hull contents instead of the hull blocks + */ + public abstract boolean isViewer(Player player); + + /** + * @return whether the player was not added before + */ + public boolean addViewer(Player player) { + return players.add(player.getUniqueId()); + } + + /** + * @return whether the player was added before + */ + public boolean removeViewer(Player player) { + return players.remove(player.getUniqueId()); + } + + /** + * Hidden from everybody, only added players may see the hull contents. + */ + public static final class HiddenByDefault extends Viewers { + @Override + public boolean isViewer(Player player) { + return players.contains(player.getUniqueId()); + } + } + + /** + * Visible to everybody, only added players see the hull blocks instead of the hull contents. + */ + public static final class VisibleByDefault extends Viewers { + @Override + public boolean isViewer(Player player) { + return !players.contains(player.getUniqueId()); + } + } +} From a9fbb4cd8d35e4fa60ff09e47144f930c1af8f71 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 15:36:02 +0100 Subject: [PATCH 07/19] Combine techhider, xray and hullhider scoreboard lines As only one of /techhider, /xray and /hullhider can be active per region, the scoreboard shows them in a single "View" line naming the active one. --- .../BauSystem_Main/src/BauSystem.properties | 4 +- .../src/BauSystem_de.properties | 4 +- .../features/hullhider/HullHiderCommand.java | 19 +---- .../features/techhider/TechHiderCommand.java | 19 +---- .../techhider/ViewScoreboardElement.java | 70 +++++++++++++++++++ .../bausystem/features/xray/XrayCommand.java | 19 +---- 6 files changed, 81 insertions(+), 54 deletions(-) create mode 100644 BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/ViewScoreboardElement.java diff --git a/BauSystem/BauSystem_Main/src/BauSystem.properties b/BauSystem/BauSystem_Main/src/BauSystem.properties index 80e591d9..fdbe8ff3 100644 --- a/BauSystem/BauSystem_Main/src/BauSystem.properties +++ b/BauSystem/BauSystem_Main/src/BauSystem.properties @@ -35,9 +35,7 @@ SCOREBOARD_LOADER = Loader SCOREBOARD_TPS = TPS SCOREBOARD_TPS_FROZEN = §eFrozen SCOREBOARD_TRACE_TICKS = Ticks -SCOREBOARD_TECHHIDER = TechHider§8: §aOn -SCOREBOARD_XRAY = XRay§8: §aOn -SCOREBOARD_HULLHIDER = HullHider§8: §aOn +SCOREBOARD_VIEW = View§8: §a{0} SCOREBOARD_LOCK_TEAM = Bau Lock§8: §eTeam SCOREBOARD_LOCK_SUPERVISOR = Bau Lock§8: §eSupervisor SCOREBOARD_LOCK_TEAM_AND_SERVERTEAM = Bau Lock§8: §e(Server) Team diff --git a/BauSystem/BauSystem_Main/src/BauSystem_de.properties b/BauSystem/BauSystem_Main/src/BauSystem_de.properties index e08803a9..3fb95eb8 100644 --- a/BauSystem/BauSystem_Main/src/BauSystem_de.properties +++ b/BauSystem/BauSystem_Main/src/BauSystem_de.properties @@ -35,9 +35,7 @@ SCOREBOARD_LOADER = Loader SCOREBOARD_TPS = TPS SCOREBOARD_TPS_FROZEN = §eEingefroren SCOREBOARD_TRACE_TICKS = Ticks -SCOREBOARD_TECHHIDER = TechHider§8: §aAn -SCOREBOARD_XRAY = XRay§8: §aAn -SCOREBOARD_HULLHIDER = HullHider§8: §aAn +SCOREBOARD_VIEW = Ansicht§8: §a{0} SCOREBOARD_LOCK_TEAM = Bau Lock§8: §eTeam SCOREBOARD_LOCK_TEAM_AND_SERVERTEAM = Bau Lock§8: §e(Server-) Team SCOREBOARD_LOCK_SERVERTEAM = Bau Lock§8: §eServerteam diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java index b8ad87de..76f333d1 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java @@ -25,7 +25,6 @@ import de.steamwar.bausystem.features.techhider.TechHiderCommand; import de.steamwar.bausystem.features.xray.XrayCommand; import de.steamwar.bausystem.region.Point; import de.steamwar.bausystem.region.Region; -import de.steamwar.bausystem.utils.ScoreboardElement; import de.steamwar.command.SWCommand; import de.steamwar.hullhider.Hull; import de.steamwar.hullhider.HullHider; @@ -53,7 +52,7 @@ import java.util.concurrent.ConcurrentHashMap; * Hides the inside of the build area hull of a region from the players who toggle it, as the hull hider of a fight would. */ @Linked -public class HullHiderCommand extends SWCommand implements Listener, ScoreboardElement { +public class HullHiderCommand extends SWCommand implements Listener { public HullHiderCommand() { super("hullhider"); @@ -161,19 +160,7 @@ public class HullHiderCommand extends SWCommand implements Listener, ScoreboardE } } - @Override - public ScoreboardGroup getGroup() { - return ScoreboardGroup.OTHER; - } - - @Override - public int order() { - return 0; - } - - @Override - public String get(Region region, Player p) { - if (!hidden.getOrDefault(region, Collections.emptySet()).contains(p)) return null; - return "§e" + BauSystem.MESSAGE.parse("SCOREBOARD_HULLHIDER", p); + public boolean isActive(Region region, Player player) { + return hidden.getOrDefault(region, Collections.emptySet()).contains(player); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java index 89956fe2..0548e99a 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/TechHiderCommand.java @@ -25,7 +25,6 @@ import de.steamwar.bausystem.features.hullhider.HullHiderCommand; import de.steamwar.bausystem.features.world.SpectatorListener; import de.steamwar.bausystem.features.xray.XrayCommand; import de.steamwar.bausystem.region.Region; -import de.steamwar.bausystem.utils.ScoreboardElement; import de.steamwar.command.PreviousArguments; import de.steamwar.command.SWCommand; import de.steamwar.command.TypeMapper; @@ -46,7 +45,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; @Linked -public class TechHiderCommand extends SWCommand implements Listener, ScoreboardElement { +public class TechHiderCommand extends SWCommand implements Listener { public TechHiderCommand() { super("techhider", "hider", "obfuscate", "hide", "hide-tech"); @@ -143,19 +142,7 @@ public class TechHiderCommand extends SWCommand implements Listener, ScoreboardE }); } - @Override - public ScoreboardGroup getGroup() { - return ScoreboardGroup.OTHER; - } - - @Override - public int order() { - return 0; - } - - @Override - public String get(Region region, Player p) { - if (!hidden.getOrDefault(region, Collections.emptySet()).contains(p)) return null; - return "§e" + BauSystem.MESSAGE.parse("SCOREBOARD_TECHHIDER", p); + public boolean isActive(Region region, Player player) { + return hidden.getOrDefault(region, Collections.emptySet()).contains(player); } } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/ViewScoreboardElement.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/ViewScoreboardElement.java new file mode 100644 index 00000000..6b9dab13 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/ViewScoreboardElement.java @@ -0,0 +1,70 @@ +/* + * 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.bausystem.features.techhider; + +import de.steamwar.bausystem.BauSystem; +import de.steamwar.bausystem.features.hullhider.HullHiderCommand; +import de.steamwar.bausystem.features.xray.XrayCommand; +import de.steamwar.bausystem.region.Region; +import de.steamwar.bausystem.utils.ScoreboardElement; +import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedInstance; +import org.bukkit.entity.Player; + +/** + * Shows which of /techhider, /xray and /hullhider is active, at most one of them can be per region. + */ +@Linked +public class ViewScoreboardElement implements ScoreboardElement { + + @LinkedInstance + public TechHiderCommand techHiderCommand; + + @LinkedInstance + public XrayCommand xrayCommand; + + @LinkedInstance + public HullHiderCommand hullHiderCommand; + + @Override + public ScoreboardGroup getGroup() { + return ScoreboardGroup.OTHER; + } + + @Override + public int order() { + return 0; + } + + @Override + public String get(Region region, Player p) { + String view; + if (techHiderCommand.isActive(region, p)) { + view = "TechHider"; + } else if (xrayCommand.isActive(region, p)) { + view = "XRay"; + } else if (hullHiderCommand.isActive(region, p)) { + view = "HullHider"; + } else { + return null; + } + return "§e" + BauSystem.MESSAGE.parse("SCOREBOARD_VIEW", p, view); + } +} 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 9e0742d0..c8f01679 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 @@ -25,7 +25,6 @@ import de.steamwar.bausystem.features.hullhider.HullHiderCommand; import de.steamwar.bausystem.features.techhider.HidingRule; import de.steamwar.bausystem.features.techhider.TechHiderCommand; import de.steamwar.bausystem.region.Region; -import de.steamwar.bausystem.utils.ScoreboardElement; import de.steamwar.command.SWCommand; import de.steamwar.core.CraftbukkitWrapper; import de.steamwar.linkage.Linked; @@ -49,7 +48,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; @Linked -public class XrayCommand extends SWCommand implements Listener, ScoreboardElement { +public class XrayCommand extends SWCommand implements Listener { public XrayCommand() { super("xray"); @@ -167,19 +166,7 @@ public class XrayCommand extends SWCommand implements Listener, ScoreboardElemen }); } - @Override - public ScoreboardGroup getGroup() { - return ScoreboardGroup.OTHER; - } - - @Override - public int order() { - return 0; - } - - @Override - public String get(Region region, Player p) { - if (!hidden.getOrDefault(region, Collections.emptySet()).contains(p)) return null; - return "§e" + BauSystem.MESSAGE.parse("SCOREBOARD_XRAY", p); + public boolean isActive(Region region, Player player) { + return hidden.getOrDefault(region, Collections.emptySet()).contains(player); } } From abf44b2306635f594ed196bac3fff097fb46866e Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 15:43:35 +0100 Subject: [PATCH 08/19] Allow @Linked on fields The value of a @Linked field of a linked class is linked like a linked class itself, e.g. registered as listener, and unlinked again on unlink. The field has to be initialised when linking. HullHiderCommand and TPSSystem use it instead of registering their listeners by hand. --- .../features/hullhider/HullHiderCommand.java | 2 +- .../features/tpslimit/TPSSystem.java | 4 ++- .../de/steamwar/linkage/AbstractLinker.java | 34 +++++++++++++++++++ .../src/de/steamwar/linkage/Linked.java | 8 +++-- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java index 76f333d1..fc7a606c 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java @@ -57,10 +57,10 @@ public class HullHiderCommand extends SWCommand implements Listener { public HullHiderCommand() { super("hullhider"); - Bukkit.getPluginManager().registerEvents(hullHider, BauSystem.getInstance()); Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), this::onTick, 1, 1); } + @Linked private final HullHider hullHider = new HullHider<>(); // Read on netty threads by the BauTechHider private final Map hulls = new ConcurrentHashMap<>(); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/tpslimit/TPSSystem.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/tpslimit/TPSSystem.java index 2789c4bf..7c503168 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/tpslimit/TPSSystem.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/tpslimit/TPSSystem.java @@ -50,6 +50,9 @@ import java.util.Arrays; @Linked public class TPSSystem implements Listener { + @Linked + private final TickManager tickManager = TickManager.impl; + public TPSSystem() { if (TickManager.impl.canFreeze()) { new TPSFreezeCommand(); @@ -68,7 +71,6 @@ public class TPSSystem implements Listener { new TickDefaultCommand(); new TPSBaseCommand(); new TickBaseCommand(); - Bukkit.getPluginManager().registerEvents(TickManager.impl, BauSystem.getInstance()); } @EventHandler diff --git a/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java b/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java index 4a2db53d..00d4b43a 100644 --- a/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java +++ b/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java @@ -28,6 +28,7 @@ import java.io.BufferedReader; import java.io.InputStreamReader; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -38,6 +39,7 @@ public abstract class AbstractLinker { protected final T plugin; private Map, Object> instances = new HashMap<>(); + private final List fieldInstances = new ArrayList<>(); protected AbstractLinker(@NonNull T plugin) { this.plugin = plugin; @@ -90,6 +92,7 @@ public abstract class AbstractLinker { ((Enable) any).enable(); } linkObject(any); + linkFields(any); }); } catch (SecurityException e) { Throwable cause = e.getCause(); @@ -119,6 +122,31 @@ public abstract class AbstractLinker { } } + private void linkFields(Object any) { + for (Field field : any.getClass().getDeclaredFields()) { + if (field.getAnnotation(Linked.class) == null) continue; + + Object value; + try { + field.setAccessible(true); + value = field.get(Modifier.isStatic(field.getModifiers()) ? null : any); + } catch (IllegalAccessException e) { + throw new SecurityException(e.getMessage(), e); + } + + if (value == null) { + String message = "Linked field " + any.getClass().getName() + "." + field.getName() + " is not initialised"; + throw new SecurityException(message, new IllegalStateException(message)); + } + + fieldInstances.add(value); + if (value instanceof Enable) { + ((Enable) value).enable(); + } + linkObject(value); + } + } + public final void unlink() { instances.forEach((aClass, any) -> { unlinkObject(any); @@ -126,6 +154,12 @@ public abstract class AbstractLinker { ((Disable) any).disable(); } }); + fieldInstances.forEach(any -> { + unlinkObject(any); + if (any instanceof Disable) { + ((Disable) any).disable(); + } + }); } public final void addLinkableInstance(@NonNull Object instance) { diff --git a/CommonCore/Linkage/src/de/steamwar/linkage/Linked.java b/CommonCore/Linkage/src/de/steamwar/linkage/Linked.java index 465600b1..6e1c6a8f 100644 --- a/CommonCore/Linkage/src/de/steamwar/linkage/Linked.java +++ b/CommonCore/Linkage/src/de/steamwar/linkage/Linked.java @@ -26,8 +26,12 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * On a class: the class is instantiated and linked. + * On a field of a linked class: the (already initialised) value of the field is linked as well, e.g. registered as listener. + */ @IndexAnnotated -@Retention(RetentionPolicy.SOURCE) -@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.FIELD}) public @interface Linked { } From edb13fb7df4b868f1fb8def976ca9ad3f7a76fe4 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 15:43:35 +0100 Subject: [PATCH 09/19] Stop registering linked listeners twice ModifyCommand and WorldEditRenderer registered themselves as listener although the SpigotLinker already does, so all their event handlers ran twice. --- LobbySystem/src/de/steamwar/lobby/command/ModifyCommand.java | 2 -- .../src/de/steamwar/core/worldedit/WorldEditRenderer.java | 1 - 2 files changed, 3 deletions(-) diff --git a/LobbySystem/src/de/steamwar/lobby/command/ModifyCommand.java b/LobbySystem/src/de/steamwar/lobby/command/ModifyCommand.java index 8e8f9d99..7d0840d4 100644 --- a/LobbySystem/src/de/steamwar/lobby/command/ModifyCommand.java +++ b/LobbySystem/src/de/steamwar/lobby/command/ModifyCommand.java @@ -25,7 +25,6 @@ import de.steamwar.lobby.LobbySystem; import de.steamwar.lobby.listener.PlayerSpawn; import de.steamwar.sql.SteamwarUser; import de.steamwar.sql.UserPerm; -import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.entity.HumanEntity; import org.bukkit.entity.Player; @@ -47,7 +46,6 @@ public class ModifyCommand extends SWCommand implements Listener { public ModifyCommand() { super("modify"); - Bukkit.getPluginManager().registerEvents(this, LobbySystem.getInstance()); } @Register diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/WorldEditRenderer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/WorldEditRenderer.java index b66c67ad..f11aec11 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/WorldEditRenderer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/WorldEditRenderer.java @@ -55,7 +55,6 @@ public class WorldEditRenderer implements Listener { public WorldEditRenderer() { INSTANCE = this; we = WorldEditWrapper.getWorldEditPlugin(); - Bukkit.getPluginManager().registerEvents(this, Core.getInstance()); Bukkit.getScheduler().runTaskTimer(Core.getInstance(), () -> { for (Player player : Bukkit.getOnlinePlayers()) { From d76c3962683280ff123cba9e408158a713119434 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 15:57:25 +0100 Subject: [PATCH 10/19] Do not hide entities with the BauSystem hullhider HullHider gets a hideEntities parameter: the fight still hides entities inside hidden hulls, /hullhider only hides blocks. As the TechHider also dropped spawn packets at hidden positions, ForPlayer#canAccessEntitySpawn decides about them now, which the BauTechHider always allows. --- .../features/hullhider/HullHiderCommand.java | 7 ++----- .../features/techhider/BauTechHider.java | 11 +++++++---- .../fightsystem/utils/FightHullHider.java | 2 +- .../src/de/steamwar/hullhider/HullHider.java | 16 ++++++++++++++++ .../techhider/AccessPrivilegeProvider.java | 7 +++++++ .../src/de/steamwar/techhider/TechHider.java | 2 +- 6 files changed, 34 insertions(+), 11 deletions(-) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java index fc7a606c..b5c2a3cf 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java @@ -36,7 +36,6 @@ import de.steamwar.techhider.ObfuscationConfig; import net.md_5.bungee.api.ChatMessageType; import org.bukkit.Bukkit; import org.bukkit.World; -import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -61,7 +60,8 @@ public class HullHiderCommand extends SWCommand implements Listener { } @Linked - private final HullHider hullHider = new HullHider<>(); + // Only blocks are hidden, entities stay visible + private final HullHider hullHider = new HullHider<>(false); // Read on netty threads by the BauTechHider private final Map hulls = new ConcurrentHashMap<>(); private final Map> hidden = new ConcurrentHashMap<>(); @@ -143,9 +143,6 @@ public class HullHiderCommand extends SWCommand implements Listener { private void onTick() { if (hidden.isEmpty()) return; - for (Entity entity : world().getEntities()) { - hullHider.checkEntity(entity); - } hullHider.sendUncoveredBlocks(); } diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java index 46867bc2..9b91d6a4 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java @@ -30,7 +30,6 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; -import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.entity.Player; import java.util.ArrayList; @@ -38,6 +37,7 @@ import java.util.List; /** * The single TechHider of the server. It combines the hiding rules of the spectator techhider, /techhider and /xray and the hulls of /hullhider per player. + * Entities are never hidden. */ @Linked public class BauTechHider implements AccessPrivilegeProvider { @@ -128,10 +128,13 @@ public class BauTechHider implements AccessPrivilegeProvider { @Override public boolean canAccessEntity(int entityId) { - if (hulls.length == 0) return true; + return true; + } - net.minecraft.world.entity.Entity nmsEntity = ((CraftWorld) player.getWorld()).getHandle().moonrise$getEntityLookup().get(entityId); - return nmsEntity == null || canAccessPosition(nmsEntity.getBlockX(), nmsEntity.getBlockY(), nmsEntity.getBlockZ()); + // Entities are not hidden, even if they spawn inside a hidden hull + @Override + public boolean canAccessEntitySpawn(int entityId, int blockX, int blockY, int blockZ) { + return true; } @Override 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 cc5074ce..d667ba45 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java @@ -50,7 +50,7 @@ import java.util.function.BiConsumer; */ public class FightHullHider { - private final HullHider hullHider = new HullHider<>(); + private final HullHider hullHider = new HullHider<>(true); public FightHullHider() { if (!TechHiderWrapper.ENABLED) return; diff --git a/HiderCore/src/de/steamwar/hullhider/HullHider.java b/HiderCore/src/de/steamwar/hullhider/HullHider.java index 92ecb24f..177d9ffe 100644 --- a/HiderCore/src/de/steamwar/hullhider/HullHider.java +++ b/HiderCore/src/de/steamwar/hullhider/HullHider.java @@ -42,10 +42,18 @@ import java.util.function.BiConsumer; */ public class HullHider implements Listener { + private final boolean hideEntities; private final Map hullMap = new LinkedHashMap<>(); // Read on netty threads, replaced on every change private volatile Hull[] hulls = new Hull[0]; + /** + * @param hideEntities whether entities and REntities inside the hidden part of a hull are hidden from non-viewers as well + */ + public HullHider(boolean hideEntities) { + this.hideEntities = hideEntities; + } + /** * The hull hides everything from non-viewers until it is initialised. */ @@ -157,6 +165,8 @@ public class HullHider implements Listener { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onDeath(EntityDeathEvent e) { + if (!hideEntities) return; + for (Hull hull : hulls) { hull.removeEntity(e.getEntity()); } @@ -166,6 +176,8 @@ public class HullHider implements Listener { * Has to be called every tick for every entity, as entities can move into or out of the hidden part of a hull. */ public void checkEntity(Entity entity) { + if (!hideEntities) return; + for (Hull hull : hulls) { hull.checkEntity(entity); } @@ -181,12 +193,16 @@ public class HullHider implements Listener { } public void updateREntity(REntity e) { + if (!hideEntities) return; + for (Hull hull : hulls) { hull.checkREntity(e); } } public void despawnREntity(REntity e) { + if (!hideEntities) return; + for (Hull hull : hulls) { hull.removeREntity(e); } diff --git a/HiderCore/src/de/steamwar/techhider/AccessPrivilegeProvider.java b/HiderCore/src/de/steamwar/techhider/AccessPrivilegeProvider.java index be0599cb..ff39a9fe 100644 --- a/HiderCore/src/de/steamwar/techhider/AccessPrivilegeProvider.java +++ b/HiderCore/src/de/steamwar/techhider/AccessPrivilegeProvider.java @@ -52,6 +52,13 @@ public interface AccessPrivilegeProvider { boolean canAccessEntity(int entityId); + /** + * @return whether the player may see the entity spawning at this position + */ + default boolean canAccessEntitySpawn(int entityId, int blockX, int blockY, int blockZ) { + return canAccessEntity(entityId) && canAccessPosition(blockX, blockY, blockZ); + } + boolean canAccessBlockEntity(int blockX, int blockY, int blockZ, BlockEntityType type); /** diff --git a/HiderCore/src/de/steamwar/techhider/TechHider.java b/HiderCore/src/de/steamwar/techhider/TechHider.java index af640d18..8bf4cb1d 100644 --- a/HiderCore/src/de/steamwar/techhider/TechHider.java +++ b/HiderCore/src/de/steamwar/techhider/TechHider.java @@ -372,7 +372,7 @@ public class TechHider { } private Packet processAddEntityPacket(ForPlayer access, ClientboundAddEntityPacket packet) { - if (access.canAccessEntity(packet.getId()) && access.canAccessPosition(Mth.floor(packet.getX()), Mth.floor(packet.getY()), Mth.floor(packet.getZ()))) { + if (access.canAccessEntitySpawn(packet.getId(), Mth.floor(packet.getX()), Mth.floor(packet.getY()), Mth.floor(packet.getZ()))) { return packet; } else { return null; From b78fab3172a2423f1618e420940e2b30870a9c7f Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 16:02:31 +0100 Subject: [PATCH 11/19] Stop the loader task and listener when a loader ends Every loader started a timer and registered itself as listener, but neither was stopped, so each loader kept running every tick until the server restarted. Both are now stopped when the loader is stopped or replaced by a new one. --- .../bausystem/features/loader/Loader.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/loader/Loader.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/loader/Loader.java index dd6e3f6d..398436db 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/loader/Loader.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/loader/Loader.java @@ -36,9 +36,11 @@ import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; +import org.bukkit.event.HandlerList; import org.bukkit.event.Listener; import org.bukkit.event.inventory.ClickType; import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.scheduler.BukkitTask; import java.util.ArrayList; import java.util.List; @@ -63,13 +65,14 @@ public class Loader implements Listener, SWPlayer.Component { private List elements = new ArrayList<>(); private int currentElement = 0; private long waitTime = 0; + private final BukkitTask task; public Loader(Player p) { this.p = p; this.recorder = new LoaderRecorder(p, elements); Bukkit.getPluginManager().registerEvents(this, BauSystem.getInstance()); - BauSystem.runTaskTimer(BauSystem.getInstance(), () -> { + task = BauSystem.runTaskTimer(BauSystem.getInstance(), () -> { if (stage != Stage.RUNNING && stage != Stage.SINGLE) return; if (!Permission.BUILD.hasPermission(p)) return; if (waitTime > 0) { @@ -136,13 +139,27 @@ public class Loader implements Listener, SWPlayer.Component { } public void stop() { + end(); + if (getLoader(p) == this) { + SWPlayer.of(p).removeComponent(Loader.class); + } + } + + // Also called when the loader is replaced by a new one + @Override + public void onUnmount(SWPlayer player) { + end(); + } + + private void end() { stage = Stage.END; if (recorder != null) { recorder.stop(); recorder = null; } elements.clear(); - SWPlayer.of(p).removeComponent(Loader.class); + task.cancel(); + HandlerList.unregisterAll(this); } public boolean setTicksBetweenShots(int delay) { From 9598b1c961ca31497bcb6915d05c7df09feb0b73 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 16:11:28 +0100 Subject: [PATCH 12/19] Add @LinkedTimer A method of a linked class annotated with @LinkedTimer(delay, period) is run repeatedly by the linker, on Spigot and Velocity. The method must not have parameters, its return value is ignored. It is called through a MethodHandle bound once when linking instead of reflection; LambdaMetafactory cannot be used as the linker and the linked classes are in different class loaders. The unit defaults to ticks and can be set to milliseconds up to hours; the Unit enum converts it to ticks for Spigot and to a TimeUnit for Velocity. HullHiderCommand uses it for its tick task. --- .../features/hullhider/HullHiderCommand.java | 4 +- .../de/steamwar/linkage/AbstractLinker.java | 52 ++++++++++++ .../src/de/steamwar/linkage/LinkedTimer.java | 81 +++++++++++++++++++ .../src/de/steamwar/linkage/SpigotLinker.java | 5 ++ .../steamwar/velocitycore/VelocityCore.java | 6 ++ 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 CommonCore/Linkage/src/de/steamwar/linkage/LinkedTimer.java diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java index b5c2a3cf..2b2a99aa 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java @@ -32,6 +32,7 @@ import de.steamwar.hullhider.HullRegion; import de.steamwar.hullhider.Viewers; import de.steamwar.linkage.Linked; import de.steamwar.linkage.LinkedInstance; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.techhider.ObfuscationConfig; import net.md_5.bungee.api.ChatMessageType; import org.bukkit.Bukkit; @@ -55,8 +56,6 @@ public class HullHiderCommand extends SWCommand implements Listener { public HullHiderCommand() { super("hullhider"); - - Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), this::onTick, 1, 1); } @Linked @@ -140,6 +139,7 @@ public class HullHiderCommand extends SWCommand implements Listener { }); } + @LinkedTimer(delay = 1, period = 1) private void onTick() { if (hidden.isEmpty()) return; diff --git a/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java b/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java index 00d4b43a..9959ecef 100644 --- a/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java +++ b/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java @@ -22,11 +22,16 @@ package de.steamwar.linkage; import de.steamwar.linkage.api.Disable; import de.steamwar.linkage.api.Enable; import lombok.NonNull; +import lombok.SneakyThrows; import lombok.experimental.StandardException; import java.io.BufferedReader; import java.io.InputStreamReader; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; @@ -120,6 +125,45 @@ public abstract class AbstractLinker { Throwable cause = e.getCause(); throw new LinkException(cause.getMessage(), cause); } + + // After the injection, as timers may run immediately on another thread + for (Object any : instances.values()) { + scheduleTimers(any); + } + for (Object any : fieldInstances) { + scheduleTimers(any); + } + } + + private void scheduleTimers(Object any) throws LinkException { + for (Method method : any.getClass().getDeclaredMethods()) { + LinkedTimer timer = method.getAnnotation(LinkedTimer.class); + if (timer == null) continue; + + String name = any.getClass().getName() + "." + method.getName(); + if (method.getParameterCount() != 0) { + throw new LinkException("Linked timer " + name + " must not have parameters"); + } + + MethodHandle handle; + try { + handle = MethodHandles.privateLookupIn(method.getDeclaringClass(), MethodHandles.lookup()).unreflect(method); + } catch (IllegalAccessException e) { + throw new LinkException("Could not access linked timer " + name, e); + } + if (!Modifier.isStatic(method.getModifiers())) { + handle = handle.bindTo(any); + } + // Discards the return value + MethodHandle timerHandle = handle.asType(MethodType.methodType(void.class)); + + scheduleTimer(() -> runTimer(timerHandle), timer.delay(), timer.period(), timer.unit()); + } + } + + @SneakyThrows + private static void runTimer(MethodHandle timer) { + timer.invokeExact(); } private void linkFields(Object any) { @@ -186,6 +230,14 @@ public abstract class AbstractLinker { protected void linkObject(Object any) { } + /** + * Runs the runnable repeatedly for a {@link LinkedTimer}. + * + * @param delay time until the first run, in unit + * @param period time between two runs, in unit + */ + protected abstract void scheduleTimer(Runnable runnable, long delay, long period, LinkedTimer.Unit unit); + /** * There is no need in calling {@link Disable#disable()} ()} by this method. */ diff --git a/CommonCore/Linkage/src/de/steamwar/linkage/LinkedTimer.java b/CommonCore/Linkage/src/de/steamwar/linkage/LinkedTimer.java new file mode 100644 index 00000000..fdf96607 --- /dev/null +++ b/CommonCore/Linkage/src/de/steamwar/linkage/LinkedTimer.java @@ -0,0 +1,81 @@ +/* + * 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.linkage; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * Runs the annotated method of a linked class (or of the value of a {@link Linked} field) repeatedly. + * The method must not have parameters, its return value is ignored. It may be static. + * The timer is not stopped on unlink, as the server stops anyway. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface LinkedTimer { + + /** + * Time until the first run, in {@link #unit()}. + */ + long delay(); + + /** + * Time between two runs, in {@link #unit()}. + */ + long period(); + + Unit unit() default Unit.TICKS; + + @AllArgsConstructor + enum Unit { + /** + * 1 tick = 50 ms + */ + TICKS(50, TimeUnit.MILLISECONDS), + MILLISECONDS(1, TimeUnit.MILLISECONDS), + SECONDS(1, TimeUnit.SECONDS), + MINUTES(1, TimeUnit.MINUTES), + HOURS(1, TimeUnit.HOURS); + + private final long factor; + @Getter + private final TimeUnit timeUnit; + + /** + * @return the value in ticks, rounded down + */ + public long toTicks(long value) { + return timeUnit.toMillis(value * factor) / 50; + } + + /** + * @return the value in {@link #getTimeUnit()} + */ + public long toTimeUnit(long value) { + return value * factor; + } + } +} diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/linkage/SpigotLinker.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/linkage/SpigotLinker.java index 37ccaae3..36c0532f 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/linkage/SpigotLinker.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/linkage/SpigotLinker.java @@ -61,6 +61,11 @@ public class SpigotLinker extends AbstractLinker { } } + @Override + protected void scheduleTimer(Runnable runnable, long delay, long period, LinkedTimer.Unit unit) { + Bukkit.getScheduler().runTaskTimer(plugin, runnable, unit.toTicks(delay), unit.toTicks(period)); + } + @Override protected void unlinkObject(Object any) { if (any instanceof Listener) { diff --git a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java index 1701a10d..d94e8e3d 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java +++ b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java @@ -32,6 +32,7 @@ import de.steamwar.command.*; import de.steamwar.linkage.AbstractLinker; import de.steamwar.linkage.EventMode; import de.steamwar.linkage.LinkedInstance; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.messages.Chatter; import de.steamwar.network.packets.PacketHandler; import de.steamwar.persistent.ReloadablePlugin; @@ -164,6 +165,11 @@ public class VelocityCore implements ReloadablePlugin { } } + @Override + protected void scheduleTimer(Runnable runnable, long delay, long period, LinkedTimer.Unit unit) { + schedule(runnable).delay(unit.toTimeUnit(delay), unit.getTimeUnit()).repeat(unit.toTimeUnit(period), unit.getTimeUnit()).schedule(); + } + @Override protected void unlinkObject(Object any) { if (any instanceof PacketHandler) { From 9fd2bd17d4b6cfb2237dc3a5edd1907e4bfa4ab0 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 16:25:20 +0100 Subject: [PATCH 13/19] Use @LinkedTimer for the timers of linked classes The repeating timers that linked classes and the plugin main classes started once for the whole runtime now use @LinkedTimer, on Spigot (BauSystem, SpigotCore, LobbySystem, MissileWars, TowerRun, Teamserver) and Velocity. The FightInfoPacketSenders of MissileWars and TowerRun are linked themselves instead of being started by the main class. --- .../observer/ObserverTracerListener.java | 14 ++-- .../rayvisualizer/RayVisualizerCommand.java | 18 ++-- .../features/script/event/EventListener.java | 14 ++-- .../features/simulator/SimulatorCursor.java | 11 ++- .../team/boundary/BoundaryViewer.java | 24 +++--- .../features/world/AFKStopperListener.java | 20 ++--- .../steamwar/lobby/jumpandrun/JumpAndRun.java | 84 +++++++++---------- .../lobby/listener/PlayerSeatListener.java | 17 ++-- .../lobby/particle/ParticleListener.java | 39 +++++---- .../de/steamwar/lobby/team/TeamPlayer.java | 84 ++++++++++--------- .../misslewars/FightInfoPacketSender.java | 4 + .../de/steamwar/misslewars/MissileWars.java | 2 - .../src/de/steamwar/core/Core.java | 20 ++++- .../core/worldedit/WorldEditRenderer.java | 12 +-- .../de/steamwar/cursor/CursorListener.java | 14 ++-- .../teamserver/listener/FreezeListener.java | 14 ++-- .../towerrun/FightInfoPacketSender.java | 4 + .../src/de/steamwar/towerrun/TowerRun.java | 2 - .../steamwar/velocitycore/EventStarter.java | 4 +- .../steamwar/velocitycore/VelocityCore.java | 19 +++-- .../velocitycore/commands/CheckCommand.java | 9 +- .../velocitycore/mods/FabricModSender.java | 25 +++--- .../velocitycore/tablist/TablistManager.java | 4 +- 23 files changed, 241 insertions(+), 217 deletions(-) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/observer/ObserverTracerListener.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/observer/ObserverTracerListener.java index d823bff4..2cf8e639 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/observer/ObserverTracerListener.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/observer/ObserverTracerListener.java @@ -24,6 +24,7 @@ import de.steamwar.bausystem.Permission; import de.steamwar.bausystem.utils.BauMemberUpdateEvent; import de.steamwar.core.SWPlayer; import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import org.bukkit.Bukkit; import org.bukkit.GameMode; import org.bukkit.Material; @@ -43,13 +44,12 @@ public class ObserverTracerListener implements Listener { static Set enabled = new HashSet<>(); - public ObserverTracerListener() { - Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> { - SWPlayer.allWithSingleComponent(ObserverTracer.class).forEach(pair -> { - if (pair.getSwPlayer().getPlayer().getGameMode() != GameMode.SPECTATOR) return; - pair.getComponent().show(); - }); - }, 15L, 15L); + @LinkedTimer(delay = 15, period = 15) + private void showTracers() { + SWPlayer.allWithSingleComponent(ObserverTracer.class).forEach(pair -> { + if (pair.getSwPlayer().getPlayer().getGameMode() != GameMode.SPECTATOR) return; + pair.getComponent().show(); + }); } @EventHandler diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java index af5d4eb0..469db717 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/rayvisualizer/RayVisualizerCommand.java @@ -28,6 +28,7 @@ import de.steamwar.command.SWCommand; import de.steamwar.entity.CRay; import de.steamwar.entity.REntityServer; import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import org.bukkit.*; import org.bukkit.block.data.BlockData; import org.bukkit.entity.Player; @@ -104,15 +105,16 @@ public class RayVisualizerCommand extends SWCommand implements Listener { public RayVisualizerCommand() { super("rayvisualizer"); + } - Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> { - if (server.getPlayers().isEmpty()) return; - List primedList = WORLD.getEntitiesByClass(TNTPrimed.class) - .stream().toList(); - rayData.reset(); - if (primedList.isEmpty()) return; - calculateRays(primedList, primedList); - }, 1, 1); + @LinkedTimer(delay = 1, period = 1) + private void updateRays() { + if (server.getPlayers().isEmpty()) return; + List primedList = WORLD.getEntitiesByClass(TNTPrimed.class) + .stream().toList(); + rayData.reset(); + if (primedList.isEmpty()) return; + calculateRays(primedList, primedList); } private void calculateRays(List fromTNTs, List toTNTs) { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/script/event/EventListener.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/script/event/EventListener.java index 6265cec4..995583b3 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/script/event/EventListener.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/script/event/EventListener.java @@ -27,6 +27,7 @@ import de.steamwar.bausystem.features.script.lua.libs.StorageLib; import de.steamwar.bausystem.region.Region; import de.steamwar.core.SWPlayer; import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.entity.EntityType; @@ -47,13 +48,12 @@ import org.luaj.vm2.LuaValue; @Linked public class EventListener implements Listener { - static { - Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> { - long millis = System.currentTimeMillis(); - SWPlayer.allWithSingleComponent(ScriptRunner.ScriptData.class) - .filter(pair -> millis - pair.getComponent().getLastF() > 200) - .forEach(pair -> pair.getComponent().setLastF(Long.MAX_VALUE)); - }, 1, 1); + @LinkedTimer(delay = 1, period = 1) + private static void resetLastF() { + long millis = System.currentTimeMillis(); + SWPlayer.allWithSingleComponent(ScriptRunner.ScriptData.class) + .filter(pair -> millis - pair.getComponent().getLastF() > 200) + .forEach(pair -> pair.getComponent().setLastF(Long.MAX_VALUE)); } @EventHandler(priority = EventPriority.MONITOR) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/SimulatorCursor.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/SimulatorCursor.java index 6f674a71..271316f8 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/SimulatorCursor.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/simulator/SimulatorCursor.java @@ -44,9 +44,9 @@ import de.steamwar.entity.REntity; import de.steamwar.entity.REntityServer; import de.steamwar.inventory.SWAnvilInv; import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import lombok.AllArgsConstructor; import lombok.Getter; -import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.entity.Player; @@ -136,11 +136,10 @@ public class SimulatorCursor implements Listener { private static final Map LAST_SNEAKS = new HashMap<>(); - static { - Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> { - long millis = System.currentTimeMillis(); - LAST_SNEAKS.entrySet().removeIf(entry -> millis - entry.getValue() > 200); - }, 1, 1); + @LinkedTimer(delay = 1, period = 1) + private static void clearLastSneaks() { + long millis = System.currentTimeMillis(); + LAST_SNEAKS.entrySet().removeIf(entry -> millis - entry.getValue() > 200); } @EventHandler(priority = EventPriority.HIGH) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/team/boundary/BoundaryViewer.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/team/boundary/BoundaryViewer.java index fe242037..139155ef 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/team/boundary/BoundaryViewer.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/team/boundary/BoundaryViewer.java @@ -19,11 +19,10 @@ package de.steamwar.bausystem.features.team.boundary; -import de.steamwar.bausystem.BauSystem; import de.steamwar.bausystem.region.Point; import de.steamwar.bausystem.region.Region; import de.steamwar.linkage.Linked; -import org.bukkit.Bukkit; +import de.steamwar.linkage.LinkedTimer; import org.bukkit.Particle; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -38,17 +37,16 @@ public class BoundaryViewer implements Listener { public static Set viewers = new HashSet<>(); - { - Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> { - if (viewers.isEmpty()) { - return; - } - viewers.forEach(player -> { - Region region = Region.getRegion(player.getLocation()); - if (region.getType().isGlobal()) return; - showRegion(region, player); - }); - }, 20, 20); + @LinkedTimer(delay = 20, period = 20) + private void showBoundaries() { + if (viewers.isEmpty()) { + return; + } + viewers.forEach(player -> { + Region region = Region.getRegion(player.getLocation()); + if (region.getType().isGlobal()) return; + showRegion(region, player); + }); } private void showRegion(Region region, Player player) { diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/AFKStopperListener.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/AFKStopperListener.java index 5c061545..097ef2bc 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/AFKStopperListener.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/world/AFKStopperListener.java @@ -23,6 +23,7 @@ import de.steamwar.bausystem.BauSystem; import de.steamwar.core.CRIUWakeupEvent; import de.steamwar.core.CheckpointUtils; import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.entity.Player; @@ -38,17 +39,16 @@ public class AFKStopperListener implements Listener { private long lastMovementTime = System.currentTimeMillis(); - public AFKStopperListener() { - Bukkit.getScheduler().runTaskTimer(BauSystem.getInstance(), () -> { - long currentTime = System.currentTimeMillis(); - if (currentTime - lastMovementTime > 10 * 60000) { // 10 Minutes - for (Player p : Bukkit.getOnlinePlayers()) { - p.kickPlayer(BauSystem.MESSAGE.parse("AFK_KICK_MESSAGE", p)); - } - } else if (currentTime - lastMovementTime > 9 * 60000) { - BauSystem.MESSAGE.broadcast("AFK_WARNING_MESSAGE"); + @LinkedTimer(delay = 1, period = 1, unit = LinkedTimer.Unit.MINUTES) + private void checkAfk() { + long currentTime = System.currentTimeMillis(); + if (currentTime - lastMovementTime > 10 * 60000) { // 10 Minutes + for (Player p : Bukkit.getOnlinePlayers()) { + p.kickPlayer(BauSystem.MESSAGE.parse("AFK_KICK_MESSAGE", p)); } - }, 1200, 1200); //every minute + } else if (currentTime - lastMovementTime > 9 * 60000) { + BauSystem.MESSAGE.broadcast("AFK_WARNING_MESSAGE"); + } } @EventHandler diff --git a/LobbySystem/src/de/steamwar/lobby/jumpandrun/JumpAndRun.java b/LobbySystem/src/de/steamwar/lobby/jumpandrun/JumpAndRun.java index 87b29fe5..4f129be5 100644 --- a/LobbySystem/src/de/steamwar/lobby/jumpandrun/JumpAndRun.java +++ b/LobbySystem/src/de/steamwar/lobby/jumpandrun/JumpAndRun.java @@ -20,6 +20,7 @@ package de.steamwar.lobby.jumpandrun; import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.lobby.LobbySystem; import de.steamwar.lobby.listener.PlayerSpawn; import de.steamwar.lobby.util.LeaderboardManager; @@ -60,52 +61,51 @@ public class JumpAndRun implements Listener { private static final LeaderboardManager LEADERBOARD = new LeaderboardManager(LobbySystem.getEntityServer(false), JUMP_AND_RUN_CONFIG, new Location(Bukkit.getWorlds().get(0), 2338.5, 42.5, 1231.5)); - { - Bukkit.getScheduler().runTaskTimer(LobbySystem.getInstance(), () -> { - Set toReset = new HashSet<>(); - CURRENT_POS.forEach((player, index) -> { - if (System.currentTimeMillis() - CLICKED.getOrDefault(player, 0L) > 500) { - CLICKED.remove(player); - CLICKED_COUNT.remove(player); - } else { - CLICKED_COUNT.put(player, CLICKED_COUNT.getOrDefault(player, -1) + 1); - } + @LinkedTimer(delay = 1, period = 1) + private void updatePlayers() { + Set toReset = new HashSet<>(); + CURRENT_POS.forEach((player, index) -> { + if (System.currentTimeMillis() - CLICKED.getOrDefault(player, 0L) > 500) { + CLICKED.remove(player); + CLICKED_COUNT.remove(player); + } else { + CLICKED_COUNT.put(player, CLICKED_COUNT.getOrDefault(player, -1) + 1); + } - Location location = player.getLocation(); - Vector point = points.get(index); - if (index < points.size() - 1) { - Vector nextPoint = points.get(index + 1); - double y = Math.min(point.getY(), nextPoint.getY()) - 2; - if (location.getY() < y) { - location.setX(point.getX()); - location.setY(point.getY()); - location.setZ(point.getZ()); - player.teleport(location); - player.playSound(location, Sound.ENTITY_ENDERMAN_TELEPORT, 1, 1); - FAILS.put(player, FAILS.getOrDefault(player, 0) + 1); - } + Location location = player.getLocation(); + Vector point = points.get(index); + if (index < points.size() - 1) { + Vector nextPoint = points.get(index + 1); + double y = Math.min(point.getY(), nextPoint.getY()) - 2; + if (location.getY() < y) { + location.setX(point.getX()); + location.setY(point.getY()); + location.setZ(point.getZ()); + player.teleport(location); + player.playSound(location, Sound.ENTITY_ENDERMAN_TELEPORT, 1, 1); + FAILS.put(player, FAILS.getOrDefault(player, 0) + 1); } + } - int count = CLICKED_COUNT.getOrDefault(player, -1); - if (count >= 0) { - if (count > 20) { - toReset.add(player); - return; - } - count = Math.min(count / 2, 30); - LobbySystem.getMessage().sendPrefixless("JUMP_AND_RUN_CANCEL", player, ChatMessageType.ACTION_BAR, "§e" + BAR_EMPTY.substring(0, count) + "§7" + BAR_EMPTY.substring(count)); - } else { - long time = System.currentTimeMillis() - START.get(player); - SimpleDateFormat format = new SimpleDateFormat(LobbySystem.getMessage().parse("JUMP_AND_RUN_TIME", player), Locale.ROOT); - String parsed = format.format(new Date(time)); - LobbySystem.getMessage().sendPrefixless("JUMP_AND_RUN_PROGRESS", player, ChatMessageType.ACTION_BAR, index + 1, points.size(), FAILS.get(player), parsed); + int count = CLICKED_COUNT.getOrDefault(player, -1); + if (count >= 0) { + if (count > 20) { + toReset.add(player); + return; } - }); - toReset.forEach(player -> { - reset(player); - player.teleport(Bukkit.getWorlds().get(0).getSpawnLocation().clone().add(0.5, 0, 0.5)); - }); - }, 1, 1); + count = Math.min(count / 2, 30); + LobbySystem.getMessage().sendPrefixless("JUMP_AND_RUN_CANCEL", player, ChatMessageType.ACTION_BAR, "§e" + BAR_EMPTY.substring(0, count) + "§7" + BAR_EMPTY.substring(count)); + } else { + long time = System.currentTimeMillis() - START.get(player); + SimpleDateFormat format = new SimpleDateFormat(LobbySystem.getMessage().parse("JUMP_AND_RUN_TIME", player), Locale.ROOT); + String parsed = format.format(new Date(time)); + LobbySystem.getMessage().sendPrefixless("JUMP_AND_RUN_PROGRESS", player, ChatMessageType.ACTION_BAR, index + 1, points.size(), FAILS.get(player), parsed); + } + }); + toReset.forEach(player -> { + reset(player); + player.teleport(Bukkit.getWorlds().get(0).getSpawnLocation().clone().add(0.5, 0, 0.5)); + }); } public static boolean isPlayerInJumpAndRun(Player player) { diff --git a/LobbySystem/src/de/steamwar/lobby/listener/PlayerSeatListener.java b/LobbySystem/src/de/steamwar/lobby/listener/PlayerSeatListener.java index 3e452b30..0b66cc8c 100644 --- a/LobbySystem/src/de/steamwar/lobby/listener/PlayerSeatListener.java +++ b/LobbySystem/src/de/steamwar/lobby/listener/PlayerSeatListener.java @@ -20,7 +20,7 @@ package de.steamwar.lobby.listener; import de.steamwar.linkage.Linked; -import de.steamwar.lobby.LobbySystem; +import de.steamwar.linkage.LinkedTimer; import org.bukkit.*; import org.bukkit.block.data.Bisected; import org.bukkit.block.data.type.Stairs; @@ -34,7 +34,6 @@ import org.bukkit.event.block.Action; import org.bukkit.event.entity.EntityDismountEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerQuitEvent; -import org.bukkit.scheduler.BukkitRunnable; import java.util.HashSet; import java.util.Set; @@ -46,15 +45,11 @@ public class PlayerSeatListener implements Listener { private Set seats = new HashSet<>(); - static { - new BukkitRunnable() { - @Override - public void run() { - for (Arrow arrow : world.getEntitiesByClass(Arrow.class)) { - arrow.setTicksLived(1); - } - } - }.runTaskTimer(LobbySystem.getInstance(), 20 * 60, 20 * 60); + @LinkedTimer(delay = 1, period = 1, unit = LinkedTimer.Unit.MINUTES) + private static void keepArrows() { + for (Arrow arrow : world.getEntitiesByClass(Arrow.class)) { + arrow.setTicksLived(1); + } } @EventHandler diff --git a/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java b/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java index 52261647..1e1eed30 100644 --- a/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java +++ b/LobbySystem/src/de/steamwar/lobby/particle/ParticleListener.java @@ -20,7 +20,7 @@ package de.steamwar.lobby.particle; import de.steamwar.linkage.Linked; -import de.steamwar.lobby.LobbySystem; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.lobby.jumpandrun.JumpAndRun; import de.steamwar.lobby.listener.PlayerSpawn; import de.steamwar.lobby.util.LobbyPlayer; @@ -49,25 +49,24 @@ public class ParticleListener implements Listener { private static Map movingPlayers = new HashMap<>(); - public ParticleListener() { - Bukkit.getScheduler().runTaskTimer(LobbySystem.getInstance(), () -> { - movingPlayers.replaceAll((player, integer) -> integer - 1); - movingPlayers.entrySet().removeIf(entry -> entry.getValue() <= 0); - deg += 0.1; - if (deg > 360) deg = 0; - Bukkit.getOnlinePlayers().forEach(player -> { - if (disabled(player)) return; - if (JumpAndRun.isPlayerInJumpAndRun(player)) return; - LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player.getUniqueId()); - ParticleEnum particle = lobbyPlayer.getParticle(); - if (particle == null) return; - ParticleData particleData = particle.getParticle(); - ParticleElement particleElement = particleData.getParticleElement(); - if (particleElement.tickType() == ParticleTickType.ALWAYS) { - particleElement.tick(new ParticleTickData(player.getWorld(), player, deg, movingPlayers.containsKey(player))); - } - }); - }, 0, 1); + @LinkedTimer(delay = 0, period = 1) + private void tickParticles() { + movingPlayers.replaceAll((player, integer) -> integer - 1); + movingPlayers.entrySet().removeIf(entry -> entry.getValue() <= 0); + deg += 0.1; + if (deg > 360) deg = 0; + Bukkit.getOnlinePlayers().forEach(player -> { + if (disabled(player)) return; + if (JumpAndRun.isPlayerInJumpAndRun(player)) return; + LobbyPlayer lobbyPlayer = LobbyPlayer.getLobbyPlayer(player.getUniqueId()); + ParticleEnum particle = lobbyPlayer.getParticle(); + if (particle == null) return; + ParticleData particleData = particle.getParticle(); + ParticleElement particleElement = particleData.getParticleElement(); + if (particleElement.tickType() == ParticleTickType.ALWAYS) { + particleElement.tick(new ParticleTickData(player.getWorld(), player, deg, movingPlayers.containsKey(player))); + } + }); } @EventHandler(priority = EventPriority.NORMAL) diff --git a/LobbySystem/src/de/steamwar/lobby/team/TeamPlayer.java b/LobbySystem/src/de/steamwar/lobby/team/TeamPlayer.java index 8a294914..987fa29f 100644 --- a/LobbySystem/src/de/steamwar/lobby/team/TeamPlayer.java +++ b/LobbySystem/src/de/steamwar/lobby/team/TeamPlayer.java @@ -20,6 +20,7 @@ package de.steamwar.lobby.team; import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.lobby.LobbySystem; import de.steamwar.lobby.display.NPC; import de.steamwar.sql.SteamwarUser; @@ -76,55 +77,58 @@ public class TeamPlayer implements Listener { entities.put(name, npc); } + private final AtomicInteger count = new AtomicInteger(); + { SteamwarUser.getServerTeam().forEach(user -> { spawnTeamPlayer(world, user); }); - AtomicInteger count = new AtomicInteger(); LobbySystem.getInstance().getLogger().log(Level.INFO, "Loaded " + entities.size() + " team players"); - Bukkit.getScheduler().runTaskTimer(LobbySystem.getInstance(), () -> { - Collection active = world.getEntitiesByClass(Villager.class); - if (active.size() > entities.size()) { - Set duplicates = new HashSet<>(); - active.stream() - .filter(villager -> !duplicates.add(villager.getCustomName())) - .forEach(Entity::remove); - } + } - count.incrementAndGet(); - if (count.get() % (20 * 60 * 60) == 0) { - count.set(0); - List steamwarUsers = SteamwarUser.getServerTeam(); - AtomicInteger added = new AtomicInteger(); - steamwarUsers.forEach(user -> { - if (!entities.containsKey(user.getUserName())) { - spawnTeamPlayer(world, user); - added.incrementAndGet(); - } - }); - AtomicInteger removed = new AtomicInteger(); - List names = steamwarUsers.stream().map(SteamwarUser::getUserName).collect(Collectors.toList()); - new HashSet<>(entities.keySet()).stream().filter(name -> !names.contains(name)).forEach(name -> { - villagers.remove(name).remove(); - entities.remove(name).delete(); - removed.incrementAndGet(); - }); - if (added.get() > 0 || removed.get() > 0) { - LobbySystem.getInstance().getLogger().log(Level.INFO, "Loaded " + added.get() + " team players, removed " + removed.get() + " team players"); - } - } - world.getEntitiesByClasses(Villager.class).forEach(entity -> { - NPC npc = entities.get(entity.getName()); - if (npc != null) { - if (illegalLocation(entity.getLocation())) { - entity.teleport(npc.getLocation()); - return; - } - npc.setLocation(entity.getLocation()); + @LinkedTimer(delay = 1, period = 1) + private void updateTeamPlayers() { + Collection active = world.getEntitiesByClass(Villager.class); + if (active.size() > entities.size()) { + Set duplicates = new HashSet<>(); + active.stream() + .filter(villager -> !duplicates.add(villager.getCustomName())) + .forEach(Entity::remove); + } + + count.incrementAndGet(); + if (count.get() % (20 * 60 * 60) == 0) { + count.set(0); + List steamwarUsers = SteamwarUser.getServerTeam(); + AtomicInteger added = new AtomicInteger(); + steamwarUsers.forEach(user -> { + if (!entities.containsKey(user.getUserName())) { + spawnTeamPlayer(world, user); + added.incrementAndGet(); } }); - }, 1L, 1L); + AtomicInteger removed = new AtomicInteger(); + List names = steamwarUsers.stream().map(SteamwarUser::getUserName).collect(Collectors.toList()); + new HashSet<>(entities.keySet()).stream().filter(name -> !names.contains(name)).forEach(name -> { + villagers.remove(name).remove(); + entities.remove(name).delete(); + removed.incrementAndGet(); + }); + if (added.get() > 0 || removed.get() > 0) { + LobbySystem.getInstance().getLogger().log(Level.INFO, "Loaded " + added.get() + " team players, removed " + removed.get() + " team players"); + } + } + world.getEntitiesByClasses(Villager.class).forEach(entity -> { + NPC npc = entities.get(entity.getName()); + if (npc != null) { + if (illegalLocation(entity.getLocation())) { + entity.teleport(npc.getLocation()); + return; + } + npc.setLocation(entity.getLocation()); + } + }); } private boolean illegalLocation(Location location) { diff --git a/MissileWars/src/de/steamwar/misslewars/FightInfoPacketSender.java b/MissileWars/src/de/steamwar/misslewars/FightInfoPacketSender.java index a2bcd52e..a4f80b21 100644 --- a/MissileWars/src/de/steamwar/misslewars/FightInfoPacketSender.java +++ b/MissileWars/src/de/steamwar/misslewars/FightInfoPacketSender.java @@ -19,6 +19,8 @@ package de.steamwar.misslewars; +import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.network.NetworkSender; import de.steamwar.network.packets.common.FightInfoPacket; import de.steamwar.sql.SteamwarUser; @@ -28,6 +30,7 @@ import org.bukkit.World; import java.util.List; import java.util.stream.Collectors; +@Linked public class FightInfoPacketSender implements Runnable { private final World world = Bukkit.getWorlds().get(0); @@ -41,6 +44,7 @@ public class FightInfoPacketSender implements Runnable { private final int redLeader = Config.RedLeader != null ? SteamwarUser.get(Config.RedLeader).getId() : 0; @Override + @LinkedTimer(delay = 1, period = 1, unit = LinkedTimer.Unit.SECONDS) public void run() { if (Config.test()) { return; diff --git a/MissileWars/src/de/steamwar/misslewars/MissileWars.java b/MissileWars/src/de/steamwar/misslewars/MissileWars.java index 7734cc74..6323e850 100644 --- a/MissileWars/src/de/steamwar/misslewars/MissileWars.java +++ b/MissileWars/src/de/steamwar/misslewars/MissileWars.java @@ -75,8 +75,6 @@ public class MissileWars extends JavaPlugin { StateDependent.setupState(fightState); - Bukkit.getScheduler().runTaskTimer(this, new FightInfoPacketSender(), 20, 20); - Bukkit.getWorlds().forEach(world -> { world.setGameRule(GameRule.KEEP_INVENTORY, true); world.setGameRule(GameRule.MOB_GRIEFING, true); diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/Core.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/Core.java index 8f076c0a..f14144e0 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/Core.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/Core.java @@ -26,6 +26,7 @@ import de.steamwar.command.TabCompletionCache; import de.steamwar.command.TypeMapper; import de.steamwar.core.authlib.SteamwarGameProfileRepository; import de.steamwar.linkage.AbstractLinker; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.linkage.SpigotLinker; import de.steamwar.message.Message; import de.steamwar.network.NetworkReceiver; @@ -119,10 +120,6 @@ public class Core extends JavaPlugin { TinyProtocol.init(); CheckpointUtils.signalHandler(); - Bukkit.getScheduler().runTaskTimer(this, TabCompletionCache::invalidateOldEntries, 20, 20); - Bukkit.getScheduler().runTaskTimer(Core.getInstance(), SteamwarUser::clear, 72000, 72000); - Bukkit.getScheduler().runTaskTimer(Core.getInstance(), SchematicNode::clear, 20L * 30, 20L * 30); - try { getLogger().log(Level.INFO, "Running on: " + new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec("hostname").getInputStream())).readLine()); } catch (IOException e) { @@ -130,6 +127,21 @@ public class Core extends JavaPlugin { } } + @LinkedTimer(delay = 1, period = 1, unit = LinkedTimer.Unit.SECONDS) + private void invalidateTabCompletions() { + TabCompletionCache.invalidateOldEntries(); + } + + @LinkedTimer(delay = 1, period = 1, unit = LinkedTimer.Unit.HOURS) + private void clearUserCache() { + SteamwarUser.clear(); + } + + @LinkedTimer(delay = 30, period = 30, unit = LinkedTimer.Unit.SECONDS) + private void clearSchematicCache() { + SchematicNode.clear(); + } + @Override public void onDisable() { TinyProtocol.instance.close(); diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/WorldEditRenderer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/WorldEditRenderer.java index f11aec11..964890e7 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/WorldEditRenderer.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/WorldEditRenderer.java @@ -30,6 +30,7 @@ import com.sk89q.worldedit.regions.RegionSelector; import com.sk89q.worldedit.world.World; import de.steamwar.core.Core; import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.linkage.PluginCheck; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -55,12 +56,13 @@ public class WorldEditRenderer implements Listener { public WorldEditRenderer() { INSTANCE = this; we = WorldEditWrapper.getWorldEditPlugin(); + } - Bukkit.getScheduler().runTaskTimer(Core.getInstance(), () -> { - for (Player player : Bukkit.getOnlinePlayers()) { - renderPlayer(player, true); - } - }, 20, 20); + @LinkedTimer(delay = 20, period = 20) + private void renderPlayers() { + for (Player player : Bukkit.getOnlinePlayers()) { + renderPlayer(player, true); + } } public static void renderPlayer(Player player) { diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/cursor/CursorListener.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/cursor/CursorListener.java index 99e286ca..0b919898 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/cursor/CursorListener.java +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/cursor/CursorListener.java @@ -1,13 +1,12 @@ package de.steamwar.cursor; import com.comphenix.tinyprotocol.TinyProtocol; -import de.steamwar.core.Core; 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.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -29,12 +28,13 @@ public class CursorListener implements Listener { TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Pos.class, this::updateCursorFromPacket); TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.Rot.class, this::updateCursorFromPacket); TinyProtocol.instance.addFilter(ServerboundMovePlayerPacket.PosRot.class, this::updateCursorFromPacket); + } - Bukkit.getScheduler().runTaskTimer(Core.getInstance(), () -> { - SWPlayer.allWithSingleComponent(Cursor.class) - .map(SWPlayer.SWPlayerWithComponent::getComponent) - .forEach(Cursor::renderDeduplicated); - }, 1, 1); + @LinkedTimer(delay = 1, period = 1) + private void renderCursors() { + SWPlayer.allWithSingleComponent(Cursor.class) + .map(SWPlayer.SWPlayerWithComponent::getComponent) + .forEach(Cursor::renderDeduplicated); } public Packet updateCursorFromPacket(Player player, Packet packet) { diff --git a/Teamserver/src/de/steamwar/teamserver/listener/FreezeListener.java b/Teamserver/src/de/steamwar/teamserver/listener/FreezeListener.java index 12c279c7..c040942c 100644 --- a/Teamserver/src/de/steamwar/teamserver/listener/FreezeListener.java +++ b/Teamserver/src/de/steamwar/teamserver/listener/FreezeListener.java @@ -20,6 +20,7 @@ package de.steamwar.teamserver.listener; import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.teamserver.Builder; import net.md_5.bungee.api.ChatMessageType; import org.bukkit.Bukkit; @@ -41,13 +42,12 @@ public class FreezeListener implements Listener { public static boolean freeze = true; - public FreezeListener() { - Bukkit.getScheduler().runTaskTimer(Builder.getInstance(), () -> { - if (!freeze) return; - Bukkit.getOnlinePlayers().forEach(player -> { - Builder.MESSAGE.sendPrefixless("REGION_FREEZE_ENABLED", player, ChatMessageType.ACTION_BAR); - }); - }, 1, 1); + @LinkedTimer(delay = 1, period = 1) + private void showFreeze() { + if (!freeze) return; + Bukkit.getOnlinePlayers().forEach(player -> { + Builder.MESSAGE.sendPrefixless("REGION_FREEZE_ENABLED", player, ChatMessageType.ACTION_BAR); + }); } @EventHandler diff --git a/TowerRun/src/de/steamwar/towerrun/FightInfoPacketSender.java b/TowerRun/src/de/steamwar/towerrun/FightInfoPacketSender.java index 764e1eea..43c9d882 100644 --- a/TowerRun/src/de/steamwar/towerrun/FightInfoPacketSender.java +++ b/TowerRun/src/de/steamwar/towerrun/FightInfoPacketSender.java @@ -19,6 +19,8 @@ package de.steamwar.towerrun; +import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.network.NetworkSender; import de.steamwar.network.packets.common.FightInfoPacket; import de.steamwar.sql.SteamwarUser; @@ -33,6 +35,7 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +@Linked public class FightInfoPacketSender implements Runnable { private final World world = Bukkit.getWorlds().get(0); @@ -44,6 +47,7 @@ public class FightInfoPacketSender implements Runnable { private final String redName = "§cAlive"; @Override + @LinkedTimer(delay = 1, period = 1, unit = LinkedTimer.Unit.SECONDS) public void run() { if (Config.test()) { return; diff --git a/TowerRun/src/de/steamwar/towerrun/TowerRun.java b/TowerRun/src/de/steamwar/towerrun/TowerRun.java index 10298547..1fdc8ae3 100644 --- a/TowerRun/src/de/steamwar/towerrun/TowerRun.java +++ b/TowerRun/src/de/steamwar/towerrun/TowerRun.java @@ -71,7 +71,5 @@ public class TowerRun extends JavaPlugin { Bukkit.shutdown(); return; } - - Bukkit.getScheduler().runTaskTimer(this, new FightInfoPacketSender(), 20, 20); } } diff --git a/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java index 0c2c0aa8..35cf8a4d 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java +++ b/VelocityCore/src/de/steamwar/velocitycore/EventStarter.java @@ -20,6 +20,7 @@ package de.steamwar.velocitycore; import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.messages.Chatter; import de.steamwar.messages.Message; import de.steamwar.persistent.Subserver; @@ -35,7 +36,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Queue; import java.util.Set; -import java.util.concurrent.TimeUnit; import static de.steamwar.persistent.Storage.eventServer; @@ -58,13 +58,13 @@ public class EventStarter { public EventStarter() { EventFight.loadAllComingFights(); - VelocityCore.schedule(this::run).repeat(10, TimeUnit.SECONDS).schedule(); } public static Map getEventServer() { return eventServer; } + @LinkedTimer(delay = 0, period = 10, unit = LinkedTimer.Unit.SECONDS) private void run() { if (eventServer.entrySet().removeIf(entry -> Subserver.getSubserver(entry.getValue().getServer()) == null)) { EventFight.loadAllComingFights(); diff --git a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java index d94e8e3d..936628f2 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java +++ b/VelocityCore/src/de/steamwar/velocitycore/VelocityCore.java @@ -133,7 +133,6 @@ public class VelocityCore implements ReloadablePlugin { return tabCompleter.apply(sender, s); } }); - schedule(TabCompletionCache::invalidateOldEntries).repeat(1, TimeUnit.SECONDS).schedule(); initStaticServers(); @@ -188,12 +187,6 @@ public class VelocityCore implements ReloadablePlugin { return; } - schedule(() -> { - SteamwarUser.clear(); - Team.clear(); - SteamwarPrefix.clearCache(); - }).repeat(1, TimeUnit.HOURS).schedule(); - DiscordConfig discordConfig = DiscordConfig.load(); if (discordConfig != null) { try { @@ -204,6 +197,18 @@ public class VelocityCore implements ReloadablePlugin { } } + @LinkedTimer(delay = 0, period = 1, unit = LinkedTimer.Unit.SECONDS) + private void invalidateTabCompletions() { + TabCompletionCache.invalidateOldEntries(); + } + + @LinkedTimer(delay = 0, period = 1, unit = LinkedTimer.Unit.HOURS) + private void clearCaches() { + SteamwarUser.clear(); + Team.clear(); + SteamwarPrefix.clearCache(); + } + @Subscribe @Override public void onProxyShutdown(ProxyShutdownEvent event) { diff --git a/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java b/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java index b130a2d7..5f87b699 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java +++ b/VelocityCore/src/de/steamwar/velocitycore/commands/CheckCommand.java @@ -24,6 +24,7 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ServerConnection; import de.steamwar.command.SWCommand; import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.messages.Chatter; import de.steamwar.messages.Message; import de.steamwar.messages.PlayerChatter; @@ -41,10 +42,8 @@ import net.kyori.adventure.text.format.NamedTextColor; import java.awt.*; import java.sql.Timestamp; import java.time.Instant; -import java.time.format.DateTimeFormatter; import java.util.*; import java.util.List; -import java.util.concurrent.TimeUnit; import java.util.function.BooleanSupplier; import java.util.logging.Level; @@ -76,7 +75,11 @@ public class CheckCommand extends SWCommand { public CheckCommand() { super("check"); - VelocityCore.schedule(() -> Chatter.allStream().forEach(CheckCommand::sendReminder)).delay(10, TimeUnit.MINUTES).repeat(10, TimeUnit.MINUTES).schedule(); + } + + @LinkedTimer(delay = 10, period = 10, unit = LinkedTimer.Unit.MINUTES) + private static void sendReminders() { + Chatter.allStream().forEach(CheckCommand::sendReminder); } @Override diff --git a/VelocityCore/src/de/steamwar/velocitycore/mods/FabricModSender.java b/VelocityCore/src/de/steamwar/velocitycore/mods/FabricModSender.java index cf32fc53..e64107c5 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/mods/FabricModSender.java +++ b/VelocityCore/src/de/steamwar/velocitycore/mods/FabricModSender.java @@ -29,11 +29,11 @@ import com.velocitypowered.api.event.player.ServerConnectedEvent; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.protocol.ProtocolUtils; import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.persistent.Storage; import de.steamwar.sql.Mod; import de.steamwar.sql.SWException; import de.steamwar.sql.SteamwarUser; -import de.steamwar.velocitycore.VelocityCore; import de.steamwar.velocitycore.listeners.BasicListener; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -55,20 +55,21 @@ public class FabricModSender extends BasicListener { neededFabricMods.add("fabricloader"); neededQuiltMods.add("quilt_loader"); + } - VelocityCore.schedule(() -> { - synchronized (Storage.fabricExpectPluginMessage) { - for (Map.Entry entry : Storage.fabricExpectPluginMessage.entrySet()) { - if (!Storage.fabricCheckedPlayers.containsKey(entry.getKey())) { - continue; - } - if (System.currentTimeMillis() - entry.getValue() > TimeUnit.SECONDS.toMillis(20)) { - Storage.fabricExpectPluginMessage.remove(entry.getKey()); - return; - } + @LinkedTimer(delay = 0, period = 1, unit = LinkedTimer.Unit.SECONDS) + private void removeExpiredExpectations() { + synchronized (Storage.fabricExpectPluginMessage) { + for (Map.Entry entry : Storage.fabricExpectPluginMessage.entrySet()) { + if (!Storage.fabricCheckedPlayers.containsKey(entry.getKey())) { + continue; + } + if (System.currentTimeMillis() - entry.getValue() > TimeUnit.SECONDS.toMillis(20)) { + Storage.fabricExpectPluginMessage.remove(entry.getKey()); + return; } } - }).repeat(1, TimeUnit.SECONDS).schedule(); + } } public void handlePluginMessage(PluginMessageEvent e) { diff --git a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java index 9631720d..057c9966 100644 --- a/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java +++ b/VelocityCore/src/de/steamwar/velocitycore/tablist/TablistManager.java @@ -27,6 +27,7 @@ import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.server.RegisteredServer; import de.steamwar.linkage.Linked; +import de.steamwar.linkage.LinkedTimer; import de.steamwar.linkage.api.Disable; import de.steamwar.network.packets.common.FightInfoPacket; import de.steamwar.persistent.Storage; @@ -38,7 +39,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; @Linked public class TablistManager extends BasicListener implements Disable { @@ -63,7 +63,6 @@ public class TablistManager extends BasicListener implements Disable { private int seconds = 0; public TablistManager() { - VelocityCore.schedule(this::updateTablist).repeat(1, TimeUnit.SECONDS).schedule(); synchronized (tablists) { VelocityCore.getProxy().getAllPlayers().forEach(player -> tablists.computeIfAbsent(player, Tablist::new).onServerSwitch()); } @@ -102,6 +101,7 @@ public class TablistManager extends BasicListener implements Disable { } } + @LinkedTimer(delay = 0, period = 1, unit = LinkedTimer.Unit.SECONDS) private void updateTablist() { List subservers = new ArrayList<>(); for (RegisteredServer server : new ArrayList<>(VelocityCore.getProxy().getAllServers())) { From 5651455a5fd2a2ce423c1ceac51d34fe9e7f7d49 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 17:07:18 +0100 Subject: [PATCH 14/19] Stop hull rays from passing between occluding blocks A ray only moves diagonally if it could also reach the block by moving along the single axes of the direction one after another without passing an occluding block, so it no longer squeezes between blocks touching at an edge or corner. When a block stops occluding, the diagonal rays it blocked are continued, so incremental updates match a full initialisation. Double slabs now occlude as well, as slab materials do not; block updates pass the block data instead of the material for that. To keep the initialisation fast, a ray only checks a diagonal step after the cheaper checks, and the branch directions are computed once per hull. --- .../fightsystem/record/PacketProcessor.java | 4 +- .../fightsystem/utils/FightHullHider.java | 6 +- HiderCore/src/de/steamwar/hullhider/Hull.java | 130 +++++++++++++++--- .../src/de/steamwar/hullhider/HullHider.java | 7 +- 4 files changed, 119 insertions(+), 28 deletions(-) diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/record/PacketProcessor.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/record/PacketProcessor.java index c0b6738f..c50eb60d 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/record/PacketProcessor.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/record/PacketProcessor.java @@ -51,7 +51,7 @@ import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import org.bukkit.*; import org.bukkit.craftbukkit.CraftWorld; -import org.bukkit.craftbukkit.util.CraftMagicNumbers; +import org.bukkit.craftbukkit.block.data.CraftBlockData; import org.bukkit.enchantments.Enchantment; import org.bukkit.entity.EntityType; import org.bukkit.entity.Player; @@ -473,7 +473,7 @@ public class PacketProcessor implements Listener { level.removeBlockEntity(pos); level.setBlock(pos, blockData, blockState1); level.getChunkSource().blockChanged(pos); - FightSystem.getHullHider().blockUpdate(Config.world.getBlockAt(x, y, z), CraftMagicNumbers.getMaterial(Block.stateById(blockState)).getItemType()); + FightSystem.getHullHider().blockUpdate(Config.world.getBlockAt(x, y, z), CraftBlockData.fromData(Block.stateById(blockState))); }); } 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 d667ba45..8c7eee13 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java @@ -33,8 +33,8 @@ import de.steamwar.fightsystem.states.StateDependentTask; import de.steamwar.hullhider.Hull; import de.steamwar.hullhider.HullHider; import de.steamwar.hullhider.Viewers; -import org.bukkit.Material; import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -119,8 +119,8 @@ public class FightHullHider { }); } - public void blockUpdate(Block block, Material changedType) { - hullHider.blockUpdate(block, changedType); + public void blockUpdate(Block block, BlockData changedData) { + hullHider.blockUpdate(block, changedData); } public boolean isBlockHidden(Player player, int x, int y, int z) { diff --git a/HiderCore/src/de/steamwar/hullhider/Hull.java b/HiderCore/src/de/steamwar/hullhider/Hull.java index 745d5d88..b39a215e 100644 --- a/HiderCore/src/de/steamwar/hullhider/Hull.java +++ b/HiderCore/src/de/steamwar/hullhider/Hull.java @@ -37,8 +37,11 @@ import org.bukkit.Chunk; import org.bukkit.GameMode; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.Tag; import org.bukkit.World; import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Slab; import org.bukkit.craftbukkit.block.data.CraftBlockData; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; @@ -64,6 +67,7 @@ public class Hull { private final BitSet occluding; private final BitSet visibility; private final HashMap visibilityDirections = new HashMap<>(); // Contains the visible directions of each occluding visible block + private final HashMap branches = new HashMap<>(); // The diagonal directions a ray along a single axis branches into private final HashSet uncoveredSurface = new HashSet<>(); private final Viewers viewers; @@ -91,6 +95,9 @@ public class Hull { for (IntVector direction : directions) { visibilityDirections.put(direction, new BitSet(region.volume())); + branches.put(direction, direction.sqLength() == 1 + ? Arrays.stream(directions).filter(v -> v.sqLength() >= 2 && dot(v, direction) == 1).toArray(IntVector[]::new) + : NO_BRANCHES); } } @@ -98,6 +105,15 @@ public class Hull { return material.isOccluding() || hiddenBlocks.contains(material); } + private boolean isOccluding(BlockData data) { + return isOccluding(data.getMaterial()) || isDoubleSlab(data); + } + + // Double slabs are full blocks, but slab materials do not occlude + private static boolean isDoubleSlab(BlockData data) { + return data instanceof Slab slab && slab.getType() == Slab.Type.DOUBLE; + } + public boolean isBlockHidden(Player player, int x, int y, int z) { return region.contains(x, y, z) && !isViewer(player) && !visibility.get(region.toIndex(x, y, z)); } @@ -248,37 +264,88 @@ public class Hull { long start = System.currentTimeMillis(); region.forEach((x, y, z) -> { - IntVector block = new IntVector(x, y, z); - if (isOccluding(world.getBlockAt(x, y, z).getType())) { - occluding.set(block.toId(region)); + Block block = world.getBlockAt(x, y, z); + Material type = block.getType(); + // Only slabs need their block data + if (isOccluding(type) || Tag.SLABS.isTagged(type) && isDoubleSlab(block.getBlockData())) { + occluding.set(region.toIndex(x, y, z)); } }); forEachBorder((root, direction) -> uncoverBlocks(new NullList<>(), root, direction)); plugin.getLogger().log(Level.INFO, () -> "[HullHider] initialisation finished: " + (System.currentTimeMillis() - start) + " ms, visible blocks: " + visibility.cardinality()); } - public void updateBlockVisibility(Block b, Material changedType) { + public void updateBlockVisibility(Block b, BlockData changedData) { IntVector root = new IntVector(b.getX(), b.getY(), b.getZ()); if (root.notInRegion(region)) return; int id = root.toId(region); - if (!occluding.get(id) || isOccluding(changedType)) return; + if (!occluding.get(id) || isOccluding(changedData)) return; occluding.clear(id); - if (!visibility.get(id)) return; List uncovered = new ArrayList<>(); - for (Map.Entry directionalVisibility : visibilityDirections.entrySet()) { - if (directionalVisibility.getValue().get(id)) { - directionalVisibility.getValue().clear(id); - uncoverBlocks(uncovered, root, directionalVisibility.getKey()); + if (visibility.get(id)) { + for (Map.Entry directionalVisibility : visibilityDirections.entrySet()) { + if (directionalVisibility.getValue().get(id)) { + directionalVisibility.getValue().clear(id); + uncoverBlocks(uncovered, root, directionalVisibility.getKey()); + } } } + // Also if the block was not visible itself, it might have blocked diagonal rays passing it + uncoverDiagonalsBeside(uncovered, root); uncoveredSurface.addAll(new HashSet<>(uncovered)); uncoveredSurface.remove(root); } + /** + * Continues the diagonal rays that could not pass the block while it occluded. + * The block lies between the start and the end of a diagonal step if it is the start moved along some, but not all, axes of the direction. + */ + private void uncoverDiagonalsBeside(List uncovered, IntVector root) { + for (IntVector diagonal : directions) { + if (diagonal.sqLength() < 2) continue; + + for (IntVector part : directions) { + if (part.sqLength() == 0 || part.sqLength() >= diagonal.sqLength() || !isPartOf(part, diagonal)) continue; + + IntVector from = root.add(-part.x, -part.y, -part.z); + if (from.notInRegion(region)) continue; + + int fromId = from.toId(region); + if (!occluding.get(fromId) && reachedBy(fromId, diagonal) && canPass(from, diagonal)) { + uncoverBlocks(uncovered, from.add(diagonal), diagonal, visibilityDirections.get(diagonal), NO_BRANCHES); + } + } + } + } + + private static boolean isPartOf(IntVector part, IntVector direction) { + return (part.x == 0 || part.x == direction.x) + && (part.y == 0 || part.y == direction.y) + && (part.z == 0 || part.z == direction.z); + } + + /** + * @return whether a ray moving in the diagonal direction reached the block: a diagonal ray or a branch of a cardinal ray + */ + private boolean reachedBy(int id, IntVector diagonal) { + if (visibilityDirections.get(diagonal).get(id)) return true; + + for (IntVector cardinal : directions) { + if (cardinal.sqLength() == 1 && dot(cardinal, diagonal) == 1 && visibilityDirections.get(cardinal).get(id)) { + return true; + } + } + return false; + } + + private static int dot(IntVector a, IntVector b) { + return a.x * b.x + a.y * b.y + a.z * b.z; + } + public void sendUncoveredBlocks() { Map> sectionWise = new HashMap<>(); @@ -346,14 +413,7 @@ public class Hull { } private void uncoverBlocks(List uncovered, IntVector block, IntVector direction) { - uncoverBlocks( - uncovered, block, direction, visibilityDirections.get(direction), - direction.sqLength() == 1 - ? Arrays.stream(directions) - .filter(v -> v.sqLength() >= 2 && v.x * direction.x + v.y * direction.y + v.z * direction.z == 1) - .toArray(IntVector[]::new) - : NO_BRANCHES - ); + uncoverBlocks(uncovered, block, direction, visibilityDirections.get(direction), branches.get(direction)); } private void uncoverBlocks(List uncovered, IntVector block, IntVector direction, BitSet directionalVisibility, IntVector[] branchDirections) { @@ -370,12 +430,42 @@ public class Hull { if (occluding.get(id)) return; - uncoverBlocks(uncovered, block.add(direction), direction, directionalVisibility, branchDirections); + uncoverNext(uncovered, block, direction, directionalVisibility, branchDirections); for (IntVector branchDirection : branchDirections) { - uncoverBlocks(uncovered, block.add(branchDirection), branchDirection, visibilityDirections.get(branchDirection), NO_BRANCHES); + uncoverNext(uncovered, block, branchDirection, visibilityDirections.get(branchDirection), NO_BRANCHES); } } + private void uncoverNext(List uncovered, IntVector block, IntVector direction, BitSet directionalVisibility, IntVector[] branchDirections) { + int x = block.x + direction.x; + int y = block.y + direction.y; + int z = block.z + direction.z; + // The cheap checks first: most blocks were already reached from this direction + if (!region.contains(x, y, z) || directionalVisibility.get(region.toIndex(x, y, z)) || !canPass(block, direction)) return; + + uncoverBlocks(uncovered, new IntVector(x, y, z), direction, directionalVisibility, branchDirections); + } + + /** + * A ray can only move diagonally if it could also get there by moving along the single axes of the direction one after another, + * in any order, without passing an occluding block. Otherwise, it would pass between blocks that only touch at an edge or corner. + */ + private boolean canPass(IntVector block, IntVector direction) { + if (direction.sqLength() <= 1) return true; + // The ray ends there anyway; inside the region, all blocks in between are inside as well + if (!region.contains(block.x + direction.x, block.y + direction.y, block.z + direction.z)) return true; + + return canPass(block.x, block.y, block.z, direction.x, direction.y, direction.z); + } + + private boolean canPass(int x, int y, int z, int dx, int dy, int dz) { + if (Math.abs(dx) + Math.abs(dy) + Math.abs(dz) <= 1) return true; + + return (dx != 0 && !occluding.get(region.toIndex(x + dx, y, z)) && canPass(x + dx, y, z, 0, dy, dz)) + || (dy != 0 && !occluding.get(region.toIndex(x, y + dy, z)) && canPass(x, y + dy, z, dx, 0, dz)) + || (dz != 0 && !occluding.get(region.toIndex(x, y, z + dz)) && canPass(x, y, z + dz, dx, dy, 0)); + } + @Getter @AllArgsConstructor diff --git a/HiderCore/src/de/steamwar/hullhider/HullHider.java b/HiderCore/src/de/steamwar/hullhider/HullHider.java index 177d9ffe..c2a5d1b7 100644 --- a/HiderCore/src/de/steamwar/hullhider/HullHider.java +++ b/HiderCore/src/de/steamwar/hullhider/HullHider.java @@ -23,6 +23,7 @@ import de.steamwar.entity.REntity; import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; @@ -148,13 +149,13 @@ public class HullHider implements Listener { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onBlockPhysic(BlockPhysicsEvent e) { if (e.getBlock() == e.getSourceBlock() || e.getChangedType() == Material.AIR) { - blockUpdate(e.getBlock(), e.getChangedType()); + blockUpdate(e.getBlock(), e.getChangedBlockData()); } } - public void blockUpdate(Block block, Material changedType) { + public void blockUpdate(Block block, BlockData changedData) { for (Hull hull : hulls) { - hull.updateBlockVisibility(block, changedType); + hull.updateBlockVisibility(block, changedData); } } From e1ff37bea2f533340cd4292fb3bf7c450bf08af0 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 20:09:45 +0100 Subject: [PATCH 15/19] Apply the gamemode techhider with /hullhider Players using /hullhider also get the techhider of the region's gamemode in the build area, like in a fight: its hidden blocks are replaced and occlude in the hull. --- .../features/hullhider/HullHiderCommand.java | 20 ++++++++++++++++--- .../features/techhider/BauTechHider.java | 1 + 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java index 2b2a99aa..f0ee5ecf 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java @@ -21,6 +21,7 @@ package de.steamwar.bausystem.features.hullhider; import de.steamwar.bausystem.BauSystem; import de.steamwar.bausystem.features.techhider.HiddenHull; +import de.steamwar.bausystem.features.techhider.HidingRule; import de.steamwar.bausystem.features.techhider.TechHiderCommand; import de.steamwar.bausystem.features.xray.XrayCommand; import de.steamwar.bausystem.region.Point; @@ -63,6 +64,7 @@ public class HullHiderCommand extends SWCommand implements Listener { private final HullHider hullHider = new HullHider<>(false); // Read on netty threads by the BauTechHider private final Map hulls = new ConcurrentHashMap<>(); + private final Map hidingRules = new ConcurrentHashMap<>(); private final Map> hidden = new ConcurrentHashMap<>(); @LinkedInstance @@ -99,10 +101,12 @@ public class HullHiderCommand extends SWCommand implements Listener { Point max = region.getBuildArea().getMaxPoint(true); HullRegion hullRegion = HullRegion.fromInclusive(min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ()); - // Only the hidden blocks of an active techhider occlude, the hull hider is used without one - Hull hull = new Hull(BauSystem.getInstance(), world(), hullRegion, false, Collections.emptySet(), new Viewers.VisibleByDefault()); + // The techhider of the gamemode is applied as well, its hidden blocks occlude as they are replaced by the obfuscation block + ObfuscationConfig obfuscation = ObfuscationConfig.of(region.getGameModeConfig().Techhider); + Hull hull = new Hull(BauSystem.getInstance(), world(), hullRegion, false, region.getGameModeConfig().Techhider.HiddenBlocks, new Viewers.VisibleByDefault()); hullHider.addHull(region, hull); - hulls.put(region, new HiddenHull(hull, ObfuscationConfig.of(region.getGameModeConfig().Techhider).getObfuscationState())); + hulls.put(region, new HiddenHull(hull, obfuscation.getObfuscationState())); + hidingRules.put(region, HidingRule.of(region.getBuildArea(), obfuscation)); } hidden.computeIfAbsent(region, rg -> ConcurrentHashMap.newKeySet()).add(player); @@ -123,6 +127,7 @@ public class HullHiderCommand extends SWCommand implements Listener { if (players.isEmpty()) { hidden.remove(region); hulls.remove(region); + hidingRules.remove(region); hullHider.removeHull(region); } else { hullHider.removeViewer(player, region); @@ -130,6 +135,15 @@ public class HullHiderCommand extends SWCommand implements Listener { } } + public void addHidingRules(Player player, List rules) { + hidden.forEach((region, players) -> { + HidingRule rule = hidingRules.get(region); + if (rule != null && players.contains(player)) { + rules.add(rule); + } + }); + } + public void addHiddenHulls(Player player, List hiddenHulls) { hidden.forEach((region, players) -> { HiddenHull hiddenHull = hulls.get(region); diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java index 9b91d6a4..99a7f255 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java @@ -69,6 +69,7 @@ public class BauTechHider implements AccessPrivilegeProvider { List rules = new ArrayList<>(); xrayCommand.addHidingRules(p, rules); techHiderCommand.addHidingRules(p, rules); + hullHiderCommand.addHidingRules(p, rules); spectatorListener.addHidingRules(p, rules); List hulls = new ArrayList<>(); From baa4cf34e52161ce981a1b1750398422b7ac5545 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 21:18:24 +0100 Subject: [PATCH 16/19] Calculate hull visibility on block faces Instead of a block occluding as a whole, the hull stores which of its six faces are full, using the occlusion shapes the client culls faces with. A ray inside a block can only leave it through an open face and enter the next block through its open opposite face; if only that face is full, the ray reaches its surface, so the block is visible, but stops there. Slabs, stairs and stacked slabs now hide what is behind them; double slabs need no special case anymore. Faces only open up on block updates, after which the rays that could not get into, out of or past the block are continued. Hidden blocks occlude by the same decision as the techhider, so waterlogged blocks count as well when water is hidden. The hull therefore takes the ObfuscationConfig, which the fight shares with its TechHiderWrapper. --- .../features/hullhider/HullHiderCommand.java | 2 +- .../fightsystem/utils/FightHullHider.java | 7 +- .../fightsystem/utils/TechHiderWrapper.java | 3 +- HiderCore/src/de/steamwar/hullhider/Hull.java | 215 +++++++++++------- 4 files changed, 147 insertions(+), 80 deletions(-) diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java index f0ee5ecf..08669a80 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java @@ -103,7 +103,7 @@ public class HullHiderCommand extends SWCommand implements Listener { // The techhider of the gamemode is applied as well, its hidden blocks occlude as they are replaced by the obfuscation block ObfuscationConfig obfuscation = ObfuscationConfig.of(region.getGameModeConfig().Techhider); - Hull hull = new Hull(BauSystem.getInstance(), world(), hullRegion, false, region.getGameModeConfig().Techhider.HiddenBlocks, new Viewers.VisibleByDefault()); + Hull hull = new Hull(BauSystem.getInstance(), world(), hullRegion, false, obfuscation, new Viewers.VisibleByDefault()); hullHider.addHull(region, hull); hulls.put(region, new HiddenHull(hull, obfuscation.getObfuscationState())); hidingRules.put(region, HidingRule.of(region.getBuildArea(), obfuscation)); 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 8c7eee13..fd961d96 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java @@ -33,6 +33,8 @@ import de.steamwar.fightsystem.states.StateDependentTask; import de.steamwar.hullhider.Hull; import de.steamwar.hullhider.HullHider; import de.steamwar.hullhider.Viewers; +import de.steamwar.techhider.ObfuscationConfig; +import lombok.Getter; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; import org.bukkit.entity.Player; @@ -51,13 +53,16 @@ import java.util.function.BiConsumer; public class FightHullHider { private final HullHider hullHider = new HullHider<>(true); + // Shared with the TechHiderWrapper + @Getter + private final ObfuscationConfig obfuscation = ObfuscationConfig.of(Config.GameModeConfig.Techhider); public FightHullHider() { if (!TechHiderWrapper.ENABLED) return; Fight.teams().forEach(team -> { Region region = team.getSchemRegion(); - hullHider.addHull(team, new Hull(FightSystem.getPlugin(), Config.world, region.toHullRegion(), region.getMinY() != Config.PlayerRegion.getMinY(), Config.GameModeConfig.Techhider.HiddenBlocks, new Viewers.HiddenByDefault())); + hullHider.addHull(team, new Hull(FightSystem.getPlugin(), Config.world, region.toHullRegion(), region.getMinY() != Config.PlayerRegion.getMinY(), obfuscation, new Viewers.HiddenByDefault())); }); new StateDependentListener(TechHiderWrapper.ENABLED, FightState.All, new MembershipListener()); 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 6f0b1290..534e7cee 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/TechHiderWrapper.java @@ -69,8 +69,7 @@ public class TechHiderWrapper extends StateDependent implements Listener { @Override public void enable() { - ObfuscationConfig obfuscation = ObfuscationConfig.of(Config.GameModeConfig.Techhider); - new TechHider(newProvider(obfuscation)); + new TechHider(newProvider(hullHider.getObfuscation())); } private AccessPrivilegeProvider newProvider(ObfuscationConfig obfuscation) { diff --git a/HiderCore/src/de/steamwar/hullhider/Hull.java b/HiderCore/src/de/steamwar/hullhider/Hull.java index b39a215e..3c37ff26 100644 --- a/HiderCore/src/de/steamwar/hullhider/Hull.java +++ b/HiderCore/src/de/steamwar/hullhider/Hull.java @@ -24,24 +24,26 @@ import com.mojang.authlib.GameProfile; import de.steamwar.core.CraftbukkitWrapper; import de.steamwar.core.ProtocolWrapper; import de.steamwar.entity.REntity; +import de.steamwar.techhider.ObfuscationConfig; import it.unimi.dsi.fastutil.shorts.Short2ObjectArrayMap; import lombok.AllArgsConstructor; import lombok.Getter; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.core.SectionPos; import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.VoxelShape; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.GameMode; import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.Tag; import org.bukkit.World; import org.bukkit.block.Block; import org.bukkit.block.data.BlockData; -import org.bukkit.block.data.type.Slab; +import org.bukkit.craftbukkit.CraftWorld; import org.bukkit.craftbukkit.block.data.CraftBlockData; import org.bukkit.entity.Entity; import org.bukkit.entity.Player; @@ -49,24 +51,28 @@ import org.bukkit.plugin.Plugin; import java.util.*; import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.logging.Level; import java.util.stream.IntStream; public class Hull { private static final IntVector[] NO_BRANCHES = new IntVector[0]; + private static final int ALL_FACES = 0b111111; + private static final byte UNKNOWN_FACES = -1; private final Plugin plugin; private final World world; @Getter private final HullRegion region; private final boolean groundVisible; - private final Set hiddenBlocks; + private final ObfuscationConfig obfuscation; private final IntVector[] directions; - private final BitSet occluding; + private final byte[] fullFaces; // The faces of each block that are fully covered, see face() + private byte[] fullFacesByState; // Cache, per hull as the hidden blocks differ private final BitSet visibility; - private final HashMap visibilityDirections = new HashMap<>(); // Contains the visible directions of each occluding visible block + private final HashMap visibilityDirections = new HashMap<>(); // The blocks the rays of each direction got inside private final HashMap branches = new HashMap<>(); // The diagonal directions a ray along a single axis branches into private final HashSet uncoveredSurface = new HashSet<>(); @@ -76,17 +82,17 @@ public class Hull { /** * @param groundVisible whether the bottom layer of the region is uncovered (if the region does not reach the ground) - * @param hiddenBlocks blocks that occlude like solid blocks, as the techhider replaces them by an occluding block + * @param obfuscation the techhider applied to the hull, the blocks it hides occlude as they are replaced by an occluding block * @param viewers which players may see the hull contents, a new instance for every hull */ - public Hull(Plugin plugin, World world, HullRegion region, boolean groundVisible, Set hiddenBlocks, Viewers viewers) { + public Hull(Plugin plugin, World world, HullRegion region, boolean groundVisible, ObfuscationConfig obfuscation, Viewers viewers) { this.plugin = plugin; this.world = world; this.region = region; this.groundVisible = groundVisible; - this.hiddenBlocks = hiddenBlocks; + this.obfuscation = obfuscation; this.viewers = viewers; - this.occluding = new BitSet(region.volume()); + this.fullFaces = new byte[region.volume()]; this.visibility = new BitSet(region.volume()); directions = IntStream.range(0, 27) @@ -101,17 +107,42 @@ public class Hull { } } - private boolean isOccluding(Material material) { - return material.isOccluding() || hiddenBlocks.contains(material); + /** + * @return the bit of the face of a block in the single-axis direction + */ + private static int face(int dx, int dy, int dz) { + if (dx != 0) return dx > 0 ? 1 : 1 << 1; + if (dy != 0) return dy > 0 ? 1 << 2 : 1 << 3; + return dz > 0 ? 1 << 4 : 1 << 5; } - private boolean isOccluding(BlockData data) { - return isOccluding(data.getMaterial()) || isDoubleSlab(data); + private int fullFacesOf(BlockState state) { + if (fullFacesByState == null) { + fullFacesByState = new byte[net.minecraft.world.level.block.Block.BLOCK_STATE_REGISTRY.size()]; + Arrays.fill(fullFacesByState, UNKNOWN_FACES); + } + + int id = net.minecraft.world.level.block.Block.getId(state); + if (fullFacesByState[id] == UNKNOWN_FACES) { + fullFacesByState[id] = (byte) computeFullFaces(state); + } + return fullFacesByState[id]; } - // Double slabs are full blocks, but slab materials do not occlude - private static boolean isDoubleSlab(BlockData data) { - return data instanceof Slab slab && slab.getType() == Slab.Type.DOUBLE; + // The faces that hide what is behind them, as the client culls them + private int computeFullFaces(BlockState state) { + // Hidden blocks are replaced by an occluding block, the same decision as the techhider's + if (obfuscation.hidesBlock(state.getBlock()) || obfuscation.hidesBlockState(state)) return ALL_FACES; + if (!state.canOcclude()) return 0; + + VoxelShape shape = state.getOcclusionShape(); + int faces = 0; + for (Direction direction : Direction.values()) { + if (net.minecraft.world.level.block.Block.isFaceFull(shape, direction)) { + faces |= face(direction.getStepX(), direction.getStepY(), direction.getStepZ()); + } + } + return faces; } public boolean isBlockHidden(Player player, int x, int y, int z) { @@ -247,7 +278,7 @@ public class Hull { public void fill(boolean visible) { visibility.set(0, visibility.size(), visible); - occluding.set(0, occluding.size(), !visible); + Arrays.fill(fullFaces, (byte) (visible ? 0 : ALL_FACES)); uncoveredSurface.clear(); for (BitSet directionalVisibility : visibilityDirections.values()) { directionalVisibility.set(0, directionalVisibility.size(), visible); @@ -256,67 +287,59 @@ public class Hull { public void initialize() { visibility.clear(); - occluding.clear(); uncoveredSurface.clear(); for (BitSet directionalVisibility : visibilityDirections.values()) { directionalVisibility.clear(); } long start = System.currentTimeMillis(); - region.forEach((x, y, z) -> { - Block block = world.getBlockAt(x, y, z); - Material type = block.getType(); - // Only slabs need their block data - if (isOccluding(type) || Tag.SLABS.isTagged(type) && isDoubleSlab(block.getBlockData())) { - occluding.set(region.toIndex(x, y, z)); - } - }); - forEachBorder((root, direction) -> uncoverBlocks(new NullList<>(), root, direction)); + ServerLevel level = ((CraftWorld) world).getHandle(); + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); + region.forEach((x, y, z) -> fullFaces[region.toIndex(x, y, z)] = (byte) fullFacesOf(level.getBlockState(pos.set(x, y, z)))); + forEachBorder((root, direction) -> startRay(new NullList<>(), root, direction)); plugin.getLogger().log(Level.INFO, () -> "[HullHider] initialisation finished: " + (System.currentTimeMillis() - start) + " ms, visible blocks: " + visibility.cardinality()); } public void updateBlockVisibility(Block b, BlockData changedData) { IntVector root = new IntVector(b.getX(), b.getY(), b.getZ()); - if (root.notInRegion(region)) return; + if (root.notInRegion(region)) return; int id = root.toId(region); - if (!occluding.get(id) || isOccluding(changedData)) return; - - occluding.clear(id); + // Faces only open up, the hull never hides anything again + int faces = fullFaces[id] & fullFacesOf(((CraftBlockData) changedData).getState()); + if (faces == fullFaces[id]) return; + fullFaces[id] = (byte) faces; List uncovered = new ArrayList<>(); - if (visibility.get(id)) { - for (Map.Entry directionalVisibility : visibilityDirections.entrySet()) { - if (directionalVisibility.getValue().get(id)) { - directionalVisibility.getValue().clear(id); - uncoverBlocks(uncovered, root, directionalVisibility.getKey()); - } + // Rays inside the block that could not leave it + for (Map.Entry directionalVisibility : visibilityDirections.entrySet()) { + if (directionalVisibility.getValue().get(id)) { + directionalVisibility.getValue().clear(id); + uncoverBlocks(uncovered, root, directionalVisibility.getKey()); } } - // Also if the block was not visible itself, it might have blocked diagonal rays passing it - uncoverDiagonalsBeside(uncovered, root); + // Rays that could not get into the block or past it + uncoverBeside(uncovered, root); + forEachBorderStart(root, direction -> startRay(uncovered, root, direction)); uncoveredSurface.addAll(new HashSet<>(uncovered)); uncoveredSurface.remove(root); } /** - * Continues the diagonal rays that could not pass the block while it occluded. - * The block lies between the start and the end of a diagonal step if it is the start moved along some, but not all, axes of the direction. + * Continues the rays inside the blocks in front of the block, or of which a diagonal ray moves past the block. + * The block lies in front of or beside the start of a step if it is the start moved along some or all axes of the direction. */ - private void uncoverDiagonalsBeside(List uncovered, IntVector root) { - for (IntVector diagonal : directions) { - if (diagonal.sqLength() < 2) continue; + private void uncoverBeside(List uncovered, IntVector root) { + for (IntVector direction : directions) { + if (direction.sqLength() == 0) continue; for (IntVector part : directions) { - if (part.sqLength() == 0 || part.sqLength() >= diagonal.sqLength() || !isPartOf(part, diagonal)) continue; + if (part.sqLength() == 0 || !isPartOf(part, direction)) continue; IntVector from = root.add(-part.x, -part.y, -part.z); - if (from.notInRegion(region)) continue; - - int fromId = from.toId(region); - if (!occluding.get(fromId) && reachedBy(fromId, diagonal) && canPass(from, diagonal)) { - uncoverBlocks(uncovered, from.add(diagonal), diagonal, visibilityDirections.get(diagonal), NO_BRANCHES); + if (!from.notInRegion(region) && isInside(from.toId(region), direction)) { + uncoverNext(uncovered, from, direction, visibilityDirections.get(direction), branches.get(direction)); } } } @@ -329,13 +352,13 @@ public class Hull { } /** - * @return whether a ray moving in the diagonal direction reached the block: a diagonal ray or a branch of a cardinal ray + * @return whether a ray moving in the direction is inside the block: a ray of this direction or a branch of a ray along a single axis */ - private boolean reachedBy(int id, IntVector diagonal) { - if (visibilityDirections.get(diagonal).get(id)) return true; + private boolean isInside(int id, IntVector direction) { + if (visibilityDirections.get(direction).get(id)) return true; for (IntVector cardinal : directions) { - if (cardinal.sqLength() == 1 && dot(cardinal, diagonal) == 1 && visibilityDirections.get(cardinal).get(id)) { + if (cardinal.sqLength() == 1 && dot(cardinal, direction) == 1 && visibilityDirections.get(cardinal).get(id)) { return true; } } @@ -387,6 +410,16 @@ public class Hull { } } + // The rays forEachBorder starts at the block + private void forEachBorderStart(IntVector block, Consumer f) { + if (groundVisible && block.y == region.getMinY()) f.accept(new IntVector(0, 1, 0)); + if (block.y == region.getMaxY() - 1) f.accept(new IntVector(0, -1, 0)); + if (block.z == region.getMinZ()) f.accept(new IntVector(0, 0, 1)); + if (block.z == region.getMaxZ() - 1) f.accept(new IntVector(0, 0, -1)); + if (block.x == region.getMinX()) f.accept(new IntVector(1, 0, 0)); + if (block.x == region.getMaxX() - 1) f.accept(new IntVector(-1, 0, 0)); + } + private void forEachBorder(BiConsumer f) { for (int x = region.getMinX(); x < region.getMaxX(); x++) { for (int z = region.getMinZ(); z < region.getMaxZ(); z++) { @@ -412,23 +445,31 @@ public class Hull { } } + /** + * Starts a ray at the border of the region, coming from outside. + */ + private void startRay(List uncovered, IntVector block, IntVector direction) { + if (faceFull(block.x, block.y, block.z, -direction.x, -direction.y, -direction.z)) { + reveal(uncovered, block, block.toId(region)); + } else { + uncoverBlocks(uncovered, block, direction); + } + } + private void uncoverBlocks(List uncovered, IntVector block, IntVector direction) { uncoverBlocks(uncovered, block, direction, visibilityDirections.get(direction), branches.get(direction)); } + /** + * Continues a ray that got inside the block. + */ private void uncoverBlocks(List uncovered, IntVector block, IntVector direction, BitSet directionalVisibility, IntVector[] branchDirections) { if (block.notInRegion(region)) return; int id = block.toId(region); if (directionalVisibility.get(id)) return; directionalVisibility.set(id); - - if (!visibility.get(id)) { - visibility.set(id); - uncovered.add(block); - } - - if (occluding.get(id)) return; + reveal(uncovered, block, id); uncoverNext(uncovered, block, direction, directionalVisibility, branchDirections); for (IntVector branchDirection : branchDirections) { @@ -441,29 +482,51 @@ public class Hull { int y = block.y + direction.y; int z = block.z + direction.z; // The cheap checks first: most blocks were already reached from this direction - if (!region.contains(x, y, z) || directionalVisibility.get(region.toIndex(x, y, z)) || !canPass(block, direction)) return; + if (!region.contains(x, y, z) || directionalVisibility.get(region.toIndex(x, y, z))) return; - uncoverBlocks(uncovered, new IntVector(x, y, z), direction, directionalVisibility, branchDirections); + if (canMove(block.x, block.y, block.z, direction.x, direction.y, direction.z, true)) { + uncoverBlocks(uncovered, new IntVector(x, y, z), direction, directionalVisibility, branchDirections); + return; + } + + // The ray might still reach the face of the next block, mostly it is visible already + int id = region.toIndex(x, y, z); + if (!visibility.get(id) && canMove(block.x, block.y, block.z, direction.x, direction.y, direction.z, false)) { + reveal(uncovered, new IntVector(x, y, z), id); + } + } + + private void reveal(List uncovered, IntVector block, int id) { + if (!visibility.get(id)) { + visibility.set(id); + uncovered.add(block); + } } /** - * A ray can only move diagonally if it could also get there by moving along the single axes of the direction one after another, - * in any order, without passing an occluding block. Otherwise, it would pass between blocks that only touch at an edge or corner. + * A ray moves along the single axes of its direction one after another, in any order that does not pass a full face. + * So a diagonal ray does not pass between blocks that only touch at an edge or corner. + * + * @param enter whether the ray has to get inside the target block, otherwise reaching its face suffices */ - private boolean canPass(IntVector block, IntVector direction) { - if (direction.sqLength() <= 1) return true; - // The ray ends there anyway; inside the region, all blocks in between are inside as well - if (!region.contains(block.x + direction.x, block.y + direction.y, block.z + direction.z)) return true; + private boolean canMove(int x, int y, int z, int dx, int dy, int dz, boolean enter) { + if (dx == 0 && dy == 0 && dz == 0) return true; - return canPass(block.x, block.y, block.z, direction.x, direction.y, direction.z); + // The blocks in between have to be entered + return (dx != 0 && canCross(x, y, z, dx, 0, 0, enter || dy != 0 || dz != 0) && canMove(x + dx, y, z, 0, dy, dz, enter)) + || (dy != 0 && canCross(x, y, z, 0, dy, 0, enter || dx != 0 || dz != 0) && canMove(x, y + dy, z, dx, 0, dz, enter)) + || (dz != 0 && canCross(x, y, z, 0, 0, dz, enter || dx != 0 || dy != 0) && canMove(x, y, z + dz, dx, dy, 0, enter)); } - private boolean canPass(int x, int y, int z, int dx, int dy, int dz) { - if (Math.abs(dx) + Math.abs(dy) + Math.abs(dz) <= 1) return true; + /** + * @return whether a ray inside the block can leave it to the neighbouring block in the single-axis direction (and enter it) + */ + private boolean canCross(int x, int y, int z, int dx, int dy, int dz, boolean enter) { + return !faceFull(x, y, z, dx, dy, dz) && (!enter || !faceFull(x + dx, y + dy, z + dz, -dx, -dy, -dz)); + } - return (dx != 0 && !occluding.get(region.toIndex(x + dx, y, z)) && canPass(x + dx, y, z, 0, dy, dz)) - || (dy != 0 && !occluding.get(region.toIndex(x, y + dy, z)) && canPass(x, y + dy, z, dx, 0, dz)) - || (dz != 0 && !occluding.get(region.toIndex(x, y, z + dz)) && canPass(x, y, z + dz, dx, dy, 0)); + private boolean faceFull(int x, int y, int z, int dx, int dy, int dz) { + return (fullFaces[region.toIndex(x, y, z)] & face(dx, dy, dz)) != 0; } From d7d452ebe60f5041dfd0671fb935211a042e9258 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 21:37:08 +0100 Subject: [PATCH 17/19] Let doors and trapdoors occlude in hulls Doors and trapdoors do not occlude for the client, so they had no full face in the hull and rays passed through closed trapdoors, revealing blocks behind them along diagonals. Their face masks now come from their shape, so the face their panel covers is full. --- HiderCore/src/de/steamwar/hullhider/Hull.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/HiderCore/src/de/steamwar/hullhider/Hull.java b/HiderCore/src/de/steamwar/hullhider/Hull.java index 3c37ff26..081e47fe 100644 --- a/HiderCore/src/de/steamwar/hullhider/Hull.java +++ b/HiderCore/src/de/steamwar/hullhider/Hull.java @@ -34,6 +34,9 @@ import net.minecraft.core.SectionPos; import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket; import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.EmptyBlockGetter; +import net.minecraft.world.level.block.DoorBlock; +import net.minecraft.world.level.block.TrapDoorBlock; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.phys.shapes.VoxelShape; import org.bukkit.Bukkit; @@ -129,13 +132,21 @@ public class Hull { return fullFacesByState[id]; } - // The faces that hide what is behind them, as the client culls them + // The faces that hide what is behind them, mostly as the client culls them private int computeFullFaces(BlockState state) { // Hidden blocks are replaced by an occluding block, the same decision as the techhider's if (obfuscation.hidesBlock(state.getBlock()) || obfuscation.hidesBlockState(state)) return ALL_FACES; - if (!state.canOcclude()) return 0; - VoxelShape shape = state.getOcclusionShape(); + VoxelShape shape; + if (state.canOcclude()) { + shape = state.getOcclusionShape(); + } else if (state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) { + // Their panel covers a face, although the client does not cull the faces behind them + shape = state.getShape(EmptyBlockGetter.INSTANCE, BlockPos.ZERO); + } else { + return 0; + } + int faces = 0; for (Direction direction : Direction.values()) { if (net.minecraft.world.level.block.Block.isFaceFull(shape, direction)) { From 4dbe2ccac71bc57e94b008ffc215a2d4473f0ea4 Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 21:58:48 +0100 Subject: [PATCH 18/19] Speed up uncovering blocks in hulls The steps that can lead into or past a changed block are computed once per hull instead of searching all directions and parts on every block update, which makes an update about three times faster. --- HiderCore/src/de/steamwar/hullhider/Hull.java | 62 ++++++++++++------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/HiderCore/src/de/steamwar/hullhider/Hull.java b/HiderCore/src/de/steamwar/hullhider/Hull.java index 081e47fe..02f8c56c 100644 --- a/HiderCore/src/de/steamwar/hullhider/Hull.java +++ b/HiderCore/src/de/steamwar/hullhider/Hull.java @@ -77,6 +77,7 @@ public class Hull { private final BitSet visibility; private final HashMap visibilityDirections = new HashMap<>(); // The blocks the rays of each direction got inside private final HashMap branches = new HashMap<>(); // The diagonal directions a ray along a single axis branches into + private final StepBeside[] stepsBeside; // The steps that can lead into or past a block, see uncoverBeside() private final HashSet uncoveredSurface = new HashSet<>(); private final Viewers viewers; @@ -108,6 +109,7 @@ public class Hull { ? Arrays.stream(directions).filter(v -> v.sqLength() >= 2 && dot(v, direction) == 1).toArray(IntVector[]::new) : NO_BRANCHES); } + stepsBeside = computeStepsBeside(); } /** @@ -333,49 +335,61 @@ public class Hull { uncoverBeside(uncovered, root); forEachBorderStart(root, direction -> startRay(uncovered, root, direction)); - uncoveredSurface.addAll(new HashSet<>(uncovered)); + uncoveredSurface.addAll(uncovered); uncoveredSurface.remove(root); } /** * Continues the rays inside the blocks in front of the block, or of which a diagonal ray moves past the block. - * The block lies in front of or beside the start of a step if it is the start moved along some or all axes of the direction. */ private void uncoverBeside(List uncovered, IntVector root) { - for (IntVector direction : directions) { - if (direction.sqLength() == 0) continue; + for (StepBeside step : stepsBeside) { + int x = root.x - step.part.x; + int y = root.y - step.part.y; + int z = root.z - step.part.z; + if (!region.contains(x, y, z)) continue; - for (IntVector part : directions) { - if (part.sqLength() == 0 || !isPartOf(part, direction)) continue; - - IntVector from = root.add(-part.x, -part.y, -part.z); - if (!from.notInRegion(region) && isInside(from.toId(region), direction)) { - uncoverNext(uncovered, from, direction, visibilityDirections.get(direction), branches.get(direction)); + int id = region.toIndex(x, y, z); + for (BitSet ray : step.rays) { + if (ray.get(id)) { + uncoverNext(uncovered, new IntVector(x, y, z), step.direction, step.directionalVisibility, step.branchDirections); + break; } } } } + /** + * The block lies in front of or beside the start of a step if it is the start moved along some or all axes of the direction. + * The rays of the direction and the rays along a single axis branching into it take the step. + */ + private StepBeside[] computeStepsBeside() { + List steps = new ArrayList<>(); + for (IntVector direction : directions) { + if (direction.sqLength() == 0) continue; + + BitSet[] rays = Arrays.stream(directions) + .filter(ray -> ray.equals(direction) || ray.sqLength() == 1 && direction.sqLength() > 1 && dot(ray, direction) == 1) + .map(visibilityDirections::get) + .toArray(BitSet[]::new); + for (IntVector part : directions) { + if (part.sqLength() > 0 && isPartOf(part, direction)) { + steps.add(new StepBeside(direction, part, visibilityDirections.get(direction), branches.get(direction), rays)); + } + } + } + return steps.toArray(new StepBeside[0]); + } + + private record StepBeside(IntVector direction, IntVector part, BitSet directionalVisibility, IntVector[] branchDirections, BitSet[] rays) { + } + private static boolean isPartOf(IntVector part, IntVector direction) { return (part.x == 0 || part.x == direction.x) && (part.y == 0 || part.y == direction.y) && (part.z == 0 || part.z == direction.z); } - /** - * @return whether a ray moving in the direction is inside the block: a ray of this direction or a branch of a ray along a single axis - */ - private boolean isInside(int id, IntVector direction) { - if (visibilityDirections.get(direction).get(id)) return true; - - for (IntVector cardinal : directions) { - if (cardinal.sqLength() == 1 && dot(cardinal, direction) == 1 && visibilityDirections.get(cardinal).get(id)) { - return true; - } - } - return false; - } - private static int dot(IntVector a, IntVector b) { return a.x * b.x + a.y * b.y + a.z * b.z; } From 9eea5f5e252e3ae31c5996ea89ae5ca2d4a49a1f Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Sat, 31 Oct 2026 22:24:51 +0100 Subject: [PATCH 19/19] Keep hidden block entities in chunks without their data Hidden block entities were removed from chunk data, so the client did not render blocks like shulker boxes at all. They are now sent with empty data, hiding their contents but not the block itself. --- .../src/de/steamwar/techhider/ChunkHider.java | 33 ++++++++++++------- HiderCore/src/hidercore.accesswidener | 1 + 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/HiderCore/src/de/steamwar/techhider/ChunkHider.java b/HiderCore/src/de/steamwar/techhider/ChunkHider.java index 6cabee7b..ab4eb45e 100644 --- a/HiderCore/src/de/steamwar/techhider/ChunkHider.java +++ b/HiderCore/src/de/steamwar/techhider/ChunkHider.java @@ -23,6 +23,7 @@ import de.steamwar.core.ProtocolUtils; import de.steamwar.techhider.AccessPrivilegeProvider.ForPlayer; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import net.minecraft.nbt.CompoundTag; import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData; import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; import net.minecraft.util.SimpleBitStorage; @@ -31,6 +32,7 @@ import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; import org.bukkit.entity.Player; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.function.UnaryOperator; @@ -263,21 +265,30 @@ public class ChunkHider { private List filterBlockEntities(ForPlayer access, List blockEntities, int chunkX, int chunkZ) { int fourBitBitmask = 0b0000_1111; - return blockEntities.stream() - .filter((blockEntityInfo) -> { - BlockEntityType type = blockEntityInfo.type; - int packedXZ = blockEntityInfo.packedXZ; + List filtered = new ArrayList<>(blockEntities.size()); + for (ClientboundLevelChunkPacketData.BlockEntityInfo blockEntityInfo : blockEntities) { + BlockEntityType type = blockEntityInfo.type; + int packedXZ = blockEntityInfo.packedXZ; - int localX = (packedXZ >> 4) & fourBitBitmask; - int localZ = packedXZ & fourBitBitmask; + int localX = (packedXZ >> 4) & fourBitBitmask; + int localZ = packedXZ & fourBitBitmask; - int worldX = (chunkX * SECTION_SPAN_SIZE) + localX; - int worldZ = (chunkZ * SECTION_SPAN_SIZE) + localZ; + int worldX = (chunkX * SECTION_SPAN_SIZE) + localX; + int worldZ = (chunkZ * SECTION_SPAN_SIZE) + localZ; - int worldY = blockEntityInfo.y; + int worldY = blockEntityInfo.y; - return access.canAccessPosition(worldX, worldY, worldZ) && access.canAccessBlockEntity(worldX, worldY, worldZ, type); - }).toList(); + // A hidden position is obfuscated as a whole + if (!access.canAccessPosition(worldX, worldY, worldZ)) continue; + + if (access.canAccessBlockEntity(worldX, worldY, worldZ, type)) { + filtered.add(blockEntityInfo); + } else { + // Without its block entity, the client would not render blocks like shulker boxes, so only their data is hidden + filtered.add(new ClientboundLevelChunkPacketData.BlockEntityInfo(packedXZ, worldY, type, new CompoundTag())); + } + } + return filtered; } private void copyOverSectionBiomeData(ByteBuf oldData, ByteBuf newData) { diff --git a/HiderCore/src/hidercore.accesswidener b/HiderCore/src/hidercore.accesswidener index 347067a1..9a266fac 100644 --- a/HiderCore/src/hidercore.accesswidener +++ b/HiderCore/src/hidercore.accesswidener @@ -25,3 +25,4 @@ accessible class net/minecraft/network/protocol/game/ClientboundLevelChunkPacket accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo type Lnet/minecraft/world/level/block/entity/BlockEntityType; accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo packedXZ I accessible field net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo y I +accessible method net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo (IILnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/nbt/CompoundTag;)V