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

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

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

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