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());
+ }
+ }
+}