From bb2e47a917639c2cd52755f2159255d45cf78d65 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 31 May 2026 23:03:40 +0200 Subject: [PATCH] Add player-spawn config for stuck players and limit teleport height - Add PlayerSpawnConfig for separate spawn location when players are stuck in blocks - Add PlayerSpawnConfigLoader for YAML parsing - Update MineConfig with playerSpawn section and getSpawnForStuckPlayer() helper - Update ConfigLoader to include playerSpawn configuration - Update MineFileManager to preserve playerSpawn when saving - Update MineResetHandler.teleportPlayers() to use player-spawn location - Update defaults.yml with player-spawn documentation - Limit findSafeTeleportLocation() to max 3 blocks upward search - Priority: same level nearby, then up (max 3), then down, then diagonal - Prevents teleporting players too high in the air If player-spawn is not configured, falls back to teleport.location for backward compatibility. --- .../lomines/core/service/MineFileManager.java | 4 ++ .../lomines/data/config/ConfigLoader.java | 5 ++ .../loki/lomines/data/config/MineConfig.java | 33 +++++++-- .../loader/PlayerSpawnConfigLoader.java | 68 +++++++++++++++++++ .../data/config/spawn/PlayerSpawnConfig.java | 47 +++++++++++++ .../lomines/handler/MineResetHandler.java | 10 +-- .../lomines/util/block/BlockUpdateUtil.java | 36 +++++++--- src/main/resources/defaults.yml | 8 +++ 8 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 src/main/java/dev/loki/lomines/data/config/loader/PlayerSpawnConfigLoader.java create mode 100644 src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java diff --git a/src/main/java/dev/loki/lomines/core/service/MineFileManager.java b/src/main/java/dev/loki/lomines/core/service/MineFileManager.java index 0ca04ce..2e4a109 100644 --- a/src/main/java/dev/loki/lomines/core/service/MineFileManager.java +++ b/src/main/java/dev/loki/lomines/core/service/MineFileManager.java @@ -8,6 +8,7 @@ import dev.loki.lomines.data.config.block.FillMode; import dev.loki.lomines.data.config.region.RegionConfig; import dev.loki.lomines.data.config.reset.ResetConfig; import dev.loki.lomines.data.config.reward.RewardConfig; +import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig; import dev.loki.lomines.data.config.teleport.TeleportConfig; import dev.loki.lomines.data.config.ui.UIConfig; import dev.loki.lomines.integration.worldguard.WorldGuardConfig; @@ -15,6 +16,7 @@ import dev.loki.lomines.util.location.Cuboid; import dev.loki.lomines.util.location.LocationParser; import org.bukkit.Location; import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; import java.io.IOException; @@ -76,6 +78,7 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) { .teleport(TeleportConfig.disabled()) .ui(UIConfig.defaults()) .worldGuard(wgConfig) + .playerSpawn(PlayerSpawnConfig.disabled()) .build(); // Save using new loader @@ -160,6 +163,7 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) { .teleport(config.teleport()) .ui(config.ui()) .worldGuard(config.worldGuard()) + .playerSpawn(config.playerSpawn()) .build(); configLoader.save(updated); diff --git a/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java index 21d3685..638c654 100644 --- a/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java +++ b/src/main/java/dev/loki/lomines/data/config/ConfigLoader.java @@ -26,6 +26,7 @@ public final class ConfigLoader { private TeleportConfigLoader teleportLoader; private UIConfigLoader uiLoader; private WorldGuardConfigLoader worldGuardLoader; + private PlayerSpawnConfigLoader playerSpawnLoader; public ConfigLoader(Path dataFolder) { this.dataFolder = dataFolder; @@ -66,6 +67,7 @@ public final class ConfigLoader { yaml.set("rewards", List.of()); worldGuardLoader.setDefaults(yaml); + playerSpawnLoader.setDefaults(yaml); } private void initLoaders() { @@ -76,6 +78,7 @@ public final class ConfigLoader { teleportLoader = new TeleportConfigLoader(); uiLoader = new UIConfigLoader(defaults); worldGuardLoader = new WorldGuardConfigLoader(defaults); + playerSpawnLoader = new PlayerSpawnConfigLoader(); } /** @@ -100,6 +103,7 @@ public final class ConfigLoader { .teleport(teleportLoader.parse(yaml)) .ui(uiLoader.parse(yaml)) .worldGuard(worldGuardLoader.parse(yaml)) + .playerSpawn(playerSpawnLoader.parse(yaml)) .build(); } catch (Exception e) { throw new ConfigLoadException("Failed to load mine '" + mineName + "': " + e.getMessage(), e); @@ -120,6 +124,7 @@ public final class ConfigLoader { teleportLoader.save(yaml, config.teleport()); uiLoader.save(yaml, config.ui()); worldGuardLoader.save(yaml, config.worldGuard()); + playerSpawnLoader.save(yaml, config.playerSpawn()); try { Files.createDirectories(configPath.getParent()); diff --git a/src/main/java/dev/loki/lomines/data/config/MineConfig.java b/src/main/java/dev/loki/lomines/data/config/MineConfig.java index 753a422..2a1c8bb 100644 --- a/src/main/java/dev/loki/lomines/data/config/MineConfig.java +++ b/src/main/java/dev/loki/lomines/data/config/MineConfig.java @@ -4,11 +4,15 @@ import dev.loki.lomines.data.config.block.BlockConfig; import dev.loki.lomines.data.config.region.RegionConfig; import dev.loki.lomines.data.config.reset.ResetConfig; import dev.loki.lomines.data.config.reward.RewardConfig; +import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig; import dev.loki.lomines.data.config.teleport.TeleportConfig; import dev.loki.lomines.data.config.ui.UIConfig; import dev.loki.lomines.integration.worldguard.WorldGuardConfig; +import org.bukkit.Location; + import java.util.Objects; +import java.util.Optional; /** * Clean, type-safe, section-based mine configuration. @@ -22,7 +26,8 @@ public record MineConfig( RewardConfig rewards, TeleportConfig teleport, UIConfig ui, - WorldGuardConfig worldGuard + WorldGuardConfig worldGuard, + PlayerSpawnConfig playerSpawn ) { public MineConfig { @@ -38,7 +43,10 @@ public record MineConfig( Objects.requireNonNull(rewards, "Reward config cannot be null"); Objects.requireNonNull(teleport, "Teleport config cannot be null"); Objects.requireNonNull(ui, "UI config cannot be null"); - // worldGuard can be null (disabled by default) + // worldGuard and playerSpawn can be null (disabled by default) + if (playerSpawn == null) { + playerSpawn = PlayerSpawnConfig.disabled(); + } } /** @@ -74,10 +82,21 @@ public record MineConfig( RewardConfig.empty(), TeleportConfig.disabled(), UIConfig.defaults(), - WorldGuardConfig.disabled() + WorldGuardConfig.disabled(), + PlayerSpawnConfig.disabled() ); } + /** + * Returns the spawn location for stuck players. + * If playerSpawn is not set, falls back to teleport location. + */ + public Optional getSpawnForStuckPlayer() { + return playerSpawn.enabled() + ? playerSpawn.getLocation() + : teleport.getLocation(); + } + // --- Builder --- public static class Builder { @@ -89,6 +108,7 @@ public record MineConfig( private TeleportConfig teleport = TeleportConfig.disabled(); private UIConfig ui = UIConfig.defaults(); private WorldGuardConfig worldGuard = WorldGuardConfig.disabled(); + private PlayerSpawnConfig playerSpawn = PlayerSpawnConfig.disabled(); private Builder(String name) { this.name = name; @@ -129,8 +149,13 @@ public record MineConfig( return this; } + public Builder playerSpawn(PlayerSpawnConfig playerSpawn) { + this.playerSpawn = playerSpawn; + return this; + } + public MineConfig build() { - return new MineConfig(name, region, blocks, reset, rewards, teleport, ui, worldGuard); + return new MineConfig(name, region, blocks, reset, rewards, teleport, ui, worldGuard, playerSpawn); } } } diff --git a/src/main/java/dev/loki/lomines/data/config/loader/PlayerSpawnConfigLoader.java b/src/main/java/dev/loki/lomines/data/config/loader/PlayerSpawnConfigLoader.java new file mode 100644 index 0000000..f0880e2 --- /dev/null +++ b/src/main/java/dev/loki/lomines/data/config/loader/PlayerSpawnConfigLoader.java @@ -0,0 +1,68 @@ +package dev.loki.lomines.data.config.loader; + +import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig; +import dev.loki.lomines.util.location.LocationParser; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.configuration.file.YamlConfiguration; + +/** + * Loader for player spawn configuration section. + */ +public final class PlayerSpawnConfigLoader { + + /** + * Parses player spawn configuration from YAML. + */ + public PlayerSpawnConfig parse(YamlConfiguration yaml) { + boolean enabled = yaml.getBoolean("player-spawn.enabled", false); + if (!enabled) { + return PlayerSpawnConfig.disabled(); + } + + String locStr = yaml.getString("player-spawn.location"); + if (locStr == null || locStr.isBlank()) { + return PlayerSpawnConfig.disabled(); + } + + return PlayerSpawnConfig.at(parseLocation(locStr)); + } + + /** + * Saves player spawn configuration to YAML. + */ + public void save(YamlConfiguration yaml, PlayerSpawnConfig spawn) { + yaml.set("player-spawn.enabled", spawn.enabled()); + spawn.getLocation().ifPresent(loc -> + yaml.set("player-spawn.location", LocationParser.format(loc))); + } + + /** + * Sets default values for player spawn configuration. + */ + public void setDefaults(YamlConfiguration yaml) { + yaml.set("player-spawn.enabled", false); + yaml.set("player-spawn.location", ""); + } + + private Location parseLocation(String str) { + String[] parts = str.split(";"); + if (parts.length < 4) { + throw new IllegalArgumentException("Invalid location format: " + str); + } + + World world = Bukkit.getWorld(parts[0]); + if (world == null) { + throw new IllegalArgumentException("Unknown world: " + parts[0]); + } + + double x = Double.parseDouble(parts[1]); + double y = Double.parseDouble(parts[2]); + double z = Double.parseDouble(parts[3]); + float yaw = parts.length > 4 ? Float.parseFloat(parts[4]) : 0; + float pitch = parts.length > 5 ? Float.parseFloat(parts[5]) : 0; + + return new Location(world, x, y, z, yaw, pitch); + } +} diff --git a/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java b/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java new file mode 100644 index 0000000..8cecd00 --- /dev/null +++ b/src/main/java/dev/loki/lomines/data/config/spawn/PlayerSpawnConfig.java @@ -0,0 +1,47 @@ +package dev.loki.lomines.data.config.spawn; + +import org.bukkit.Location; + +import java.util.Objects; +import java.util.Optional; + +/** + * Configuration for player spawn location when stuck in blocks. + * Used to teleport players out when they are suffocating after mine reset. + */ +public record PlayerSpawnConfig( + boolean enabled, + Location location +) { + + public PlayerSpawnConfig { + if (!enabled) { + location = null; + } else if (location != null) { + // Defensive copy + location = location.clone(); + Objects.requireNonNull(location.getWorld(), "Spawn world cannot be null"); + } + } + + /** + * Returns the spawn location if enabled. + */ + public Optional getLocation() { + return Optional.ofNullable(enabled ? location : null); + } + + /** + * Disabled spawn config (default). + */ + public static PlayerSpawnConfig disabled() { + return new PlayerSpawnConfig(false, null); + } + + /** + * Creates enabled config with location. + */ + public static PlayerSpawnConfig at(Location location) { + return new PlayerSpawnConfig(true, location); + } +} diff --git a/src/main/java/dev/loki/lomines/handler/MineResetHandler.java b/src/main/java/dev/loki/lomines/handler/MineResetHandler.java index e6f2a57..76e81d6 100644 --- a/src/main/java/dev/loki/lomines/handler/MineResetHandler.java +++ b/src/main/java/dev/loki/lomines/handler/MineResetHandler.java @@ -171,11 +171,13 @@ public final class MineResetHandler { } /** - * Teleports players standing inside the mine to a safe location near the configured destination. + * Teleports players standing inside the mine to a safe location. + * Uses player-spawn location if configured, otherwise uses teleport location. * Prevents players from suffocating in blocks by finding a safe teleport spot. */ private void teleportPlayers() { - var destOpt = mine.getConfig().teleport().getLocation(); + // Get spawn location for stuck players (player-spawn config) or fall back to teleport location + var destOpt = mine.getConfig().getSpawnForStuckPlayer(); if (destOpt.isEmpty()) { return; } @@ -184,8 +186,8 @@ public final class MineResetHandler { return; } - // Find a safe teleport location to prevent suffocation - Location safeDest = BlockUpdateUtil.findSafeTeleportLocation(dest); + // Find a safe teleport location (max 3 blocks up to avoid teleporting too high) + Location safeDest = BlockUpdateUtil.findSafeTeleportLocation(dest, 3); for (Player p : dest.getWorld().getPlayers()) { if (mine.contains(p.getLocation())) { diff --git a/src/main/java/dev/loki/lomines/util/block/BlockUpdateUtil.java b/src/main/java/dev/loki/lomines/util/block/BlockUpdateUtil.java index daa26fe..503801c 100644 --- a/src/main/java/dev/loki/lomines/util/block/BlockUpdateUtil.java +++ b/src/main/java/dev/loki/lomines/util/block/BlockUpdateUtil.java @@ -130,11 +130,13 @@ public final class BlockUpdateUtil { /** * Finds a safe teleport location near the given destination. * Checks for suffocation hazards (blocks at head/body level). + *

Will not teleport too high - limited to maxUpOffset blocks above original.

* * @param destination the desired destination + * @param maxUpOffset maximum blocks to search upward (to avoid teleporting too high) * @return a safe location (may be the same as destination if safe) */ - public static Location findSafeTeleportLocation(Location destination) { + public static Location findSafeTeleportLocation(Location destination, int maxUpOffset) { if (destination == null || destination.getWorld() == null) { return destination; } @@ -146,29 +148,40 @@ public final class BlockUpdateUtil { float yaw = destination.getYaw(); float pitch = destination.getPitch(); + // Clamp maxUpOffset to reasonable range (2-10) + maxUpOffset = Math.max(2, Math.min(maxUpOffset, 10)); + // Check if original location is safe if (isSafeLocation(world, x, y, z)) { return destination; } - // Search upward for a safe spot (priority: don't drop player down) - for (int offset = 1; offset <= 5; offset++) { + // Search nearby blocks first (same Y level, closer distance) + int[][] nearby = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + for (int[] offset : nearby) { + if (isSafeLocation(world, x + offset[0], y, z + offset[1])) { + return new Location(world, x + offset[0] + 0.5, y, z + offset[1] + 0.5, yaw, pitch); + } + } + + // Search upward (limited by maxUpOffset to avoid teleporting too high) + for (int offset = 1; offset <= maxUpOffset; offset++) { if (isSafeLocation(world, x, y + offset, z)) { return new Location(world, x + 0.5, y + offset, z + 0.5, yaw, pitch); } } - // Search downward if no safe spot above + // Search downward for (int offset = 1; offset <= 5 && y - offset >= world.getMinHeight(); offset++) { if (isSafeLocation(world, x, y - offset, z)) { return new Location(world, x + 0.5, y - offset, z + 0.5, yaw, pitch); } } - // Search nearby blocks - int[][] nearby = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {-1, -1}, {1, -1}, {-1, 1}}; - for (int[] offset : nearby) { - for (int yOffset = 0; yOffset <= 3; yOffset++) { + // Search nearby with small Y offset + int[][] nearbyDiagonal = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {-1, -1}, {1, -1}, {-1, 1}}; + for (int[] offset : nearbyDiagonal) { + for (int yOffset = -1; yOffset <= maxUpOffset; yOffset++) { int newY = y + yOffset; if (newY < world.getMinHeight() || newY >= world.getMaxHeight()) { continue; @@ -183,6 +196,13 @@ public final class BlockUpdateUtil { return new Location(world, x + 0.5, y, z + 0.5, yaw, pitch); } + /** + * Finds a safe teleport location with default max up offset of 3 blocks. + */ + public static Location findSafeTeleportLocation(Location destination) { + return findSafeTeleportLocation(destination, 3); + } + /** * Checks if a location is safe for teleport (no suffocation). */ diff --git a/src/main/resources/defaults.yml b/src/main/resources/defaults.yml index 233442e..9b60ad0 100644 --- a/src/main/resources/defaults.yml +++ b/src/main/resources/defaults.yml @@ -63,6 +63,14 @@ teleport: enabled: false location: "" +# Player spawn location for stuck players +# Used when a player gets stuck in blocks after mine reset +# If not set, falls back to 'teleport.location' +player-spawn: + enabled: false + location: "" + # Example: "world;100.5;64;200.5;90;0" + # WorldGuard integration worldguard: # Enable automatic region creation when mines are created