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.
This commit is contained in:
loki5512344 2026-05-31 23:03:40 +02:00
parent 0603096f60
commit bb2e47a917
8 changed files with 195 additions and 16 deletions

View file

@ -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.region.RegionConfig;
import dev.loki.lomines.data.config.reset.ResetConfig; import dev.loki.lomines.data.config.reset.ResetConfig;
import dev.loki.lomines.data.config.reward.RewardConfig; 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.teleport.TeleportConfig;
import dev.loki.lomines.data.config.ui.UIConfig; import dev.loki.lomines.data.config.ui.UIConfig;
import dev.loki.lomines.integration.worldguard.WorldGuardConfig; 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 dev.loki.lomines.util.location.LocationParser;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.Material; import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.configuration.file.YamlConfiguration;
import java.io.IOException; import java.io.IOException;
@ -76,6 +78,7 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) {
.teleport(TeleportConfig.disabled()) .teleport(TeleportConfig.disabled())
.ui(UIConfig.defaults()) .ui(UIConfig.defaults())
.worldGuard(wgConfig) .worldGuard(wgConfig)
.playerSpawn(PlayerSpawnConfig.disabled())
.build(); .build();
// Save using new loader // Save using new loader
@ -160,6 +163,7 @@ public record MineFileManager(Path minesFolder, ConfigLoader configLoader) {
.teleport(config.teleport()) .teleport(config.teleport())
.ui(config.ui()) .ui(config.ui())
.worldGuard(config.worldGuard()) .worldGuard(config.worldGuard())
.playerSpawn(config.playerSpawn())
.build(); .build();
configLoader.save(updated); configLoader.save(updated);

View file

@ -26,6 +26,7 @@ public final class ConfigLoader {
private TeleportConfigLoader teleportLoader; private TeleportConfigLoader teleportLoader;
private UIConfigLoader uiLoader; private UIConfigLoader uiLoader;
private WorldGuardConfigLoader worldGuardLoader; private WorldGuardConfigLoader worldGuardLoader;
private PlayerSpawnConfigLoader playerSpawnLoader;
public ConfigLoader(Path dataFolder) { public ConfigLoader(Path dataFolder) {
this.dataFolder = dataFolder; this.dataFolder = dataFolder;
@ -66,6 +67,7 @@ public final class ConfigLoader {
yaml.set("rewards", List.of()); yaml.set("rewards", List.of());
worldGuardLoader.setDefaults(yaml); worldGuardLoader.setDefaults(yaml);
playerSpawnLoader.setDefaults(yaml);
} }
private void initLoaders() { private void initLoaders() {
@ -76,6 +78,7 @@ public final class ConfigLoader {
teleportLoader = new TeleportConfigLoader(); teleportLoader = new TeleportConfigLoader();
uiLoader = new UIConfigLoader(defaults); uiLoader = new UIConfigLoader(defaults);
worldGuardLoader = new WorldGuardConfigLoader(defaults); worldGuardLoader = new WorldGuardConfigLoader(defaults);
playerSpawnLoader = new PlayerSpawnConfigLoader();
} }
/** /**
@ -100,6 +103,7 @@ public final class ConfigLoader {
.teleport(teleportLoader.parse(yaml)) .teleport(teleportLoader.parse(yaml))
.ui(uiLoader.parse(yaml)) .ui(uiLoader.parse(yaml))
.worldGuard(worldGuardLoader.parse(yaml)) .worldGuard(worldGuardLoader.parse(yaml))
.playerSpawn(playerSpawnLoader.parse(yaml))
.build(); .build();
} catch (Exception e) { } catch (Exception e) {
throw new ConfigLoadException("Failed to load mine '" + mineName + "': " + e.getMessage(), 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()); teleportLoader.save(yaml, config.teleport());
uiLoader.save(yaml, config.ui()); uiLoader.save(yaml, config.ui());
worldGuardLoader.save(yaml, config.worldGuard()); worldGuardLoader.save(yaml, config.worldGuard());
playerSpawnLoader.save(yaml, config.playerSpawn());
try { try {
Files.createDirectories(configPath.getParent()); Files.createDirectories(configPath.getParent());

View file

@ -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.region.RegionConfig;
import dev.loki.lomines.data.config.reset.ResetConfig; import dev.loki.lomines.data.config.reset.ResetConfig;
import dev.loki.lomines.data.config.reward.RewardConfig; 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.teleport.TeleportConfig;
import dev.loki.lomines.data.config.ui.UIConfig; import dev.loki.lomines.data.config.ui.UIConfig;
import dev.loki.lomines.integration.worldguard.WorldGuardConfig; import dev.loki.lomines.integration.worldguard.WorldGuardConfig;
import org.bukkit.Location;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
/** /**
* Clean, type-safe, section-based mine configuration. * Clean, type-safe, section-based mine configuration.
@ -22,7 +26,8 @@ public record MineConfig(
RewardConfig rewards, RewardConfig rewards,
TeleportConfig teleport, TeleportConfig teleport,
UIConfig ui, UIConfig ui,
WorldGuardConfig worldGuard WorldGuardConfig worldGuard,
PlayerSpawnConfig playerSpawn
) { ) {
public MineConfig { public MineConfig {
@ -38,7 +43,10 @@ public record MineConfig(
Objects.requireNonNull(rewards, "Reward config cannot be null"); Objects.requireNonNull(rewards, "Reward config cannot be null");
Objects.requireNonNull(teleport, "Teleport config cannot be null"); Objects.requireNonNull(teleport, "Teleport config cannot be null");
Objects.requireNonNull(ui, "UI 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(), RewardConfig.empty(),
TeleportConfig.disabled(), TeleportConfig.disabled(),
UIConfig.defaults(), 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<Location> getSpawnForStuckPlayer() {
return playerSpawn.enabled()
? playerSpawn.getLocation()
: teleport.getLocation();
}
// --- Builder --- // --- Builder ---
public static class Builder { public static class Builder {
@ -89,6 +108,7 @@ public record MineConfig(
private TeleportConfig teleport = TeleportConfig.disabled(); private TeleportConfig teleport = TeleportConfig.disabled();
private UIConfig ui = UIConfig.defaults(); private UIConfig ui = UIConfig.defaults();
private WorldGuardConfig worldGuard = WorldGuardConfig.disabled(); private WorldGuardConfig worldGuard = WorldGuardConfig.disabled();
private PlayerSpawnConfig playerSpawn = PlayerSpawnConfig.disabled();
private Builder(String name) { private Builder(String name) {
this.name = name; this.name = name;
@ -129,8 +149,13 @@ public record MineConfig(
return this; return this;
} }
public Builder playerSpawn(PlayerSpawnConfig playerSpawn) {
this.playerSpawn = playerSpawn;
return this;
}
public MineConfig build() { 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);
} }
} }
} }

View file

@ -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);
}
}

View file

@ -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<Location> 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);
}
}

View file

@ -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. * Prevents players from suffocating in blocks by finding a safe teleport spot.
*/ */
private void teleportPlayers() { 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()) { if (destOpt.isEmpty()) {
return; return;
} }
@ -184,8 +186,8 @@ public final class MineResetHandler {
return; return;
} }
// Find a safe teleport location to prevent suffocation // Find a safe teleport location (max 3 blocks up to avoid teleporting too high)
Location safeDest = BlockUpdateUtil.findSafeTeleportLocation(dest); Location safeDest = BlockUpdateUtil.findSafeTeleportLocation(dest, 3);
for (Player p : dest.getWorld().getPlayers()) { for (Player p : dest.getWorld().getPlayers()) {
if (mine.contains(p.getLocation())) { if (mine.contains(p.getLocation())) {

View file

@ -130,11 +130,13 @@ public final class BlockUpdateUtil {
/** /**
* Finds a safe teleport location near the given destination. * Finds a safe teleport location near the given destination.
* Checks for suffocation hazards (blocks at head/body level). * Checks for suffocation hazards (blocks at head/body level).
* <p>Will not teleport too high - limited to maxUpOffset blocks above original.</p>
* *
* @param destination the desired destination * @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) * @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) { if (destination == null || destination.getWorld() == null) {
return destination; return destination;
} }
@ -146,29 +148,40 @@ public final class BlockUpdateUtil {
float yaw = destination.getYaw(); float yaw = destination.getYaw();
float pitch = destination.getPitch(); 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 // Check if original location is safe
if (isSafeLocation(world, x, y, z)) { if (isSafeLocation(world, x, y, z)) {
return destination; return destination;
} }
// Search upward for a safe spot (priority: don't drop player down) // Search nearby blocks first (same Y level, closer distance)
for (int offset = 1; offset <= 5; offset++) { 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)) { if (isSafeLocation(world, x, y + offset, z)) {
return new Location(world, x + 0.5, y + offset, z + 0.5, yaw, pitch); 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++) { for (int offset = 1; offset <= 5 && y - offset >= world.getMinHeight(); offset++) {
if (isSafeLocation(world, x, y - offset, z)) { if (isSafeLocation(world, x, y - offset, z)) {
return new Location(world, x + 0.5, y - offset, z + 0.5, yaw, pitch); return new Location(world, x + 0.5, y - offset, z + 0.5, yaw, pitch);
} }
} }
// Search nearby blocks // Search nearby with small Y offset
int[][] nearby = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {-1, -1}, {1, -1}, {-1, 1}}; int[][] nearbyDiagonal = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {-1, -1}, {1, -1}, {-1, 1}};
for (int[] offset : nearby) { for (int[] offset : nearbyDiagonal) {
for (int yOffset = 0; yOffset <= 3; yOffset++) { for (int yOffset = -1; yOffset <= maxUpOffset; yOffset++) {
int newY = y + yOffset; int newY = y + yOffset;
if (newY < world.getMinHeight() || newY >= world.getMaxHeight()) { if (newY < world.getMinHeight() || newY >= world.getMaxHeight()) {
continue; continue;
@ -183,6 +196,13 @@ public final class BlockUpdateUtil {
return new Location(world, x + 0.5, y, z + 0.5, yaw, pitch); 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). * Checks if a location is safe for teleport (no suffocation).
*/ */

View file

@ -63,6 +63,14 @@ teleport:
enabled: false enabled: false
location: "" 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 integration
worldguard: worldguard:
# Enable automatic region creation when mines are created # Enable automatic region creation when mines are created