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/BauSystem.properties b/BauSystem/BauSystem_Main/src/BauSystem.properties index c5adfb70..fdbe8ff3 100644 --- a/BauSystem/BauSystem_Main/src/BauSystem.properties +++ b/BauSystem/BauSystem_Main/src/BauSystem.properties @@ -35,8 +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_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 @@ -1033,6 +1032,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..3fb95eb8 100644 --- a/BauSystem/BauSystem_Main/src/BauSystem_de.properties +++ b/BauSystem/BauSystem_Main/src/BauSystem_de.properties @@ -35,8 +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_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 @@ -966,6 +965,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..08669a80 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/hullhider/HullHiderCommand.java @@ -0,0 +1,177 @@ +/* + * 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.HidingRule; +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.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.linkage.LinkedTimer; +import de.steamwar.techhider.ObfuscationConfig; +import net.md_5.bungee.api.ChatMessageType; +import org.bukkit.Bukkit; +import org.bukkit.World; +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 { + + public HullHiderCommand() { + super("hullhider"); + } + + @Linked + // 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 hidingRules = 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()); + + // 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, obfuscation, new Viewers.VisibleByDefault()); + hullHider.addHull(region, hull); + hulls.put(region, new HiddenHull(hull, obfuscation.getObfuscationState())); + hidingRules.put(region, HidingRule.of(region.getBuildArea(), obfuscation)); + } + + 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); + hidingRules.remove(region); + hullHider.removeHull(region); + } else { + hullHider.removeViewer(player, region); + hullHider.resendChunks(player, region); + } + } + + 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); + if (hiddenHull != null && players.contains(player)) { + hiddenHulls.add(hiddenHull); + } + }); + } + + @LinkedTimer(delay = 1, period = 1) + private void onTick() { + if (hidden.isEmpty()) return; + + 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()); + } + } + + 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/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) { 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/techhider/BauTechHider.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java new file mode 100644 index 00000000..99a7f255 --- /dev/null +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/techhider/BauTechHider.java @@ -0,0 +1,161 @@ +/* + * 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.hullhider.HullHiderCommand; +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 and the hulls of /hullhider per player. + * Entities are never hidden. + */ +@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 + public XrayCommand xrayCommand; + @LinkedInstance + public TechHiderCommand techHiderCommand; + @LinkedInstance + public SpectatorListener spectatorListener; + @LinkedInstance + public HullHiderCommand hullHiderCommand; + + public BauTechHider() { + new TechHider(this); + } + + @Override + public ForPlayer forPlayer(Player p) { + 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 + List rules = new ArrayList<>(); + xrayCommand.addHidingRules(p, rules); + techHiderCommand.addHidingRules(p, rules); + hullHiderCommand.addHidingRules(p, rules); + spectatorListener.addHidingRules(p, rules); + + List hulls = new ArrayList<>(); + hullHiderCommand.addHiddenHulls(p, hulls); + return new BauPlayerAccess(p, rules.toArray(NO_RULES), hulls.toArray(NO_HULLS)); + } + + @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, HiddenHull[] hulls) 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; + } + 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; + } + + @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; + } + + // 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 + 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(); + } + 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/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..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 @@ -21,17 +21,17 @@ 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; -import de.steamwar.bausystem.utils.ScoreboardElement; import de.steamwar.command.PreviousArguments; import de.steamwar.command.SWCommand; 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,21 +41,26 @@ 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 -public class TechHiderCommand extends SWCommand implements Listener, ScoreboardElement { +public class TechHiderCommand extends SWCommand implements Listener { public TechHiderCommand() { 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; + @LinkedInstance + public HullHiderCommand hullHiderCommand; + @Register(description = "TECHHIDER_HELP") public void toggleHider(@Validator Player player) { Region region = Region.getRegion(player.getLocation()); @@ -64,26 +69,20 @@ 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; } 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); @@ -128,6 +127,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); @@ -135,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/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/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/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..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 @@ -21,14 +21,15 @@ 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; -import de.steamwar.bausystem.utils.ScoreboardElement; 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,22 +44,27 @@ 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 -public class XrayCommand extends SWCommand implements Listener, ScoreboardElement { +public class XrayCommand extends SWCommand implements Listener { public XrayCommand() { 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; + @LinkedInstance + public HullHiderCommand hullHiderCommand; + @Register(description = "XRAY_HELP") public void toggleHider(@Validator Player player) { Region region = Region.getRegion(player.getLocation()); @@ -67,23 +73,24 @@ 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; } 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); @@ -139,13 +146,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 @@ -160,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); } } diff --git a/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java b/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java index 4a2db53d..9959ecef 100644 --- a/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java +++ b/CommonCore/Linkage/src/de/steamwar/linkage/AbstractLinker.java @@ -22,12 +22,18 @@ 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; import java.util.List; import java.util.Map; @@ -38,6 +44,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 +97,7 @@ public abstract class AbstractLinker { ((Enable) any).enable(); } linkObject(any); + linkFields(any); }); } catch (SecurityException e) { Throwable cause = e.getCause(); @@ -117,6 +125,70 @@ 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) { + 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() { @@ -126,6 +198,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) { @@ -152,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/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 { } 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/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/FightSystem.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/FightSystem.java index 94b45ea4..c4346a1e 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; @@ -116,9 +116,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(); @@ -170,7 +170,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/record/PacketProcessor.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/record/PacketProcessor.java index 72d728c6..c50eb60d 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; @@ -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/HullHider.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java similarity index 52% 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..fd961d96 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/HullHider.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/FightHullHider.java @@ -30,61 +30,62 @@ 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 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.Bukkit; -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; 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 { + private final HullHider hullHider = new HullHider<>(true); + // Shared with the TechHiderWrapper @Getter - private final Map hullMap = new HashMap<>(); - private final Hull[] hulls; + private final ObfuscationConfig obfuscation = ObfuscationConfig.of(Config.GameModeConfig.Techhider); - 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(), obfuscation, new Viewers.HiddenByDefault())); + }); 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(); - - // 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)) { - FightSystem.getTechHider().reloadChunks(player, team.getExtendRegion(), Region.EMPTY); - } - } + hullHider.initialize(team); } 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 +97,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 +115,33 @@ 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); + hullHider.forEach((hullTeam, hull) -> { + if (Config.isReferee(player) || hullTeam == team || (fp != null && fp.canEntern())) { + hullHider.addViewer(player, hullTeam); } else { - hull.getValue().removeViewer(player); + hullHider.removeViewer(player, hullTeam); } - } + }); } - 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()); - } - } - - public void blockUpdate(Block block, Material changedType) { - for (Hull hull : hulls) { - hull.updateBlockVisibility(block, changedType); - } + public void blockUpdate(Block block, BlockData changedData) { + hullHider.blockUpdate(block, changedData); } 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/Hull.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/Hull.java deleted file mode 100644 index 16af9f4c..00000000 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/utils/Hull.java +++ /dev/null @@ -1,392 +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.fightsystem.utils; - -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; -import net.minecraft.core.BlockPos; -import net.minecraft.core.SectionPos; -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.GameMode; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.block.Block; -import org.bukkit.craftbukkit.block.data.CraftBlockData; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.BiConsumer; -import java.util.logging.Level; -import java.util.stream.IntStream; - -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); - } - - @Getter - private final Region region; - private final boolean groundVisible; - private final IntVector[] directions; - - 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 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 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(); - this.occluding = new BitSet(region.volume()); - this.visibility = new BitSet(region.volume()); - - directions = IntStream.range(0, 27) - .mapToObj(v -> new IntVector(v % 3 - 1, (v / 3) % 3 - 1, v / 9 - 1)) - .toArray(IntVector[]::new); - - for (IntVector direction : directions) { - visibilityDirections.put(direction, new BitSet(region.volume())); - } - } - - 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())); - } - - public boolean isViewer(Player player) { - return player != null && viewers.contains(player.getUniqueId()); - } - - /** - * 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). - */ - public void addViewer(Player player) { - if (viewers.add(player.getUniqueId())) { - for (Entity entity : entities) { - trackEntity(player, entity); - } - // techhider triggers block change sending - } - } - - public void forgetViewer(Player player) { - viewers.remove(player.getUniqueId()); - } - - public void checkEntity(Entity entity) { - Location location = entity.getLocation(); - if (region.inRegion(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); - } - } - } else { - if (entities.remove(entity)) { - for (Player player : Bukkit.getOnlinePlayers()) { - if (!isViewer(player) && !player.equals(entity)) trackEntity(player, entity); - } - } - } - } - - public void trackEntity(Player player, Entity entity) { - if (entity instanceof Player) { - TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.REMOVE, new GameProfile(entity.getUniqueId(), entity.getName()), GameMode.CREATIVE)); - } - - player.showEntity(FightSystem.getPlugin(), entity); - } - - public void untrackEntity(Player player, Entity entity) { - player.hideEntity(FightSystem.getPlugin(), entity); - - if (entity instanceof Player) { - TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.ADD, new GameProfile(entity.getUniqueId(), entity.getName()), GameMode.CREATIVE)); - } - } - - public void removeEntity(Entity entity) { - entities.remove(entity); - } - - 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))) { - if (rentities.add(entity)) entity.hide(true); - } else { - if (rentities.remove(entity)) entity.hide(false); - } - } - - public void removeREntity(REntity entity) { - rentities.remove(entity); - } - - public void fill(boolean visible) { - visibility.set(0, visibility.size(), visible); - occluding.set(0, occluding.size(), !visible); - uncoveredSurface.clear(); - for (BitSet directionalVisibility : visibilityDirections.values()) { - directionalVisibility.set(0, directionalVisibility.size(), visible); - } - } - - 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) -> { - IntVector block = new IntVector(x, y, z); - if (isOccluding(Config.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()); - } - - public void updateBlockVisibility(Block b, Material changedType) { - 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; - - 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()); - } - } - - uncoveredSurface.addAll(new HashSet<>(uncovered)); - uncoveredSurface.remove(root); - } - - public void sendUncoveredBlocks() { - Map> sectionWise = new HashMap<>(); - - for (IntVector uncovered : uncoveredSurface) { - sectionWise.computeIfAbsent(new IntVector(uncovered.x >> 4, uncovered.y >> 4, uncovered.z >> 4), section -> new ArrayList<>()).add(uncovered); - } - uncoveredSurface.clear(); - - for (Map.Entry> entry : sectionWise.entrySet()) { - Object result; - List changes = entry.getValue(); - 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(); - } - - if (changes.size() > 1) { - IntVector section = changes.get(0); - section = new IntVector(section.getX() >> 4, section.getY() >> 4, section.getZ() >> 4); - int xOffset = 16 * section.getX(); - int yOffset = 16 * section.getY(); - int zOffset = 16 * section.getZ(); - - short[] pos = new short[changes.size()]; - for (int i = 0; i < changes.size(); i++) { - IntVector change = changes.get(i); - - pos[i] = (short) (((change.getX() - xOffset) << 8) + ((change.getZ() - zOffset) << 4) + (change.getY() - yOffset)); - } - - result = new ClientboundSectionBlocksUpdatePacket(SectionPos.of(section.getX(), section.getY(), section.getZ()), new Short2ObjectArrayMap<>(pos, blockdata, blockdata.length)); - } else { - IntVector pos = changes.get(0); - result = new ClientboundBlockUpdatePacket(new BlockPos(pos.getX(), pos.getY(), pos.getZ()), (BlockState) blockdata[0]); - } - Object packet = result; - Bukkit.getOnlinePlayers().stream().filter(player -> !isViewer(player)).forEach(player -> TinyProtocol.instance.sendPacket(player, packet)); - } - } - - private void forEachBorder(BiConsumer f) { - for (int x = region.getMinX(); x < region.getMaxX(); x++) { - for (int z = region.getMinZ(); z < region.getMaxZ(); z++) { - if (groundVisible) { - f.accept(new IntVector(x, region.getMinY(), z), new IntVector(0, 1, 0)); - } - f.accept(new IntVector(x, region.getMaxY() - 1, z), new IntVector(0, -1, 0)); - } - } - - for (int x = region.getMinX(); x < region.getMaxX(); x++) { - for (int y = region.getMinY(); y < region.getMaxY(); y++) { - f.accept(new IntVector(x, y, region.getMinZ()), new IntVector(0, 0, 1)); - f.accept(new IntVector(x, y, region.getMaxZ() - 1), new IntVector(0, 0, -1)); - } - } - - for (int z = region.getMinZ(); z < region.getMaxZ(); z++) { - for (int y = region.getMinY(); y < region.getMaxY(); y++) { - f.accept(new IntVector(region.getMinX(), y, z), new IntVector(1, 0, 0)); - f.accept(new IntVector(region.getMaxX() - 1, y, z), new IntVector(-1, 0, 0)); - } - } - } - - 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 - ); - } - - 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; - - uncoverBlocks(uncovered, block.add(direction), direction, directionalVisibility, branchDirections); - for (IntVector branchDirection : branchDirections) { - uncoverBlocks(uncovered, block.add(branchDirection), branchDirection, visibilityDirections.get(branchDirection), NO_BRANCHES); - } - } - - - @Getter - @AllArgsConstructor - public static class IntVector { - private final int x; - private final int y; - private final int z; - - public IntVector(Location location) { - this.x = location.getBlockX(); - this.y = location.getBlockY(); - this.z = location.getBlockZ(); - } - - public boolean notInRegion(Region region) { - return !region.inRegion(x, y, z); - } - - public int toId(Region region) { - return ((y - region.getMinY()) * region.getSizeZ() + (z - region.getMinZ())) * region.getSizeX() + (x - region.getMinX()); - } - - public IntVector add(int x, int y, int z) { - return new IntVector(this.x + x, this.y + y, this.z + z); - } - - public IntVector add(IntVector v) { - return add(v.x, v.y, v.z); - } - - public int sqLength() { - return x * x + y * y + z * z; - } - - @Override - public int hashCode() { - return y << 24 ^ x << 12 ^ z; - } - - @Override - public boolean equals(Object o) { - if (o == null || this.getClass() != o.getClass()) { - return false; - } - - IntVector v = (IntVector) o; - return x == v.x && y == v.y && z == v.z; - } - - @Override - public String toString() { - return x + "," + y + "," + z; - } - } - - private static class NullList extends AbstractList { - @Override - public void add(int index, E element) { - // Straight to /dev/null! - } - - @Override - public E get(int index) { - return null; - } - - @Override - public int size() { - return 0; - } - } -} 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..d5de73be 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,8 @@ package de.steamwar.fightsystem.utils; -import de.steamwar.techhider.ProtocolUtils; +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..534e7cee 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 { @@ -67,9 +56,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; @@ -80,29 +69,14 @@ 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)); + new TechHider(newProvider(hullHider.getObfuscation())); } - 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 +92,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 +125,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 +147,12 @@ 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); + } + + @Override + public BlockState getObfuscation(int blockX, int blockY, int blockZ) { + return obfuscation.getObfuscationState(); } } @@ -235,27 +210,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 new file mode 100644 index 00000000..a0b27617 --- /dev/null +++ b/HiderCore/build.gradle.kts @@ -0,0 +1,43 @@ +/* + * 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) + compileOnly(libs.authlib) + compileOnly(libs.datafixer) +} + +widener { + fromCatalog(libs.nms) +} diff --git a/HiderCore/src/de/steamwar/hullhider/Hull.java b/HiderCore/src/de/steamwar/hullhider/Hull.java new file mode 100644 index 00000000..02f8c56c --- /dev/null +++ b/HiderCore/src/de/steamwar/hullhider/Hull.java @@ -0,0 +1,628 @@ +/* + * 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.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 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.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; +import org.bukkit.Chunk; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.craftbukkit.CraftWorld; +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.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 ObfuscationConfig obfuscation; + private final IntVector[] directions; + + 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<>(); // 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; + 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 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, ObfuscationConfig obfuscation, Viewers viewers) { + this.plugin = plugin; + this.world = world; + this.region = region; + this.groundVisible = groundVisible; + this.obfuscation = obfuscation; + this.viewers = viewers; + this.fullFaces = new byte[region.volume()]; + this.visibility = new BitSet(region.volume()); + + directions = IntStream.range(0, 27) + .mapToObj(v -> new IntVector(v % 3 - 1, (v / 3) % 3 - 1, v / 9 - 1)) + .toArray(IntVector[]::new); + + 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); + } + stepsBeside = computeStepsBeside(); + } + + /** + * @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 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]; + } + + // 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; + + 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)) { + faces |= face(direction.getStepX(), direction.getStepY(), direction.getStepZ()); + } + } + return faces; + } + + 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)); + } + + public boolean isViewer(Player player) { + return player != null && viewers.isViewer(player); + } + + /** + * Adds the player to the {@link Viewers}, which decide whether the player may see the hull contents afterwards. + */ + public void addViewer(Player player) { + 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.removeViewer(player); + } + + public void checkEntity(Entity entity) { + Location location = entity.getLocation(); + 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); + } + } + } else { + if (entities.remove(entity)) { + for (Player player : Bukkit.getOnlinePlayers()) { + if (!isViewer(player) && !player.equals(entity)) trackEntity(player, entity); + } + } + } + } + + public void trackEntity(Player player, Entity entity) { + if (entity instanceof Player) { + TinyProtocol.instance.sendPacket(player, ProtocolWrapper.playerInfoPacketConstructor(ProtocolWrapper.PlayerInfoAction.REMOVE, new GameProfile(entity.getUniqueId(), entity.getName()), GameMode.CREATIVE)); + } + + player.showEntity(plugin, entity); + } + + public void untrackEntity(Player player, Entity 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)); + } + } + + public void removeEntity(Entity entity) { + entities.remove(entity); + } + + public void checkREntity(REntity entity) { + 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); + } + } + + public void removeREntity(REntity entity) { + 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); + Arrays.fill(fullFaces, (byte) (visible ? 0 : ALL_FACES)); + uncoveredSurface.clear(); + for (BitSet directionalVisibility : visibilityDirections.values()) { + directionalVisibility.set(0, directionalVisibility.size(), visible); + } + } + + public void initialize() { + visibility.clear(); + uncoveredSurface.clear(); + for (BitSet directionalVisibility : visibilityDirections.values()) { + directionalVisibility.clear(); + } + + long start = System.currentTimeMillis(); + 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; + + int id = root.toId(region); + // 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<>(); + // 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()); + } + } + // Rays that could not get into the block or past it + uncoverBeside(uncovered, root); + forEachBorderStart(root, direction -> startRay(uncovered, root, direction)); + + 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. + */ + private void uncoverBeside(List uncovered, IntVector root) { + 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; + + 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); + } + + 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<>(); + + for (IntVector uncovered : uncoveredSurface) { + sectionWise.computeIfAbsent(new IntVector(uncovered.x >> 4, uncovered.y >> 4, uncovered.z >> 4), section -> new ArrayList<>()).add(uncovered); + } + uncoveredSurface.clear(); + + for (Map.Entry> entry : sectionWise.entrySet()) { + Object result; + List changes = entry.getValue(); + Object[] blockdata = new Object[changes.size()]; + for (int i = 0; i < blockdata.length; i++) { + IntVector change = changes.get(i); + blockdata[i] = ((CraftBlockData) world.getBlockData(change.getX(), change.getY(), change.getZ())).getState(); + } + + if (changes.size() > 1) { + IntVector section = changes.get(0); + section = new IntVector(section.getX() >> 4, section.getY() >> 4, section.getZ() >> 4); + int xOffset = 16 * section.getX(); + int yOffset = 16 * section.getY(); + int zOffset = 16 * section.getZ(); + + short[] pos = new short[changes.size()]; + for (int i = 0; i < changes.size(); i++) { + IntVector change = changes.get(i); + + pos[i] = (short) (((change.getX() - xOffset) << 8) + ((change.getZ() - zOffset) << 4) + (change.getY() - yOffset)); + } + + result = new ClientboundSectionBlocksUpdatePacket(SectionPos.of(section.getX(), section.getY(), section.getZ()), new Short2ObjectArrayMap<>(pos, blockdata, blockdata.length)); + } else { + IntVector pos = changes.get(0); + result = new ClientboundBlockUpdatePacket(new BlockPos(pos.getX(), pos.getY(), pos.getZ()), (BlockState) blockdata[0]); + } + Object packet = result; + Bukkit.getOnlinePlayers().stream().filter(player -> !isViewer(player)).forEach(player -> TinyProtocol.instance.sendPacket(player, packet)); + } + } + + // 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++) { + if (groundVisible) { + f.accept(new IntVector(x, region.getMinY(), z), new IntVector(0, 1, 0)); + } + f.accept(new IntVector(x, region.getMaxY() - 1, z), new IntVector(0, -1, 0)); + } + } + + for (int x = region.getMinX(); x < region.getMaxX(); x++) { + for (int y = region.getMinY(); y < region.getMaxY(); y++) { + f.accept(new IntVector(x, y, region.getMinZ()), new IntVector(0, 0, 1)); + f.accept(new IntVector(x, y, region.getMaxZ() - 1), new IntVector(0, 0, -1)); + } + } + + for (int z = region.getMinZ(); z < region.getMaxZ(); z++) { + for (int y = region.getMinY(); y < region.getMaxY(); y++) { + f.accept(new IntVector(region.getMinX(), y, z), new IntVector(1, 0, 0)); + f.accept(new IntVector(region.getMaxX() - 1, y, z), new IntVector(-1, 0, 0)); + } + } + } + + /** + * 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); + reveal(uncovered, block, id); + + uncoverNext(uncovered, block, direction, directionalVisibility, branchDirections); + for (IntVector branchDirection : branchDirections) { + 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))) return; + + 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 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 canMove(int x, int y, int z, int dx, int dy, int dz, boolean enter) { + if (dx == 0 && dy == 0 && dz == 0) return true; + + // 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)); + } + + /** + * @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)); + } + + 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; + } + + + @Getter + @AllArgsConstructor + public static class IntVector { + private final int x; + private final int y; + private final int z; + + public IntVector(Location location) { + this.x = location.getBlockX(); + this.y = location.getBlockY(); + this.z = location.getBlockZ(); + } + + public boolean notInRegion(HullRegion region) { + return !region.contains(x, y, z); + } + + public int toId(HullRegion region) { + return region.toIndex(x, y, z); + } + + public IntVector add(int x, int y, int z) { + return new IntVector(this.x + x, this.y + y, this.z + z); + } + + public IntVector add(IntVector v) { + return add(v.x, v.y, v.z); + } + + public int sqLength() { + return x * x + y * y + z * z; + } + + @Override + public int hashCode() { + return y << 24 ^ x << 12 ^ z; + } + + @Override + public boolean equals(Object o) { + if (o == null || this.getClass() != o.getClass()) { + return false; + } + + IntVector v = (IntVector) o; + return x == v.x && y == v.y && z == v.z; + } + + @Override + public String toString() { + return x + "," + y + "," + z; + } + } + + private static class NullList extends AbstractList { + @Override + public void add(int index, E element) { + // Straight to /dev/null! + } + + @Override + public E get(int index) { + return null; + } + + @Override + public int size() { + return 0; + } + } +} diff --git a/HiderCore/src/de/steamwar/hullhider/HullHider.java b/HiderCore/src/de/steamwar/hullhider/HullHider.java new file mode 100644 index 00000000..c2a5d1b7 --- /dev/null +++ b/HiderCore/src/de/steamwar/hullhider/HullHider.java @@ -0,0 +1,211 @@ +/* + * 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.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; +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 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. + */ + 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); + } + + public void forEach(BiConsumer consumer) { + hullMap.forEach(consumer); + } + + public void initialize(K key) { + 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) { + hullMap.get(key).fill(visible); + } + + public void fillAll(boolean visible) { + for (Hull hull : hulls) { + hull.fill(visible); + } + } + + /** + * @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) { + 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.getChangedBlockData()); + } + } + + public void blockUpdate(Block block, BlockData changedData) { + for (Hull hull : hulls) { + hull.updateBlockVisibility(block, changedData); + } + } + + @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) { + if (!hideEntities) return; + + 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) { + if (!hideEntities) return; + + 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) { + 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/hullhider/HullRegion.java b/HiderCore/src/de/steamwar/hullhider/HullRegion.java new file mode 100644 index 00000000..7b5d30ab --- /dev/null +++ b/HiderCore/src/de/steamwar/hullhider/HullRegion.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.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()); + } + + 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 + */ + 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 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; + } +} 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()); + } + } +} diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/AccessPrivilegeProvider.java b/HiderCore/src/de/steamwar/techhider/AccessPrivilegeProvider.java similarity index 80% rename from SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/AccessPrivilegeProvider.java rename to HiderCore/src/de/steamwar/techhider/AccessPrivilegeProvider.java index 38b9b463..ff39a9fe 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/AccessPrivilegeProvider.java +++ b/HiderCore/src/de/steamwar/techhider/AccessPrivilegeProvider.java @@ -52,6 +52,18 @@ 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); + + /** + * @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/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/ChunkHider.java b/HiderCore/src/de/steamwar/techhider/ChunkHider.java similarity index 81% rename from SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/ChunkHider.java rename to HiderCore/src/de/steamwar/techhider/ChunkHider.java index caa9c96b..ab4eb45e 100644 --- a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/ChunkHider.java +++ b/HiderCore/src/de/steamwar/techhider/ChunkHider.java @@ -19,9 +19,11 @@ 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; +import net.minecraft.nbt.CompoundTag; import net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData; import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; import net.minecraft.util.SimpleBitStorage; @@ -30,6 +32,8 @@ 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; @@ -44,12 +48,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; @@ -88,9 +86,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); @@ -197,7 +208,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)); } } } @@ -206,6 +217,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); @@ -244,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/de/steamwar/techhider/ObfuscationConfig.java b/HiderCore/src/de/steamwar/techhider/ObfuscationConfig.java new file mode 100644 index 00000000..554abaa3 --- /dev/null +++ b/HiderCore/src/de/steamwar/techhider/ObfuscationConfig.java @@ -0,0 +1,115 @@ +/* + * 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 BlockState obfuscationState; + 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.obfuscationState = this.obfuscateWith.defaultBlockState(); + + 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()); + } + } +} diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/TechHider.java b/HiderCore/src/de/steamwar/techhider/TechHider.java similarity index 96% rename from SpigotCore/SpigotCore_Main/src/de/steamwar/techhider/TechHider.java rename to HiderCore/src/de/steamwar/techhider/TechHider.java index eee198e6..8bf4cb1d 100644 --- a/SpigotCore/SpigotCore_Main/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,12 +364,15 @@ 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; } } 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; @@ -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/HiderCore/src/hidercore.accesswidener b/HiderCore/src/hidercore.accesswidener new file mode 100644 index 00000000..9a266fac --- /dev/null +++ b/HiderCore/src/hidercore.accesswidener @@ -0,0 +1,28 @@ +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 +accessible method net/minecraft/network/protocol/game/ClientboundLevelChunkPacketData$BlockEntityInfo (IILnet/minecraft/world/level/block/entity/BlockEntityType;Lnet/minecraft/nbt/CompoundTag;)V 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/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 f360f616..ccce02ef 100644 --- a/MissileWars/src/de/steamwar/misslewars/MissileWars.java +++ b/MissileWars/src/de/steamwar/misslewars/MissileWars.java @@ -81,8 +81,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/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/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/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/core/worldedit/WorldEditRenderer.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/worldedit/WorldEditRenderer.java index b66c67ad..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,13 +56,13 @@ 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()) { - 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/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/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/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 9aaa9b40..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.techhider.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 dfa0a5f3..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.techhider.BlockIds; -import de.steamwar.techhider.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 68d0aa82..210a5faf 100644 --- a/SpigotCore/SpigotCore_Main/src/spigotcore.accesswidener +++ b/SpigotCore/SpigotCore_Main/src/spigotcore.accesswidener @@ -33,39 +33,5 @@ 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 -## 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 TechHider -accessible field net/minecraft/network/protocol/game/ClientboundMoveEntityPacket entityId I -## 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; - -# 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/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 5519d79a..e2e3a997 100644 --- a/TowerRun/src/de/steamwar/towerrun/TowerRun.java +++ b/TowerRun/src/de/steamwar/towerrun/TowerRun.java @@ -78,7 +78,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 1701a10d..936628f2 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; @@ -132,7 +133,6 @@ public class VelocityCore implements ReloadablePlugin { return tabCompleter.apply(sender, s); } }); - schedule(TabCompletionCache::invalidateOldEntries).repeat(1, TimeUnit.SECONDS).schedule(); initStaticServers(); @@ -164,6 +164,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) { @@ -182,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 { @@ -198,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())) { 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")