refactor: decompose god-classes, add lint gates, restructure packages
- Decompose all 11 god-classes (>200 lines) into facade + collaborators (command/, schematic/, config/, generator/, mode/, papi/, adaptive/, storage/, player/, leaderboard/) — every file is now <=200 lines - Add PMD 7 (report-only) + stricter checkstyle rules (NPath, IllegalCatch, NestedDepth, Whitespace, MissingOverride); fix real bugs found (printStackTrace, resource leaks in ConfigLoader/SQLStatsStorage) - docs/ARCHITECTURE.md: target structure + 'one folder = one purpose' rule - Pilot restructure: command/core|util/, mode/impl/solo|multi|barrier/, hook/papi/resolver/; cross-package classes made public - TODO.md: track warning cleanup (132-138) + restructure progress
This commit is contained in:
parent
1d57972038
commit
81788e71ed
70 changed files with 3428 additions and 1701 deletions
|
|
@ -18,6 +18,7 @@ import org.jetbrains.annotations.Nullable;
|
|||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Main class of LoParkour
|
||||
|
|
@ -112,8 +113,7 @@ public final class LoParkour extends LoPlugin {
|
|||
Storage.close();
|
||||
World.delete();
|
||||
} catch (Throwable t) {
|
||||
getLogger().severe("Error during plugin shutdown: " + t.getMessage());
|
||||
t.printStackTrace();
|
||||
getLogger().log(Level.SEVERE, "Error during plugin shutdown: " + t.getMessage(), t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package dev.loki.loparkour.adaptive.core;
|
||||
|
||||
import org.bukkit.block.Block;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Aggregates timing and position between parkour landings.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Track the last landed block per player
|
||||
* - Track the timestamp of the last landing per player
|
||||
* - Provide the timing window between consecutive landings
|
||||
*
|
||||
* Thread-safe: Uses ConcurrentHashMap for player caches.
|
||||
*/
|
||||
class JumpTimingTracker {
|
||||
|
||||
private final Map<UUID, Block> lastBlockCache;
|
||||
private final Map<UUID, Long> lastLandTimeCache;
|
||||
|
||||
JumpTimingTracker() {
|
||||
this.lastBlockCache = new ConcurrentHashMap<>();
|
||||
this.lastLandTimeCache = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Long lastLandTime(@NotNull UUID playerUuid) {
|
||||
return lastLandTimeCache.get(playerUuid);
|
||||
}
|
||||
|
||||
void markLand(@NotNull UUID playerUuid, long timeMillis) {
|
||||
lastLandTimeCache.put(playerUuid, timeMillis);
|
||||
}
|
||||
|
||||
void updateLastBlock(@NotNull UUID playerUuid, @NotNull Block block) {
|
||||
lastBlockCache.put(playerUuid, block);
|
||||
}
|
||||
|
||||
void unload(@NotNull UUID playerUuid) {
|
||||
lastBlockCache.remove(playerUuid);
|
||||
lastLandTimeCache.remove(playerUuid);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package dev.loki.loparkour.adaptive.core;
|
||||
|
||||
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||
import dev.loki.loparkour.generator.jump.calculation.JumpType;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Collects raw generator events into player metrics.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Record successful block landings with timing
|
||||
* - Record jump events with type and timing
|
||||
* - Detect near-miss jumps (distance > 3.5 blocks)
|
||||
* - Persist metrics when a player falls
|
||||
*/
|
||||
class MetricEventCollector {
|
||||
|
||||
private static final double NEAR_MISS_THRESHOLD = 3.5;
|
||||
|
||||
private final MetricsCacheStore cacheStore;
|
||||
private final JumpTimingTracker timingTracker;
|
||||
|
||||
MetricEventCollector(@NotNull MetricsCacheStore cacheStore, @NotNull JumpTimingTracker timingTracker) {
|
||||
this.cacheStore = cacheStore;
|
||||
this.timingTracker = timingTracker;
|
||||
}
|
||||
|
||||
void onBlockLand(@NotNull Player player, @NotNull Block block, double distance) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
PlayerMetrics metrics = cacheStore.getOrCreate(uuid);
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Long lastTime = timingTracker.lastLandTime(uuid);
|
||||
if (lastTime != null) {
|
||||
metrics.recordJump("normal", currentTime - lastTime);
|
||||
}
|
||||
timingTracker.markLand(uuid, currentTime);
|
||||
|
||||
if (distance > NEAR_MISS_THRESHOLD) {
|
||||
metrics.recordNearMiss();
|
||||
}
|
||||
|
||||
timingTracker.updateLastBlock(uuid, block);
|
||||
}
|
||||
|
||||
void onPlayerScore(@NotNull Player player, @NotNull Block block, double distance) {
|
||||
onBlockLand(player, block, distance);
|
||||
}
|
||||
|
||||
void onPlayerFall(@NotNull Player player) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
PlayerMetrics metrics = cacheStore.getOrCreate(uuid);
|
||||
cacheStore.save(metrics);
|
||||
}
|
||||
|
||||
void onJump(@NotNull Player player, @NotNull JumpType jumpType) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
PlayerMetrics metrics = cacheStore.getOrCreate(uuid);
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Long lastTime = timingTracker.lastLandTime(uuid);
|
||||
long timeDiff = (lastTime != null) ? (currentTime - lastTime) : 0;
|
||||
|
||||
metrics.recordJump(jumpType.name().toLowerCase(), timeDiff);
|
||||
}
|
||||
|
||||
void unload(@NotNull UUID playerUuid) {
|
||||
timingTracker.unload(playerUuid);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package dev.loki.loparkour.adaptive.core;
|
||||
|
||||
import dev.loki.loparkour.adaptive.model.PlayerMetrics;
|
||||
import dev.loki.loparkour.adaptive.storage.StatsRepository;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Stores and prunes the cached player metrics.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Load or create metrics per player
|
||||
* - Persist metrics to the repository
|
||||
* - Remove players on unload
|
||||
* - Schedule periodic auto-save
|
||||
*
|
||||
* Thread-safe: Uses ConcurrentHashMap for player metrics cache.
|
||||
*/
|
||||
class MetricsCacheStore {
|
||||
|
||||
private static final long AUTO_SAVE_INTERVAL_TICKS = 20 * 60 * 5; // 5 minutes
|
||||
|
||||
private final Plugin plugin;
|
||||
private final StatsRepository repository;
|
||||
private final Map<UUID, PlayerMetrics> metricsCache;
|
||||
|
||||
private BukkitRunnable autoSaveTask;
|
||||
|
||||
MetricsCacheStore(@NotNull Plugin plugin, @NotNull StatsRepository repository) {
|
||||
this.plugin = plugin;
|
||||
this.repository = repository;
|
||||
this.metricsCache = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
PlayerMetrics getOrCreate(@NotNull UUID playerUuid) {
|
||||
return metricsCache.computeIfAbsent(playerUuid, this::loadOrCreate);
|
||||
}
|
||||
|
||||
private @NotNull PlayerMetrics loadOrCreate(@NotNull UUID playerUuid) {
|
||||
PlayerMetrics loaded = repository.loadMetrics(playerUuid);
|
||||
if (loaded != null) {
|
||||
return loaded;
|
||||
}
|
||||
return new PlayerMetrics(playerUuid);
|
||||
}
|
||||
|
||||
void save(@NotNull UUID playerUuid) {
|
||||
PlayerMetrics metrics = metricsCache.get(playerUuid);
|
||||
if (metrics != null) {
|
||||
repository.saveMetrics(metrics);
|
||||
}
|
||||
}
|
||||
|
||||
void save(@NotNull PlayerMetrics metrics) {
|
||||
repository.saveMetrics(metrics);
|
||||
}
|
||||
|
||||
void saveAll() {
|
||||
for (PlayerMetrics metrics : metricsCache.values()) {
|
||||
repository.saveMetrics(metrics);
|
||||
}
|
||||
}
|
||||
|
||||
void unload(@NotNull UUID playerUuid) {
|
||||
save(playerUuid);
|
||||
metricsCache.remove(playerUuid);
|
||||
}
|
||||
|
||||
int size() {
|
||||
return metricsCache.size();
|
||||
}
|
||||
|
||||
void startAutoSave() {
|
||||
if (autoSaveTask != null) {
|
||||
autoSaveTask.cancel();
|
||||
}
|
||||
|
||||
autoSaveTask = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
saveAll();
|
||||
}
|
||||
};
|
||||
|
||||
autoSaveTask.runTaskTimerAsynchronously(plugin, AUTO_SAVE_INTERVAL_TICKS, AUTO_SAVE_INTERVAL_TICKS);
|
||||
}
|
||||
|
||||
void stopAutoSave() {
|
||||
if (autoSaveTask != null) {
|
||||
autoSaveTask.cancel();
|
||||
autoSaveTask = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,13 +7,10 @@ import dev.loki.loparkour.generator.jump.calculation.JumpType;
|
|||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Collects and manages player performance metrics.
|
||||
|
|
@ -26,19 +23,16 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||
* - Auto-save metrics every 5 minutes
|
||||
*
|
||||
* Thread-safe: Uses ConcurrentHashMap for player metrics cache.
|
||||
*
|
||||
* Facade over the metrics subsystem:
|
||||
* - {@link MetricsCacheStore} handles cache storage, pruning, and persistence
|
||||
* - {@link MetricEventCollector} handles raw generator events
|
||||
* - {@link JumpTimingTracker} aggregates timing windows between landings
|
||||
*/
|
||||
public class MetricsCollector implements GeneratorEventListener {
|
||||
|
||||
private static final double NEAR_MISS_THRESHOLD = 3.5;
|
||||
private static final long AUTO_SAVE_INTERVAL_TICKS = 20 * 60 * 5; // 5 minutes
|
||||
|
||||
private final Plugin plugin;
|
||||
private final StatsRepository repository;
|
||||
private final Map<UUID, PlayerMetrics> metricsCache;
|
||||
private final Map<UUID, Block> lastBlockCache;
|
||||
private final Map<UUID, Long> lastBlockTimeCache;
|
||||
|
||||
private BukkitRunnable autoSaveTask;
|
||||
private final MetricsCacheStore cacheStore;
|
||||
private final MetricEventCollector eventCollector;
|
||||
|
||||
/**
|
||||
* Create a new MetricsCollector.
|
||||
|
|
@ -47,11 +41,8 @@ public class MetricsCollector implements GeneratorEventListener {
|
|||
* @param repository The repository for persisting metrics
|
||||
*/
|
||||
public MetricsCollector(@NotNull Plugin plugin, @NotNull StatsRepository repository) {
|
||||
this.plugin = plugin;
|
||||
this.repository = repository;
|
||||
this.metricsCache = new ConcurrentHashMap<>();
|
||||
this.lastBlockCache = new ConcurrentHashMap<>();
|
||||
this.lastBlockTimeCache = new ConcurrentHashMap<>();
|
||||
this.cacheStore = new MetricsCacheStore(plugin, repository);
|
||||
this.eventCollector = new MetricEventCollector(cacheStore, new JumpTimingTracker());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -59,18 +50,7 @@ public class MetricsCollector implements GeneratorEventListener {
|
|||
* Should be called when the collector is initialized.
|
||||
*/
|
||||
public void startAutoSave() {
|
||||
if (autoSaveTask != null) {
|
||||
autoSaveTask.cancel();
|
||||
}
|
||||
|
||||
autoSaveTask = new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
saveAllMetrics();
|
||||
}
|
||||
};
|
||||
|
||||
autoSaveTask.runTaskTimerAsynchronously(plugin, AUTO_SAVE_INTERVAL_TICKS, AUTO_SAVE_INTERVAL_TICKS);
|
||||
cacheStore.startAutoSave();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -78,10 +58,7 @@ public class MetricsCollector implements GeneratorEventListener {
|
|||
* Should be called when the plugin is disabled.
|
||||
*/
|
||||
public void stopAutoSave() {
|
||||
if (autoSaveTask != null) {
|
||||
autoSaveTask.cancel();
|
||||
autoSaveTask = null;
|
||||
}
|
||||
cacheStore.stopAutoSave();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -92,15 +69,7 @@ public class MetricsCollector implements GeneratorEventListener {
|
|||
*/
|
||||
@NotNull
|
||||
public PlayerMetrics getMetrics(@NotNull UUID playerUuid) {
|
||||
return metricsCache.computeIfAbsent(playerUuid, uuid -> {
|
||||
// Try to load from repository
|
||||
PlayerMetrics loaded = repository.loadMetrics(uuid);
|
||||
if (loaded != null) {
|
||||
return loaded;
|
||||
}
|
||||
// Create new metrics if not found
|
||||
return new PlayerMetrics(uuid);
|
||||
});
|
||||
return cacheStore.getOrCreate(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -112,26 +81,7 @@ public class MetricsCollector implements GeneratorEventListener {
|
|||
* @param distance The distance of the jump
|
||||
*/
|
||||
public void onBlockLand(@NotNull Player player, @NotNull Block block, double distance) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
PlayerMetrics metrics = getMetrics(uuid);
|
||||
|
||||
// Calculate time since last block
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Long lastTime = lastBlockTimeCache.get(uuid);
|
||||
if (lastTime != null) {
|
||||
long timeDiff = currentTime - lastTime;
|
||||
// Record jump with timing (use "normal" as default type)
|
||||
metrics.recordJump("normal", timeDiff);
|
||||
}
|
||||
lastBlockTimeCache.put(uuid, currentTime);
|
||||
|
||||
// Detect near-miss (long jump that succeeded)
|
||||
if (distance > NEAR_MISS_THRESHOLD) {
|
||||
metrics.recordNearMiss();
|
||||
}
|
||||
|
||||
// Update last block
|
||||
lastBlockCache.put(uuid, block);
|
||||
eventCollector.onBlockLand(player, block, distance);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -141,30 +91,17 @@ public class MetricsCollector implements GeneratorEventListener {
|
|||
|
||||
@Override
|
||||
public void onPlayerScore(@NotNull Player player, @NotNull Block block, double distance) {
|
||||
onBlockLand(player, block, distance);
|
||||
eventCollector.onPlayerScore(player, block, distance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayerFall(@NotNull Player player, @Nullable Block lastBlock) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
PlayerMetrics metrics = getMetrics(uuid);
|
||||
|
||||
// Save metrics on fall (important event)
|
||||
repository.saveMetrics(metrics);
|
||||
eventCollector.onPlayerFall(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onJump(@NotNull Player player, @NotNull JumpType jumpType) {
|
||||
UUID uuid = player.getUniqueId();
|
||||
PlayerMetrics metrics = getMetrics(uuid);
|
||||
|
||||
// Calculate time since last block
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Long lastTime = lastBlockTimeCache.get(uuid);
|
||||
long timeDiff = (lastTime != null) ? (currentTime - lastTime) : 0;
|
||||
|
||||
// Record jump with type and timing
|
||||
metrics.recordJump(jumpType.name().toLowerCase(), timeDiff);
|
||||
eventCollector.onJump(player, jumpType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -173,10 +110,7 @@ public class MetricsCollector implements GeneratorEventListener {
|
|||
* @param playerUuid The player's UUID
|
||||
*/
|
||||
public void saveMetrics(@NotNull UUID playerUuid) {
|
||||
PlayerMetrics metrics = metricsCache.get(playerUuid);
|
||||
if (metrics != null) {
|
||||
repository.saveMetrics(metrics);
|
||||
}
|
||||
cacheStore.save(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -184,9 +118,7 @@ public class MetricsCollector implements GeneratorEventListener {
|
|||
* Called by auto-save task and on plugin disable.
|
||||
*/
|
||||
public void saveAllMetrics() {
|
||||
for (PlayerMetrics metrics : metricsCache.values()) {
|
||||
repository.saveMetrics(metrics);
|
||||
}
|
||||
cacheStore.saveAll();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -196,11 +128,8 @@ public class MetricsCollector implements GeneratorEventListener {
|
|||
* @param playerUuid The player's UUID
|
||||
*/
|
||||
public void unloadPlayer(@NotNull UUID playerUuid) {
|
||||
// Save before removing
|
||||
saveMetrics(playerUuid);
|
||||
metricsCache.remove(playerUuid);
|
||||
lastBlockCache.remove(playerUuid);
|
||||
lastBlockTimeCache.remove(playerUuid);
|
||||
cacheStore.unload(playerUuid);
|
||||
eventCollector.unload(playerUuid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -209,6 +138,6 @@ public class MetricsCollector implements GeneratorEventListener {
|
|||
* @return The cache size
|
||||
*/
|
||||
public int getCacheSize() {
|
||||
return metricsCache.size();
|
||||
return cacheStore.size();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,13 +40,13 @@ public class SQLStatsStorage implements StatsRepository {
|
|||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
|
||||
stmt.setString(1, playerUuid.toString());
|
||||
ResultSet rs = stmt.executeQuery();
|
||||
|
||||
if (rs.next()) {
|
||||
double rating = rs.getDouble("skill_rating");
|
||||
int sessions = rs.getInt("sessions_count");
|
||||
double confidence = Math.min(1.0, sessions * 0.05);
|
||||
return new SkillRating(playerUuid, rating, confidence, sessions);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
double rating = rs.getDouble("skill_rating");
|
||||
int sessions = rs.getInt("sessions_count");
|
||||
double confidence = Math.min(1.0, sessions * 0.05);
|
||||
return new SkillRating(playerUuid, rating, confidence, sessions);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LoParkour.getPlugin().getLogger().warning("Failed to load skill rating: " + e.getMessage());
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package dev.loki.loparkour.bootstrap;
|
|||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.command.LoParkourCommand;
|
||||
import dev.loki.loparkour.command.core.LoParkourCommand;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
|
|
@ -13,13 +13,13 @@ import dev.loki.loparkour.listener.gameplay.ParkourRestrictionListener;
|
|||
import dev.loki.loparkour.listener.schematic.SchematicWandListener;
|
||||
import dev.loki.loparkour.listener.player.PlayerConnectionListener;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.mode.impl.CoopMode;
|
||||
import dev.loki.loparkour.mode.impl.DefaultMode;
|
||||
import dev.loki.loparkour.mode.impl.GravityShiftMode;
|
||||
import dev.loki.loparkour.mode.impl.InvisibleBarrierMode;
|
||||
import dev.loki.loparkour.mode.impl.RaceMode;
|
||||
import dev.loki.loparkour.mode.impl.SpectatorMode;
|
||||
import dev.loki.loparkour.mode.impl.SpeedrunMode;
|
||||
import dev.loki.loparkour.mode.impl.barrier.InvisibleBarrierMode;
|
||||
import dev.loki.loparkour.mode.impl.multi.CoopMode;
|
||||
import dev.loki.loparkour.mode.impl.multi.RaceMode;
|
||||
import dev.loki.loparkour.mode.impl.multi.SpectatorMode;
|
||||
import dev.loki.loparkour.mode.impl.solo.DefaultMode;
|
||||
import dev.loki.loparkour.mode.impl.solo.GravityShiftMode;
|
||||
import dev.loki.loparkour.mode.impl.solo.SpeedrunMode;
|
||||
import dev.loki.loparkour.reward.core.Rewards;
|
||||
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||
import dev.loki.loparkour.world.core.World;
|
||||
|
|
|
|||
|
|
@ -1,302 +0,0 @@
|
|||
package dev.loki.loparkour.command;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.command.admin.AdminCommandHandler;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.mode.base.MultiMode;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Main command router for /LoParkour and /parkour.
|
||||
* Delegates to {@link PlayerCommandHandler}, {@link AdminCommandHandler},
|
||||
* and {@link SchematicCommandHandler}.
|
||||
*/
|
||||
public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
private static final List<String> SCHEMATIC_DIFFICULTIES = List.of("0.0", "0.25", "0.5", "0.75", "1.0");
|
||||
private static final List<String> SCHEMATIC_SUBCOMMANDS = List.of(
|
||||
"wand", "pos1", "pos2", "create", "convert", "paste", "list", "reload"
|
||||
);
|
||||
|
||||
private final PlayerCommandHandler player = new PlayerCommandHandler();
|
||||
private final AdminCommandHandler admin = new AdminCommandHandler(player);
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String label, @NotNull String[] args) {
|
||||
try {
|
||||
Player p = sender instanceof Player ? (Player) sender : null;
|
||||
|
||||
switch (args.length) {
|
||||
case 0 -> player.handleNoArgs(sender, p);
|
||||
case 1 -> player.handle(args[0], sender, p);
|
||||
case 2 -> handle2(args[0], args[1], sender, p);
|
||||
case 3 -> handle3(args[0], args[1], args[2], sender, p);
|
||||
case 4 -> handle4(args[0], args[1], args[2], args[3], sender, p);
|
||||
default -> Locales.send(sender, "commands.too_many_args");
|
||||
}
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
LoParkour.getPlugin().getLogger().log(Level.SEVERE,
|
||||
"LoParkour command failed (" + label + " " + String.join(" ", args) + ")", t);
|
||||
Locales.send(sender, "commands.error");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2-arg routing ──────────────────────────────────────────────────────────
|
||||
|
||||
private void handle2(String a1, String a2, CommandSender sender, @Nullable Player p) {
|
||||
// Admin commands that don't need a player
|
||||
switch (a1.toLowerCase()) {
|
||||
case "forcejoin", "forceleave", "reset", "recoverinventory" -> {
|
||||
admin.handle(a1, a2, sender, p);
|
||||
return;
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
if (p == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("create".equalsIgnoreCase(a1) && p.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
SchematicCommandHandler.handleCreateDifficultyOnly(a2, sender, p, player);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (a1.toLowerCase()) {
|
||||
case "join" -> handleJoin(a2, sender, p);
|
||||
case "leaderboard" -> handleLeaderboard(a2, sender, p);
|
||||
case "schematic" -> {
|
||||
if (!p.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
send(sender, Locales.getString(p, "other.no_do"));
|
||||
return;
|
||||
}
|
||||
SchematicCommandHandler.handleSubcommand(a2, sender, p, player);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3-arg routing ──────────────────────────────────────────────────────────
|
||||
|
||||
private void handle3(String a1, String a2, String a3, CommandSender sender, @Nullable Player p) {
|
||||
if (p == null) {
|
||||
return;
|
||||
}
|
||||
if (!p.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("schematic".equalsIgnoreCase(a1)) {
|
||||
if ("create".equalsIgnoreCase(a2)) {
|
||||
SchematicCommandHandler.handleCreateDifficultyOnly(a3, sender, p, player);
|
||||
return;
|
||||
}
|
||||
SchematicCommandHandler.handleSubcommandWithName(a2, a3, sender, p);
|
||||
}
|
||||
}
|
||||
|
||||
private void handle4(String a1, String a2, String a3, String a4, CommandSender sender, @Nullable Player p) {
|
||||
if (p == null) {
|
||||
return;
|
||||
}
|
||||
if (!p.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
return;
|
||||
}
|
||||
if (!"schematic".equalsIgnoreCase(a1) || !"create".equalsIgnoreCase(a2)) {
|
||||
return;
|
||||
}
|
||||
SchematicCommandHandler.handleCreateWithDifficulty(a3, a4, sender, p, player);
|
||||
}
|
||||
|
||||
// ── join logic ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void handleJoin(String arg, CommandSender sender, Player p) {
|
||||
if (!player.cooldown(sender, "join", 2500) || !ParkourOption.JOIN.mayPerform(p)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Mode mode = Registry.getMode(arg);
|
||||
if (mode != null) {
|
||||
mode.create(p);
|
||||
return;
|
||||
}
|
||||
|
||||
Player other = Bukkit.getPlayer(arg);
|
||||
if (other == null) {
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.unknown_player"));
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourPlayer pp = ParkourPlayer.getPlayer(other);
|
||||
if (pp == null) {
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.not_playing"));
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourUser user = ParkourUser.getUser(p);
|
||||
Session session = pp.session;
|
||||
if (user != null && user.session == session) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.isAcceptingPlayers()) {
|
||||
Mode sessionMode = session.generator.getMode();
|
||||
if (sessionMode instanceof MultiMode mm) {
|
||||
mm.join(p, session);
|
||||
} else {
|
||||
Modes.SPECTATOR.create(p, session);
|
||||
}
|
||||
} else {
|
||||
Modes.SPECTATOR.create(p, session);
|
||||
}
|
||||
}
|
||||
|
||||
// ── leaderboard logic ──────────────────────────────────────────────────────
|
||||
|
||||
private void handleLeaderboard(String arg, CommandSender sender, Player p) {
|
||||
if (!ParkourOption.LEADERBOARDS.mayPerform(p)) {
|
||||
send(sender, Locales.getString(p, "other.no_do"));
|
||||
return;
|
||||
}
|
||||
Mode mode = Registry.getMode(arg.toLowerCase());
|
||||
if (mode == null) {
|
||||
Menus.LEADERBOARDS.open(p);
|
||||
} else {
|
||||
Menus.SINGLE_LEADERBOARD.open(p, mode, Leaderboard.Sort.SCORE);
|
||||
}
|
||||
}
|
||||
|
||||
// ── tab complete ───────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("checkstyle:CyclomaticComplexity")
|
||||
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String alias, @NotNull String[] args) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
|
||||
if (args.length == 1) {
|
||||
if (ParkourOption.JOIN.mayPerform(sender)) {
|
||||
completions.add("join");
|
||||
completions.add("leave");
|
||||
}
|
||||
if (ParkourOption.MAIN.mayPerform(sender)) {
|
||||
completions.add("menu");
|
||||
}
|
||||
if (ParkourOption.PLAY.mayPerform(sender)) {
|
||||
completions.add("play");
|
||||
}
|
||||
if (ParkourOption.LEADERBOARDS.mayPerform(sender)) {
|
||||
completions.add("leaderboard");
|
||||
}
|
||||
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.addAll(List.of("schematic", "create", "reload", "forcejoin", "forceleave", "reset", "recoverinventory"));
|
||||
}
|
||||
return filter(args[0], completions);
|
||||
}
|
||||
|
||||
if (args.length == 2) {
|
||||
String a1 = args[0].toLowerCase();
|
||||
if ("reset".equals(a1) && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.add("everyone");
|
||||
ParkourPlayer.getPlayers().forEach(pp -> completions.add(pp.getName()));
|
||||
} else if ("join".equals(a1) && ParkourOption.JOIN.mayPerform(sender)) {
|
||||
Registry.getModes().forEach(m -> completions.add(m.getName()));
|
||||
ParkourPlayer.getPlayers().forEach(pp -> completions.add(pp.getName()));
|
||||
} else if ("leaderboard".equals(a1) && ParkourOption.LEADERBOARDS.mayPerform(sender)) {
|
||||
Registry.getModes().forEach(m -> completions.add(m.getName()));
|
||||
} else if ("schematic".equals(a1) && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.addAll(SCHEMATIC_SUBCOMMANDS);
|
||||
} else if ("create".equals(a1) && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.addAll(SCHEMATIC_DIFFICULTIES);
|
||||
} else if (("forcejoin".equals(a1) || "forceleave".equals(a1)) && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.add("everyone");
|
||||
if ("forcejoin".equals(a1)) {
|
||||
completions.add("nearest");
|
||||
}
|
||||
Bukkit.getOnlinePlayers().forEach(pl -> completions.add(pl.getName()));
|
||||
} else if ("recoverinventory".equals(a1) && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
Bukkit.getOnlinePlayers().forEach(pl -> completions.add(pl.getName()));
|
||||
}
|
||||
return filter(args[1], completions);
|
||||
}
|
||||
|
||||
if (args.length == 3 && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
String a1 = args[0].toLowerCase();
|
||||
String a2 = args[1].toLowerCase();
|
||||
if ("schematic".equals(a1)) {
|
||||
if ("paste".equals(a2)) {
|
||||
addLoadedSchematicIds(completions);
|
||||
} else if ("create".equals(a2)) {
|
||||
completions.addAll(SCHEMATIC_DIFFICULTIES);
|
||||
}
|
||||
}
|
||||
return filter(args[2], completions);
|
||||
}
|
||||
|
||||
if (args.length == 4 && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
String a1 = args[0].toLowerCase();
|
||||
String a2 = args[1].toLowerCase();
|
||||
if ("schematic".equals(a1) && "create".equals(a2)) {
|
||||
completions.addAll(SCHEMATIC_DIFFICULTIES);
|
||||
}
|
||||
return filter(args[3], completions);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private static void addLoadedSchematicIds(@NotNull List<String> completions) {
|
||||
SchematicManager manager = LoParkour.getSchematicManager();
|
||||
if (manager == null) {
|
||||
return;
|
||||
}
|
||||
for (String id : manager.getAll().keySet()) {
|
||||
completions.add(id);
|
||||
String shortId = SchematicManager.configKey(id);
|
||||
if (!shortId.equalsIgnoreCase(id)) {
|
||||
completions.add(shortId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── util ───────────────────────────────────────────────────────────────────
|
||||
|
||||
private List<String> filter(String input, List<String> options) {
|
||||
String lower = input.toLowerCase();
|
||||
return options.stream().filter(s -> s.toLowerCase().startsWith(lower)).toList();
|
||||
}
|
||||
|
||||
private void send(CommandSender sender, String msg) {
|
||||
sender.sendMessage(ColorUtil.color(msg));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +1,13 @@
|
|||
package dev.loki.loparkour.command.admin;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.player.data.InventoryData;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.command.BlockCommandSender;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Handles admin-only /LoParkour sub-commands:
|
||||
* forcejoin, forceleave, reset, recoverinventory.
|
||||
|
|
@ -30,9 +15,17 @@ import java.util.UUID;
|
|||
public class AdminCommandHandler {
|
||||
|
||||
private final PlayerCommandHandler base;
|
||||
private final ForceJoinExecutor forceJoin;
|
||||
private final ForceLeaveExecutor forceLeave;
|
||||
private final ResetExecutor reset;
|
||||
private final RecoverInventoryExecutor recoverInventory;
|
||||
|
||||
public AdminCommandHandler(PlayerCommandHandler base) {
|
||||
this.base = base;
|
||||
this.forceJoin = new ForceJoinExecutor(base);
|
||||
this.forceLeave = new ForceLeaveExecutor(base);
|
||||
this.reset = new ResetExecutor(base);
|
||||
this.recoverInventory = new RecoverInventoryExecutor(base);
|
||||
}
|
||||
|
||||
public void handle(@NotNull String arg1, @NotNull String arg2,
|
||||
|
|
@ -42,173 +35,37 @@ public class AdminCommandHandler {
|
|||
return;
|
||||
}
|
||||
|
||||
dispatchAdmin(arg1, arg2, sender);
|
||||
dispatchSchematic(arg1, arg2, sender, player);
|
||||
}
|
||||
|
||||
private void dispatchAdmin(@NotNull String arg1, @NotNull String arg2, @NotNull CommandSender sender) {
|
||||
switch (arg1.toLowerCase()) {
|
||||
case "forcejoin" -> handleForceJoin(arg2, sender);
|
||||
case "forceleave" -> handleForceLeave(arg2, sender);
|
||||
case "reset" -> handleReset(arg2, sender);
|
||||
case "recoverinventory" -> handleRecoverInventory(arg2, sender);
|
||||
case "forcejoin" -> {
|
||||
forceJoin.execute(arg2, sender);
|
||||
}
|
||||
case "forceleave" -> {
|
||||
forceLeave.execute(arg2, sender);
|
||||
}
|
||||
case "reset" -> {
|
||||
reset.execute(arg2, sender);
|
||||
}
|
||||
case "recoverinventory" -> {
|
||||
recoverInventory.execute(arg2, sender);
|
||||
}
|
||||
default -> {
|
||||
/* unknown admin subcommand: handle() falls through to schematic */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void dispatchSchematic(@NotNull String arg1, @NotNull String arg2,
|
||||
@NotNull CommandSender sender, @Nullable Player player) {
|
||||
if (player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("schematic".equals(arg1)) {
|
||||
SchematicCommandHandler.handleSubcommand(arg2, sender, player, base);
|
||||
}
|
||||
}
|
||||
|
||||
// ── forcejoin ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void handleForceJoin(String target, CommandSender sender) {
|
||||
if (!base.cooldown(sender, "forcejoin", 2500, Locales.getString(sender, "admin.cooldown"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("everyone".equalsIgnoreCase(target)) {
|
||||
Bukkit.getOnlinePlayers().forEach(p -> Modes.DEFAULT.create(p));
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_join_everyone"));
|
||||
return;
|
||||
}
|
||||
if ("nearest".equalsIgnoreCase(target)) {
|
||||
Player closest = findNearest(sender);
|
||||
if (closest == null) {
|
||||
return;
|
||||
}
|
||||
Modes.DEFAULT.create(closest);
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_join_player").formatted(closest.getName()));
|
||||
return;
|
||||
}
|
||||
Player other = Bukkit.getPlayer(target);
|
||||
if (other == null) {
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online"));
|
||||
return;
|
||||
}
|
||||
Modes.DEFAULT.create(other);
|
||||
}
|
||||
|
||||
// ── forceleave ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void handleForceLeave(String target, CommandSender sender) {
|
||||
if (!base.cooldown(sender, "forceleave", 2500, Locales.getString(sender, "admin.cooldown"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("everyone".equalsIgnoreCase(target)) {
|
||||
ParkourPlayer.getPlayers().forEach(ParkourUser::leave);
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_leave_everyone"));
|
||||
return;
|
||||
}
|
||||
Player other = Bukkit.getPlayer(target);
|
||||
if (other == null) {
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online"));
|
||||
return;
|
||||
}
|
||||
ParkourUser user = ParkourUser.getUser(other);
|
||||
if (user == null) {
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_playing"));
|
||||
return;
|
||||
}
|
||||
ParkourUser.leave(user);
|
||||
}
|
||||
|
||||
// ── reset ──────────────────────────────────────────────────────────────────
|
||||
|
||||
private void handleReset(String target, CommandSender sender) {
|
||||
if (!base.cooldown(sender, "reset", 2500, Locales.getString(sender, "admin.cooldown"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("everyone".equalsIgnoreCase(target)) {
|
||||
Registry.getModes().stream()
|
||||
.map(Mode::getLeaderboard)
|
||||
.filter(lb -> lb != null)
|
||||
.forEach(lb -> {
|
||||
lb.resetAll();
|
||||
lb.write(true);
|
||||
});
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.reset_all"));
|
||||
return;
|
||||
}
|
||||
|
||||
UUID uuid = resolveUUID(target);
|
||||
String name = resolvePlayerName(target, uuid);
|
||||
|
||||
Registry.getModes().stream()
|
||||
.map(Mode::getLeaderboard)
|
||||
.filter(lb -> lb != null)
|
||||
.forEach(lb -> {
|
||||
lb.remove(uuid);
|
||||
lb.write(true);
|
||||
});
|
||||
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.reset_player").formatted(name));
|
||||
}
|
||||
|
||||
// ── recoverinventory ───────────────────────────────────────────────────────
|
||||
|
||||
private void handleRecoverInventory(String target, CommandSender sender) {
|
||||
if (!base.cooldown(sender, "recoverinventory", 2500, Locales.getString(sender, "admin.cooldown"))) {
|
||||
return;
|
||||
}
|
||||
Player other = Bukkit.getPlayer(target);
|
||||
if (other == null) {
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online"));
|
||||
return;
|
||||
}
|
||||
|
||||
new InventoryData(other).load(result -> {
|
||||
if (result != null) {
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.recover_inventory").formatted(other.getName()));
|
||||
} else {
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.recover_inventory_missing").formatted(other.getName()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
private @Nullable Player findNearest(CommandSender sender) {
|
||||
Location from = sender instanceof Player p ? p.getLocation()
|
||||
: sender instanceof BlockCommandSender b ? b.getBlock().getLocation()
|
||||
: null;
|
||||
if (from == null || from.getWorld() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return from.getWorld().getPlayers().stream()
|
||||
.min((a, b) -> Double.compare(a.getLocation().distance(from), b.getLocation().distance(from)))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // Bukkit: name-based OfflinePlayer lookup (admin-only)
|
||||
private UUID resolveUUID(String input) {
|
||||
Player online = Bukkit.getPlayerExact(input);
|
||||
if (online != null) {
|
||||
return online.getUniqueId();
|
||||
}
|
||||
if (input.contains("-")) {
|
||||
try {
|
||||
return UUID.fromString(input);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
// fall through to offline lookup by name
|
||||
}
|
||||
}
|
||||
return Bukkit.getOfflinePlayer(input).getUniqueId();
|
||||
}
|
||||
|
||||
private String resolvePlayerName(String input, UUID uuid) {
|
||||
Player online = Bukkit.getPlayerExact(input);
|
||||
if (online != null) {
|
||||
return online.getName();
|
||||
}
|
||||
OfflinePlayer op = Bukkit.getOfflinePlayer(uuid);
|
||||
return op.getName() != null ? op.getName() : input;
|
||||
}
|
||||
|
||||
private void send(CommandSender sender, String msg) {
|
||||
sender.sendMessage(ColorUtil.color(msg));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package dev.loki.loparkour.command.admin;
|
||||
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
final class AdminResponses {
|
||||
|
||||
private AdminResponses() {
|
||||
}
|
||||
|
||||
static void send(CommandSender sender, String msg) {
|
||||
sender.sendMessage(ColorUtil.color(msg));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package dev.loki.loparkour.command.admin;
|
||||
|
||||
import org.bukkit.command.BlockCommandSender;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
final class AdminTargetResolver {
|
||||
|
||||
private AdminTargetResolver() {
|
||||
}
|
||||
|
||||
static Player findNearest(CommandSender sender) {
|
||||
Location from = sender instanceof Player p ? p.getLocation()
|
||||
: sender instanceof BlockCommandSender b ? b.getBlock().getLocation()
|
||||
: null;
|
||||
if (from == null || from.getWorld() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return from.getWorld().getPlayers().stream()
|
||||
.min((a, b) -> Double.compare(a.getLocation().distance(from), b.getLocation().distance(from)))
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
static UUID resolveUUID(String input) {
|
||||
Player online = Bukkit.getPlayerExact(input);
|
||||
if (online != null) {
|
||||
return online.getUniqueId();
|
||||
}
|
||||
if (input.contains("-")) {
|
||||
UUID parsed = tryParseUuid(input);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return Bukkit.getOfflinePlayer(input).getUniqueId();
|
||||
}
|
||||
|
||||
private static UUID tryParseUuid(String input) {
|
||||
try {
|
||||
return UUID.fromString(input);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static String resolvePlayerName(String input, UUID uuid) {
|
||||
Player online = Bukkit.getPlayerExact(input);
|
||||
if (online != null) {
|
||||
return online.getName();
|
||||
}
|
||||
OfflinePlayer op = Bukkit.getOfflinePlayer(uuid);
|
||||
return op.getName() != null ? op.getName() : input;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package dev.loki.loparkour.command.admin;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
final class ForceJoinExecutor {
|
||||
|
||||
private final PlayerCommandHandler base;
|
||||
|
||||
ForceJoinExecutor(PlayerCommandHandler base) {
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
void execute(String target, CommandSender sender) {
|
||||
if (!base.cooldown(sender, "forcejoin", 2500, Locales.getString(sender, "admin.cooldown"))) {
|
||||
return;
|
||||
}
|
||||
if ("everyone".equalsIgnoreCase(target)) {
|
||||
joinEveryone(sender);
|
||||
return;
|
||||
}
|
||||
if ("nearest".equalsIgnoreCase(target)) {
|
||||
joinNearest(sender);
|
||||
return;
|
||||
}
|
||||
joinByName(target, sender);
|
||||
}
|
||||
|
||||
private void joinEveryone(CommandSender sender) {
|
||||
Bukkit.getOnlinePlayers().forEach(p -> Modes.DEFAULT.create(p));
|
||||
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_join_everyone"));
|
||||
}
|
||||
|
||||
private void joinNearest(CommandSender sender) {
|
||||
Player closest = AdminTargetResolver.findNearest(sender);
|
||||
if (closest == null) {
|
||||
return;
|
||||
}
|
||||
Modes.DEFAULT.create(closest);
|
||||
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_join_player").formatted(closest.getName()));
|
||||
}
|
||||
|
||||
private void joinByName(String target, CommandSender sender) {
|
||||
Player other = Bukkit.getPlayer(target);
|
||||
if (other == null) {
|
||||
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online"));
|
||||
return;
|
||||
}
|
||||
Modes.DEFAULT.create(other);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package dev.loki.loparkour.command.admin;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
final class ForceLeaveExecutor {
|
||||
|
||||
private final PlayerCommandHandler base;
|
||||
|
||||
ForceLeaveExecutor(PlayerCommandHandler base) {
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
void execute(String target, CommandSender sender) {
|
||||
if (!base.cooldown(sender, "forceleave", 2500, Locales.getString(sender, "admin.cooldown"))) {
|
||||
return;
|
||||
}
|
||||
if ("everyone".equalsIgnoreCase(target)) {
|
||||
leaveEveryone(sender);
|
||||
return;
|
||||
}
|
||||
leaveByName(target, sender);
|
||||
}
|
||||
|
||||
private void leaveEveryone(CommandSender sender) {
|
||||
ParkourPlayer.getPlayers().forEach(ParkourUser::leave);
|
||||
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_leave_everyone"));
|
||||
}
|
||||
|
||||
private void leaveByName(String target, CommandSender sender) {
|
||||
Player other = Bukkit.getPlayer(target);
|
||||
if (other == null) {
|
||||
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online"));
|
||||
return;
|
||||
}
|
||||
ParkourUser user = ParkourUser.getUser(other);
|
||||
if (user == null) {
|
||||
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_playing"));
|
||||
return;
|
||||
}
|
||||
ParkourUser.leave(user);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package dev.loki.loparkour.command.admin;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.player.data.InventoryData;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
final class RecoverInventoryExecutor {
|
||||
|
||||
private final PlayerCommandHandler base;
|
||||
|
||||
RecoverInventoryExecutor(PlayerCommandHandler base) {
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
void execute(String target, CommandSender sender) {
|
||||
if (!base.cooldown(sender, "recoverinventory", 2500, Locales.getString(sender, "admin.cooldown"))) {
|
||||
return;
|
||||
}
|
||||
Player other = Bukkit.getPlayer(target);
|
||||
if (other == null) {
|
||||
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online"));
|
||||
return;
|
||||
}
|
||||
recover(other, sender);
|
||||
}
|
||||
|
||||
private void recover(Player other, CommandSender sender) {
|
||||
new InventoryData(other).load(result -> {
|
||||
if (result != null) {
|
||||
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.recover_inventory").formatted(other.getName()));
|
||||
} else {
|
||||
AdminResponses.send(sender, LoParkour.PREFIX
|
||||
+ Locales.getString(sender, "admin.recover_inventory_missing").formatted(other.getName()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package dev.loki.loparkour.command.admin;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
final class ResetExecutor {
|
||||
|
||||
private final PlayerCommandHandler base;
|
||||
|
||||
ResetExecutor(PlayerCommandHandler base) {
|
||||
this.base = base;
|
||||
}
|
||||
|
||||
void execute(String target, CommandSender sender) {
|
||||
if (!base.cooldown(sender, "reset", 2500, Locales.getString(sender, "admin.cooldown"))) {
|
||||
return;
|
||||
}
|
||||
if ("everyone".equalsIgnoreCase(target)) {
|
||||
resetEveryone(sender);
|
||||
return;
|
||||
}
|
||||
resetPlayer(target, sender);
|
||||
}
|
||||
|
||||
private void resetEveryone(CommandSender sender) {
|
||||
forEachLeaderboard(lb -> {
|
||||
lb.resetAll();
|
||||
lb.write(true);
|
||||
});
|
||||
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.reset_all"));
|
||||
}
|
||||
|
||||
private void resetPlayer(String target, CommandSender sender) {
|
||||
UUID uuid = AdminTargetResolver.resolveUUID(target);
|
||||
String name = AdminTargetResolver.resolvePlayerName(target, uuid);
|
||||
forEachLeaderboard(lb -> {
|
||||
lb.remove(uuid);
|
||||
lb.write(true);
|
||||
});
|
||||
AdminResponses.send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.reset_player").formatted(name));
|
||||
}
|
||||
|
||||
private static void forEachLeaderboard(Consumer<Leaderboard> action) {
|
||||
Registry.getModes().stream()
|
||||
.map(Mode::getLeaderboard)
|
||||
.filter(lb -> lb != null)
|
||||
.forEach(action);
|
||||
}
|
||||
}
|
||||
111
src/main/java/dev/loki/loparkour/command/core/CommandRouter.java
Normal file
111
src/main/java/dev/loki/loparkour/command/core/CommandRouter.java
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package dev.loki.loparkour.command.core;
|
||||
|
||||
import dev.loki.loparkour.command.admin.AdminCommandHandler;
|
||||
import dev.loki.loparkour.command.player.JoinCommandExecutor;
|
||||
import dev.loki.loparkour.command.player.LeaderboardCommandExecutor;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||
import dev.loki.loparkour.command.util.CommandUtil;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
class CommandRouter {
|
||||
|
||||
private final PlayerCommandHandler player;
|
||||
private final AdminCommandHandler admin;
|
||||
private final JoinCommandExecutor join;
|
||||
private final LeaderboardCommandExecutor leaderboard;
|
||||
|
||||
CommandRouter(PlayerCommandHandler player, AdminCommandHandler admin) {
|
||||
this.player = player;
|
||||
this.admin = admin;
|
||||
this.join = new JoinCommandExecutor();
|
||||
this.leaderboard = new LeaderboardCommandExecutor();
|
||||
}
|
||||
|
||||
void dispatch(String[] args, CommandSender sender, @Nullable Player p) {
|
||||
switch (args.length) {
|
||||
case 0 -> player.handleNoArgs(sender, p);
|
||||
case 1 -> player.handle(args[0], sender, p);
|
||||
case 2 -> handle2(args[0], args[1], sender, p);
|
||||
case 3 -> handle3(args[0], args[1], args[2], sender, p);
|
||||
case 4 -> handle4(args[0], args[1], args[2], args[3], sender, p);
|
||||
default -> Locales.send(sender, "commands.too_many_args");
|
||||
}
|
||||
}
|
||||
|
||||
private void handle2(String a1, String a2, CommandSender sender, @Nullable Player p) {
|
||||
if (isAdminCommand(a1)) {
|
||||
admin.handle(a1, a2, sender, p);
|
||||
return;
|
||||
}
|
||||
if (p == null) {
|
||||
return;
|
||||
}
|
||||
if (canCreateSchematic(a1, p)) {
|
||||
SchematicCommandHandler.handleCreateDifficultyOnly(a2, sender, p, player);
|
||||
return;
|
||||
}
|
||||
handlePlayerSubcommand(a1, a2, sender, p);
|
||||
}
|
||||
|
||||
private static boolean isAdminCommand(String a1) {
|
||||
return switch (a1.toLowerCase()) {
|
||||
case "forcejoin", "forceleave", "reset", "recoverinventory" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean canCreateSchematic(String a1, Player p) {
|
||||
return "create".equalsIgnoreCase(a1) && p.hasPermission(ParkourOption.ADMIN.permission);
|
||||
}
|
||||
|
||||
private void handlePlayerSubcommand(String a1, String a2, CommandSender sender, Player p) {
|
||||
switch (a1.toLowerCase()) {
|
||||
case "join" -> join.join(a2, sender, p, player);
|
||||
case "leaderboard" -> leaderboard.open(a2, sender, p);
|
||||
case "schematic" -> {
|
||||
if (!p.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
CommandUtil.send(sender, Locales.getString(p, "other.no_do"));
|
||||
return;
|
||||
}
|
||||
SchematicCommandHandler.handleSubcommand(a2, sender, p, player);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handle3(String a1, String a2, String a3, CommandSender sender, @Nullable Player p) {
|
||||
if (p == null) {
|
||||
return;
|
||||
}
|
||||
if (!p.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("schematic".equalsIgnoreCase(a1)) {
|
||||
if ("create".equalsIgnoreCase(a2)) {
|
||||
SchematicCommandHandler.handleCreateDifficultyOnly(a3, sender, p, player);
|
||||
return;
|
||||
}
|
||||
SchematicCommandHandler.handleSubcommandWithName(a2, a3, sender, p);
|
||||
}
|
||||
}
|
||||
|
||||
private void handle4(String a1, String a2, String a3, String a4, CommandSender sender, @Nullable Player p) {
|
||||
if (p == null) {
|
||||
return;
|
||||
}
|
||||
if (!p.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
return;
|
||||
}
|
||||
if (!"schematic".equalsIgnoreCase(a1) || !"create".equalsIgnoreCase(a2)) {
|
||||
return;
|
||||
}
|
||||
SchematicCommandHandler.handleCreateWithDifficulty(a3, a4, sender, p, player);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
package dev.loki.loparkour.command.core;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
class CommandTabCompleter {
|
||||
|
||||
private static final List<String> SCHEMATIC_DIFFICULTIES = List.of("0.0", "0.25", "0.5", "0.75", "1.0");
|
||||
private static final List<String> SCHEMATIC_SUBCOMMANDS = List.of(
|
||||
"wand", "pos1", "pos2", "create", "convert", "paste", "list", "reload"
|
||||
);
|
||||
|
||||
private CommandTabCompleter() {
|
||||
}
|
||||
|
||||
static List<String> complete(@NotNull CommandSender sender, @NotNull String[] args) {
|
||||
if (args.length == 1) {
|
||||
return completeFirst(sender, args[0]);
|
||||
}
|
||||
if (args.length == 2) {
|
||||
return completeSecond(sender, args[0], args[1]);
|
||||
}
|
||||
if (args.length == 3 && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
return completeThird(args[0], args[1], args[2]);
|
||||
}
|
||||
if (args.length == 4 && sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
return completeFourth(args[0], args[1], args[3]);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private static List<String> completeFirst(CommandSender sender, String input) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
if (ParkourOption.JOIN.mayPerform(sender)) {
|
||||
completions.add("join");
|
||||
completions.add("leave");
|
||||
}
|
||||
if (ParkourOption.MAIN.mayPerform(sender)) {
|
||||
completions.add("menu");
|
||||
}
|
||||
if (ParkourOption.PLAY.mayPerform(sender)) {
|
||||
completions.add("play");
|
||||
}
|
||||
if (ParkourOption.LEADERBOARDS.mayPerform(sender)) {
|
||||
completions.add("leaderboard");
|
||||
}
|
||||
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.addAll(List.of("schematic", "create", "reload", "forcejoin", "forceleave", "reset", "recoverinventory"));
|
||||
}
|
||||
return filter(input, completions);
|
||||
}
|
||||
|
||||
private static List<String> completeSecond(CommandSender sender, String a1Raw, String input) {
|
||||
String a1 = a1Raw.toLowerCase();
|
||||
return switch (a1) {
|
||||
case "reset" -> completeReset(sender, input);
|
||||
case "join" -> completeJoin(sender, input);
|
||||
case "leaderboard" -> completeLeaderboard(sender, input);
|
||||
case "schematic" -> completeSchematic(sender, input);
|
||||
case "create" -> completeCreate(sender, input);
|
||||
case "forcejoin", "forceleave" -> completeForce(sender, a1, input);
|
||||
case "recoverinventory" -> completeRecoverInventory(sender, input);
|
||||
default -> Collections.emptyList();
|
||||
};
|
||||
}
|
||||
|
||||
private static List<String> completeReset(CommandSender sender, String input) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.add("everyone");
|
||||
ParkourPlayer.getPlayers().forEach(pp -> completions.add(pp.getName()));
|
||||
}
|
||||
return filter(input, completions);
|
||||
}
|
||||
|
||||
private static List<String> completeJoin(CommandSender sender, String input) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
if (ParkourOption.JOIN.mayPerform(sender)) {
|
||||
Registry.getModes().forEach(m -> completions.add(m.getName()));
|
||||
ParkourPlayer.getPlayers().forEach(pp -> completions.add(pp.getName()));
|
||||
}
|
||||
return filter(input, completions);
|
||||
}
|
||||
|
||||
private static List<String> completeLeaderboard(CommandSender sender, String input) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
if (ParkourOption.LEADERBOARDS.mayPerform(sender)) {
|
||||
Registry.getModes().forEach(m -> completions.add(m.getName()));
|
||||
}
|
||||
return filter(input, completions);
|
||||
}
|
||||
|
||||
private static List<String> completeSchematic(CommandSender sender, String input) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.addAll(SCHEMATIC_SUBCOMMANDS);
|
||||
}
|
||||
return filter(input, completions);
|
||||
}
|
||||
|
||||
private static List<String> completeCreate(CommandSender sender, String input) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.addAll(SCHEMATIC_DIFFICULTIES);
|
||||
}
|
||||
return filter(input, completions);
|
||||
}
|
||||
|
||||
private static List<String> completeForce(CommandSender sender, String a1, String input) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
completions.add("everyone");
|
||||
if ("forcejoin".equals(a1)) {
|
||||
completions.add("nearest");
|
||||
}
|
||||
Bukkit.getOnlinePlayers().forEach(pl -> completions.add(pl.getName()));
|
||||
}
|
||||
return filter(input, completions);
|
||||
}
|
||||
|
||||
private static List<String> completeRecoverInventory(CommandSender sender, String input) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
|
||||
Bukkit.getOnlinePlayers().forEach(pl -> completions.add(pl.getName()));
|
||||
}
|
||||
return filter(input, completions);
|
||||
}
|
||||
|
||||
private static List<String> completeThird(String a1Raw, String a2Raw, String input) {
|
||||
if (!"schematic".equalsIgnoreCase(a1Raw)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return completeSchematicThird(a2Raw, input);
|
||||
}
|
||||
|
||||
private static List<String> completeSchematicThird(String a2Raw, String input) {
|
||||
List<String> completions = new ArrayList<>();
|
||||
if ("paste".equalsIgnoreCase(a2Raw)) {
|
||||
addLoadedSchematicIds(completions);
|
||||
} else if ("create".equalsIgnoreCase(a2Raw)) {
|
||||
completions.addAll(SCHEMATIC_DIFFICULTIES);
|
||||
}
|
||||
return filter(input, completions);
|
||||
}
|
||||
|
||||
private static List<String> completeFourth(String a1Raw, String a2Raw, String input) {
|
||||
if ("schematic".equalsIgnoreCase(a1Raw) && "create".equalsIgnoreCase(a2Raw)) {
|
||||
return filter(input, SCHEMATIC_DIFFICULTIES);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private static void addLoadedSchematicIds(@NotNull List<String> completions) {
|
||||
SchematicManager manager = LoParkour.getSchematicManager();
|
||||
if (manager == null) {
|
||||
return;
|
||||
}
|
||||
for (String id : manager.getAll().keySet()) {
|
||||
completions.add(id);
|
||||
String shortId = SchematicManager.configKey(id);
|
||||
if (!shortId.equalsIgnoreCase(id)) {
|
||||
completions.add(shortId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> filter(String input, List<String> options) {
|
||||
String lower = input.toLowerCase();
|
||||
return options.stream().filter(s -> s.toLowerCase().startsWith(lower)).toList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package dev.loki.loparkour.command.core;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.command.admin.AdminCommandHandler;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.command.TabCompleter;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Main command router for /LoParkour and /parkour.
|
||||
* Delegates to {@link PlayerCommandHandler}, {@link AdminCommandHandler},
|
||||
* and {@link SchematicCommandHandler}.
|
||||
*/
|
||||
public class LoParkourCommand implements CommandExecutor, TabCompleter {
|
||||
|
||||
private final PlayerCommandHandler player = new PlayerCommandHandler();
|
||||
private final AdminCommandHandler admin = new AdminCommandHandler(player);
|
||||
private final CommandRouter router = new CommandRouter(player, admin);
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String label, @NotNull String[] args) {
|
||||
try {
|
||||
Player p = sender instanceof Player ? (Player) sender : null;
|
||||
router.dispatch(args, sender, p);
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
LoParkour.getPlugin().getLogger().log(Level.SEVERE,
|
||||
"LoParkour command failed (" + label + " " + String.join(" ", args) + ")", t);
|
||||
Locales.send(sender, "commands.error");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String alias, @NotNull String[] args) {
|
||||
return CommandTabCompleter.complete(sender, args);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package dev.loki.loparkour.command.player;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.command.util.CommandUtil;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.mode.base.MultiMode;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class JoinCommandExecutor {
|
||||
|
||||
public void join(String arg, CommandSender sender, Player p, PlayerCommandHandler player) {
|
||||
if (!player.cooldown(sender, "join", 2500) || !ParkourOption.JOIN.mayPerform(p)) {
|
||||
return;
|
||||
}
|
||||
if (Registry.getMode(arg) instanceof Mode mode) {
|
||||
mode.create(p);
|
||||
return;
|
||||
}
|
||||
joinPlayingPlayer(arg, sender, p);
|
||||
}
|
||||
|
||||
private void joinPlayingPlayer(String arg, CommandSender sender, Player p) {
|
||||
Player other = Bukkit.getPlayer(arg);
|
||||
if (other == null) {
|
||||
CommandUtil.send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.unknown_player"));
|
||||
return;
|
||||
}
|
||||
ParkourPlayer pp = ParkourPlayer.getPlayer(other);
|
||||
if (pp == null) {
|
||||
CommandUtil.send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.not_playing"));
|
||||
return;
|
||||
}
|
||||
joinSession(sender, p, pp.session);
|
||||
}
|
||||
|
||||
private void joinSession(CommandSender sender, Player p, Session session) {
|
||||
ParkourUser user = ParkourUser.getUser(p);
|
||||
if (user != null && user.session == session) {
|
||||
return;
|
||||
}
|
||||
if (session.isAcceptingPlayers()) {
|
||||
joinAcceptingSession(p, session);
|
||||
} else {
|
||||
Modes.SPECTATOR.create(p, session);
|
||||
}
|
||||
}
|
||||
|
||||
private void joinAcceptingSession(Player p, Session session) {
|
||||
Mode sessionMode = session.generator.getMode();
|
||||
if (sessionMode instanceof MultiMode mm) {
|
||||
mm.join(p, session);
|
||||
} else {
|
||||
Modes.SPECTATOR.create(p, session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package dev.loki.loparkour.command.player;
|
||||
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.command.util.CommandUtil;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class LeaderboardCommandExecutor {
|
||||
|
||||
public void open(String arg, CommandSender sender, Player p) {
|
||||
if (!ParkourOption.LEADERBOARDS.mayPerform(p)) {
|
||||
CommandUtil.send(sender, Locales.getString(p, "other.no_do"));
|
||||
return;
|
||||
}
|
||||
Mode mode = Registry.getMode(arg.toLowerCase());
|
||||
if (mode == null) {
|
||||
Menus.LEADERBOARDS.open(p);
|
||||
} else {
|
||||
Menus.SINGLE_LEADERBOARD.open(p, mode, Leaderboard.Sort.SCORE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,27 +2,13 @@ package dev.loki.loparkour.command.schematic;
|
|||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.schematic.convert.LpschemConverter;
|
||||
import dev.loki.loparkour.schematic.core.ParkourSchematic;
|
||||
import dev.loki.loparkour.schematic.create.SchematicCreator;
|
||||
import dev.loki.loparkour.util.particle.ParticleUtil;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import dev.loki.loparkour.util.world.Locations;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
|
@ -38,97 +24,38 @@ public class SchematicCommandHandler {
|
|||
public static final Map<Player, Location[]> SELECTIONS = new HashMap<>();
|
||||
public static final NamespacedKey WAND_KEY = new NamespacedKey(LoParkour.getPlugin(), "schematic_wand");
|
||||
|
||||
private static ItemStack cachedWand;
|
||||
|
||||
@NotNull
|
||||
public static ItemStack getWand() {
|
||||
return buildWand().clone();
|
||||
return SchematicWand.getWand();
|
||||
}
|
||||
|
||||
public static boolean isWand(@Nullable ItemStack item) {
|
||||
if (item == null || !item.hasItemMeta()) {
|
||||
return false;
|
||||
}
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
return meta != null
|
||||
&& meta.getPersistentDataContainer().has(WAND_KEY, PersistentDataType.BYTE);
|
||||
return SchematicWand.isWand(item);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Location[] getSelection(@NotNull Player player) {
|
||||
Location[] sel = SELECTIONS.get(player);
|
||||
if (sel == null) {
|
||||
sel = new Location[]{null, null};
|
||||
SELECTIONS.put(player, sel);
|
||||
}
|
||||
return sel;
|
||||
return SelectionStore.get(player);
|
||||
}
|
||||
|
||||
public static void setPos(@NotNull Player player, @NotNull Location loc, int index) {
|
||||
Location[] updated = getSelection(player).clone();
|
||||
updated[index] = loc.clone();
|
||||
SELECTIONS.put(player, updated);
|
||||
SelectionStore.set(player, loc, index);
|
||||
}
|
||||
|
||||
public static void clearWandCache() {
|
||||
cachedWand = null;
|
||||
SchematicWand.clearCache();
|
||||
}
|
||||
|
||||
public static void sendHelp(@NotNull Player player) {
|
||||
send(player, "");
|
||||
send(player, Locales.getString(player, "schematic.help.header"));
|
||||
send(player, "");
|
||||
send(player, Locales.getString(player, "schematic.help.line_wand"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_pos1"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_pos2"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_create"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_create_named"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_convert"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_paste"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_list"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_reload"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_folder"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_docs"));
|
||||
send(player, "");
|
||||
SchematicMessenger.sendHelp(player);
|
||||
}
|
||||
|
||||
public static void handleSubcommand(String sub, CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
|
||||
Location playerLoc = player.getLocation();
|
||||
Location[] sel = getSelection(player);
|
||||
|
||||
switch (sub.toLowerCase()) {
|
||||
case "wand" -> {
|
||||
player.getInventory().addItem(getWand());
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.wand_given"));
|
||||
}
|
||||
case "pos1" -> setPosCommand(player, sel, playerLoc, 0);
|
||||
case "pos2" -> setPosCommand(player, sel, playerLoc, 1);
|
||||
case "list" -> {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.list_header"));
|
||||
LoParkour.getSchematicManager().getAll().forEach((name, s) ->
|
||||
send(player, Locales.getString(player, "schematic.list_entry")
|
||||
.formatted(name, s.getDifficulty(), s.getFormat().name().toLowerCase())));
|
||||
}
|
||||
case "reload" -> {
|
||||
if (!cooldowns.cooldown(sender, "schematic-reload", 2500)) {
|
||||
return;
|
||||
}
|
||||
Config.reload(false);
|
||||
LoParkour.getSchematicManager().reload();
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.reloaded"));
|
||||
}
|
||||
case "convert" -> handleConvert(sender, player, cooldowns);
|
||||
default -> sendHelp(player);
|
||||
}
|
||||
SchematicDispatcher.dispatch(sub, sender, player, cooldowns);
|
||||
}
|
||||
|
||||
public static void handleSubcommandWithName(String sub, String name, CommandSender sender, Player player) {
|
||||
switch (sub.toLowerCase()) {
|
||||
case "paste" -> handlePaste(name, sender, player);
|
||||
case "create" -> handleCreate(name, null, sender, player);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
SchematicDispatcher.dispatchWithName(sub, name, sender, player);
|
||||
}
|
||||
|
||||
/** {@code schematic create <name> <difficulty>} */
|
||||
|
|
@ -139,10 +66,9 @@ public class SchematicCommandHandler {
|
|||
Player player,
|
||||
PlayerCommandHandler cooldowns
|
||||
) {
|
||||
if (!cooldowns.cooldown(sender, "schematic-create", 2500)) {
|
||||
return;
|
||||
if (cooldowns.cooldown(sender, "schematic-create", 2500)) {
|
||||
SchematicHandler.create(name, difficultyRaw, sender, player);
|
||||
}
|
||||
handleCreate(name, difficultyRaw, sender, player);
|
||||
}
|
||||
|
||||
/** {@code create <difficulty>} or {@code schematic create <difficulty>} */
|
||||
|
|
@ -152,123 +78,12 @@ public class SchematicCommandHandler {
|
|||
Player player,
|
||||
PlayerCommandHandler cooldowns
|
||||
) {
|
||||
if (!cooldowns.cooldown(sender, "schematic-create", 2500)) {
|
||||
return;
|
||||
if (cooldowns.cooldown(sender, "schematic-create", 2500)) {
|
||||
SchematicHandler.create(null, difficultyRaw, sender, player);
|
||||
}
|
||||
handleCreate(null, difficultyRaw, sender, player);
|
||||
}
|
||||
|
||||
private static void handleCreate(
|
||||
@Nullable String optionalName,
|
||||
@NotNull String difficultyRaw,
|
||||
CommandSender sender,
|
||||
Player player
|
||||
) {
|
||||
Location[] sel = SELECTIONS.get(player);
|
||||
if (sel == null || sel[0] == null || sel[1] == null) {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.selection_incomplete"));
|
||||
return;
|
||||
}
|
||||
|
||||
double difficulty;
|
||||
try {
|
||||
difficulty = SchematicCreator.parseDifficulty(difficultyRaw);
|
||||
} catch (NumberFormatException e) {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_difficulty"));
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_difficulty"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SchematicCreator.CreateResult result = SchematicCreator.create(sel[0], sel[1], optionalName, difficulty);
|
||||
LoParkour.getSchematicManager().reload();
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.created")
|
||||
.formatted(result.stem(), result.difficulty(), result.file().getName()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_name"));
|
||||
} catch (Exception e) {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.create_failed").formatted(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleConvert(CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
|
||||
if (!cooldowns.cooldown(sender, "schematic-convert", 5000)) {
|
||||
return;
|
||||
}
|
||||
LpschemConverter.ConvertResult result = LpschemConverter.convertAll();
|
||||
if (result.converted() > 0) {
|
||||
LoParkour.getSchematicManager().reload();
|
||||
}
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.convert_done")
|
||||
.formatted(result.converted(), result.failed()));
|
||||
for (String line : result.messages()) {
|
||||
if (result.messages().size() <= 12) {
|
||||
send(player, "&#A0A0A0 " + line);
|
||||
}
|
||||
}
|
||||
if (result.messages().size() > 12) {
|
||||
send(player, Locales.getString(player, "schematic.convert_truncated").formatted(result.messages().size() - 12));
|
||||
}
|
||||
}
|
||||
|
||||
private static void setPosCommand(Player player, Location[] existing, Location loc, int index) {
|
||||
setPos(player, loc, index);
|
||||
String label = index == 0 ? "1" : "2";
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.pos_set")
|
||||
.formatted(label, Locations.toString(loc, true)));
|
||||
|
||||
Location[] updated = getSelection(player);
|
||||
if (updated[0] != null && updated[1] != null) {
|
||||
ParticleUtil.box(BoundingBox.of(updated[0], updated[1]), player.getWorld(), Particle.END_ROD, player, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handlePaste(String name, CommandSender sender, Player player) {
|
||||
if (!isValidSchematicName(name)) {
|
||||
send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourSchematic schematic = LoParkour.getSchematicManager().get(name);
|
||||
if (schematic == null) {
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.not_found").formatted(name));
|
||||
return;
|
||||
}
|
||||
schematic.paste(player.getLocation(), player.getWorld());
|
||||
send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.pasted").formatted(name));
|
||||
}
|
||||
|
||||
static boolean isValidSchematicName(String name) {
|
||||
if (name == null || name.isEmpty() || name.length() > 64) {
|
||||
return false;
|
||||
}
|
||||
return name.matches("^[a-zA-Z0-9_-]+$");
|
||||
return SchematicHandler.isValidSchematicName(name);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static ItemStack buildWand() {
|
||||
if (cachedWand != null) {
|
||||
return cachedWand;
|
||||
}
|
||||
|
||||
String locale = Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
|
||||
if (locale == null || locale.isBlank()) {
|
||||
locale = "en";
|
||||
}
|
||||
|
||||
ItemStack item = Locales.getItem(locale, "schematic.wand").build();
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.getPersistentDataContainer().set(WAND_KEY, PersistentDataType.BYTE, (byte) 1);
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
cachedWand = item;
|
||||
return item;
|
||||
}
|
||||
|
||||
private static void send(CommandSender sender, String msg) {
|
||||
sender.sendMessage(ColorUtil.color(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package dev.loki.loparkour.command.schematic;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
final class SchematicDispatcher {
|
||||
|
||||
private SchematicDispatcher() {
|
||||
}
|
||||
|
||||
static void dispatch(String sub, CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
|
||||
Location playerLoc = player.getLocation();
|
||||
Location[] sel = SchematicCommandHandler.getSelection(player);
|
||||
|
||||
switch (sub.toLowerCase()) {
|
||||
case "wand" -> {
|
||||
player.getInventory().addItem(SchematicCommandHandler.getWand());
|
||||
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.wand_given"));
|
||||
}
|
||||
case "pos1" -> SchematicHandler.posCommand(player, sel, playerLoc, 0);
|
||||
case "pos2" -> SchematicHandler.posCommand(player, sel, playerLoc, 1);
|
||||
case "list" -> {
|
||||
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.list_header"));
|
||||
LoParkour.getSchematicManager().getAll().forEach((name, s) ->
|
||||
SchematicMessenger.send(player, Locales.getString(player, "schematic.list_entry")
|
||||
.formatted(name, s.getDifficulty(), s.getFormat().name().toLowerCase())));
|
||||
}
|
||||
case "reload" -> {
|
||||
if (!cooldowns.cooldown(sender, "schematic-reload", 2500)) {
|
||||
return;
|
||||
}
|
||||
Config.reload(false);
|
||||
LoParkour.getSchematicManager().reload();
|
||||
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.reloaded"));
|
||||
}
|
||||
case "convert" -> SchematicHandler.convert(sender, player, cooldowns);
|
||||
default -> SchematicMessenger.sendHelp(player);
|
||||
}
|
||||
}
|
||||
|
||||
static void dispatchWithName(String sub, String name, CommandSender sender, Player player) {
|
||||
switch (sub.toLowerCase()) {
|
||||
case "paste" -> SchematicHandler.paste(name, sender, player);
|
||||
case "create" -> SchematicHandler.create(name, null, sender, player);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
package dev.loki.loparkour.command.schematic;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.command.player.PlayerCommandHandler;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.schematic.convert.LpschemConverter;
|
||||
import dev.loki.loparkour.schematic.core.ParkourSchematic;
|
||||
import dev.loki.loparkour.schematic.create.SchematicCreator;
|
||||
import dev.loki.loparkour.util.particle.ParticleUtil;
|
||||
import dev.loki.loparkour.util.world.Locations;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.BoundingBox;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
final class SchematicHandler {
|
||||
|
||||
private SchematicHandler() {
|
||||
}
|
||||
|
||||
static void create(
|
||||
@Nullable String optionalName,
|
||||
@NotNull String difficultyRaw,
|
||||
CommandSender sender,
|
||||
Player player
|
||||
) {
|
||||
Location[] sel = SchematicCommandHandler.SELECTIONS.get(player);
|
||||
if (sel == null || sel[0] == null || sel[1] == null) {
|
||||
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.selection_incomplete"));
|
||||
return;
|
||||
}
|
||||
|
||||
double difficulty;
|
||||
try {
|
||||
difficulty = SchematicCreator.parseDifficulty(difficultyRaw);
|
||||
} catch (IllegalArgumentException e) {
|
||||
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_difficulty"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SchematicCreator.CreateResult result = SchematicCreator.create(sel[0], sel[1], optionalName, difficulty);
|
||||
LoParkour.getSchematicManager().reload();
|
||||
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.created")
|
||||
.formatted(result.stem(), result.difficulty(), result.file().getName()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_name"));
|
||||
} catch (IOException e) {
|
||||
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.create_failed")
|
||||
.formatted(e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
static void paste(String name, CommandSender sender, Player player) {
|
||||
if (!isValidSchematicName(name)) {
|
||||
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.invalid_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourSchematic schematic = LoParkour.getSchematicManager().get(name);
|
||||
if (schematic == null) {
|
||||
SchematicMessenger.send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.not_found")
|
||||
.formatted(name));
|
||||
return;
|
||||
}
|
||||
schematic.paste(player.getLocation(), player.getWorld());
|
||||
SchematicMessenger.send(sender, LoParkour.PREFIX + Locales.getString(sender, "schematic.pasted")
|
||||
.formatted(name));
|
||||
}
|
||||
|
||||
static void convert(CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
|
||||
if (!cooldowns.cooldown(sender, "schematic-convert", 5000)) {
|
||||
return;
|
||||
}
|
||||
LpschemConverter.ConvertResult result = LpschemConverter.convertAll();
|
||||
if (result.converted() > 0) {
|
||||
LoParkour.getSchematicManager().reload();
|
||||
}
|
||||
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.convert_done")
|
||||
.formatted(result.converted(), result.failed()));
|
||||
for (String line : result.messages()) {
|
||||
if (result.messages().size() <= 12) {
|
||||
SchematicMessenger.send(player, "&#A0A0A0 " + line);
|
||||
}
|
||||
}
|
||||
if (result.messages().size() > 12) {
|
||||
SchematicMessenger.send(player, Locales.getString(player, "schematic.convert_truncated")
|
||||
.formatted(result.messages().size() - 12));
|
||||
}
|
||||
}
|
||||
|
||||
static void posCommand(Player player, Location[] existing, Location loc, int index) {
|
||||
SchematicCommandHandler.setPos(player, loc, index);
|
||||
String label = index == 0 ? "1" : "2";
|
||||
SchematicMessenger.send(player, LoParkour.PREFIX + Locales.getString(player, "schematic.pos_set")
|
||||
.formatted(label, Locations.toString(loc, true)));
|
||||
|
||||
Location[] updated = SchematicCommandHandler.getSelection(player);
|
||||
if (updated[0] != null && updated[1] != null) {
|
||||
ParticleUtil.box(BoundingBox.of(updated[0], updated[1]), player.getWorld(), Particle.END_ROD, player, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isValidSchematicName(String name) {
|
||||
if (name == null || name.isEmpty() || name.length() > 64) {
|
||||
return false;
|
||||
}
|
||||
return name.matches("^[a-zA-Z0-9_-]+$");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package dev.loki.loparkour.command.schematic;
|
||||
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
final class SchematicMessenger {
|
||||
|
||||
private SchematicMessenger() {
|
||||
}
|
||||
|
||||
static void send(CommandSender sender, String msg) {
|
||||
sender.sendMessage(ColorUtil.color(msg));
|
||||
}
|
||||
|
||||
static void sendHelp(@NotNull Player player) {
|
||||
send(player, "");
|
||||
send(player, Locales.getString(player, "schematic.help.header"));
|
||||
send(player, "");
|
||||
send(player, Locales.getString(player, "schematic.help.line_wand"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_pos1"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_pos2"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_create"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_create_named"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_convert"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_paste"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_list"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_reload"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_folder"));
|
||||
send(player, Locales.getString(player, "schematic.help.line_docs"));
|
||||
send(player, "");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package dev.loki.loparkour.command.schematic;
|
||||
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
|
||||
final class SchematicWand {
|
||||
|
||||
private static ItemStack cachedWand;
|
||||
|
||||
private SchematicWand() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static ItemStack getWand() {
|
||||
return build().clone();
|
||||
}
|
||||
|
||||
static boolean isWand(@Nullable ItemStack item) {
|
||||
if (item == null || !item.hasItemMeta()) {
|
||||
return false;
|
||||
}
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
return meta != null
|
||||
&& meta.getPersistentDataContainer().has(SchematicCommandHandler.WAND_KEY, PersistentDataType.BYTE);
|
||||
}
|
||||
|
||||
static void clearCache() {
|
||||
cachedWand = null;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static ItemStack build() {
|
||||
if (cachedWand != null) {
|
||||
return cachedWand;
|
||||
}
|
||||
|
||||
String locale = Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
|
||||
if (locale == null || locale.isBlank()) {
|
||||
locale = "en";
|
||||
}
|
||||
|
||||
ItemStack item = Locales.getItem(locale, "schematic.wand").build();
|
||||
ItemMeta meta = item.getItemMeta();
|
||||
if (meta != null) {
|
||||
meta.getPersistentDataContainer().set(SchematicCommandHandler.WAND_KEY, PersistentDataType.BYTE, (byte) 1);
|
||||
item.setItemMeta(meta);
|
||||
}
|
||||
cachedWand = item;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package dev.loki.loparkour.command.schematic;
|
||||
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
final class SelectionStore {
|
||||
|
||||
private SelectionStore() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static Location[] get(@NotNull Player player) {
|
||||
Location[] sel = SchematicCommandHandler.SELECTIONS.get(player);
|
||||
if (sel == null) {
|
||||
sel = new Location[]{null, null};
|
||||
SchematicCommandHandler.SELECTIONS.put(player, sel);
|
||||
}
|
||||
return sel;
|
||||
}
|
||||
|
||||
static void set(@NotNull Player player, @NotNull Location loc, int index) {
|
||||
Location[] updated = get(player).clone();
|
||||
updated[index] = loc.clone();
|
||||
SchematicCommandHandler.SELECTIONS.put(player, updated);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package dev.loki.loparkour.command.util;
|
||||
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
public final class CommandUtil {
|
||||
|
||||
private CommandUtil() {
|
||||
}
|
||||
|
||||
public static void send(CommandSender sender, String msg) {
|
||||
sender.sendMessage(ColorUtil.color(msg));
|
||||
}
|
||||
}
|
||||
|
|
@ -35,14 +35,13 @@ public class ConfigLoader {
|
|||
}
|
||||
|
||||
public static void updateConfig(@NotNull String fileName, @NotNull File configFile, @Nullable List<String> ignoredSections) {
|
||||
try {
|
||||
var plugin = LoParkour.getPlugin();
|
||||
var resourceStream = plugin.getResource(fileName);
|
||||
var plugin = LoParkour.getPlugin();
|
||||
try (var resourceStream = plugin.getResource(fileName)) {
|
||||
if (resourceStream != null) {
|
||||
ConfigUpdater.update(configFile, resourceStream, "1.0", ignoredSections, plugin.getLogger());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LoParkour.getPlugin().getLogger().log(java.util.logging.Level.SEVERE,
|
||||
plugin.getLogger().log(java.util.logging.Level.SEVERE,
|
||||
"Error while trying to update config file: " + fileName, ex);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,108 @@
|
|||
package dev.loki.loparkour.config.core;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Reads YAML as raw lines and extracts the full path of each scalar value
|
||||
* (e.g. "sql.enabled") so that user values can be matched against the template.
|
||||
*/
|
||||
final class ConfigMergeParser {
|
||||
|
||||
@NotNull
|
||||
List<String> readLines(@NotNull InputStream is) throws IOException {
|
||||
try (var r = new BufferedReader(new InputStreamReader(is))) {
|
||||
return r.lines().toList();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
List<String> readLines(@NotNull File file) throws IOException {
|
||||
return Files.readAllLines(file.toPath());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Map<String, String> extractFullPathValues(@NotNull List<String> lines) {
|
||||
Map<String, String> values = new LinkedHashMap<>();
|
||||
Deque<String> stack = new ArrayDeque<>();
|
||||
int prevIndent = -1;
|
||||
|
||||
for (String line : lines) {
|
||||
LineTokens tokens = parseLine(line);
|
||||
if (tokens == null) {
|
||||
continue;
|
||||
}
|
||||
prevIndent = popToLevel(stack, tokens.indent(), prevIndent);
|
||||
String fullPath = resolvePath(stack, tokens.leaf());
|
||||
if (tokens.isSection()) {
|
||||
stack.push(tokens.leaf());
|
||||
} else {
|
||||
values.put(fullPath, tokens.after());
|
||||
}
|
||||
prevIndent = tokens.indent();
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static LineTokens parseLine(@NotNull String line) {
|
||||
if (isSkippable(line)) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = line.trim();
|
||||
int colon = trimmed.indexOf(':');
|
||||
if (colon == -1) {
|
||||
return null;
|
||||
}
|
||||
String leaf = trimmed.substring(0, colon).trim();
|
||||
String after = trimmed.substring(colon + 1).trim();
|
||||
return new LineTokens(leaf, after, indentOf(line), isSection(after));
|
||||
}
|
||||
|
||||
private static boolean isSkippable(@NotNull String line) {
|
||||
String trimmed = line.trim();
|
||||
return trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("-");
|
||||
}
|
||||
|
||||
private static boolean isSection(@NotNull String after) {
|
||||
return after.isEmpty() || after.startsWith("#");
|
||||
}
|
||||
|
||||
static int popToLevel(@NotNull Deque<String> stack, int indent, int prevIndent) {
|
||||
while (prevIndent >= 0 && indent <= prevIndent) {
|
||||
if (!stack.isEmpty()) {
|
||||
stack.pop();
|
||||
}
|
||||
prevIndent -= 2;
|
||||
}
|
||||
return prevIndent;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static String resolvePath(@NotNull Deque<String> stack, @NotNull String leaf) {
|
||||
return stack.isEmpty() ? leaf : String.join(".", stack) + "." + leaf;
|
||||
}
|
||||
|
||||
static int indentOf(@NotNull String line) {
|
||||
int i = 0;
|
||||
while (i < line.length() && line.charAt(i) == ' ') {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
record LineTokens(String leaf, String after, int indent, boolean isSection) {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package dev.loki.loparkour.config.core;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Builds the merged file by walking the template and substituting any scalar
|
||||
* value that the user has already customized, preserving comments and sections.
|
||||
*/
|
||||
final class ConfigMergeProcessor {
|
||||
|
||||
private final ConfigMergeParser parser = new ConfigMergeParser();
|
||||
|
||||
@NotNull
|
||||
List<String> merge(
|
||||
@NotNull InputStream templateStream,
|
||||
@NotNull File existingFile,
|
||||
@Nullable List<String> ignoredKeys) throws IOException {
|
||||
|
||||
List<String> templateLines = parser.readLines(templateStream);
|
||||
List<String> existingLines = existingFile.exists() ? parser.readLines(existingFile) : List.of();
|
||||
|
||||
Set<String> ignored = normalizeIgnored(ignoredKeys);
|
||||
Map<String, String> existingValues = parser.extractFullPathValues(existingLines);
|
||||
|
||||
List<String> result = new ArrayList<>();
|
||||
Deque<String> stack = new ArrayDeque<>();
|
||||
int prevIndent = -1;
|
||||
|
||||
for (String line : templateLines) {
|
||||
ConfigMergeParser.LineTokens tokens = ConfigMergeParser.parseLine(line);
|
||||
if (tokens == null) {
|
||||
result.add(line);
|
||||
continue;
|
||||
}
|
||||
prevIndent = ConfigMergeParser.popToLevel(stack, tokens.indent(), prevIndent);
|
||||
String fullPath = ConfigMergeParser.resolvePath(stack, tokens.leaf());
|
||||
if (tokens.isSection()) {
|
||||
stack.push(tokens.leaf());
|
||||
result.add(line);
|
||||
} else if (isIgnored(fullPath, ignored)) {
|
||||
result.add(line);
|
||||
} else {
|
||||
result.add(applyExistingValue(line, fullPath, existingValues));
|
||||
}
|
||||
prevIndent = tokens.indent();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String applyExistingValue(
|
||||
@NotNull String line,
|
||||
@NotNull String fullPath,
|
||||
@NotNull Map<String, String> existingValues) {
|
||||
|
||||
String existing = existingValues.get(fullPath);
|
||||
if (existing == null) {
|
||||
return line;
|
||||
}
|
||||
return line.substring(0, line.indexOf(':') + 1) + " " + existing;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Set<String> normalizeIgnored(@Nullable List<String> keys) {
|
||||
if (keys == null) {
|
||||
return Set.of();
|
||||
}
|
||||
Set<String> s = new HashSet<>();
|
||||
for (String k : keys) {
|
||||
s.add(k.trim().toLowerCase());
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static boolean isIgnored(@NotNull String fullPath, @NotNull Set<String> ignored) {
|
||||
String lower = fullPath.toLowerCase();
|
||||
for (String ign : ignored) {
|
||||
if (lower.equals(ign) || lower.startsWith(ign + ".")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,21 +3,12 @@ package dev.loki.loparkour.config.core;
|
|||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
|
|
@ -52,7 +43,7 @@ public class ConfigUpdater {
|
|||
|
||||
File backup = createBackup(configFile);
|
||||
try {
|
||||
List<String> merged = merge(templateStream, configFile, ignoredKeys);
|
||||
List<String> merged = new ConfigMergeProcessor().merge(templateStream, configFile, ignoredKeys);
|
||||
Files.write(configFile.toPath(), merged);
|
||||
logger.info("Configuration file synced: " + configFile.getName());
|
||||
} catch (Exception e) {
|
||||
|
|
@ -64,120 +55,6 @@ public class ConfigUpdater {
|
|||
}
|
||||
}
|
||||
|
||||
// ── merge ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@NotNull
|
||||
private List<String> merge(
|
||||
@NotNull InputStream templateStream,
|
||||
@NotNull File existingFile,
|
||||
@Nullable List<String> ignoredKeys) throws IOException {
|
||||
|
||||
List<String> templateLines = readLines(templateStream);
|
||||
List<String> existingLines = existingFile.exists() ? readLines(existingFile) : List.of();
|
||||
|
||||
Set<String> ignored = normalizeIgnored(ignoredKeys);
|
||||
Map<String, String> existingValues = extractFullPathValues(existingLines);
|
||||
|
||||
List<String> result = new ArrayList<>();
|
||||
Deque<String> stack = new ArrayDeque<>();
|
||||
int prevIndent = -1;
|
||||
|
||||
for (String line : templateLines) {
|
||||
String trimmed = line.trim();
|
||||
|
||||
if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("-")) {
|
||||
result.add(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
int colon = trimmed.indexOf(':');
|
||||
if (colon == -1) {
|
||||
result.add(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
String leaf = trimmed.substring(0, colon).trim();
|
||||
String after = trimmed.substring(colon + 1).trim();
|
||||
int indent = indentOf(line);
|
||||
|
||||
while (prevIndent >= 0 && indent <= prevIndent) {
|
||||
if (!stack.isEmpty()) {
|
||||
stack.pop();
|
||||
}
|
||||
prevIndent -= 2;
|
||||
}
|
||||
|
||||
String fullPath = stack.isEmpty() ? leaf : String.join(".", stack) + "." + leaf;
|
||||
boolean section = after.isEmpty() || after.startsWith("#");
|
||||
|
||||
if (section) {
|
||||
stack.push(leaf);
|
||||
prevIndent = indent;
|
||||
result.add(line);
|
||||
} else if (isIgnored(fullPath, ignored)) {
|
||||
result.add(line);
|
||||
prevIndent = indent;
|
||||
} else {
|
||||
String existing = existingValues.get(fullPath);
|
||||
result.add(existing != null
|
||||
? line.substring(0, line.indexOf(':') + 1) + " " + existing
|
||||
: line);
|
||||
prevIndent = indent;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<String, String> extractFullPathValues(@NotNull List<String> lines) {
|
||||
Map<String, String> values = new LinkedHashMap<>();
|
||||
Deque<String> stack = new ArrayDeque<>();
|
||||
int prevIndent = -1;
|
||||
|
||||
for (String line : lines) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("-")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int colon = trimmed.indexOf(':');
|
||||
if (colon == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String leaf = trimmed.substring(0, colon).trim();
|
||||
String after = trimmed.substring(colon + 1).trim();
|
||||
int indent = indentOf(line);
|
||||
|
||||
while (prevIndent >= 0 && indent <= prevIndent) {
|
||||
if (!stack.isEmpty()) {
|
||||
stack.pop();
|
||||
}
|
||||
prevIndent -= 2;
|
||||
}
|
||||
|
||||
String fullPath = stack.isEmpty() ? leaf : String.join(".", stack) + "." + leaf;
|
||||
|
||||
if (after.isEmpty() || after.startsWith("#")) {
|
||||
stack.push(leaf);
|
||||
} else {
|
||||
values.put(fullPath, after);
|
||||
}
|
||||
prevIndent = indent;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static int indentOf(@NotNull String line) {
|
||||
int i = 0;
|
||||
while (i < line.length() && line.charAt(i) == ' ') {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private File createBackup(@NotNull File file) {
|
||||
if (!file.exists()) {
|
||||
|
|
@ -192,38 +69,4 @@ public class ConfigUpdater {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<String> readLines(@NotNull InputStream is) throws IOException {
|
||||
try (var r = new BufferedReader(new InputStreamReader(is))) {
|
||||
return r.lines().toList();
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static List<String> readLines(@NotNull File file) throws IOException {
|
||||
return Files.readAllLines(file.toPath());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Set<String> normalizeIgnored(@Nullable List<String> keys) {
|
||||
if (keys == null) {
|
||||
return Set.of();
|
||||
}
|
||||
Set<String> s = new HashSet<>();
|
||||
for (String k : keys) {
|
||||
s.add(k.trim().toLowerCase());
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
private static boolean isIgnored(@NotNull String fullPath, @NotNull Set<String> ignored) {
|
||||
String lower = fullPath.toLowerCase();
|
||||
for (String ign : ignored) {
|
||||
if (lower.equals(ign) || lower.startsWith(ign + ".")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +1,34 @@
|
|||
package dev.loki.loparkour.generator.jump.placement;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.event.ParkourSchematicGenerateEvent;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.jump.calculation.JumpCalculator;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.schematic.core.ParkourSchematic;
|
||||
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||
import dev.loki.loparkour.util.misc.Probs;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Refactored BlockPlacer using composition pattern.
|
||||
* Coordinates block selection, jump calculation, and placement.
|
||||
* Facade coordinating block selection, jump calculation, and placement.
|
||||
*/
|
||||
public class BlockPlacer {
|
||||
|
||||
|
||||
private final ParkourGenerator generator;
|
||||
private final BlockSelector blockSelector;
|
||||
private final JumpCalculator jumpCalculator;
|
||||
|
||||
private final BlockTargetSelector targetSelector;
|
||||
private final BlockWorldPlacer worldPlacer;
|
||||
private final SchematicBlockPlacer schematicPlacer;
|
||||
|
||||
public BlockPlacer(@NotNull ParkourGenerator generator) {
|
||||
this.generator = generator;
|
||||
this.blockSelector = new BlockSelector(generator);
|
||||
this.jumpCalculator = new JumpCalculator(generator, blockSelector);
|
||||
this.targetSelector = new BlockTargetSelector(generator, blockSelector, new JumpCalculator(generator, blockSelector));
|
||||
this.worldPlacer = new BlockWorldPlacer(generator);
|
||||
this.schematicPlacer = new SchematicBlockPlacer(generator, targetSelector);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generate a single block.
|
||||
*/
|
||||
|
|
@ -51,85 +45,60 @@ public class BlockPlacer {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean isTallMaterial(@NotNull Material mat) {
|
||||
return blockSelector.isFenceMaterial(mat) || blockSelector.isTrapdoorMaterial(mat) || mat == Material.GLASS_PANE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select blocks with a forced height override for tall materials (fences, trapdoors).
|
||||
*/
|
||||
@NotNull
|
||||
private List<Block> selectBlocks(int forcedHeight) {
|
||||
List<Block> blocks = new ArrayList<>();
|
||||
if (generator.state.history.isEmpty()) {
|
||||
return blocks;
|
||||
}
|
||||
Block current = generator.getLatest();
|
||||
int distance = Probs.random(generator.state.distanceChances);
|
||||
int height = forcedHeight != Integer.MIN_VALUE ? forcedHeight : Probs.random(generator.state.heightChances);
|
||||
Block nextBlock = jumpCalculator.calculateNextBlock(current, distance, height);
|
||||
blocks.add(nextBlock);
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the first block at specified locations.
|
||||
*/
|
||||
public void generateFirst(@NotNull Location spawn, @NotNull Location blockSpawn) {
|
||||
Block block = blockSpawn.getBlock();
|
||||
BlockData blockData = blockSelector.selectBlockData();
|
||||
|
||||
placeBlockData(block, blockData);
|
||||
|
||||
worldPlacer.place(block, blockData);
|
||||
generator.state.history.add(block);
|
||||
generator.state.playerSpawn = spawn;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete schematic blocks (if any).
|
||||
*/
|
||||
public void deleteSchematic() {
|
||||
if (generator.state.schematicBlocks != null) {
|
||||
generator.state.schematicBlocks.forEach(block -> block.setType(org.bukkit.Material.AIR));
|
||||
generator.state.schematicBlocks.clear();
|
||||
}
|
||||
worldPlacer.deleteSchematic();
|
||||
}
|
||||
|
||||
|
||||
private void generateSingleBlock() {
|
||||
ParkourGenerator.BlockGenerationType jumpType = selectGenerationType();
|
||||
|
||||
|
||||
switch (jumpType) {
|
||||
case DEFAULT -> placeNormalBlock();
|
||||
case SPECIAL -> placeSpecialBlock();
|
||||
case SCHEMATIC -> {
|
||||
if (!tryGenerateSchematic()) {
|
||||
placeNormalBlock(); // Fallback to normal block
|
||||
if (!schematicPlacer.placeSchematic()) {
|
||||
placeNormalBlock();
|
||||
}
|
||||
}
|
||||
default -> {}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@NotNull
|
||||
private ParkourGenerator.BlockGenerationType selectGenerationType() {
|
||||
return Probs.random(generator.state.defaultChances);
|
||||
}
|
||||
|
||||
|
||||
private void placeNormalBlock() {
|
||||
BlockData blockData = blockSelector.selectBlockData();
|
||||
if (blockData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Material mat = blockData.getMaterial();
|
||||
boolean tall = isTallMaterial(mat);
|
||||
|
||||
List<Block> blocks = tall ? selectBlocks(-1) : selectBlocks();
|
||||
boolean tall = targetSelector.isTallMaterial(blockData.getMaterial());
|
||||
List<Block> blocks = tall ? targetSelector.selectBlocks(-1) : targetSelector.selectBlocks();
|
||||
if (blocks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Block selectedBlock = blocks.get(0);
|
||||
placeBlockData(selectedBlock, blockData);
|
||||
worldPlacer.place(selectedBlock, blockData);
|
||||
generator.state.history.add(selectedBlock);
|
||||
}
|
||||
|
||||
|
|
@ -140,88 +109,14 @@ public class BlockPlacer {
|
|||
return;
|
||||
}
|
||||
|
||||
Material mat = specialBlockData.getMaterial();
|
||||
boolean tall = isTallMaterial(mat);
|
||||
|
||||
List<Block> blocks = tall ? selectBlocks(-1) : selectBlocks();
|
||||
boolean tall = targetSelector.isTallMaterial(specialBlockData.getMaterial());
|
||||
List<Block> blocks = tall ? targetSelector.selectBlocks(-1) : targetSelector.selectBlocks();
|
||||
if (blocks.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Block selectedBlock = blocks.get(0);
|
||||
placeBlockData(selectedBlock, specialBlockData);
|
||||
worldPlacer.place(selectedBlock, specialBlockData);
|
||||
generator.state.history.add(selectedBlock);
|
||||
}
|
||||
|
||||
/** Prefer no neighbor physics to avoid fragile vanilla multi-block checks in empty worlds. */
|
||||
private void placeBlockData(@NotNull Block block, @NotNull BlockData data) {
|
||||
block.setBlockData(data, false);
|
||||
|
||||
// Play effects for all players
|
||||
Location loc = block.getLocation().add(0.5, 0.5, 0.5);
|
||||
for (ParkourPlayer player : generator.getPlayers()) {
|
||||
// Spawn particles
|
||||
player.player.spawnParticle(Particle.BLOCK_CRACK, loc, 10, 0.3, 0.3, 0.3, 0.1, data);
|
||||
|
||||
// Play sound
|
||||
player.player.playSound(loc, Sound.BLOCK_STONE_PLACE, 0.5f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<Block> selectBlocks() {
|
||||
List<Block> blocks = new ArrayList<>();
|
||||
|
||||
if (generator.state.history.isEmpty()) {
|
||||
return blocks; // No previous blocks to work with
|
||||
}
|
||||
|
||||
Block current = generator.getLatest();
|
||||
|
||||
// Select jump parameters
|
||||
int distance = Probs.random(generator.state.distanceChances);
|
||||
int height = Probs.random(generator.state.heightChances);
|
||||
|
||||
// Calculate next block position
|
||||
Block nextBlock = jumpCalculator.calculateNextBlock(current, distance, height);
|
||||
blocks.add(nextBlock);
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
private boolean tryGenerateSchematic() {
|
||||
SchematicManager manager = LoParkour.getSchematicManager();
|
||||
if (manager == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double maxDifficulty = generator.getPlayers().stream()
|
||||
.mapToDouble(p -> p.schematicDifficulty)
|
||||
.min()
|
||||
.orElse(1.0);
|
||||
|
||||
ParkourSchematic schematic = manager.pick(maxDifficulty);
|
||||
if (schematic == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Block> blocks = selectBlocks();
|
||||
if (blocks.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Location pasteLocation = blocks.get(0).getLocation();
|
||||
List<Block> schematicBlocks = schematic.paste(pasteLocation, pasteLocation.getWorld());
|
||||
if (schematicBlocks.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
generator.state.history.addAll(schematicBlocks);
|
||||
generator.state.schematicBlocks = schematicBlocks;
|
||||
|
||||
for (ParkourPlayer player : generator.getPlayers()) {
|
||||
new ParkourSchematicGenerateEvent(schematic, generator, player).call();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package dev.loki.loparkour.generator.jump.placement;
|
||||
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.jump.calculation.JumpCalculator;
|
||||
import dev.loki.loparkour.util.misc.Probs;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.block.Block;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Computes the target block position for generated parkour blocks based on jump parameters.
|
||||
*/
|
||||
class BlockTargetSelector {
|
||||
|
||||
private static final int NO_FORCED_HEIGHT = Integer.MIN_VALUE;
|
||||
|
||||
private final ParkourGenerator generator;
|
||||
private final BlockSelector blockSelector;
|
||||
private final JumpCalculator jumpCalculator;
|
||||
|
||||
BlockTargetSelector(@NotNull ParkourGenerator generator, @NotNull BlockSelector blockSelector, @NotNull JumpCalculator jumpCalculator) {
|
||||
this.generator = generator;
|
||||
this.blockSelector = blockSelector;
|
||||
this.jumpCalculator = jumpCalculator;
|
||||
}
|
||||
|
||||
boolean isTallMaterial(@NotNull Material mat) {
|
||||
return blockSelector.isFenceMaterial(mat) || blockSelector.isTrapdoorMaterial(mat) || mat == Material.GLASS_PANE;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
List<Block> selectBlocks() {
|
||||
return selectBlocks(NO_FORCED_HEIGHT);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
List<Block> selectBlocks(int forcedHeight) {
|
||||
List<Block> blocks = new ArrayList<>();
|
||||
if (generator.state.history.isEmpty()) {
|
||||
return blocks;
|
||||
}
|
||||
Block current = generator.getLatest();
|
||||
int distance = Probs.random(generator.state.distanceChances);
|
||||
int height = forcedHeight != NO_FORCED_HEIGHT ? forcedHeight : Probs.random(generator.state.heightChances);
|
||||
Block nextBlock = jumpCalculator.calculateNextBlock(current, distance, height);
|
||||
blocks.add(nextBlock);
|
||||
return blocks;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package dev.loki.loparkour.generator.jump.placement;
|
||||
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Applies block data to the world and plays placement effects for all players.
|
||||
*/
|
||||
class BlockWorldPlacer {
|
||||
|
||||
private final ParkourGenerator generator;
|
||||
|
||||
BlockWorldPlacer(@NotNull ParkourGenerator generator) {
|
||||
this.generator = generator;
|
||||
}
|
||||
|
||||
void place(@NotNull Block block, @NotNull BlockData data) {
|
||||
block.setBlockData(data, false);
|
||||
|
||||
Location loc = block.getLocation().add(0.5, 0.5, 0.5);
|
||||
for (ParkourPlayer player : generator.getPlayers()) {
|
||||
player.player.spawnParticle(Particle.BLOCK_CRACK, loc, 10, 0.3, 0.3, 0.3, 0.1, data);
|
||||
player.player.playSound(loc, Sound.BLOCK_STONE_PLACE, 0.5f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void deleteSchematic() {
|
||||
if (generator.state.schematicBlocks != null) {
|
||||
generator.state.schematicBlocks.forEach(block -> block.setType(Material.AIR));
|
||||
generator.state.schematicBlocks.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
package dev.loki.loparkour.generator.jump.placement;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.event.ParkourSchematicGenerateEvent;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.schematic.core.ParkourSchematic;
|
||||
import dev.loki.loparkour.schematic.core.SchematicManager;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Pastes schematic parkour sections into the world and fires the related event.
|
||||
*/
|
||||
class SchematicBlockPlacer {
|
||||
|
||||
private final ParkourGenerator generator;
|
||||
private final BlockTargetSelector targetSelector;
|
||||
|
||||
SchematicBlockPlacer(@NotNull ParkourGenerator generator, @NotNull BlockTargetSelector targetSelector) {
|
||||
this.generator = generator;
|
||||
this.targetSelector = targetSelector;
|
||||
}
|
||||
|
||||
boolean placeSchematic() {
|
||||
SchematicManager manager = LoParkour.getSchematicManager();
|
||||
if (manager == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double maxDifficulty = generator.getPlayers().stream()
|
||||
.mapToDouble(p -> p.schematicDifficulty)
|
||||
.min()
|
||||
.orElse(1.0);
|
||||
|
||||
ParkourSchematic schematic = manager.pick(maxDifficulty);
|
||||
if (schematic == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Block> blocks = targetSelector.selectBlocks();
|
||||
if (blocks.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Location pasteLocation = blocks.get(0).getLocation();
|
||||
List<Block> schematicBlocks = schematic.paste(pasteLocation, pasteLocation.getWorld());
|
||||
if (schematicBlocks.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
generator.state.history.addAll(schematicBlocks);
|
||||
generator.state.schematicBlocks = schematicBlocks;
|
||||
|
||||
for (ParkourPlayer player : generator.getPlayers()) {
|
||||
new ParkourSchematicGenerateEvent(schematic, generator, player).call();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +1,13 @@
|
|||
package dev.loki.loparkour.hook.papi;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.spectator.ParkourSpectator;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class PAPIHook extends PlaceholderExpansion {
|
||||
|
||||
private static final Pattern INFINITE_REGEX = Pattern.compile("(.+)_(\\d+)");
|
||||
private final PlaceholderDispatcher dispatcher = new PlaceholderDispatcher();
|
||||
|
||||
@Override
|
||||
public @NotNull String getIdentifier() {
|
||||
|
|
@ -48,173 +35,7 @@ public class PAPIHook extends PlaceholderExpansion {
|
|||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"checkstyle:MethodLength", "checkstyle:CyclomaticComplexity"})
|
||||
public String onPlaceholderRequest(Player player, @NotNull String params) {
|
||||
// placeholders that don't require a player
|
||||
switch (params) {
|
||||
case "version", "ver" -> {
|
||||
return LoParkour.getPlugin().getDescription().getVersion();
|
||||
}
|
||||
case "leader", "record_player" -> {
|
||||
Score score = Modes.DEFAULT.getLeaderboard().getScoreAtRank(1);
|
||||
return score != null ? score.name() : "?";
|
||||
}
|
||||
case "leader_score", "record_score", "record" -> {
|
||||
Score score = Modes.DEFAULT.getLeaderboard().getScoreAtRank(1);
|
||||
return score != null ? Integer.toString(score.score()) : "?";
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
|
||||
if (params.contains("player_rank_")) {
|
||||
return getInfiniteScore(params.replace("player_rank_", ""), Score::name);
|
||||
} else if (params.contains("score_rank_")) {
|
||||
return getInfiniteScore(params.replace("score_rank_", ""), Score::score);
|
||||
} else if (params.contains("time_rank_")) {
|
||||
return getInfiniteScore(params.replace("time_rank_", ""), Score::time);
|
||||
} else if (params.contains("difficulty_rank_")) {
|
||||
return getInfiniteScore(params.replace("difficulty_rank_", ""), Score::difficulty);
|
||||
} else if (params.contains("difficulty_string_rank_")) {
|
||||
return getInfiniteScore(params.replace("difficulty_string_rank_", ""),
|
||||
score -> {
|
||||
String diff = score.difficulty();
|
||||
if ("?".equals(diff)) {
|
||||
return parseDifficulty(2.0);
|
||||
}
|
||||
try {
|
||||
return parseDifficulty(Double.parseDouble(diff));
|
||||
} catch (NumberFormatException e) {
|
||||
return parseDifficulty(2.0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// placeholders that require player
|
||||
if (player == null) {
|
||||
return "player doesn't exist";
|
||||
}
|
||||
|
||||
switch (params) {
|
||||
case "rank" -> {
|
||||
return Integer.toString(Modes.DEFAULT.getLeaderboard().getRank(player.getUniqueId()));
|
||||
}
|
||||
case "highscore", "high_score" -> {
|
||||
return Integer.toString(Modes.DEFAULT.getLeaderboard().get(player.getUniqueId()).score());
|
||||
}
|
||||
case "high_score_time" -> {
|
||||
return Modes.DEFAULT.getLeaderboard().get(player.getUniqueId()).time();
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
ParkourPlayer pp = null;
|
||||
if (user instanceof ParkourPlayer) {
|
||||
pp = (ParkourPlayer) user;
|
||||
} else if (user instanceof ParkourSpectator) {
|
||||
pp = ((ParkourSpectator) user).closest;
|
||||
}
|
||||
|
||||
if (pp != null && pp.session.generator != null) {
|
||||
ParkourGenerator generator = pp.session.generator;
|
||||
switch (params) {
|
||||
case "score", "current_score" -> {
|
||||
return Integer.toString(generator.state.score);
|
||||
}
|
||||
case "time", "current_time" -> {
|
||||
return generator.getFormattedTime();
|
||||
}
|
||||
case "blocklead", "lead" -> {
|
||||
return Integer.toString(pp.blockLead);
|
||||
}
|
||||
case "style" -> {
|
||||
return pp.style;
|
||||
}
|
||||
case "time_pref", "time_preference" -> {
|
||||
return Integer.toString(pp.selectedTime);
|
||||
}
|
||||
case "scoreboard" -> {
|
||||
return pp.showScoreboard.toString();
|
||||
}
|
||||
case "difficulty" -> {
|
||||
return Double.toString(pp.schematicDifficulty);
|
||||
}
|
||||
case "difficulty_string" -> {
|
||||
return parseDifficulty(pp.schematicDifficulty);
|
||||
}
|
||||
default -> {
|
||||
if (params.contains("score_until_")) {
|
||||
String replaced = params.replace("score_until_", "");
|
||||
try {
|
||||
int interval = Integer.parseInt(replaced);
|
||||
if (interval > 0) {
|
||||
return Integer.toString(interval - (generator.state.score % interval));
|
||||
} else {
|
||||
return "0";
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
return "0";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return dispatcher.dispatch(player, params);
|
||||
}
|
||||
|
||||
private String parseDifficulty(double difficulty) {
|
||||
if (difficulty <= 0.25) {
|
||||
return "easy";
|
||||
} else if (difficulty <= 0.5) {
|
||||
return "medium";
|
||||
} else if (difficulty <= 0.75) {
|
||||
return "hard";
|
||||
} else if (difficulty <= 1) {
|
||||
return "very hard";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
private String getInfiniteScore(String rankData, Function<Score, ?> f) {
|
||||
int rank;
|
||||
Leaderboard leaderboard = null;
|
||||
Matcher matcher = INFINITE_REGEX.matcher(rankData);
|
||||
|
||||
try {
|
||||
// use mode-specific format
|
||||
// x_mode_rank
|
||||
if (matcher.matches()) {
|
||||
String name = matcher.group(1);
|
||||
rank = Integer.parseInt(matcher.group(2));
|
||||
|
||||
Mode mode = Registry.getMode(name);
|
||||
if (mode != null) {
|
||||
leaderboard = mode.getLeaderboard();
|
||||
}
|
||||
// use generic format
|
||||
// x_rank
|
||||
} else {
|
||||
rank = Integer.parseInt(rankData);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
return "?";
|
||||
}
|
||||
|
||||
if (leaderboard == null) {
|
||||
leaderboard = Modes.DEFAULT.getLeaderboard();
|
||||
}
|
||||
|
||||
if (rank > 0) {
|
||||
Score score = leaderboard.getScoreAtRank(rank);
|
||||
|
||||
if (score == null) {
|
||||
return "?";
|
||||
} else {
|
||||
return String.valueOf(f.apply(score));
|
||||
}
|
||||
} else {
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package dev.loki.loparkour.hook.papi;
|
||||
|
||||
import dev.loki.loparkour.hook.papi.resolver.GlobalPlaceholderResolver;
|
||||
import dev.loki.loparkour.hook.papi.resolver.PlayerPlaceholderResolver;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
class PlaceholderDispatcher {
|
||||
|
||||
private final GlobalPlaceholderResolver globalResolver = new GlobalPlaceholderResolver();
|
||||
private final PlayerPlaceholderResolver playerResolver = new PlayerPlaceholderResolver();
|
||||
|
||||
String dispatch(Player player, @NotNull String params) {
|
||||
String resolved = globalResolver.resolve(params);
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
}
|
||||
if (player == null) {
|
||||
return "player doesn't exist";
|
||||
}
|
||||
return playerResolver.resolve(player, params);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package dev.loki.loparkour.hook.papi.resolver;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
|
||||
public class GlobalPlaceholderResolver {
|
||||
|
||||
private final ScoreRankResolver rankResolver = new ScoreRankResolver();
|
||||
private final PlaceholderFormatters formatters = new PlaceholderFormatters();
|
||||
|
||||
public String resolve(String params) {
|
||||
String value = resolveStatic(params);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
return resolveRanked(params);
|
||||
}
|
||||
|
||||
private String resolveStatic(String params) {
|
||||
switch (params) {
|
||||
case "version", "ver" -> {
|
||||
return LoParkour.getPlugin().getDescription().getVersion();
|
||||
}
|
||||
case "leader", "record_player" -> {
|
||||
Score score = Modes.DEFAULT.getLeaderboard().getScoreAtRank(1);
|
||||
return score != null ? score.name() : "?";
|
||||
}
|
||||
case "leader_score", "record_score", "record" -> {
|
||||
Score score = Modes.DEFAULT.getLeaderboard().getScoreAtRank(1);
|
||||
return score != null ? Integer.toString(score.score()) : "?";
|
||||
}
|
||||
default -> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveRanked(String params) {
|
||||
if (params.contains("player_rank_")) {
|
||||
return rankResolver.resolve(params.replace("player_rank_", ""), Score::name);
|
||||
}
|
||||
if (params.contains("score_rank_")) {
|
||||
return rankResolver.resolve(params.replace("score_rank_", ""), Score::score);
|
||||
}
|
||||
if (params.contains("time_rank_")) {
|
||||
return rankResolver.resolve(params.replace("time_rank_", ""), Score::time);
|
||||
}
|
||||
if (params.contains("difficulty_rank_")) {
|
||||
return rankResolver.resolve(params.replace("difficulty_rank_", ""), Score::difficulty);
|
||||
}
|
||||
if (params.contains("difficulty_string_rank_")) {
|
||||
return rankResolver.resolve(params.replace("difficulty_string_rank_", ""),
|
||||
formatters::difficultyString);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package dev.loki.loparkour.hook.papi.resolver;
|
||||
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
|
||||
class PlaceholderFormatters {
|
||||
|
||||
String parseDifficulty(double difficulty) {
|
||||
if (difficulty <= 0.25) {
|
||||
return "easy";
|
||||
} else if (difficulty <= 0.5) {
|
||||
return "medium";
|
||||
} else if (difficulty <= 0.75) {
|
||||
return "hard";
|
||||
} else if (difficulty <= 1) {
|
||||
return "very hard";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
String difficultyString(Score score) {
|
||||
String diff = score.difficulty();
|
||||
if ("?".equals(diff)) {
|
||||
return parseDifficulty(2.0);
|
||||
}
|
||||
try {
|
||||
return parseDifficulty(Double.parseDouble(diff));
|
||||
} catch (NumberFormatException e) {
|
||||
return parseDifficulty(2.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
package dev.loki.loparkour.hook.papi.resolver;
|
||||
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.player.spectator.ParkourSpectator;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class PlayerPlaceholderResolver {
|
||||
|
||||
private final PlaceholderFormatters formatters = new PlaceholderFormatters();
|
||||
|
||||
public String resolve(Player player, String params) {
|
||||
String value = resolveLeaderboard(player, params);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
return resolveSession(player, params);
|
||||
}
|
||||
|
||||
private String resolveLeaderboard(Player player, String params) {
|
||||
switch (params) {
|
||||
case "rank" -> {
|
||||
return Integer.toString(Modes.DEFAULT.getLeaderboard().getRank(player.getUniqueId()));
|
||||
}
|
||||
case "highscore", "high_score" -> {
|
||||
return Integer.toString(Modes.DEFAULT.getLeaderboard().get(player.getUniqueId()).score());
|
||||
}
|
||||
case "high_score_time" -> {
|
||||
return Modes.DEFAULT.getLeaderboard().get(player.getUniqueId()).time();
|
||||
}
|
||||
default -> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveSession(Player player, String params) {
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
ParkourPlayer pp = extractParkourPlayer(user);
|
||||
if (pp == null || pp.session.generator == null) {
|
||||
return null;
|
||||
}
|
||||
return resolveGenerator(pp, params);
|
||||
}
|
||||
|
||||
private ParkourPlayer extractParkourPlayer(ParkourUser user) {
|
||||
if (user instanceof ParkourPlayer) {
|
||||
return (ParkourPlayer) user;
|
||||
}
|
||||
if (user instanceof ParkourSpectator) {
|
||||
return ((ParkourSpectator) user).closest;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveGenerator(ParkourPlayer pp, String params) {
|
||||
switch (params) {
|
||||
case "score", "current_score" -> {
|
||||
return Integer.toString(pp.session.generator.state.score);
|
||||
}
|
||||
case "time", "current_time" -> {
|
||||
return pp.session.generator.getFormattedTime();
|
||||
}
|
||||
case "blocklead", "lead" -> {
|
||||
return Integer.toString(pp.blockLead);
|
||||
}
|
||||
case "style" -> {
|
||||
return pp.style;
|
||||
}
|
||||
case "time_pref", "time_preference" -> {
|
||||
return Integer.toString(pp.selectedTime);
|
||||
}
|
||||
case "scoreboard" -> {
|
||||
return pp.showScoreboard.toString();
|
||||
}
|
||||
case "difficulty" -> {
|
||||
return Double.toString(pp.schematicDifficulty);
|
||||
}
|
||||
case "difficulty_string" -> {
|
||||
return formatters.parseDifficulty(pp.schematicDifficulty);
|
||||
}
|
||||
default -> {
|
||||
return resolveScoreUntil(pp, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveScoreUntil(ParkourPlayer pp, String params) {
|
||||
if (!params.contains("score_until_")) {
|
||||
return null;
|
||||
}
|
||||
String replaced = params.replace("score_until_", "");
|
||||
try {
|
||||
int interval = Integer.parseInt(replaced);
|
||||
if (interval > 0) {
|
||||
return Integer.toString(interval - (pp.session.generator.state.score % interval));
|
||||
}
|
||||
return "0";
|
||||
} catch (NumberFormatException e) {
|
||||
return "0";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package dev.loki.loparkour.hook.papi.resolver;
|
||||
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
class ScoreRankResolver {
|
||||
|
||||
private static final Pattern INFINITE_REGEX = Pattern.compile("(.+)_(\\d+)");
|
||||
|
||||
String resolve(String rankData, Function<Score, ?> valueFunction) {
|
||||
Matcher matcher = INFINITE_REGEX.matcher(rankData);
|
||||
if (matcher.matches()) {
|
||||
return resolveModeRank(matcher, valueFunction);
|
||||
}
|
||||
return resolveGenericRank(rankData, valueFunction);
|
||||
}
|
||||
|
||||
private String resolveModeRank(Matcher matcher, Function<Score, ?> valueFunction) {
|
||||
try {
|
||||
int rank = Integer.parseInt(matcher.group(2));
|
||||
String name = matcher.group(1);
|
||||
Leaderboard leaderboard = Modes.DEFAULT.getLeaderboard();
|
||||
Mode mode = Registry.getMode(name);
|
||||
if (mode != null) {
|
||||
leaderboard = mode.getLeaderboard();
|
||||
}
|
||||
return resolveScore(leaderboard, rank, valueFunction);
|
||||
} catch (NumberFormatException e) {
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveGenericRank(String rankData, Function<Score, ?> valueFunction) {
|
||||
try {
|
||||
int rank = Integer.parseInt(rankData);
|
||||
return resolveScore(Modes.DEFAULT.getLeaderboard(), rank, valueFunction);
|
||||
} catch (NumberFormatException e) {
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveScore(Leaderboard leaderboard, int rank, Function<Score, ?> valueFunction) {
|
||||
if (rank <= 0) {
|
||||
return "?";
|
||||
}
|
||||
Score score = leaderboard.getScoreAtRank(rank);
|
||||
if (score == null) {
|
||||
return "?";
|
||||
}
|
||||
return String.valueOf(valueFunction.apply(score));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,19 @@
|
|||
package dev.loki.loparkour.leaderboard.core;
|
||||
import dev.loki.loparkour.leaderboard.persistence.LeaderboardStorage;
|
||||
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
import dev.loki.loparkour.leaderboard.persistence.LeaderboardStorage;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Facade for leaderboard: CRUD operations with automatic sorting.
|
||||
* Delegates to LeaderboardStorage and LeaderboardSorter.
|
||||
* Delegates to LeaderboardStorage, LeaderboardSorter and LeaderboardRecords.
|
||||
*/
|
||||
public class Leaderboard {
|
||||
|
||||
|
|
@ -35,11 +34,13 @@ public class Leaderboard {
|
|||
|
||||
private final LeaderboardStorage storage;
|
||||
private final LeaderboardSorter sorter;
|
||||
private final LeaderboardRecords records;
|
||||
|
||||
public Leaderboard(@NotNull String mode, LeaderboardSorter.Sort sort) {
|
||||
this.mode = mode.toLowerCase();
|
||||
this.sort = sort;
|
||||
this.sorter = new LeaderboardSorter(sort);
|
||||
this.records = new LeaderboardRecords(scores, sorter);
|
||||
this.storage = new LeaderboardStorage(mode, scores, sorter);
|
||||
|
||||
storage.initAndSchedule();
|
||||
|
|
@ -81,27 +82,7 @@ public class Leaderboard {
|
|||
*/
|
||||
@Nullable
|
||||
public Score put(@NotNull UUID uuid, @NotNull Score score) {
|
||||
Score previous;
|
||||
synchronized (scores) {
|
||||
previous = scores.get(uuid);
|
||||
if (previous != null && !isBetterThan(score, previous)) {
|
||||
return previous; // keep the better score
|
||||
}
|
||||
scores.put(uuid, score);
|
||||
}
|
||||
|
||||
sorter.sortInPlace(scores);
|
||||
|
||||
return previous;
|
||||
}
|
||||
|
||||
/** Returns true if {@code candidate} is strictly better than {@code existing}. */
|
||||
private static boolean isBetterThan(@NotNull Score candidate, @NotNull Score existing) {
|
||||
if (candidate.score() != existing.score()) {
|
||||
return candidate.score() > existing.score();
|
||||
}
|
||||
// same score — faster time wins
|
||||
return candidate.getTimeMillis() < existing.getTimeMillis();
|
||||
return records.put(uuid, score);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -112,18 +93,14 @@ public class Leaderboard {
|
|||
*/
|
||||
@Nullable
|
||||
public Score remove(@NotNull UUID uuid) {
|
||||
return scores.remove(uuid);
|
||||
return records.remove(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all registered scores for this mode
|
||||
*/
|
||||
public void resetAll() {
|
||||
List<UUID> keys;
|
||||
synchronized (scores) {
|
||||
keys = new ArrayList<>(scores.keySet());
|
||||
}
|
||||
keys.forEach(this::remove);
|
||||
records.resetAll();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -132,7 +109,7 @@ public class Leaderboard {
|
|||
*/
|
||||
@NotNull
|
||||
public Score get(@NotNull UUID uuid) {
|
||||
return scores.getOrDefault(uuid, new Score("?", "?", "?", 0));
|
||||
return records.get(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -140,11 +117,7 @@ public class Leaderboard {
|
|||
* @return The rank. Starts from 1. Returns 0 if no ranking is found
|
||||
*/
|
||||
public int getRank(@NotNull UUID uuid) {
|
||||
List<UUID> keys;
|
||||
synchronized (scores) {
|
||||
keys = new ArrayList<>(scores.keySet());
|
||||
}
|
||||
return keys.indexOf(uuid) + 1;
|
||||
return records.getRank(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -156,14 +129,7 @@ public class Leaderboard {
|
|||
*/
|
||||
@Nullable
|
||||
public Score getScoreAtRank(int rank) {
|
||||
List<Score> values;
|
||||
synchronized (scores) {
|
||||
if (scores.size() < rank) {
|
||||
return null;
|
||||
}
|
||||
values = new ArrayList<>(scores.values());
|
||||
}
|
||||
return values.get(rank - 1);
|
||||
return records.getScoreAtRank(rank);
|
||||
}
|
||||
|
||||
// ── Sorting ───────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
package dev.loki.loparkour.leaderboard.core;
|
||||
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Manages individual score entries for a leaderboard: add, update, remove and
|
||||
* read operations. Keeps the backing map sorted after each mutation.
|
||||
*/
|
||||
class LeaderboardRecords {
|
||||
|
||||
private final Map<UUID, Score> scores;
|
||||
private final LeaderboardSorter sorter;
|
||||
|
||||
LeaderboardRecords(@NotNull Map<UUID, Score> scores, @NotNull LeaderboardSorter sorter) {
|
||||
this.scores = scores;
|
||||
this.sorter = sorter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new score — only replaces the old one if it's strictly better
|
||||
* (higher score, or same score with faster time).
|
||||
*
|
||||
* @param uuid The player's uuid
|
||||
* @param score The {@link Score} instance associated with a player's run
|
||||
* @return the previous best score, if there was one
|
||||
*/
|
||||
@Nullable
|
||||
Score put(@NotNull UUID uuid, @NotNull Score score) {
|
||||
Score previous;
|
||||
synchronized (scores) {
|
||||
previous = scores.get(uuid);
|
||||
if (previous != null && !isBetterThan(score, previous)) {
|
||||
return previous;
|
||||
}
|
||||
scores.put(uuid, score);
|
||||
}
|
||||
|
||||
sorter.sortInPlace(scores);
|
||||
|
||||
return previous;
|
||||
}
|
||||
|
||||
/** Returns true if {@code candidate} is strictly better than {@code existing}. */
|
||||
private static boolean isBetterThan(@NotNull Score candidate, @NotNull Score existing) {
|
||||
if (candidate.score() != existing.score()) {
|
||||
return candidate.score() > existing.score();
|
||||
}
|
||||
return candidate.getTimeMillis() < existing.getTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the score of a player by deleting it from the internal map
|
||||
*
|
||||
* @param uuid The UUID
|
||||
* @return the previous value if one was found
|
||||
*/
|
||||
@Nullable
|
||||
Score remove(@NotNull UUID uuid) {
|
||||
return scores.remove(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all registered scores for this mode
|
||||
*/
|
||||
void resetAll() {
|
||||
List<UUID> keys;
|
||||
synchronized (scores) {
|
||||
keys = new ArrayList<>(scores.keySet());
|
||||
}
|
||||
keys.forEach(this::remove);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uuid The {@link UUID} to get
|
||||
* @return The {@link Score} associated with the player. If null, returns a {@link Score} instance with "?"
|
||||
*/
|
||||
@NotNull
|
||||
Score get(@NotNull UUID uuid) {
|
||||
return scores.getOrDefault(uuid, new Score("?", "?", "?", 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uuid The uuid
|
||||
* @return The rank. Starts from 1. Returns 0 if no ranking is found
|
||||
*/
|
||||
int getRank(@NotNull UUID uuid) {
|
||||
List<UUID> keys;
|
||||
synchronized (scores) {
|
||||
keys = new ArrayList<>(scores.keySet());
|
||||
}
|
||||
return keys.indexOf(uuid) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the score at a specified rank.
|
||||
* Ranks start at 1.
|
||||
*
|
||||
* @param rank The rank
|
||||
* @return the {@link Score} instance, null if one isn't found
|
||||
*/
|
||||
@Nullable
|
||||
Score getScoreAtRank(int rank) {
|
||||
List<Score> values;
|
||||
synchronized (scores) {
|
||||
if (scores.size() < rank) {
|
||||
return null;
|
||||
}
|
||||
values = new ArrayList<>(scores.values());
|
||||
}
|
||||
return values.get(rank - 1);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
package dev.loki.loparkour.mode.base;
|
||||
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.mode.impl.CoopMode;
|
||||
import dev.loki.loparkour.mode.impl.DefaultMode;
|
||||
import dev.loki.loparkour.mode.impl.GravityShiftMode;
|
||||
import dev.loki.loparkour.mode.impl.InvisibleBarrierMode;
|
||||
import dev.loki.loparkour.mode.impl.RaceMode;
|
||||
import dev.loki.loparkour.mode.impl.SpectatorMode;
|
||||
import dev.loki.loparkour.mode.impl.SpeedrunMode;
|
||||
import dev.loki.loparkour.mode.impl.barrier.InvisibleBarrierMode;
|
||||
import dev.loki.loparkour.mode.impl.multi.CoopMode;
|
||||
import dev.loki.loparkour.mode.impl.multi.RaceMode;
|
||||
import dev.loki.loparkour.mode.impl.multi.SpectatorMode;
|
||||
import dev.loki.loparkour.mode.impl.solo.DefaultMode;
|
||||
import dev.loki.loparkour.mode.impl.solo.GravityShiftMode;
|
||||
import dev.loki.loparkour.mode.impl.solo.SpeedrunMode;
|
||||
|
||||
public final class Modes {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,222 +0,0 @@
|
|||
package dev.loki.loparkour.mode.impl;
|
||||
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.ModeMessages;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import dev.loki.loparkour.util.item.Item;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
public class InvisibleBarrierMode implements Mode {
|
||||
|
||||
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return "invisible-barrier";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Item getItem(String locale) {
|
||||
return Locales.getItem(locale, "play.single.invisible-barrier");
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Leaderboard getLeaderboard() {
|
||||
return leaderboard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(Player player) {
|
||||
if (!ModeMessages.checkJoiningEnabled(player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||
if (pp != null && pp.session.generator instanceof BarrierGenerator) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.closeInventory();
|
||||
Session.create(session -> new BarrierGenerator(session), null, null, player);
|
||||
}
|
||||
|
||||
static class BarrierGenerator extends ParkourGenerator {
|
||||
|
||||
private static final double RENDER_DISTANCE = 24.0;
|
||||
private static final double PARTICLE_STEP = 0.1;
|
||||
|
||||
private final Set<Location> activeBarriers = new HashSet<>();
|
||||
private final Random random = new Random();
|
||||
private final Map<Location, Particle.DustOptions> blockColors = new HashMap<>();
|
||||
private int particleTick = 0;
|
||||
|
||||
BarrierGenerator(@NotNull Session session) {
|
||||
super(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateFirst(@NotNull Location spawn, @NotNull Location block) {
|
||||
super.generateFirst(spawn, block);
|
||||
convertLastBlockToBarrier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generate(int amount) {
|
||||
super.generate(amount);
|
||||
int start = Math.max(0, state.history.size() - amount);
|
||||
for (int i = start; i < state.history.size(); i++) {
|
||||
convertBlockToBarrier(state.history.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
particleTick++;
|
||||
if (particleTick % 4 == 0) {
|
||||
renderOutlines();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(boolean regenerate) {
|
||||
activeBarriers.clear();
|
||||
blockColors.clear();
|
||||
particleTick = 0;
|
||||
super.reset(regenerate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mode getMode() {
|
||||
return Modes.INVISIBLE_BARRIER;
|
||||
}
|
||||
|
||||
private void convertLastBlockToBarrier() {
|
||||
if (state.history.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
convertBlockToBarrier(state.history.get(state.history.size() - 1));
|
||||
}
|
||||
|
||||
private void convertBlockToBarrier(@NotNull Block block) {
|
||||
Location loc = block.getLocation();
|
||||
if (block.getType() == Material.BARRIER) {
|
||||
return;
|
||||
}
|
||||
block.setType(Material.BARRIER, false);
|
||||
activeBarriers.add(loc);
|
||||
Color color = Color.fromRGB(random.nextInt(256), random.nextInt(256), random.nextInt(256));
|
||||
blockColors.put(loc, new Particle.DustOptions(color, 1.0f));
|
||||
}
|
||||
|
||||
private void renderOutlines() {
|
||||
if (activeBarriers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Self-clean invalid entries
|
||||
Iterator<Map.Entry<Location, Particle.DustOptions>> it = blockColors.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<Location, Particle.DustOptions> entry = it.next();
|
||||
if (entry.getKey().getBlock().getType() != Material.BARRIER) {
|
||||
it.remove();
|
||||
activeBarriers.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
for (ParkourPlayer pp : getPlayers()) {
|
||||
Player player = pp.player;
|
||||
Location playerLoc = player.getLocation();
|
||||
|
||||
for (Location loc : activeBarriers) {
|
||||
if (loc.getWorld() != playerLoc.getWorld()) {
|
||||
continue;
|
||||
}
|
||||
if (loc.distanceSquared(playerLoc) > RENDER_DISTANCE * RENDER_DISTANCE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Particle.DustOptions options = blockColors.get(loc);
|
||||
if (options == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drawBlockOutline(player, loc, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Point(double x, double y, double z) {}
|
||||
|
||||
private void drawBlockOutline(@NotNull Player player, @NotNull Location loc, @NotNull Particle.DustOptions options) {
|
||||
double x = loc.getX();
|
||||
double y = loc.getY();
|
||||
double z = loc.getZ();
|
||||
Point p = new Point(x, y, z);
|
||||
Point q = new Point(x + 1, y, z);
|
||||
Point r = new Point(x + 1, y, z + 1);
|
||||
Point s = new Point(x, y, z + 1);
|
||||
|
||||
// Bottom edges (y offset = 0)
|
||||
drawEdge(player, p, q, options);
|
||||
drawEdge(player, q, r, options);
|
||||
drawEdge(player, r, s, options);
|
||||
drawEdge(player, s, p, options);
|
||||
|
||||
// Top edges (y offset = 1)
|
||||
Point pt = new Point(x, y + 1, z);
|
||||
Point qt = new Point(x + 1, y + 1, z);
|
||||
Point rt = new Point(x + 1, y + 1, z + 1);
|
||||
Point st = new Point(x, y + 1, z + 1);
|
||||
drawEdge(player, pt, qt, options);
|
||||
drawEdge(player, qt, rt, options);
|
||||
drawEdge(player, rt, st, options);
|
||||
drawEdge(player, st, pt, options);
|
||||
|
||||
// Vertical edges
|
||||
drawEdge(player, p, pt, options);
|
||||
drawEdge(player, q, qt, options);
|
||||
drawEdge(player, r, rt, options);
|
||||
drawEdge(player, s, st, options);
|
||||
}
|
||||
|
||||
private void drawEdge(@NotNull Player player, @NotNull Point start, @NotNull Point end,
|
||||
@NotNull Particle.DustOptions options) {
|
||||
double dx = end.x - start.x;
|
||||
double dy = end.y - start.y;
|
||||
double dz = end.z - start.z;
|
||||
double length = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
int steps = Math.max(1, (int) (length / PARTICLE_STEP));
|
||||
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
double t = (double) i / steps;
|
||||
double px = start.x + dx * t;
|
||||
double py = start.y + dy * t;
|
||||
double pz = start.z + dz * t;
|
||||
player.spawnParticle(Particle.REDSTONE, px, py, pz, 1, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package dev.loki.loparkour.mode.impl.barrier;
|
||||
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import org.bukkit.Location;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
class BarrierGenerator extends ParkourGenerator {
|
||||
|
||||
private final BarrierRenderer renderer = new BarrierRenderer();
|
||||
|
||||
BarrierGenerator(@NotNull Session session) {
|
||||
super(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generateFirst(@NotNull Location spawn, @NotNull Location block) {
|
||||
super.generateFirst(spawn, block);
|
||||
renderer.convertLastBlockToBarrier(state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void generate(int amount) {
|
||||
super.generate(amount);
|
||||
int start = Math.max(0, state.history.size() - amount);
|
||||
for (int i = start; i < state.history.size(); i++) {
|
||||
renderer.convertBlockToBarrier(state.history.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
renderer.tick(getPlayers());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(boolean regenerate) {
|
||||
renderer.reset();
|
||||
super.reset(regenerate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mode getMode() {
|
||||
return Modes.INVISIBLE_BARRIER;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
package dev.loki.loparkour.mode.impl.barrier;
|
||||
|
||||
import dev.loki.loparkour.generator.core.model.GeneratorState;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import org.bukkit.Color;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
class BarrierRenderer {
|
||||
|
||||
private static final double RENDER_DISTANCE = 24.0;
|
||||
private static final double PARTICLE_STEP = 0.1;
|
||||
|
||||
private final Set<Location> activeBarriers = new HashSet<>();
|
||||
private final Random random = new Random();
|
||||
private final Map<Location, Particle.DustOptions> blockColors = new HashMap<>();
|
||||
private int particleTick = 0;
|
||||
|
||||
void convertLastBlockToBarrier(@NotNull GeneratorState state) {
|
||||
if (state.history.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
convertBlockToBarrier(state.history.get(state.history.size() - 1));
|
||||
}
|
||||
|
||||
void convertBlockToBarrier(@NotNull Block block) {
|
||||
Location loc = block.getLocation();
|
||||
if (block.getType() == Material.BARRIER) {
|
||||
return;
|
||||
}
|
||||
block.setType(Material.BARRIER, false);
|
||||
activeBarriers.add(loc);
|
||||
Color color = Color.fromRGB(random.nextInt(256), random.nextInt(256), random.nextInt(256));
|
||||
blockColors.put(loc, new Particle.DustOptions(color, 1.0f));
|
||||
}
|
||||
|
||||
void tick(@NotNull List<ParkourPlayer> players) {
|
||||
particleTick++;
|
||||
if (particleTick % 4 == 0) {
|
||||
renderOutlines(players);
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
activeBarriers.clear();
|
||||
blockColors.clear();
|
||||
particleTick = 0;
|
||||
}
|
||||
|
||||
private void renderOutlines(@NotNull List<ParkourPlayer> players) {
|
||||
if (activeBarriers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
purgeStaleBarriers();
|
||||
|
||||
for (ParkourPlayer pp : players) {
|
||||
renderForPlayer(pp.player, pp.player.getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
private void purgeStaleBarriers() {
|
||||
Iterator<Map.Entry<Location, Particle.DustOptions>> it = blockColors.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<Location, Particle.DustOptions> entry = it.next();
|
||||
if (entry.getKey().getBlock().getType() != Material.BARRIER) {
|
||||
it.remove();
|
||||
activeBarriers.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void renderForPlayer(@NotNull Player player, @NotNull Location playerLoc) {
|
||||
for (Location loc : activeBarriers) {
|
||||
if (!isVisible(playerLoc, loc)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Particle.DustOptions options = blockColors.get(loc);
|
||||
if (options == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
drawBlockOutline(player, loc, options);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isVisible(@NotNull Location playerLoc, @NotNull Location loc) {
|
||||
return loc.getWorld() == playerLoc.getWorld()
|
||||
&& loc.distanceSquared(playerLoc) <= RENDER_DISTANCE * RENDER_DISTANCE;
|
||||
}
|
||||
|
||||
private record Point(double x, double y, double z) {
|
||||
}
|
||||
|
||||
private void drawBlockOutline(@NotNull Player player, @NotNull Location loc, @NotNull Particle.DustOptions options) {
|
||||
double x = loc.getX();
|
||||
double y = loc.getY();
|
||||
double z = loc.getZ();
|
||||
Point p = new Point(x, y, z);
|
||||
Point q = new Point(x + 1, y, z);
|
||||
Point r = new Point(x + 1, y, z + 1);
|
||||
Point s = new Point(x, y, z + 1);
|
||||
|
||||
drawEdge(player, p, q, options);
|
||||
drawEdge(player, q, r, options);
|
||||
drawEdge(player, r, s, options);
|
||||
drawEdge(player, s, p, options);
|
||||
|
||||
Point pt = new Point(x, y + 1, z);
|
||||
Point qt = new Point(x + 1, y + 1, z);
|
||||
Point rt = new Point(x + 1, y + 1, z + 1);
|
||||
Point st = new Point(x, y + 1, z + 1);
|
||||
drawEdge(player, pt, qt, options);
|
||||
drawEdge(player, qt, rt, options);
|
||||
drawEdge(player, rt, st, options);
|
||||
drawEdge(player, st, pt, options);
|
||||
|
||||
drawEdge(player, p, pt, options);
|
||||
drawEdge(player, q, qt, options);
|
||||
drawEdge(player, r, rt, options);
|
||||
drawEdge(player, s, st, options);
|
||||
}
|
||||
|
||||
private void drawEdge(@NotNull Player player, @NotNull Point start, @NotNull Point end,
|
||||
@NotNull Particle.DustOptions options) {
|
||||
double dx = end.x - start.x;
|
||||
double dy = end.y - start.y;
|
||||
double dz = end.z - start.z;
|
||||
double length = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
int steps = Math.max(1, (int) (length / PARTICLE_STEP));
|
||||
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
double t = (double) i / steps;
|
||||
double px = start.x + dx * t;
|
||||
double py = start.y + dy * t;
|
||||
double pz = start.z + dz * t;
|
||||
player.spawnParticle(Particle.REDSTONE, px, py, pz, 1, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package dev.loki.loparkour.mode.impl.barrier;
|
||||
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.leaderboard.core.Leaderboard;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.ModeMessages;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import dev.loki.loparkour.util.item.Item;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class InvisibleBarrierMode implements Mode {
|
||||
|
||||
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return "invisible-barrier";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Item getItem(String locale) {
|
||||
return Locales.getItem(locale, "play.single.invisible-barrier");
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Leaderboard getLeaderboard() {
|
||||
return leaderboard;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void create(Player player) {
|
||||
if (!ModeMessages.checkJoiningEnabled(player)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
|
||||
if (pp != null && pp.session.generator instanceof BarrierGenerator) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.closeInventory();
|
||||
Session.create(session -> new BarrierGenerator(session), null, null, player);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.mode.impl;
|
||||
package dev.loki.loparkour.mode.impl.multi;
|
||||
import dev.loki.loparkour.mode.base.MultiMode;
|
||||
import dev.loki.loparkour.mode.base.ModeMessages;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.mode.impl;
|
||||
package dev.loki.loparkour.mode.impl.multi;
|
||||
import dev.loki.loparkour.mode.base.ModeMessages;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.mode.impl;
|
||||
package dev.loki.loparkour.mode.impl.multi;
|
||||
import dev.loki.loparkour.mode.base.ModeMessages;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.mode.impl;
|
||||
package dev.loki.loparkour.mode.impl.solo;
|
||||
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.mode.impl;
|
||||
package dev.loki.loparkour.mode.impl.solo;
|
||||
import dev.loki.loparkour.mode.base.ModeMessages;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dev.loki.loparkour.mode.impl;
|
||||
package dev.loki.loparkour.mode.impl.solo;
|
||||
import dev.loki.loparkour.mode.base.ModeMessages;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
|
|
@ -2,19 +2,14 @@ package dev.loki.loparkour.player.core;
|
|||
|
||||
import com.google.gson.annotations.Expose;
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.core.model.Profile;
|
||||
import dev.loki.loparkour.menu.core.ParkourOption;
|
||||
import dev.loki.loparkour.mode.base.MultiMode;
|
||||
import dev.loki.loparkour.player.data.PreviousData;
|
||||
import dev.loki.loparkour.player.service.ParkourHotbar;
|
||||
import dev.loki.loparkour.player.service.PlayerSettingsManager;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import dev.loki.loparkour.storage.Storage;
|
||||
import dev.loki.loparkour.world.core.Divider;
|
||||
import dev.lolib.scheduler.Scheduler;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.entity.Player;
|
||||
|
|
@ -22,10 +17,8 @@ import org.bukkit.potion.PotionEffect;
|
|||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
/**
|
||||
|
|
@ -37,9 +30,6 @@ public class ParkourPlayer extends ParkourUser {
|
|||
|
||||
public static final Map<String, PlayerSettingsManager.OptionContainer> PLAYER_COLUMNS = PlayerSettingsManager.getColumnMappings();
|
||||
|
||||
// Track scored blocks to prevent duplicate scoring (String key = "x,y,z" to avoid Bukkit Block reference equality)
|
||||
private final Set<String> scoredBlocks = new HashSet<>();
|
||||
|
||||
public @Expose Double schematicDifficulty;
|
||||
public @Expose Integer blockLead;
|
||||
public @Expose Boolean particles;
|
||||
|
|
@ -51,6 +41,11 @@ public class ParkourPlayer extends ParkourUser {
|
|||
public @Expose String style;
|
||||
public @Expose String locale;
|
||||
public @Expose List<String> collectedRewards;
|
||||
|
||||
private final PlayerScoringTracker scoring = new PlayerScoringTracker();
|
||||
private final PlayerPersistence persistence = new PlayerPersistence();
|
||||
private final PlayerSpawnSetup spawnSetup = new PlayerSpawnSetup();
|
||||
|
||||
/**
|
||||
* Creates a new instance of a ParkourPlayer<br>
|
||||
* If you are using the API, please use {@link ParkourPlayer#register(Player, Session)} instead
|
||||
|
|
@ -107,36 +102,32 @@ public class ParkourPlayer extends ParkourUser {
|
|||
}
|
||||
|
||||
session.removePlayers(this);
|
||||
|
||||
// Clear scored blocks to prevent memory leak
|
||||
scoredBlocks.clear();
|
||||
|
||||
save(LoParkour.getPlugin().isEnabled());
|
||||
// Clear scored blocks to prevent memory leak
|
||||
scoring.clear();
|
||||
|
||||
persistence.save(this, LoParkour.getPlugin().isEnabled());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear scored blocks set, typically on fall/respawn so same coordinates can be re-scored.
|
||||
*/
|
||||
public void clearScoredBlocks() {
|
||||
scoredBlocks.clear();
|
||||
}
|
||||
|
||||
private static String blockKey(Block block) {
|
||||
return block.getX() + "," + block.getY() + "," + block.getZ();
|
||||
scoring.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if player has already scored on this block.
|
||||
*/
|
||||
public boolean hasScored(Block block) {
|
||||
return scoredBlocks.contains(blockKey(block));
|
||||
return scoring.hasScored(block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark block as scored to prevent duplicate scoring.
|
||||
*/
|
||||
public void markScored(Block block) {
|
||||
scoredBlocks.add(blockKey(block));
|
||||
scoring.markScored(block);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -172,36 +163,20 @@ public class ParkourPlayer extends ParkourUser {
|
|||
* Saves the player's data to their file
|
||||
*/
|
||||
public void save(boolean async) {
|
||||
Runnable write = () -> Storage.writePlayer(this);
|
||||
|
||||
if (async) {
|
||||
Scheduler.get(LoParkour.getPlugin()).runAsync(write);
|
||||
} else {
|
||||
write.run();
|
||||
}
|
||||
persistence.save(this, async);
|
||||
}
|
||||
|
||||
public void setup(Location to) {
|
||||
if (to != null) {
|
||||
teleport(to);
|
||||
}
|
||||
|
||||
player.setGameMode(GameMode.ADVENTURE);
|
||||
|
||||
if (Config.CONFIG.getBoolean("options.inventory-handling")) {
|
||||
Scheduler.get(LoParkour.getPlugin()).runLater(() -> ParkourHotbar.apply(this), 5);
|
||||
} else {
|
||||
sendTranslated("other.customize");
|
||||
}
|
||||
spawnSetup.setup(this, to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates hotbar items with current language
|
||||
*/
|
||||
public void updateHotbar() {
|
||||
ParkourHotbar.apply(this);
|
||||
spawnSetup.updateHotbar(this);
|
||||
}
|
||||
|
||||
public record OptionContainer(ParkourOption option, BiConsumer<ParkourPlayer, String> consumer) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package dev.loki.loparkour.player.core;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.storage.Storage;
|
||||
import dev.lolib.scheduler.Scheduler;
|
||||
|
||||
/**
|
||||
* Handles persistence of player data to the configured storage backend.
|
||||
*/
|
||||
final class PlayerPersistence {
|
||||
|
||||
void save(ParkourPlayer player, boolean async) {
|
||||
Runnable write = () -> Storage.writePlayer(player);
|
||||
|
||||
if (async) {
|
||||
Scheduler.get(LoParkour.getPlugin()).runAsync(write);
|
||||
} else {
|
||||
write.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package dev.loki.loparkour.player.core;
|
||||
|
||||
import org.bukkit.block.Block;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Tracks the blocks a player has already scored on to prevent duplicate scoring.
|
||||
*/
|
||||
final class PlayerScoringTracker {
|
||||
|
||||
// Track scored blocks to prevent duplicate scoring (String key = "x,y,z" to avoid Bukkit Block reference equality)
|
||||
private final Set<String> scoredBlocks = new HashSet<>();
|
||||
|
||||
boolean hasScored(Block block) {
|
||||
return scoredBlocks.contains(blockKey(block));
|
||||
}
|
||||
|
||||
void markScored(Block block) {
|
||||
scoredBlocks.add(blockKey(block));
|
||||
}
|
||||
|
||||
void clear() {
|
||||
scoredBlocks.clear();
|
||||
}
|
||||
|
||||
private static String blockKey(Block block) {
|
||||
return block.getX() + "," + block.getY() + "," + block.getZ();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package dev.loki.loparkour.player.core;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.player.service.ParkourHotbar;
|
||||
import dev.lolib.scheduler.Scheduler;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Location;
|
||||
|
||||
/**
|
||||
* Prepares a parkour player's world state such as location, game mode and hotbar.
|
||||
*/
|
||||
final class PlayerSpawnSetup {
|
||||
|
||||
void setup(ParkourPlayer player, Location to) {
|
||||
if (to != null) {
|
||||
player.teleport(to);
|
||||
}
|
||||
|
||||
player.player.setGameMode(GameMode.ADVENTURE);
|
||||
|
||||
if (Config.CONFIG.getBoolean("options.inventory-handling")) {
|
||||
Scheduler.get(LoParkour.getPlugin()).runLater(() -> ParkourHotbar.apply(player), 5);
|
||||
} else {
|
||||
player.sendTranslated("other.customize");
|
||||
}
|
||||
}
|
||||
|
||||
void updateHotbar(ParkourPlayer player) {
|
||||
ParkourHotbar.apply(player);
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Loads {@code .nbt}, {@code .schem}, and {@code .schematic} files from {@code plugins/LoParkour/schematics/}.
|
||||
|
|
@ -100,7 +101,7 @@ public final class SchematicManager {
|
|||
ok++;
|
||||
} catch (IOException e) {
|
||||
LoParkour.getPlugin().getLogger().severe("Failed to load structure: " + file.getName());
|
||||
e.printStackTrace();
|
||||
LoParkour.getPlugin().getLogger().log(Level.SEVERE, "Detailed cause:", e);
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
|
|
@ -126,7 +127,7 @@ public final class SchematicManager {
|
|||
ok++;
|
||||
} catch (IOException e) {
|
||||
LoParkour.getPlugin().getLogger().severe("Failed to load schematic: " + file.getName());
|
||||
e.printStackTrace();
|
||||
LoParkour.getPlugin().getLogger().log(Level.SEVERE, "Detailed cause:", e);
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package dev.loki.loparkour.storage.sql;
|
||||
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* CRUD for player data.
|
||||
*/
|
||||
class SQLPlayerRepository {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(SQLPlayerRepository.class.getName());
|
||||
|
||||
private SQLPlayerRepository() {
|
||||
}
|
||||
|
||||
static void readPlayer(@NotNull SQLConnectionManager manager, @NotNull ParkourPlayer player) {
|
||||
if (!manager.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String query = SQLQueryBuilder.selectPlayer();
|
||||
|
||||
try (Connection conn = manager.getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
|
||||
stmt.setString(1, player.player.getUniqueId().toString());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
SQLDataMapper.PlayerData data = SQLDataMapper.mapPlayerData(rs);
|
||||
if (data != null) {
|
||||
SQLDataMapper.applyPlayerData(player, data);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (SQLException e) {
|
||||
LOGGER.severe("Failed to read player data: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static void writePlayer(@NotNull SQLConnectionManager manager, @NotNull ParkourPlayer player) {
|
||||
if (!manager.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String query = SQLQueryBuilder.upsertPlayer();
|
||||
|
||||
try (Connection conn = manager.getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
|
||||
SQLDataMapper.setPlayerParameters(stmt, player);
|
||||
stmt.executeUpdate();
|
||||
|
||||
} catch (SQLException e) {
|
||||
LOGGER.severe("Failed to write player data: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package dev.loki.loparkour.storage.sql;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Creates the database schema (DDL) for parkour storage.
|
||||
*/
|
||||
class SQLSchemaManager {
|
||||
|
||||
private static final String ADAPTIVE_STATS_TABLE = """
|
||||
CREATE TABLE IF NOT EXISTS loparkour_player_stats (
|
||||
player_uuid VARCHAR(36) PRIMARY KEY,
|
||||
skill_rating DOUBLE DEFAULT 1.0,
|
||||
sessions_count INT DEFAULT 0,
|
||||
total_jumps INT DEFAULT 0,
|
||||
total_falls INT DEFAULT 0,
|
||||
longest_streak INT DEFAULT 0,
|
||||
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_skill_rating (skill_rating),
|
||||
INDEX idx_last_updated (last_updated)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""";
|
||||
|
||||
private SQLSchemaManager() {
|
||||
}
|
||||
|
||||
static void createTables(@NotNull SQLConnectionManager manager, @NotNull String mode) throws SQLException {
|
||||
String tableName = SQLQueryBuilder.getTableName(mode);
|
||||
try (Connection conn = manager.getConnection()) {
|
||||
createScoresTable(conn, tableName);
|
||||
createPlayersTable(conn);
|
||||
createAdaptiveStatsTable(conn);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createScoresTable(@NotNull Connection conn, @NotNull String tableName) throws SQLException {
|
||||
try (PreparedStatement stmt = conn.prepareStatement(SQLQueryBuilder.createScoresTable(tableName))) {
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private static void createPlayersTable(@NotNull Connection conn) throws SQLException {
|
||||
try (PreparedStatement stmt = conn.prepareStatement(SQLQueryBuilder.createPlayersTable())) {
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private static void createAdaptiveStatsTable(@NotNull Connection conn) throws SQLException {
|
||||
try (PreparedStatement stmt = conn.prepareStatement(ADAPTIVE_STATS_TABLE)) {
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package dev.loki.loparkour.storage.sql;
|
||||
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* CRUD for leaderboard scores.
|
||||
*/
|
||||
class SQLScoreRepository {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(SQLScoreRepository.class.getName());
|
||||
|
||||
private SQLScoreRepository() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
static Map<UUID, Score> readScores(@NotNull SQLConnectionManager manager, @NotNull String mode) {
|
||||
if (!manager.isConnected()) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
String query = SQLQueryBuilder.selectScores(SQLQueryBuilder.getTableName(mode));
|
||||
|
||||
try (Connection conn = manager.getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(query);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
|
||||
return SQLDataMapper.mapScores(rs);
|
||||
|
||||
} catch (SQLException e) {
|
||||
LOGGER.severe("Failed to read scores for mode " + mode + ": " + e.getMessage());
|
||||
return Map.of();
|
||||
}
|
||||
}
|
||||
|
||||
static void writeScores(@NotNull SQLConnectionManager manager,
|
||||
@NotNull String mode,
|
||||
@NotNull Map<UUID, Score> scores) {
|
||||
if (!manager.isConnected() || scores.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String query = SQLQueryBuilder.upsertScore(SQLQueryBuilder.getTableName(mode));
|
||||
|
||||
try (Connection conn = manager.getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
|
||||
for (Map.Entry<UUID, Score> entry : scores.entrySet()) {
|
||||
SQLDataMapper.setScoreParameters(stmt, entry.getValue(), entry.getKey());
|
||||
stmt.addBatch();
|
||||
}
|
||||
|
||||
stmt.executeBatch();
|
||||
LOGGER.info("Wrote " + scores.size() + " scores for mode: " + mode);
|
||||
|
||||
} catch (SQLException e) {
|
||||
LOGGER.severe("Failed to write scores for mode " + mode + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,17 +4,14 @@ import dev.loki.loparkour.leaderboard.model.Score;
|
|||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Refactored StorageSQL using composition pattern.
|
||||
* Coordinates connection management, query building, and data mapping.
|
||||
* Facade for SQL storage. Coordinates connection lifecycle and delegates
|
||||
* CRUD to dedicated repository classes.
|
||||
*/
|
||||
public class StorageSQL {
|
||||
|
||||
|
|
@ -23,7 +20,7 @@ public class StorageSQL {
|
|||
|
||||
private static final Logger LOGGER = Logger.getLogger(StorageSQL.class.getName());
|
||||
private static SQLConnectionManager connectionManager;
|
||||
|
||||
|
||||
/**
|
||||
* Check if database is connected.
|
||||
*/
|
||||
|
|
@ -38,7 +35,7 @@ public class StorageSQL {
|
|||
}
|
||||
return connectionManager;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Run callback when database is connected.
|
||||
*/
|
||||
|
|
@ -49,7 +46,7 @@ public class StorageSQL {
|
|||
LOGGER.warning("Database not connected, skipping operation");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Close database connection.
|
||||
*/
|
||||
|
|
@ -59,7 +56,7 @@ public class StorageSQL {
|
|||
connectionManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialize database connection and tables.
|
||||
*/
|
||||
|
|
@ -67,15 +64,15 @@ public class StorageSQL {
|
|||
try {
|
||||
connectionManager = new SQLConnectionManager();
|
||||
connectionManager.connect();
|
||||
|
||||
createTables(mode);
|
||||
|
||||
|
||||
SQLSchemaManager.createTables(connectionManager, mode);
|
||||
|
||||
LOGGER.info("Database initialized successfully for mode: " + mode);
|
||||
} catch (SQLException e) {
|
||||
LOGGER.severe("Failed to initialize database: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read scores for specific mode.
|
||||
*/
|
||||
|
|
@ -84,22 +81,9 @@ public class StorageSQL {
|
|||
if (!isConnected()) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
String tableName = SQLQueryBuilder.getTableName(mode);
|
||||
String query = SQLQueryBuilder.selectScores(tableName);
|
||||
|
||||
try (Connection conn = connectionManager.getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(query);
|
||||
ResultSet rs = stmt.executeQuery()) {
|
||||
|
||||
return SQLDataMapper.mapScores(rs);
|
||||
|
||||
} catch (SQLException e) {
|
||||
LOGGER.severe("Failed to read scores for mode " + mode + ": " + e.getMessage());
|
||||
return Map.of();
|
||||
}
|
||||
return SQLScoreRepository.readScores(connectionManager, mode);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write scores for specific mode.
|
||||
*/
|
||||
|
|
@ -107,26 +91,9 @@ public class StorageSQL {
|
|||
if (!isConnected() || scores.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String tableName = SQLQueryBuilder.getTableName(mode);
|
||||
String query = SQLQueryBuilder.upsertScore(tableName);
|
||||
|
||||
try (Connection conn = connectionManager.getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
|
||||
for (Map.Entry<UUID, Score> entry : scores.entrySet()) {
|
||||
SQLDataMapper.setScoreParameters(stmt, entry.getValue(), entry.getKey());
|
||||
stmt.addBatch();
|
||||
}
|
||||
|
||||
stmt.executeBatch();
|
||||
LOGGER.info("Wrote " + scores.size() + " scores for mode: " + mode);
|
||||
|
||||
} catch (SQLException e) {
|
||||
LOGGER.severe("Failed to write scores for mode " + mode + ": " + e.getMessage());
|
||||
}
|
||||
SQLScoreRepository.writeScores(connectionManager, mode, scores);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read player data from database.
|
||||
*/
|
||||
|
|
@ -134,26 +101,9 @@ public class StorageSQL {
|
|||
if (!isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String query = SQLQueryBuilder.selectPlayer();
|
||||
|
||||
try (Connection conn = connectionManager.getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
|
||||
stmt.setString(1, player.player.getUniqueId().toString());
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
SQLDataMapper.PlayerData data = SQLDataMapper.mapPlayerData(rs);
|
||||
if (data != null) {
|
||||
SQLDataMapper.applyPlayerData(player, data);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (SQLException e) {
|
||||
LOGGER.severe("Failed to read player data: " + e.getMessage());
|
||||
}
|
||||
SQLPlayerRepository.readPlayer(connectionManager, player);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write player data to database.
|
||||
*/
|
||||
|
|
@ -161,49 +111,6 @@ public class StorageSQL {
|
|||
if (!isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
String query = SQLQueryBuilder.upsertPlayer();
|
||||
|
||||
try (Connection conn = connectionManager.getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
|
||||
SQLDataMapper.setPlayerParameters(stmt, player);
|
||||
stmt.executeUpdate();
|
||||
|
||||
} catch (SQLException e) {
|
||||
LOGGER.severe("Failed to write player data: " + e.getMessage());
|
||||
}
|
||||
SQLPlayerRepository.writePlayer(connectionManager, player);
|
||||
}
|
||||
|
||||
private static void createTables(@NotNull String mode) throws SQLException {
|
||||
String tableName = SQLQueryBuilder.getTableName(mode);
|
||||
|
||||
try (Connection conn = connectionManager.getConnection()) {
|
||||
// Create scores table
|
||||
try (PreparedStatement stmt = conn.prepareStatement(SQLQueryBuilder.createScoresTable(tableName))) {
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
|
||||
// Create players table
|
||||
try (PreparedStatement stmt = conn.prepareStatement(SQLQueryBuilder.createPlayersTable())) {
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
|
||||
// Adaptive player stats
|
||||
try (PreparedStatement stmt = conn.prepareStatement(
|
||||
"CREATE TABLE IF NOT EXISTS loparkour_player_stats ("
|
||||
+ "player_uuid VARCHAR(36) PRIMARY KEY, "
|
||||
+ "skill_rating DOUBLE DEFAULT 1.0, "
|
||||
+ "sessions_count INT DEFAULT 0, "
|
||||
+ "total_jumps INT DEFAULT 0, "
|
||||
+ "total_falls INT DEFAULT 0, "
|
||||
+ "longest_streak INT DEFAULT 0, "
|
||||
+ "last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, "
|
||||
+ "INDEX idx_skill_rating (skill_rating), "
|
||||
+ "INDEX idx_last_updated (last_updated)"
|
||||
+ ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4")) {
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue