From 138c502ac2ecba91acd10d248fc56811c1e5700e Mon Sep 17 00:00:00 2001 From: joelb Date: Thu, 9 Apr 2026 19:58:42 +0200 Subject: [PATCH 1/5] - Changed old methods to new worldguard methods --- .../combat/region/worldguard/WorldGuardRegion.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/region/worldguard/WorldGuardRegion.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/region/worldguard/WorldGuardRegion.java index 4d0a5a8b..0ffc17de 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/region/worldguard/WorldGuardRegion.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/region/worldguard/WorldGuardRegion.java @@ -8,13 +8,14 @@ import org.bukkit.World; record WorldGuardRegion(World world, ProtectedRegion region) implements Region { + @Override public Point getCenter() { BlockVector3 min = this.region.getMinimumPoint(); BlockVector3 max = this.region.getMaximumPoint(); - double x = (double) (min.getX() + max.getX()) / 2; - double z = (double) (min.getZ() + max.getZ()) / 2; + double x = (min.x() + max.x()) / 2.0; + double z = (min.z() + max.z()) / 2.0; return new Point(this.world, x, z); } @@ -22,12 +23,13 @@ public Point getCenter() { @Override public Location getMin() { BlockVector3 min = this.region.getMinimumPoint(); - return new Location(this.world, min.getX(), min.getY(), min.getZ()); + return new Location(this.world, min.x(), min.y(), min.z()); } @Override public Location getMax() { BlockVector3 max = this.region.getMaximumPoint(); - return new Location(this.world, max.getX(), max.getY(), max.getZ()); + return new Location(this.world, max.x(), max.y(), max.z()); } + } From 3616b5995ef378778a45dfd5d480d41a6e6d35b6 Mon Sep 17 00:00:00 2001 From: joelb Date: Thu, 9 Apr 2026 19:59:04 +0200 Subject: [PATCH 2/5] fix(knockback): prevent roof teleport exploit & improve safe knockback handling Fixed an issue where players using fight charge could remain inside knockback regions and get teleported onto the map roof (above playable area). ### Key changes: - Reworked knockback direction to push players toward nearest region edge instead of center-based vector - Added velocity dampening system for more consistent knockback behavior - Improved vertical handling: - Separate ground vs air vertical values - Prevent excessive vertical stacking while airborne ### Teleport fallback system: - Introduced optional smart fallback teleport when knockback fails - Added continuous velocity check before teleporting (prevents false triggers) - Prevent duplicate fallback tasks using active tracking set - Improved force knockback logic with edge distance + velocity validation ### Safe location handling: - Implemented safe ground detection system: - Avoid unsafe blocks (lava, cactus, magma, etc.) - Ensure 2-block air clearance above ground - Added configurable fallback scanning from highest block - Prevent teleport if no safe location is found (optional) ### Stability improvements: - Added recursion limit for region expansion to prevent infinite loops - Improved region overlap handling during location generation - Reduced unnecessary teleports and edge-case glitches ### Config additions: - vertical / maxAirVertical - dampenVelocity / dampenFactor - useTeleport (smart fallback) - safeGroundCheck + safeHighestFallback - unsafeGroundBlocks - groundOffset - maxAttempts - cancelIfNoSafeGround Result: - Eliminates roof teleport exploit - More natural and predictable knockback - Safer teleport fallback system - Better handling of edge cases and overlapping regions --- .../fight/knockback/KnockbackService.java | 258 ++++++++++++++++-- .../fight/knockback/KnockbackSettings.java | 139 +++++++++- 2 files changed, 363 insertions(+), 34 deletions(-) diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackService.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackService.java index bae8afab..b4135a9c 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackService.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackService.java @@ -1,21 +1,20 @@ package com.eternalcode.combat.fight.knockback; import com.eternalcode.combat.config.implementation.PluginConfig; -import com.eternalcode.combat.region.Point; import com.eternalcode.combat.region.Region; import com.eternalcode.combat.region.RegionProvider; import com.eternalcode.commons.bukkit.scheduler.MinecraftScheduler; +import com.eternalcode.commons.scheduler.Task; import io.papermc.lib.PaperLib; -import java.time.Duration; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; import org.bukkit.Location; +import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.util.Vector; +import java.time.Duration; +import java.util.*; + public final class KnockbackService { private final PluginConfig config; @@ -23,6 +22,7 @@ public final class KnockbackService { private final RegionProvider regionProvider; private final Map insideRegion = new HashMap<>(); + private final Set fallbackActive = new HashSet<>(); public KnockbackService(PluginConfig config, MinecraftScheduler scheduler, RegionProvider regionProvider) { this.config = config; @@ -31,57 +31,257 @@ public KnockbackService(PluginConfig config, MinecraftScheduler scheduler, Regio } public void knockbackLater(Region region, Player player, Duration duration) { - this.scheduler.runLater(() -> this.knockback(region, player), duration); + scheduler.runLater(() -> this.knockback(region, player), duration); + } + + public void knockback(Region region, Player player) { + + if (player.isInsideVehicle()) { + player.leaveVehicle(); + } + + Location loc = player.getLocation(); + Vector direction = getDirectionToEdge(region, loc); + + if (config.knockback.dampenVelocity) { + player.setVelocity(player.getVelocity().multiply(config.knockback.dampenFactor)); + } + + boolean onGround = Math.abs(player.getVelocity().getY()) < 0.08; + + double y = onGround + ? config.knockback.vertical + : Math.min(player.getVelocity().getY(), config.knockback.maxAirVertical); + + Vector velocity = direction.multiply(config.knockback.multiplier).setY(y); + + player.setFallDistance(0); + player.setVelocity(velocity); + + + if (config.knockback.useTeleport) { + scheduleSmartFallback(player, region); + } } public void forceKnockbackLater(Player player, Region region) { - if (insideRegion.containsKey(player.getUniqueId())) { + UUID uuid = player.getUniqueId(); + + if (insideRegion.containsKey(uuid)) { return; } - insideRegion.put(player.getUniqueId(), region); + insideRegion.put(uuid, region); scheduler.runLater(player.getLocation(), () -> { - insideRegion.remove(player.getUniqueId()); - Location playerLocation = player.getLocation(); - if (!region.contains(playerLocation) && !regionProvider.isInRegion(playerLocation)) { + insideRegion.remove(uuid); + + Location loc = player.getLocation(); + double velocity = player.getVelocity().lengthSquared(); + + if (velocity > 0.02) { return; } - if (player.isInsideVehicle()) { - player.leaveVehicle(); + if (!region.contains(loc)) { + return; + } + + double distanceToEdge = getDistanceToEdge(region, loc); + + if (distanceToEdge > 1.5) { + return; + } + + if (fallbackActive.contains(uuid)) { + return; + } + + Location generated = generate( + player.getLocation(), + Point2D.from(region.getMin()), + Point2D.from(region.getMax()), + 0 + ); + + Location safe = makeSafe(generated); + if (safe == null || safe.getWorld() == null) { + return; } - Location location = generate(playerLocation, Point2D.from(region.getMin()), Point2D.from(region.getMax())); + PaperLib.teleportAsync(player, safe.clone(), TeleportCause.PLUGIN); - PaperLib.teleportAsync(player, location, TeleportCause.PLUGIN); - }, this.config.knockback.forceDelay); + }, config.knockback.forceDelay); } - private Location generate(Location playerLocation, Point2D minX, Point2D maxX) { + private void scheduleSmartFallback(Player player, Region region) { + UUID uuid = player.getUniqueId(); + + if (fallbackActive.contains(uuid)) { + return; + } + + fallbackActive.add(uuid); + + final Task[] taskRef = new Task[1]; + + taskRef[0] = scheduler.timer(() -> { + + Location check = player.getLocation(); + double velocity = player.getVelocity().lengthSquared(); + + if (!region.contains(check)) { + fallbackActive.remove(uuid); + taskRef[0].cancel(); + return; + } + + if (velocity > 0.02) { + return; + } + + Location generated = generate( + player.getLocation(), + Point2D.from(region.getMin()), + Point2D.from(region.getMax()), + 0 + ); + + Location safe = makeSafe(generated); + if (safe == null || safe.getWorld() == null) { + fallbackActive.remove(uuid); + taskRef[0].cancel(); + return; + } + + PaperLib.teleportAsync(player, safe.clone(), TeleportCause.PLUGIN); + + fallbackActive.remove(uuid); + taskRef[0].cancel(); + + }, + Duration.ofMillis(100), + Duration.ofMillis(100)); + } + + private Location makeSafe(Location loc) { + if (loc == null || loc.getWorld() == null) return loc; + + return config.knockback.safeGroundCheck + ? findSafeGround(loc) + : loc.getWorld().getHighestBlockAt(loc).getLocation().add(0, config.knockback.groundOffset, 0); + } + + private Location findSafeGround(Location loc) { + + if (loc.getWorld() == null) return loc; + + Location check = loc.clone(); + int minY = loc.getWorld().getMinHeight(); + + for (int y = check.getBlockY(); y > minY; y--) { + check.setY(y); + + Material type = check.getBlock().getType(); + Material above = check.clone().add(0, 1, 0).getBlock().getType(); + Material above2 = check.clone().add(0, 2, 0).getBlock().getType(); + + if (type.isSolid() + && !config.knockback.unsafeGroundBlocks.contains(type) + && above.isAir() + && above2.isAir()) { + + return check.clone().add(0, config.knockback.groundOffset, 0); + } + } + + return getSafeHighest(loc); + } + + private Location getSafeHighest(Location loc) { + if (loc == null || loc.getWorld() == null) return loc; + + if (!config.knockback.safeHighestFallback) { + return loc.getWorld() + .getHighestBlockAt(loc) + .getLocation() + .add(0, config.knockback.groundOffset, 0); + } + + Location highest = loc.getWorld().getHighestBlockAt(loc).getLocation(); + int minY = loc.getWorld().getMinHeight(); + + int startY = highest.getBlockY(); + int maxScan = config.knockback.safeHighestMaxScan; + + int endY = (maxScan < 0) + ? minY + : Math.max(minY, startY - maxScan); + + for (int y = startY; y > endY; y--) { + highest.setY(y); + + Material type = highest.getBlock().getType(); + Material above = highest.clone().add(0, 1, 0).getBlock().getType(); + Material above2 = highest.clone().add(0, 2, 0).getBlock().getType(); + + if (type.isSolid() + && !config.knockback.unsafeGroundBlocks.contains(type) + && above.isAir() + && above2.isAir()) { + + return highest.clone().add(0, config.knockback.groundOffset, 0); + } + } + + return config.knockback.cancelIfNoSafeGround ? null : loc; + } + + private Location generate(Location playerLocation, Point2D minX, Point2D maxX, int attempts) { + if (attempts >= config.knockback.maxAttempts) { + return playerLocation; + } + Location location = KnockbackOutsideRegionGenerator.generate(minX, maxX, playerLocation); + Optional otherRegion = regionProvider.getRegion(location); if (otherRegion.isPresent()) { + Region region = otherRegion.get(); - return generate(playerLocation, minX.min(region.getMin()), maxX.max(region.getMax())); + + return generate( + playerLocation, + minX.min(region.getMin()), + maxX.max(region.getMax()), + attempts + 1 + ); } return location; } - public void knockback(Region region, Player player) { - if (player.isInsideVehicle()) { - player.leaveVehicle(); - } + private Vector getDirectionToEdge(Region region, Location loc) { + double dxMin = loc.getX() - region.getMin().getX(); + double dxMax = region.getMax().getX() - loc.getX(); + double dzMin = loc.getZ() - region.getMin().getZ(); + double dzMax = region.getMax().getZ() - loc.getZ(); + + double min = Math.min(Math.min(dxMin, dxMax), Math.min(dzMin, dzMax)); - Point point = region.getCenter(); - Location subtract = player.getLocation().subtract(point.x(), 0, point.z()); + if (Math.abs(min - dxMin) < 1e-6) return new Vector(-1, 0, 0); + if (Math.abs(min - dxMax) < 1e-6) return new Vector(1, 0, 0); + if (Math.abs(min - dzMin) < 1e-6) return new Vector(0, 0, -1); + + return new Vector(0, 0, 1); + } - Vector knockbackVector = new Vector(subtract.getX(), 0, subtract.getZ()).normalize(); - double multiplier = this.config.knockback.multiplier; - Vector configuredVector = new Vector(multiplier, 0.5, multiplier); + private double getDistanceToEdge(Region region, Location loc) { + double dxMin = loc.getX() - region.getMin().getX(); + double dxMax = region.getMax().getX() - loc.getX(); + double dzMin = loc.getZ() - region.getMin().getZ(); + double dzMax = region.getMax().getZ() - loc.getZ(); - player.setVelocity(knockbackVector.multiply(configuredVector)); + return Math.min(Math.min(dxMin, dxMax), Math.min(dzMin, dzMax)); } } diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackSettings.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackSettings.java index 8b722435..48d08248 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackSettings.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackSettings.java @@ -2,18 +2,147 @@ import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; +import org.bukkit.Material; + import java.time.Duration; +import java.util.Set; public class KnockbackSettings extends OkaeriConfig { @Comment({ - "# Adjust the knockback multiplier for restricted regions.", - "# Higher values increase the knockback distance. Avoid using negative values.", - "# A value of 1.0 typically knocks players 2-4 blocks away." + "# Horizontal knockback strength multiplier.", + "# This controls how far the player is pushed horizontally.", + "#", + "# Applied in: direction.multiply(multiplier)", + "# Example: 1.0 ≈ 2–4 blocks push" + }) + public double multiplier = 1.0; + + @Comment({ + "# Vertical velocity applied when the player is considered on ground.", + "#", + "# Used when Y velocity is near zero (< 0.08).", + "# Higher values = more upward knockback.", + "# Recommended: 0.15 - 0.3" + }) + public double vertical = 0.2; + + @Comment({ + "# Maximum vertical velocity when player is already airborne.", + "#", + "# Prevents stacking vertical velocity or launching too high.", + "# Used as: min(currentY, maxAirVertical)" }) - public double multiplier = 1; + public double maxAirVertical = 0.2; - @Comment({ "# Time after which the player will be force knocked back outside the safe zone" }) + @Comment({ + "# Delay before force teleport is applied after entering a region.", + "#", + "# Used in forceKnockbackLater().", + "# Prevents instant teleport when crossing region borders." + }) public Duration forceDelay = Duration.ofSeconds(1); + @Comment({ + "# Enables teleport fallback after knockback.", + "#", + "# If knockback does not push the player outside the region,", + "# a safe location will be generated and player will be teleported." + }) + public boolean useTeleport = false; + + @Comment({ + "# Blocks that are considered unsafe to stand on when searching for ground.", + "# These blocks will be ignored during safe ground detection.", + "#", + "# Used in findSafeGround():", + "# - Skips these blocks even if they are solid", + "# - Prevents teleporting onto dangerous or invalid blocks", + "#", + "# Examples:", + "# - BARRIER (invisible collision)", + "# - LAVA / WATER (damage / movement issues)", + "# - CACTUS / MAGMA (damage blocks)", + }) + public Set unsafeGroundBlocks = Set.of( + Material.BARRIER, + Material.LAVA, + Material.WATER, + Material.CACTUS, + Material.MAGMA_BLOCK, + Material.FIRE, + Material.SOUL_FIRE + ); + + @Comment({ + "# Use custom safe ground detection instead of highest block. (recommended)", + "#", + "# true -> scans downward for safe landing", + "# false -> uses Bukkit getHighestBlockAt()", + "#", + "# Safe mode prevents:", + "# - Landing on roofs", + "# - Landing on barriers", + "# - Unsafe teleport positions" + }) + public boolean safeGroundCheck = true; + + @Comment({ + "# Enables safe fallback scanning instead of using Bukkit highest block directly.", + "#", + "# true -> scans downward from highest block to find safe ground", + "# false -> uses Bukkit getHighestBlockAt (faster but unsafe)" + }) + public boolean safeHighestFallback = true; + + @Comment({ + "# Maximum vertical scan distance for safe highest fallback.", + "#", + "# Prevents excessive scanning in very tall worlds.", + "# Set to -1 to scan all the way down to min world height." + }) + public int safeHighestMaxScan = -1; + + @Comment({ + "# If true, prevents teleport if no safe ground is found at all.", + "#", + "# true -> cancel teleport", + "# false -> fallback to original location" + }) + public boolean cancelIfNoSafeGround = false; + + @Comment({ + "# Y offset added after finding ground.", + "#", + "# Usually 1.0 = player stands exactly on block.", + "# Can be increased slightly to prevent clipping issues." + }) + public double groundOffset = 1.0; + + @Comment({ + "# Reduces player's current velocity BEFORE applying knockback.", + "#", + "# Helps create smoother and more consistent knockback.", + "# Prevents stacking velocity from previous movement." + }) + public boolean dampenVelocity = true; + + @Comment({ + "# Multiplier applied when dampening velocity.", + "#", + "# 1.0 = no change", + "# 0.0 = completely stop player", + "# Recommended: 0.6 - 0.9" + }) + public double dampenFactor = 0.8; + + @Comment({ + "# Maximum recursive attempts when generating a safe location.", + "#", + "# Used in generate() method.", + "# Prevents infinite loops when regions overlap or chain.", + "#", + "# If exceeded -> fallback to player current location." + }) + public int maxAttempts = 5; } From b007f707ac76440dd2dd3647b7276b2f8605f146 Mon Sep 17 00:00:00 2001 From: joelb Date: Wed, 15 Apr 2026 10:30:45 +0200 Subject: [PATCH 3/5] fix(combat): prevent elytra escape exploit & improve glide restriction handling Fixed an issue where players could abuse elytra (e.g. cliff jumping, trident launching, or mid-air tagging) to escape combat and bypass restrictions. ### Key changes: - Fully disabled elytra usage during combat - Blocked glide activation via EntityToggleGlideEvent - Added continuous glide checks to stop mid-air abuse - Forced downward velocity to prevent spacebar glide spam ### Elytra handling system: - Introduced automatic elytra unequip on combat tag - Prevents players from staying airborne when entering combat - Moves elytra safely to inventory or drops if full - Eliminates pre-glide and mid-air tagging exploits ### Inventory protection: - Blocked equipping elytra during combat: - Click equip - Shift-click - Hotbar swap (number keys) - Prevents all known re-equip bypass methods ### Flight & movement improvements: - Disabled flight properly on combat start - Prevents glide reactivation while moving ### Messaging improvements: - Added dedicated elytra combat message - Clear feedback when elytra is blocked or removed ### Config additions: - unequipElytraOnCombat - blockElytraEquipDuringCombat - elytraDisabledDuringCombat Result: - Eliminates cliff jump & trident launch exploits - Prevents all elytra-based combat escapes - Provides consistent and predictable combat behavior - Matches behavior of high-end PvP combat systems --- .../config/implementation/CombatSettings.java | 8 ++ .../implementation/MessagesSettings.java | 9 ++ .../FightActionBlockerController.java | 87 ++++++++++++++++++- 3 files changed, 102 insertions(+), 2 deletions(-) diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/config/implementation/CombatSettings.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/config/implementation/CombatSettings.java index f84740e4..8378dae9 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/config/implementation/CombatSettings.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/config/implementation/CombatSettings.java @@ -33,6 +33,14 @@ public class CombatSettings extends OkaeriConfig { }) public boolean disableFlying = true; + @Comment({ + "# Forcefully unequip elytra when a player enters combat.", + "# This prevents players from abusing glide by jumping before being tagged.", + "# The elytra will be moved to the player's inventory.", + "# Recommended: true" + }) + public boolean unequipElytraOnCombat = true; + @Comment({ "# Prevent players from boosting themselves while flying with fireworks", "# This setting blocks usage of fireworks to boost elytra flight during combat" diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/config/implementation/MessagesSettings.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/config/implementation/MessagesSettings.java index d7a8849a..618b464d 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/config/implementation/MessagesSettings.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/config/implementation/MessagesSettings.java @@ -70,6 +70,15 @@ public class MessagesSettings extends OkaeriConfig { public Notice commandDisabledDuringCombat = Notice.chat( "Command blocked! Cannot use this during combat!"); + @Comment({ + "# Message displayed when a player attempts to use an elytra during combat.", + "# This includes gliding, equipping, or having it forcefully removed.", + "# Informs the player that elytra usage is disabled in combat." + }) + public Notice elytraDisabledDuringCombat = Notice.chat( + "Elytra disabled! Cannot use elytra during combat!" + ); + @Comment({ "# Message displayed when a player uses a command with incorrect arguments.", "# The {USAGE} placeholder is replaced with the correct command syntax." diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightActionBlockerController.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightActionBlockerController.java index cd193717..371a64d4 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightActionBlockerController.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightActionBlockerController.java @@ -16,14 +16,18 @@ import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityToggleGlideEvent; +import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.inventory.InventoryType; import org.bukkit.event.player.PlayerCommandPreprocessEvent; import org.bukkit.event.player.PlayerMoveEvent; import org.bukkit.event.player.PlayerToggleFlightEvent; +import java.util.HashMap; import java.util.List; import java.util.UUID; + +import org.bukkit.inventory.ItemStack; import org.bukkit.util.StringUtil; public class FightActionBlockerController implements Listener { @@ -109,6 +113,7 @@ void onToggleGlide(EntityToggleGlideEvent event) { if (event.isGliding()) { event.setCancelled(true); + player.setGliding(false); } } @@ -127,11 +132,12 @@ void onMoveWhileGliding(PlayerMoveEvent event) { if (player.isGliding()) { player.setGliding(false); + + player.setFallDistance(0f); + player.setVelocity(player.getVelocity().setY(-1)); } } - - @EventHandler void onFly(PlayerToggleFlightEvent event) { if (!this.config.combat.disableFlying) { @@ -152,6 +158,38 @@ void onFly(PlayerToggleFlightEvent event) { } } + @EventHandler + void onTag(com.eternalcode.combat.fight.event.FightTagEvent event) { + UUID uniqueId = event.getPlayer(); + Player player = this.server.getPlayer(uniqueId); + + if (player == null) { + return; + } + + if (this.config.combat.disableFlying) { + GameMode gameMode = player.getGameMode(); + + if (gameMode != GameMode.CREATIVE && gameMode != GameMode.SPECTATOR) { + player.setAllowFlight(false); + player.setFlying(false); + } + } + + if (this.config.combat.unequipElytraOnCombat) { + ItemStack chest = player.getInventory().getChestplate(); + + if (chest != null && chest.getType() == Material.ELYTRA) { + removeChestplateIfElytra(player); + + this.noticeService.create() + .player(uniqueId) + .notice(this.config.messagesSettings.elytraDisabledDuringCombat) + .send(); + } + } + } + @EventHandler void onUnTag(FightUntagEvent event) { if (!this.config.combat.disableFlying) { @@ -213,4 +251,49 @@ void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { } } + + @EventHandler + void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) { + return; + } + + if (!this.config.combat.unequipElytraOnCombat) { + return; + } + + UUID uniqueId = player.getUniqueId(); + + if (!this.fightManager.isInCombat(uniqueId)) { + return; + } + + if (event.getCurrentItem() == null) { + return; + } + + if (event.getCurrentItem().getType() == Material.ELYTRA) { + event.setCancelled(true); + + this.noticeService.create() + .player(uniqueId) + .notice(this.config.messagesSettings.elytraDisabledDuringCombat) + .send(); + } + } + + private void removeChestplateIfElytra(Player player) { + ItemStack chestplate = player.getInventory().getChestplate(); + + if (chestplate != null && chestplate.getType() == Material.ELYTRA) { + player.getInventory().setChestplate(null); + + HashMap leftover = player.getInventory().addItem(chestplate); + if (!leftover.isEmpty()) { + leftover.values().forEach(item -> + player.getWorld().dropItemNaturally(player.getLocation(), item) + ); + } + } + } } From 81612615cb3578a0665ac7edfeeb8cacf1e07469 Mon Sep 17 00:00:00 2001 From: Rollczi Date: Tue, 28 Apr 2026 23:27:46 +0200 Subject: [PATCH 4/5] CR --- .../com/eternalcode/combat/CombatPlugin.java | 15 +- .../combat/fight/blocker/CommandsBlocker.java | 54 ++++ .../combat/fight/blocker/ElytraBlocker.java | 162 ++++++++++ .../combat/fight/blocker/FlyingBlocker.java | 86 +++++ .../InventoryContainersBlocker.java} | 6 +- .../fight/blocker/PlaceBlockBlocker.java | 81 +++++ .../FightActionBlockerController.java | 299 ------------------ 7 files changed, 397 insertions(+), 306 deletions(-) create mode 100644 eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/CommandsBlocker.java create mode 100644 eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/ElytraBlocker.java create mode 100644 eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/FlyingBlocker.java rename eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/{controller/FightInventoryController.java => blocker/InventoryContainersBlocker.java} (86%) create mode 100644 eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/PlaceBlockBlocker.java delete mode 100644 eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightActionBlockerController.java diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java index 48874171..b88762e4 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java @@ -8,10 +8,13 @@ import com.eternalcode.combat.bridge.BridgeService; import com.eternalcode.combat.crystalpvp.RespawnAnchorListener; import com.eternalcode.combat.crystalpvp.EndCrystalListener; +import com.eternalcode.combat.fight.blocker.CommandsBlocker; +import com.eternalcode.combat.fight.blocker.ElytraBlocker; +import com.eternalcode.combat.fight.blocker.FlyingBlocker; import com.eternalcode.combat.fight.controller.FightBypassAdminController; import com.eternalcode.combat.fight.controller.FightBypassCreativeController; import com.eternalcode.combat.fight.controller.FightBypassPermissionController; -import com.eternalcode.combat.fight.controller.FightInventoryController; +import com.eternalcode.combat.fight.blocker.InventoryContainersBlocker; import com.eternalcode.combat.fight.death.DeathFlareController; import com.eternalcode.combat.fight.death.DeathLightningController; import com.eternalcode.combat.fight.drop.DropKeepInventoryService; @@ -32,7 +35,7 @@ import com.eternalcode.combat.fight.drop.impl.PercentDropModifier; import com.eternalcode.combat.fight.drop.impl.PlayersHealthDropModifier; import com.eternalcode.combat.fight.FightTagCommand; -import com.eternalcode.combat.fight.controller.FightActionBlockerController; +import com.eternalcode.combat.fight.blocker.PlaceBlockBlocker; import com.eternalcode.combat.fight.controller.FightMessageController; import com.eternalcode.combat.fight.controller.FightTagController; import com.eternalcode.combat.fight.controller.FightUnTagController; @@ -181,7 +184,7 @@ public void onEnable() { new FightBypassAdminController(server, pluginConfig), new FightBypassPermissionController(server, pluginConfig), new FightBypassCreativeController(server, pluginConfig), - new FightActionBlockerController(this.fightManager, noticeService, pluginConfig, server), + new PlaceBlockBlocker(this.fightManager, noticeService, pluginConfig), new FightPearlController(pluginConfig.pearl, noticeService, this.fightManager, this.fightPearlService), new DeathFlareController(pluginConfig, server, scheduler, this), new DeathLightningController(pluginConfig, server), @@ -196,7 +199,11 @@ public void onEnable() { new EndCrystalListener(this, this.fightManager, pluginConfig), new RespawnAnchorListener(this, this.fightManager, pluginConfig), new FireworkController(this.fightManager, pluginConfig, noticeService), - new FightInventoryController(this.fightManager, pluginConfig, noticeService) + new InventoryContainersBlocker(this.fightManager, pluginConfig, noticeService), + new CommandsBlocker(this.fightManager, noticeService, pluginConfig), + new ElytraBlocker(this.fightManager, noticeService, pluginConfig, server), + new FlyingBlocker(this.fightManager, pluginConfig, server), + new PlaceBlockBlocker(this.fightManager, noticeService, pluginConfig) ); eventManager.subscribe( diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/CommandsBlocker.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/CommandsBlocker.java new file mode 100644 index 00000000..8f6867ab --- /dev/null +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/CommandsBlocker.java @@ -0,0 +1,54 @@ +package com.eternalcode.combat.fight.blocker; + +import com.eternalcode.combat.WhitelistBlacklistMode; +import com.eternalcode.combat.config.implementation.PluginConfig; +import com.eternalcode.combat.fight.FightManager; +import com.eternalcode.combat.notification.NoticeService; +import java.util.UUID; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.util.StringUtil; + +public class CommandsBlocker implements Listener { + + private final FightManager fightManager; + private final NoticeService noticeService; + private final PluginConfig config; + + public CommandsBlocker(FightManager fightManager, NoticeService noticeService, PluginConfig config) { + this.fightManager = fightManager; + this.noticeService = noticeService; + this.config = config; + } + + @EventHandler + void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { + Player player = event.getPlayer(); + UUID playerUniqueId = player.getUniqueId(); + + if (!this.fightManager.isInCombat(playerUniqueId)) { + return; + } + + String command = event.getMessage().substring(1); + + boolean isAnyMatch = this.config.commands.restrictedCommands.stream() + .anyMatch(restrictedCommand -> StringUtil.startsWithIgnoreCase(command, restrictedCommand)); + + WhitelistBlacklistMode mode = this.config.commands.commandRestrictionMode; + + boolean shouldCancel = mode.shouldBlock(isAnyMatch); + + if (shouldCancel) { + event.setCancelled(true); + this.noticeService.create() + .player(playerUniqueId) + .notice(this.config.messagesSettings.commandDisabledDuringCombat) + .send(); + + } + } + +} diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/ElytraBlocker.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/ElytraBlocker.java new file mode 100644 index 00000000..05e833bb --- /dev/null +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/ElytraBlocker.java @@ -0,0 +1,162 @@ +package com.eternalcode.combat.fight.blocker; + +import com.eternalcode.combat.config.implementation.PluginConfig; +import com.eternalcode.combat.fight.FightManager; +import com.eternalcode.combat.fight.event.FightTagEvent; +import com.eternalcode.combat.notification.NoticeService; +import java.util.HashMap; +import java.util.UUID; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.EntityToggleGlideEvent; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.bukkit.inventory.ItemStack; + +public class ElytraBlocker implements Listener { + + private final FightManager fightManager; + private final NoticeService noticeService; + private final PluginConfig config; + private final Server server; + + public ElytraBlocker(FightManager fightManager, NoticeService noticeService, PluginConfig config, Server server) { + this.fightManager = fightManager; + this.noticeService = noticeService; + this.config = config; + this.server = server; + } + + @EventHandler + void onToggleGlide(EntityToggleGlideEvent event) { + if (!this.config.combat.disableElytraUsage) { + return; + } + + if (!(event.getEntity() instanceof Player player)) { + return; + } + UUID uniqueId = player.getUniqueId(); + + if (!this.fightManager.isInCombat(uniqueId)) { + return; + } + + if (event.isGliding()) { + event.setCancelled(true); + player.setGliding(false); + } + } + + + @EventHandler + void onMoveWhileGliding(PlayerMoveEvent event) { + if (!this.config.combat.disableElytraUsage) { + return; + } + + Player player = event.getPlayer(); + UUID uniqueId = player.getUniqueId(); + + if (!this.fightManager.isInCombat(uniqueId)) { + return; + } + + if (player.isGliding()) { + player.setGliding(false); + + player.setFallDistance(0f); + player.setVelocity(player.getVelocity().setY(-1)); + } + } + + @EventHandler + void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) { + return; + } + + if (!this.config.combat.unequipElytraOnCombat) { + return; + } + + UUID uniqueId = player.getUniqueId(); + + if (!this.fightManager.isInCombat(uniqueId)) { + return; + } + + if (event.getCurrentItem() == null) { + return; + } + + if (event.getCurrentItem().getType() == Material.ELYTRA) { + event.setCancelled(true); + + this.noticeService.create() + .player(uniqueId) + .notice(this.config.messagesSettings.elytraDisabledDuringCombat) + .send(); + } + } + + + @EventHandler + void onTag(FightTagEvent event) { + UUID uniqueId = event.getPlayer(); + Player player = this.server.getPlayer(uniqueId); + + if (player == null) { + return; + } + + if (this.config.combat.unequipElytraOnCombat) { + ItemStack chest = player.getInventory().getChestplate(); + + if (chest != null && chest.getType() == Material.ELYTRA) { + removeChestplateIfElytra(player); + + this.noticeService.create() + .player(uniqueId) + .notice(this.config.messagesSettings.elytraDisabledDuringCombat) + .send(); + } + } + } + + private void removeChestplateIfElytra(Player player) { + ItemStack chestplate = player.getInventory().getChestplate(); + + if (chestplate != null && chestplate.getType() == Material.ELYTRA) { + player.getInventory().setChestplate(null); + + HashMap leftover = player.getInventory().addItem(chestplate); + if (!leftover.isEmpty()) { + leftover.values().forEach(item -> + player.getWorld().dropItemNaturally(player.getLocation(), item) + ); + } + } + } + + @EventHandler + void onDamage(EntityDamageEvent event) { + if (!this.config.combat.disableElytraOnDamage) { + return; + } + + if (!(event.getEntity() instanceof Player player)) { + return; + } + UUID uniqueId = player.getUniqueId(); + + if (this.fightManager.isInCombat(uniqueId) && player.isGliding()) { + player.setGliding(false); + } + } + +} diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/FlyingBlocker.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/FlyingBlocker.java new file mode 100644 index 00000000..5b3ecb01 --- /dev/null +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/FlyingBlocker.java @@ -0,0 +1,86 @@ +package com.eternalcode.combat.fight.blocker; + +import com.eternalcode.combat.config.implementation.PluginConfig; +import com.eternalcode.combat.fight.FightManager; +import com.eternalcode.combat.fight.event.FightTagEvent; +import com.eternalcode.combat.fight.event.FightUntagEvent; +import java.util.UUID; +import org.bukkit.GameMode; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerToggleFlightEvent; + +public class FlyingBlocker implements Listener { + + private final FightManager fightManager; + private final PluginConfig config; + private final Server server; + + public FlyingBlocker(FightManager fightManager, PluginConfig config, Server server) { + this.fightManager = fightManager; + this.config = config; + this.server = server; + } + + + @EventHandler + void onFly(PlayerToggleFlightEvent event) { + if (!this.config.combat.disableFlying) { + return; + } + + Player player = event.getPlayer(); + UUID uniqueId = player.getUniqueId(); + + if (!this.fightManager.isInCombat(uniqueId)) { + return; + } + + if (event.isFlying()) { + player.setAllowFlight(false); + + event.setCancelled(true); + } + } + + + @EventHandler + void onTag(FightTagEvent event) { + UUID uniqueId = event.getPlayer(); + Player player = this.server.getPlayer(uniqueId); + + if (player == null) { + return; + } + + if (this.config.combat.disableFlying) { + GameMode gameMode = player.getGameMode(); + + if (gameMode != GameMode.CREATIVE && gameMode != GameMode.SPECTATOR) { + player.setAllowFlight(false); + player.setFlying(false); + } + } + } + + @EventHandler + void onUnTag(FightUntagEvent event) { + if (!this.config.combat.disableFlying) { + return; + } + + UUID uniqueId = event.getPlayer(); + Player player = this.server.getPlayer(uniqueId); + + if (player == null) { + return; + } + GameMode playerGameMode = player.getGameMode(); + if (playerGameMode == GameMode.CREATIVE || playerGameMode == GameMode.SPECTATOR) { + player.setAllowFlight(true); + } + } + +} diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightInventoryController.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/InventoryContainersBlocker.java similarity index 86% rename from eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightInventoryController.java rename to eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/InventoryContainersBlocker.java index 3858cfb9..4479a693 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightInventoryController.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/InventoryContainersBlocker.java @@ -1,4 +1,4 @@ -package com.eternalcode.combat.fight.controller; +package com.eternalcode.combat.fight.blocker; import com.eternalcode.combat.config.implementation.PluginConfig; import com.eternalcode.combat.fight.FightManager; @@ -10,13 +10,13 @@ import org.bukkit.event.inventory.InventoryOpenEvent; import org.bukkit.event.inventory.InventoryType; -public class FightInventoryController implements Listener { +public class InventoryContainersBlocker implements Listener { private final FightManager fightManager; private final PluginConfig config; private final NoticeService noticeService; - public FightInventoryController(FightManager fightManager, PluginConfig config, NoticeService noticeService) { + public InventoryContainersBlocker(FightManager fightManager, PluginConfig config, NoticeService noticeService) { this.fightManager = fightManager; this.config = config; this.noticeService = noticeService; diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/PlaceBlockBlocker.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/PlaceBlockBlocker.java new file mode 100644 index 00000000..4e669f2f --- /dev/null +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/PlaceBlockBlocker.java @@ -0,0 +1,81 @@ +package com.eternalcode.combat.fight.blocker; + +import com.eternalcode.combat.config.implementation.PluginConfig; +import com.eternalcode.combat.config.implementation.BlockPlacementSettings; +import com.eternalcode.combat.fight.FightManager; +import com.eternalcode.combat.notification.NoticeService; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockPlaceEvent; + +import java.util.List; +import java.util.UUID; + +public class PlaceBlockBlocker implements Listener { + + private final FightManager fightManager; + private final NoticeService noticeService; + private final PluginConfig config; + + public PlaceBlockBlocker(FightManager fightManager, NoticeService noticeService, PluginConfig config) { + this.fightManager = fightManager; + this.noticeService = noticeService; + this.config = config; + } + + @EventHandler + void onPlace(BlockPlaceEvent event) { + if (!this.config.blockPlacement.disableBlockPlacing) { + return; + } + + Player player = event.getPlayer(); + UUID uniqueId = player.getUniqueId(); + + if (!this.fightManager.isInCombat(uniqueId)) { + return; + } + + Block block = event.getBlock(); + int level = block.getY(); + + List specificBlocksToPreventPlacing = this.config.blockPlacement.restrictedBlockTypes; + + boolean isPlacementBlocked = this.isPlacementBlocked(level); + + if (isPlacementBlocked && specificBlocksToPreventPlacing.isEmpty()) { + event.setCancelled(true); + this.noticeService.create() + .player(uniqueId) + .notice(this.config.messagesSettings.blockPlacingBlockedDuringCombat) + .placeholder("{Y}", String.valueOf(this.config.blockPlacement.blockPlacementYCoordinate)) + .placeholder("{MODE}", this.config.blockPlacement.blockPlacementModeDisplayName) + .send(); + + } + + Material blockMaterial = block.getType(); + boolean isBlockInDisabledList = specificBlocksToPreventPlacing.contains(blockMaterial); + if (isPlacementBlocked && isBlockInDisabledList) { + event.setCancelled(true); + + this.noticeService.create() + .player(uniqueId) + .notice(this.config.messagesSettings.blockPlacingBlockedDuringCombat) + .placeholder("{Y}", String.valueOf(this.config.blockPlacement.blockPlacementYCoordinate)) + .placeholder("{MODE}", this.config.blockPlacement.blockPlacementModeDisplayName) + .send(); + + } + } + + private boolean isPlacementBlocked(int level) { + return this.config.blockPlacement.blockPlacementMode == BlockPlacementSettings.BlockPlacingMode.ABOVE + ? level > this.config.blockPlacement.blockPlacementYCoordinate + : level < this.config.blockPlacement.blockPlacementYCoordinate; + } + +} diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightActionBlockerController.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightActionBlockerController.java deleted file mode 100644 index 371a64d4..00000000 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/controller/FightActionBlockerController.java +++ /dev/null @@ -1,299 +0,0 @@ -package com.eternalcode.combat.fight.controller; - -import com.eternalcode.combat.config.implementation.PluginConfig; -import com.eternalcode.combat.WhitelistBlacklistMode; -import com.eternalcode.combat.config.implementation.BlockPlacementSettings; -import com.eternalcode.combat.fight.FightManager; -import com.eternalcode.combat.fight.event.FightUntagEvent; -import com.eternalcode.combat.notification.NoticeService; -import org.bukkit.GameMode; -import org.bukkit.Material; -import org.bukkit.Server; -import org.bukkit.block.Block; -import org.bukkit.entity.Player; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.event.block.BlockPlaceEvent; -import org.bukkit.event.entity.EntityDamageEvent; -import org.bukkit.event.entity.EntityToggleGlideEvent; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryOpenEvent; -import org.bukkit.event.inventory.InventoryType; -import org.bukkit.event.player.PlayerCommandPreprocessEvent; -import org.bukkit.event.player.PlayerMoveEvent; -import org.bukkit.event.player.PlayerToggleFlightEvent; - -import java.util.HashMap; -import java.util.List; -import java.util.UUID; - -import org.bukkit.inventory.ItemStack; -import org.bukkit.util.StringUtil; - -public class FightActionBlockerController implements Listener { - - private final FightManager fightManager; - private final NoticeService noticeService; - private final PluginConfig config; - private final Server server; - - public FightActionBlockerController(FightManager fightManager, NoticeService noticeService, PluginConfig config, Server server) { - this.fightManager = fightManager; - this.noticeService = noticeService; - this.config = config; - this.server = server; - } - - @EventHandler - void onPlace(BlockPlaceEvent event) { - if (!this.config.blockPlacement.disableBlockPlacing) { - return; - } - - Player player = event.getPlayer(); - UUID uniqueId = player.getUniqueId(); - - if (!this.fightManager.isInCombat(uniqueId)) { - return; - } - - Block block = event.getBlock(); - int level = block.getY(); - - List specificBlocksToPreventPlacing = this.config.blockPlacement.restrictedBlockTypes; - - boolean isPlacementBlocked = this.isPlacementBlocked(level); - - if (isPlacementBlocked && specificBlocksToPreventPlacing.isEmpty()) { - event.setCancelled(true); - this.noticeService.create() - .player(uniqueId) - .notice(this.config.messagesSettings.blockPlacingBlockedDuringCombat) - .placeholder("{Y}", String.valueOf(this.config.blockPlacement.blockPlacementYCoordinate)) - .placeholder("{MODE}", this.config.blockPlacement.blockPlacementModeDisplayName) - .send(); - - } - - Material blockMaterial = block.getType(); - boolean isBlockInDisabledList = specificBlocksToPreventPlacing.contains(blockMaterial); - if (isPlacementBlocked && isBlockInDisabledList) { - event.setCancelled(true); - - this.noticeService.create() - .player(uniqueId) - .notice(this.config.messagesSettings.blockPlacingBlockedDuringCombat) - .placeholder("{Y}", String.valueOf(this.config.blockPlacement.blockPlacementYCoordinate)) - .placeholder("{MODE}", this.config.blockPlacement.blockPlacementModeDisplayName) - .send(); - - } - } - - private boolean isPlacementBlocked(int level) { - return this.config.blockPlacement.blockPlacementMode == BlockPlacementSettings.BlockPlacingMode.ABOVE - ? level > this.config.blockPlacement.blockPlacementYCoordinate - : level < this.config.blockPlacement.blockPlacementYCoordinate; - } - - @EventHandler - void onToggleGlide(EntityToggleGlideEvent event) { - if (!this.config.combat.disableElytraUsage) { - return; - } - - if (!(event.getEntity() instanceof Player player)) { - return; - } - UUID uniqueId = player.getUniqueId(); - - if (!this.fightManager.isInCombat(uniqueId)) { - return; - } - - if (event.isGliding()) { - event.setCancelled(true); - player.setGliding(false); - } - } - - @EventHandler - void onMoveWhileGliding(PlayerMoveEvent event) { - if (!this.config.combat.disableElytraUsage) { - return; - } - - Player player = event.getPlayer(); - UUID uniqueId = player.getUniqueId(); - - if (!this.fightManager.isInCombat(uniqueId)) { - return; - } - - if (player.isGliding()) { - player.setGliding(false); - - player.setFallDistance(0f); - player.setVelocity(player.getVelocity().setY(-1)); - } - } - - @EventHandler - void onFly(PlayerToggleFlightEvent event) { - if (!this.config.combat.disableFlying) { - return; - } - - Player player = event.getPlayer(); - UUID uniqueId = player.getUniqueId(); - - if (!this.fightManager.isInCombat(uniqueId)) { - return; - } - - if (event.isFlying()) { - player.setAllowFlight(false); - - event.setCancelled(true); - } - } - - @EventHandler - void onTag(com.eternalcode.combat.fight.event.FightTagEvent event) { - UUID uniqueId = event.getPlayer(); - Player player = this.server.getPlayer(uniqueId); - - if (player == null) { - return; - } - - if (this.config.combat.disableFlying) { - GameMode gameMode = player.getGameMode(); - - if (gameMode != GameMode.CREATIVE && gameMode != GameMode.SPECTATOR) { - player.setAllowFlight(false); - player.setFlying(false); - } - } - - if (this.config.combat.unequipElytraOnCombat) { - ItemStack chest = player.getInventory().getChestplate(); - - if (chest != null && chest.getType() == Material.ELYTRA) { - removeChestplateIfElytra(player); - - this.noticeService.create() - .player(uniqueId) - .notice(this.config.messagesSettings.elytraDisabledDuringCombat) - .send(); - } - } - } - - @EventHandler - void onUnTag(FightUntagEvent event) { - if (!this.config.combat.disableFlying) { - return; - } - - UUID uniqueId = event.getPlayer(); - Player player = this.server.getPlayer(uniqueId); - - if (player == null) { - return; - } - GameMode playerGameMode = player.getGameMode(); - if (playerGameMode == GameMode.CREATIVE || playerGameMode == GameMode.SPECTATOR) { - player.setAllowFlight(true); - } - } - - @EventHandler - void onDamage(EntityDamageEvent event) { - if (!this.config.combat.disableElytraOnDamage) { - return; - } - - if (!(event.getEntity() instanceof Player player)) { - return; - } - UUID uniqueId = player.getUniqueId(); - - if (this.fightManager.isInCombat(uniqueId) && player.isGliding()) { - player.setGliding(false); - } - } - - @EventHandler - void onPlayerCommandPreprocess(PlayerCommandPreprocessEvent event) { - Player player = event.getPlayer(); - UUID playerUniqueId = player.getUniqueId(); - - if (!this.fightManager.isInCombat(playerUniqueId)) { - return; - } - - String command = event.getMessage().substring(1); - - boolean isAnyMatch = this.config.commands.restrictedCommands.stream() - .anyMatch(restrictedCommand -> StringUtil.startsWithIgnoreCase(command, restrictedCommand)); - - WhitelistBlacklistMode mode = this.config.commands.commandRestrictionMode; - - boolean shouldCancel = mode.shouldBlock(isAnyMatch); - - if (shouldCancel) { - event.setCancelled(true); - this.noticeService.create() - .player(playerUniqueId) - .notice(this.config.messagesSettings.commandDisabledDuringCombat) - .send(); - - } - } - - @EventHandler - void onInventoryClick(InventoryClickEvent event) { - if (!(event.getWhoClicked() instanceof Player player)) { - return; - } - - if (!this.config.combat.unequipElytraOnCombat) { - return; - } - - UUID uniqueId = player.getUniqueId(); - - if (!this.fightManager.isInCombat(uniqueId)) { - return; - } - - if (event.getCurrentItem() == null) { - return; - } - - if (event.getCurrentItem().getType() == Material.ELYTRA) { - event.setCancelled(true); - - this.noticeService.create() - .player(uniqueId) - .notice(this.config.messagesSettings.elytraDisabledDuringCombat) - .send(); - } - } - - private void removeChestplateIfElytra(Player player) { - ItemStack chestplate = player.getInventory().getChestplate(); - - if (chestplate != null && chestplate.getType() == Material.ELYTRA) { - player.getInventory().setChestplate(null); - - HashMap leftover = player.getInventory().addItem(chestplate); - if (!leftover.isEmpty()) { - leftover.values().forEach(item -> - player.getWorld().dropItemNaturally(player.getLocation(), item) - ); - } - } - } -} From 8639b0a47f804c880eac98ec24e26e88ab7d90ae Mon Sep 17 00:00:00 2001 From: Rollczi Date: Thu, 4 Jun 2026 15:26:43 +0200 Subject: [PATCH 5/5] CR 2 --- .../com/eternalcode/combat/CombatPlugin.java | 4 +- .../combat/fight/blocker/ElytraBlocker.java | 85 +----- .../fight/blocker/ElytraEquipBlocker.java | 132 ++++++++ .../KnockbackOutsideRegionGenerator.java | 49 +-- .../fight/knockback/KnockbackService.java | 282 ++++++------------ .../fight/knockback/KnockbackSettings.java | 219 +++++++------- .../combat/fight/knockback/Point2D.java | 18 +- 7 files changed, 359 insertions(+), 430 deletions(-) create mode 100644 eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/ElytraEquipBlocker.java diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java index b88762e4..9e627da3 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/CombatPlugin.java @@ -10,6 +10,7 @@ import com.eternalcode.combat.crystalpvp.EndCrystalListener; import com.eternalcode.combat.fight.blocker.CommandsBlocker; import com.eternalcode.combat.fight.blocker.ElytraBlocker; +import com.eternalcode.combat.fight.blocker.ElytraEquipBlocker; import com.eternalcode.combat.fight.blocker.FlyingBlocker; import com.eternalcode.combat.fight.controller.FightBypassAdminController; import com.eternalcode.combat.fight.controller.FightBypassCreativeController; @@ -201,7 +202,8 @@ public void onEnable() { new FireworkController(this.fightManager, pluginConfig, noticeService), new InventoryContainersBlocker(this.fightManager, pluginConfig, noticeService), new CommandsBlocker(this.fightManager, noticeService, pluginConfig), - new ElytraBlocker(this.fightManager, noticeService, pluginConfig, server), + new ElytraBlocker(this.fightManager, pluginConfig), + new ElytraEquipBlocker(this.fightManager, noticeService, pluginConfig, server), new FlyingBlocker(this.fightManager, pluginConfig, server), new PlaceBlockBlocker(this.fightManager, noticeService, pluginConfig) ); diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/ElytraBlocker.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/ElytraBlocker.java index 05e833bb..d4770160 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/ElytraBlocker.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/ElytraBlocker.java @@ -2,33 +2,28 @@ import com.eternalcode.combat.config.implementation.PluginConfig; import com.eternalcode.combat.fight.FightManager; -import com.eternalcode.combat.fight.event.FightTagEvent; -import com.eternalcode.combat.notification.NoticeService; -import java.util.HashMap; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import java.time.Duration; import java.util.UUID; -import org.bukkit.Material; -import org.bukkit.Server; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityToggleGlideEvent; -import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.player.PlayerMoveEvent; -import org.bukkit.inventory.ItemStack; public class ElytraBlocker implements Listener { private final FightManager fightManager; - private final NoticeService noticeService; private final PluginConfig config; - private final Server server; + private final Cache fallProtection = Caffeine.newBuilder() + .expireAfterWrite(Duration.ofSeconds(10)) + .build(); - public ElytraBlocker(FightManager fightManager, NoticeService noticeService, PluginConfig config, Server server) { + public ElytraBlocker(FightManager fightManager, PluginConfig config) { this.fightManager = fightManager; - this.noticeService = noticeService; this.config = config; - this.server = server; } @EventHandler @@ -69,77 +64,25 @@ void onMoveWhileGliding(PlayerMoveEvent event) { if (player.isGliding()) { player.setGliding(false); + player.setVelocity(player.getVelocity().setY(-10)); player.setFallDistance(0f); - player.setVelocity(player.getVelocity().setY(-1)); + fallProtection.put(uniqueId, true); + } } @EventHandler - void onInventoryClick(InventoryClickEvent event) { - if (!(event.getWhoClicked() instanceof Player player)) { - return; - } - - if (!this.config.combat.unequipElytraOnCombat) { - return; - } - - UUID uniqueId = player.getUniqueId(); - - if (!this.fightManager.isInCombat(uniqueId)) { + public void onEntityDamage(EntityDamageEvent event) { + if (!(event.getEntity() instanceof Player player)) { return; } - if (event.getCurrentItem() == null) { + if (event.getCause() != EntityDamageEvent.DamageCause.FALL) { return; } - if (event.getCurrentItem().getType() == Material.ELYTRA) { + if (fallProtection.get(player.getUniqueId(), key -> false)) { event.setCancelled(true); - - this.noticeService.create() - .player(uniqueId) - .notice(this.config.messagesSettings.elytraDisabledDuringCombat) - .send(); - } - } - - - @EventHandler - void onTag(FightTagEvent event) { - UUID uniqueId = event.getPlayer(); - Player player = this.server.getPlayer(uniqueId); - - if (player == null) { - return; - } - - if (this.config.combat.unequipElytraOnCombat) { - ItemStack chest = player.getInventory().getChestplate(); - - if (chest != null && chest.getType() == Material.ELYTRA) { - removeChestplateIfElytra(player); - - this.noticeService.create() - .player(uniqueId) - .notice(this.config.messagesSettings.elytraDisabledDuringCombat) - .send(); - } - } - } - - private void removeChestplateIfElytra(Player player) { - ItemStack chestplate = player.getInventory().getChestplate(); - - if (chestplate != null && chestplate.getType() == Material.ELYTRA) { - player.getInventory().setChestplate(null); - - HashMap leftover = player.getInventory().addItem(chestplate); - if (!leftover.isEmpty()) { - leftover.values().forEach(item -> - player.getWorld().dropItemNaturally(player.getLocation(), item) - ); - } } } diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/ElytraEquipBlocker.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/ElytraEquipBlocker.java new file mode 100644 index 00000000..99b894fb --- /dev/null +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/blocker/ElytraEquipBlocker.java @@ -0,0 +1,132 @@ +package com.eternalcode.combat.fight.blocker; + +import com.eternalcode.combat.config.implementation.PluginConfig; +import com.eternalcode.combat.fight.FightManager; +import com.eternalcode.combat.fight.event.FightTagEvent; +import com.eternalcode.combat.notification.NoticeService; +import java.util.HashMap; +import java.util.UUID; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.ItemStack; + +public class ElytraEquipBlocker implements Listener { + + private final FightManager fightManager; + private final NoticeService noticeService; + private final PluginConfig config; + private final Server server; + + public ElytraEquipBlocker(FightManager fightManager, NoticeService noticeService, PluginConfig config, Server server) { + this.fightManager = fightManager; + this.noticeService = noticeService; + this.config = config; + this.server = server; + } + + @EventHandler + void onInventoryClick(InventoryClickEvent event) { + if (!(event.getWhoClicked() instanceof Player player)) { + return; + } + + if (!this.config.combat.unequipElytraOnCombat) { + return; + } + + UUID uniqueId = player.getUniqueId(); + + if (!this.fightManager.isInCombat(uniqueId)) { + return; + } + + if (event.getCurrentItem() == null) { + return; + } + + if (event.getCurrentItem().getType() == Material.ELYTRA) { + event.setCancelled(true); + + this.noticeService.create() + .player(uniqueId) + .notice(this.config.messagesSettings.elytraDisabledDuringCombat) + .send(); + } + } + + + @EventHandler + void onTag(FightTagEvent event) { + UUID uniqueId = event.getPlayer(); + Player player = this.server.getPlayer(uniqueId); + + if (player == null) { + return; + } + + if (this.config.combat.unequipElytraOnCombat) { + ItemStack chest = player.getInventory().getChestplate(); + + if (chest != null && chest.getType() == Material.ELYTRA) { + removeChestplateIfElytra(player); + + this.noticeService.create() + .player(uniqueId) + .notice(this.config.messagesSettings.elytraDisabledDuringCombat) + .send(); + } + } + } + + private void removeChestplateIfElytra(Player player) { + ItemStack chestplate = player.getInventory().getChestplate(); + + if (chestplate != null && chestplate.getType() == Material.ELYTRA) { + player.getInventory().setChestplate(null); + + HashMap leftover = player.getInventory().addItem(chestplate); + leftover.values().forEach(item -> + player.getWorld().dropItemNaturally(player.getLocation(), item) + ); + } + } + + @EventHandler + void onInteract(PlayerInteractEvent event) { + if (!this.config.combat.unequipElytraOnCombat) { + return; + } + + if (event.getItem() == null) { + return; + } + + if (event.getItem().getType() != Material.ELYTRA) { + return; + } + + Player player = event.getPlayer(); + UUID uniqueId = player.getUniqueId(); + + if (!this.fightManager.isInCombat(uniqueId)) { + return; + } + + switch (event.getAction()) { + case RIGHT_CLICK_AIR, RIGHT_CLICK_BLOCK -> { + event.setCancelled(true); + + this.noticeService.create() + .player(uniqueId) + .notice(this.config.messagesSettings.elytraDisabledDuringCombat) + .send(); + } + } + } + +} diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackOutsideRegionGenerator.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackOutsideRegionGenerator.java index 75cc3801..e51fb0ed 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackOutsideRegionGenerator.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackOutsideRegionGenerator.java @@ -3,53 +3,21 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.NavigableMap; +import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import java.util.function.Function; import org.bukkit.Location; class KnockbackOutsideRegionGenerator { - static Location generate(Point2D min, Point2D max, Location playerLocation) { - NavigableMap> points = generatePoints(min, max, Point2D.from(playerLocation)); - NavigableMap distances = new TreeMap<>(); - double totalWeight = 0; - - Double maxDistance = points.lastKey(); - - for (double distance : points.keySet()) { - double weight = createWeight(distance, maxDistance); - distances.put(distance, weight); - totalWeight += weight; - } - - double rand = Math.random() * totalWeight; - double cumulativeWeight = 0; - - for (Map.Entry entry : distances.entrySet()) { - double distance = entry.getKey(); - double weight = entry.getValue(); - - cumulativeWeight += weight; - if (rand <= cumulativeWeight) { - return getRandom(points.get(distance)) - .toLocation(playerLocation); - } - } - - return getRandom(points.firstEntry().getValue()) - .toLocation(playerLocation); - } - - private static Point2D getRandom(List points) { - return points.get((int) (Math.random() * points.size())); - } - - private static double createWeight(double distance, double maxDistance) { - double last = Math.log(maxDistance); - double weight = last - Math.log(distance); - return Math.pow(weight, 10); + static Optional generate(Point2D min, Point2D max, Location playerLocation, Function> safeMapper) { + return generatePoints(min, max, Point2D.from(playerLocation)).entrySet().stream() + .flatMap(entry -> entry.getValue().stream()) + .map(point2D -> point2D.toLocation(playerLocation)) + .flatMap(safeMapper.andThen(validPoint -> validPoint.stream())) + .findFirst(); } private static NavigableMap> generatePoints(Point2D min, Point2D max, Point2D location) { @@ -78,5 +46,4 @@ private static double distance(Point2D p1, Point2D p2) { return Math.sqrt(Math.pow(p2.x() - p1.x(), 2) + Math.pow(p2.z() - p1.z(), 2)); } - } diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackService.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackService.java index b4135a9c..bc921f13 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackService.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackService.java @@ -4,10 +4,10 @@ import com.eternalcode.combat.region.Region; import com.eternalcode.combat.region.RegionProvider; import com.eternalcode.commons.bukkit.scheduler.MinecraftScheduler; -import com.eternalcode.commons.scheduler.Task; import io.papermc.lib.PaperLib; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.World; import org.bukkit.entity.Player; import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; import org.bukkit.util.Vector; @@ -17,12 +17,15 @@ public final class KnockbackService { + private static final int FORCE_TELEPORT_MAX_ATTEMPTS = 10; + private static final double KNOCKBACK_DAMPEN_FACTOR = 0.8; + public static final int DEPTH_OF_SEARCHING = 10; + private final PluginConfig config; private final MinecraftScheduler scheduler; private final RegionProvider regionProvider; private final Map insideRegion = new HashMap<>(); - private final Set fallbackActive = new HashSet<>(); public KnockbackService(PluginConfig config, MinecraftScheduler scheduler, RegionProvider regionProvider) { this.config = config; @@ -35,253 +38,134 @@ public void knockbackLater(Region region, Player player, Duration duration) { } public void knockback(Region region, Player player) { - if (player.isInsideVehicle()) { player.leaveVehicle(); } - Location loc = player.getLocation(); - Vector direction = getDirectionToEdge(region, loc); - - if (config.knockback.dampenVelocity) { - player.setVelocity(player.getVelocity().multiply(config.knockback.dampenFactor)); - } - - boolean onGround = Math.abs(player.getVelocity().getY()) < 0.08; + Location currentLocation = player.getLocation(); + Vector shortestDirection = getShortestDirectionToEdge(region, currentLocation); - double y = onGround - ? config.knockback.vertical - : Math.min(player.getVelocity().getY(), config.knockback.maxAirVertical); + // Reduces player's current velocity BEFORE applying knockback. Helps create a smoother and more consistent knockback. + player.setVelocity(player.getVelocity().multiply(KNOCKBACK_DAMPEN_FACTOR)); - Vector velocity = direction.multiply(config.knockback.multiplier).setY(y); + double y = Math.min(player.getVelocity().getY(), config.knockback.vertical); + Vector velocity = shortestDirection.multiply(config.knockback.multiplier).setY(y); player.setFallDistance(0); player.setVelocity(velocity); + } - - if (config.knockback.useTeleport) { - scheduleSmartFallback(player, region); - } + private Vector getShortestDirectionToEdge(Region region, Location current) { + double directionToBottomEdge = current.getX() - region.getMin().getX(); + double directionToTopEdge = region.getMax().getX() - current.getX(); + double directionToLeftEdge = current.getZ() - region.getMin().getZ(); + double directionToRightEdge = region.getMax().getZ() - current.getZ(); + + double shortestDirection = Math.min( + Math.min(directionToBottomEdge, directionToTopEdge), + Math.min(directionToLeftEdge, directionToRightEdge) + ); + + if (shortestDirection == directionToBottomEdge) + return new Vector(-1, 0, 0); + if (shortestDirection == directionToTopEdge) + return new Vector(1, 0, 0); + if (shortestDirection == directionToLeftEdge) + return new Vector(0, 0, -1); + if (shortestDirection == directionToRightEdge) + return new Vector(0, 0, 1); + throw new IllegalStateException("Failed to determine direction to edge"); } public void forceKnockbackLater(Player player, Region region) { - UUID uuid = player.getUniqueId(); + UUID playerId = player.getUniqueId(); - if (insideRegion.containsKey(uuid)) { + if (insideRegion.containsKey(playerId)) { return; } - insideRegion.put(uuid, region); + insideRegion.put(playerId, region); scheduler.runLater(player.getLocation(), () -> { - - insideRegion.remove(uuid); + insideRegion.remove(playerId); Location loc = player.getLocation(); - double velocity = player.getVelocity().lengthSquared(); - - if (velocity > 0.02) { - return; - } if (!region.contains(loc)) { return; } - double distanceToEdge = getDistanceToEdge(region, loc); - - if (distanceToEdge > 1.5) { - return; - } - - if (fallbackActive.contains(uuid)) { - return; + if (player.isInsideVehicle()) { + player.leaveVehicle(); } - Location generated = generate( - player.getLocation(), - Point2D.from(region.getMin()), - Point2D.from(region.getMax()), - 0 - ); - - Location safe = makeSafe(generated); - if (safe == null || safe.getWorld() == null) { - return; - } - - PaperLib.teleportAsync(player, safe.clone(), TeleportCause.PLUGIN); - - }, config.knockback.forceDelay); + generate(player.getLocation(), Point2D.from(region.getMin()), Point2D.from(region.getMax()), 0) + .ifPresent(location -> PaperLib.teleportAsync(player, location, TeleportCause.PLUGIN)); + }, config.knockback.forceTeleport.delay); } - private void scheduleSmartFallback(Player player, Region region) { - UUID uuid = player.getUniqueId(); - - if (fallbackActive.contains(uuid)) { - return; + private Optional generate(Location playerLocation, Point2D min, Point2D max, int attempts) { + if (attempts >= FORCE_TELEPORT_MAX_ATTEMPTS) { + return Optional.of(playerLocation); } - fallbackActive.add(uuid); - - final Task[] taskRef = new Task[1]; - - taskRef[0] = scheduler.timer(() -> { - - Location check = player.getLocation(); - double velocity = player.getVelocity().lengthSquared(); - - if (!region.contains(check)) { - fallbackActive.remove(uuid); - taskRef[0].cancel(); - return; - } - - if (velocity > 0.02) { - return; - } - - Location generated = generate( - player.getLocation(), - Point2D.from(region.getMin()), - Point2D.from(region.getMax()), - 0 - ); - - Location safe = makeSafe(generated); - if (safe == null || safe.getWorld() == null) { - fallbackActive.remove(uuid); - taskRef[0].cancel(); - return; - } - - PaperLib.teleportAsync(player, safe.clone(), TeleportCause.PLUGIN); - - fallbackActive.remove(uuid); - taskRef[0].cancel(); - - }, - Duration.ofMillis(100), - Duration.ofMillis(100)); - } - - private Location makeSafe(Location loc) { - if (loc == null || loc.getWorld() == null) return loc; - - return config.knockback.safeGroundCheck - ? findSafeGround(loc) - : loc.getWorld().getHighestBlockAt(loc).getLocation().add(0, config.knockback.groundOffset, 0); - } - - private Location findSafeGround(Location loc) { - - if (loc.getWorld() == null) return loc; - - Location check = loc.clone(); - int minY = loc.getWorld().getMinHeight(); - - for (int y = check.getBlockY(); y > minY; y--) { - check.setY(y); - - Material type = check.getBlock().getType(); - Material above = check.clone().add(0, 1, 0).getBlock().getType(); - Material above2 = check.clone().add(0, 2, 0).getBlock().getType(); - - if (type.isSolid() - && !config.knockback.unsafeGroundBlocks.contains(type) - && above.isAir() - && above2.isAir()) { - - return check.clone().add(0, config.knockback.groundOffset, 0); - } + Optional location = KnockbackOutsideRegionGenerator.generate(min, max, playerLocation, candidate -> makeSafe(candidate)); + if (location.isEmpty()) { + int expand = (attempts + 1) * (attempts + 1); + return generate(playerLocation, min.expandMin(expand, expand), max.expandMax(expand, expand), attempts + 1); } - return getSafeHighest(loc); - } - - private Location getSafeHighest(Location loc) { - if (loc == null || loc.getWorld() == null) return loc; - - if (!config.knockback.safeHighestFallback) { - return loc.getWorld() - .getHighestBlockAt(loc) - .getLocation() - .add(0, config.knockback.groundOffset, 0); + Optional otherRegion = regionProvider.getRegion(location.get()); + if (otherRegion.isPresent()) { + Region region = otherRegion.get(); + return generate(playerLocation, min.min(region.getMin()), max.max(region.getMax()), attempts + 1); } - Location highest = loc.getWorld().getHighestBlockAt(loc).getLocation(); - int minY = loc.getWorld().getMinHeight(); - - int startY = highest.getBlockY(); - int maxScan = config.knockback.safeHighestMaxScan; + return location.map(loc -> loc.add(0.5, 0, 0.5)); + } - int endY = (maxScan < 0) - ? minY - : Math.max(minY, startY - maxScan); + private Optional makeSafe(Location random) { + Location maybeSafe = random.clone(); + random.getWorld(); + int minY = maybeSafe.getBlockY() - DEPTH_OF_SEARCHING; - for (int y = startY; y > endY; y--) { - highest.setY(y); + CachedPillarOfBlocks pillar = new CachedPillarOfBlocks(maybeSafe.getBlockX(), maybeSafe.getBlockZ(), maybeSafe.getWorld()); - Material type = highest.getBlock().getType(); - Material above = highest.clone().add(0, 1, 0).getBlock().getType(); - Material above2 = highest.clone().add(0, 2, 0).getBlock().getType(); + for (int y = maybeSafe.getBlockY(); y > minY; y--) { + maybeSafe.setY(y); - if (type.isSolid() - && !config.knockback.unsafeGroundBlocks.contains(type) - && above.isAir() - && above2.isAir()) { + Material ground = pillar.get(maybeSafe.getBlockY() - 1); + Material legs = pillar.get(maybeSafe.getBlockY()); + Material head = pillar.get(maybeSafe.getBlockY() + 1); - return highest.clone().add(0, config.knockback.groundOffset, 0); + if (ground.isSolid() + && !config.knockback.forceTeleport.unsafeGroundBlocks.contains(ground) + && config.knockback.forceTeleport.airBlocks.contains(legs) + && config.knockback.forceTeleport.airBlocks.contains(head) + ) { + return Optional.of(maybeSafe); } } - return config.knockback.cancelIfNoSafeGround ? null : loc; + return Optional.empty(); } - private Location generate(Location playerLocation, Point2D minX, Point2D maxX, int attempts) { - if (attempts >= config.knockback.maxAttempts) { - return playerLocation; - } - - Location location = KnockbackOutsideRegionGenerator.generate(minX, maxX, playerLocation); - - Optional otherRegion = regionProvider.getRegion(location); - if (otherRegion.isPresent()) { + private static class CachedPillarOfBlocks { - Region region = otherRegion.get(); + private final int x; + private final int z; + private final World world; + private final Map blocksTypes = new HashMap<>(); - return generate( - playerLocation, - minX.min(region.getMin()), - maxX.max(region.getMax()), - attempts + 1 - ); + private CachedPillarOfBlocks(int x, int z, World world) { + this.x = x; + this.z = z; + this.world = world; } - return location; - } - - private Vector getDirectionToEdge(Region region, Location loc) { - double dxMin = loc.getX() - region.getMin().getX(); - double dxMax = region.getMax().getX() - loc.getX(); - double dzMin = loc.getZ() - region.getMin().getZ(); - double dzMax = region.getMax().getZ() - loc.getZ(); - - double min = Math.min(Math.min(dxMin, dxMax), Math.min(dzMin, dzMax)); - - if (Math.abs(min - dxMin) < 1e-6) return new Vector(-1, 0, 0); - if (Math.abs(min - dxMax) < 1e-6) return new Vector(1, 0, 0); - if (Math.abs(min - dzMin) < 1e-6) return new Vector(0, 0, -1); - - return new Vector(0, 0, 1); + public Material get(int y) { + return this.blocksTypes.computeIfAbsent(y, __ -> world.getType(x, y, z)); + } } - private double getDistanceToEdge(Region region, Location loc) { - double dxMin = loc.getX() - region.getMin().getX(); - double dxMax = region.getMax().getX() - loc.getX(); - double dzMin = loc.getZ() - region.getMin().getZ(); - double dzMax = region.getMax().getZ() - loc.getZ(); - - return Math.min(Math.min(dxMin, dxMax), Math.min(dzMin, dzMax)); - } } diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackSettings.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackSettings.java index 48d08248..0302fbde 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackSettings.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/KnockbackSettings.java @@ -2,6 +2,7 @@ import eu.okaeri.configs.OkaeriConfig; import eu.okaeri.configs.annotation.Comment; +import java.util.EnumSet; import org.bukkit.Material; import java.time.Duration; @@ -28,121 +29,111 @@ public class KnockbackSettings extends OkaeriConfig { public double vertical = 0.2; @Comment({ - "# Maximum vertical velocity when player is already airborne.", - "#", - "# Prevents stacking vertical velocity or launching too high.", - "# Used as: min(currentY, maxAirVertical)" - }) - public double maxAirVertical = 0.2; - - @Comment({ - "# Delay before force teleport is applied after entering a region.", - "#", - "# Used in forceKnockbackLater().", - "# Prevents instant teleport when crossing region borders." - }) - public Duration forceDelay = Duration.ofSeconds(1); - - @Comment({ - "# Enables teleport fallback after knockback.", - "#", - "# If knockback does not push the player outside the region,", - "# a safe location will be generated and player will be teleported." - }) - public boolean useTeleport = false; - - @Comment({ - "# Blocks that are considered unsafe to stand on when searching for ground.", - "# These blocks will be ignored during safe ground detection.", - "#", - "# Used in findSafeGround():", - "# - Skips these blocks even if they are solid", - "# - Prevents teleporting onto dangerous or invalid blocks", - "#", - "# Examples:", - "# - BARRIER (invisible collision)", - "# - LAVA / WATER (damage / movement issues)", - "# - CACTUS / MAGMA (damage blocks)", - }) - public Set unsafeGroundBlocks = Set.of( - Material.BARRIER, - Material.LAVA, - Material.WATER, - Material.CACTUS, - Material.MAGMA_BLOCK, - Material.FIRE, - Material.SOUL_FIRE - ); - - @Comment({ - "# Use custom safe ground detection instead of highest block. (recommended)", - "#", - "# true -> scans downward for safe landing", - "# false -> uses Bukkit getHighestBlockAt()", - "#", - "# Safe mode prevents:", - "# - Landing on roofs", - "# - Landing on barriers", - "# - Unsafe teleport positions" - }) - public boolean safeGroundCheck = true; - - @Comment({ - "# Enables safe fallback scanning instead of using Bukkit highest block directly.", - "#", - "# true -> scans downward from highest block to find safe ground", - "# false -> uses Bukkit getHighestBlockAt (faster but unsafe)" + "# Force teleport settings for players that fail to be knocked back outside the region.", + "# If a player cannot be knocked back outside the region after multiple attempts,", + "# they will be teleported to a safe location outside the region after a short delay.", + "# This ensures players don't get stuck inside the region due to knockback failures." }) - public boolean safeHighestFallback = true; + public ForceTeleport forceTeleport = new ForceTeleport(); + + public static class ForceTeleport extends OkaeriConfig { + + @Comment({ + "# Delay before force teleport is applied after entering a region.", + "# Recommended: 1-3 seconds to allow knockback attempts before teleporting.", + }) + public Duration delay = Duration.ofSeconds(1); + + @Comment({ + "# Blocks that are considered unsafe to stand on when searching for ground.", + "# These blocks will be ignored during safe ground detection.", + "#", + "# Used in findSafeGround():", + "# - Skips these blocks even if they are solid", + "# - Prevents teleporting onto dangerous or invalid blocks", + "#", + "# Examples:", + "# - BARRIER (invisible collision)", + "# - LAVA / WATER (damage / movement issues)", + "# - CACTUS / MAGMA (damage blocks)", + }) + public Set unsafeGroundBlocks = EnumSet.of( + Material.BARRIER, + Material.LAVA, + Material.WATER, + Material.CACTUS, + Material.MAGMA_BLOCK, + Material.FIRE, + Material.SOUL_FIRE, + Material.COBWEB, + Material.SWEET_BERRY_BUSH, + Material.BEDROCK, + Material.TNT, + Material.SEAGRASS, + Material.TALL_SEAGRASS, + Material.BUBBLE_COLUMN, + Material.POWDER_SNOW, + Material.WITHER_ROSE + ); + + @Comment({ + "# Safe blocks that players can be teleported into", + "# These blocks don't cause damage and allow free movement", + "# Players can safely spawn in these blocks or pass through them", + "# Includes air, grass, flowers, and other non-solid blocks" + }) + public Set airBlocks = EnumSet.of( + Material.AIR, + Material.CAVE_AIR, + Material.VOID_AIR, + Material.TALL_SEAGRASS, + Material.SEAGRASS, + Material.GRASS, + Material.TALL_GRASS, + Material.VINE, + Material.STRUCTURE_VOID, + Material.DEAD_BUSH, + Material.DANDELION, + Material.POPPY, + Material.BLUE_ORCHID, + Material.ALLIUM, + Material.AZURE_BLUET, + Material.RED_TULIP, + Material.ORANGE_TULIP, + Material.WHITE_TULIP, + Material.PINK_TULIP, + Material.OXEYE_DAISY, + Material.CORNFLOWER, + Material.LILY_OF_THE_VALLEY, + Material.SUNFLOWER, + Material.LILAC, + Material.ROSE_BUSH, + Material.PEONY, + Material.WITHER_ROSE, + Material.LARGE_FERN, + Material.RAIL, + Material.POWERED_RAIL, + Material.DETECTOR_RAIL, + Material.ACTIVATOR_RAIL, + Material.REDSTONE_WIRE, + Material.COMPARATOR, + Material.REPEATER, + Material.LEVER, + Material.STRING, + Material.SNOW, + Material.CRIMSON_ROOTS, + Material.WARPED_ROOTS, + Material.CRIMSON_FUNGUS, + Material.WARPED_FUNGUS, + Material.TORCH, + Material.WALL_TORCH, + Material.REDSTONE_TORCH, + Material.REDSTONE_WALL_TORCH, + Material.SOUL_TORCH, + Material.SOUL_WALL_TORCH + ); + + } - @Comment({ - "# Maximum vertical scan distance for safe highest fallback.", - "#", - "# Prevents excessive scanning in very tall worlds.", - "# Set to -1 to scan all the way down to min world height." - }) - public int safeHighestMaxScan = -1; - - @Comment({ - "# If true, prevents teleport if no safe ground is found at all.", - "#", - "# true -> cancel teleport", - "# false -> fallback to original location" - }) - public boolean cancelIfNoSafeGround = false; - - @Comment({ - "# Y offset added after finding ground.", - "#", - "# Usually 1.0 = player stands exactly on block.", - "# Can be increased slightly to prevent clipping issues." - }) - public double groundOffset = 1.0; - @Comment({ - "# Reduces player's current velocity BEFORE applying knockback.", - "#", - "# Helps create smoother and more consistent knockback.", - "# Prevents stacking velocity from previous movement." - }) - public boolean dampenVelocity = true; - - @Comment({ - "# Multiplier applied when dampening velocity.", - "#", - "# 1.0 = no change", - "# 0.0 = completely stop player", - "# Recommended: 0.6 - 0.9" - }) - public double dampenFactor = 0.8; - - @Comment({ - "# Maximum recursive attempts when generating a safe location.", - "#", - "# Used in generate() method.", - "# Prevents infinite loops when regions overlap or chain.", - "#", - "# If exceeded -> fallback to player current location." - }) - public int maxAttempts = 5; } diff --git a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/Point2D.java b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/Point2D.java index ec31f567..c415e8a4 100644 --- a/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/Point2D.java +++ b/eternalcombat-plugin/src/main/java/com/eternalcode/combat/fight/knockback/Point2D.java @@ -5,11 +5,13 @@ record Point2D(int x, int z) { - Location toLocation(Location location) { - World world = location.getWorld(); - int y = world.getHighestBlockYAt(x, z) + 1; + Location toLocation(Location playerLocation) { + World world = playerLocation.getWorld(); + int y = world.getEnvironment() == World.Environment.NETHER + ? playerLocation.getBlockY() + 1 + : world.getHighestBlockYAt(x, z) + 1; - return new Location(world, x, y, z, location.getYaw(), location.getPitch()); + return new Location(world, x, y, z, playerLocation.getYaw(), playerLocation.getPitch()); } static Point2D from(Location location) { @@ -24,4 +26,12 @@ public Point2D max(Location max) { return new Point2D(Math.max(this.x, max.getBlockX()), Math.max(this.z, max.getBlockZ())); } + public Point2D expandMax(int x, int z) { + return new Point2D(this.x + x, this.z + z); + } + + public Point2D expandMin(int x, int z) { + return new Point2D(this.x - x, this.z - z); + } + }