refactor: reorganize packages and replace LPSchematic with WorldEdit

Restructure core modules into subpackages (generator, schematic, config, session, etc.); add .schem/.nbt schematic pipeline, commands, adaptive stats skeleton, and bootstrap wiring.
This commit is contained in:
loki5512344 2026-06-01 14:02:48 +02:00
parent 6ba6e5137d
commit fd0ba2cc0e
203 changed files with 5652 additions and 2367 deletions

View file

@ -0,0 +1,262 @@
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.Arrays;
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;
}
}
if (p == null) return;
if (a1.equalsIgnoreCase("create") && 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);
}
}
}
// ── 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 (a1.equalsIgnoreCase("schematic")) {
if (a2.equalsIgnoreCase("create")) {
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 (!a1.equalsIgnoreCase("schematic") || !a2.equalsIgnoreCase("create")) 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
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 (a1.equals("reset") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
completions.add("everyone");
ParkourPlayer.getPlayers().forEach(pp -> completions.add(pp.getName()));
} else if (a1.equals("join") && ParkourOption.JOIN.mayPerform(sender)) {
Registry.getModes().forEach(m -> completions.add(m.getName()));
ParkourPlayer.getPlayers().forEach(pp -> completions.add(pp.getName()));
} else if (a1.equals("leaderboard") && ParkourOption.LEADERBOARDS.mayPerform(sender)) {
Registry.getModes().forEach(m -> completions.add(m.getName()));
} else if (a1.equals("schematic") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
completions.addAll(SCHEMATIC_SUBCOMMANDS);
} else if (a1.equals("create") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
completions.addAll(SCHEMATIC_DIFFICULTIES);
} else if ((a1.equals("forcejoin") || a1.equals("forceleave")) && sender.hasPermission(ParkourOption.ADMIN.permission)) {
completions.add("everyone");
if (a1.equals("forcejoin")) completions.add("nearest");
Bukkit.getOnlinePlayers().forEach(pl -> completions.add(pl.getName()));
} else if (a1.equals("recoverinventory") && 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 (a1.equals("schematic")) {
if (a2.equals("paste")) {
addLoadedSchematicIds(completions);
} else if (a2.equals("create")) {
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 (a1.equals("schematic") && a2.equals("create")) {
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));
}
}

View file

@ -1,172 +0,0 @@
package dev.loki.loparkour.command;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
import dev.loki.loparkour.schematic.lpschem.LPSchematicBuilder;
import dev.loki.loparkour.util.ColorUtil;
import dev.loki.loparkour.util.Locations;
import dev.loki.loparkour.util.ParticleUtil;
import org.bukkit.Location;
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.Material;
import org.bukkit.util.BoundingBox;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Handles /LoParkour schematic sub-commands (wand, pos1, pos2, save, paste, list, reload).
*/
public class SchematicCommandHandler {
public static final Map<Player, Location[]> selections = new HashMap<>();
private static final ItemStack WAND = buildWand();
private static ItemStack buildWand() {
ItemStack item = new ItemStack(Material.GOLDEN_AXE);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(ColorUtil.color("<red><bold>LPSchematic Wand"));
meta.setLore(List.of(
ColorUtil.color("<gray>Left click: first position"),
ColorUtil.color("<gray>Right click: second position")
));
item.setItemMeta(meta);
}
return item;
}
public static ItemStack getWand() {
return WAND.clone();
}
public static void sendHelp(Player player) {
send(player, "");
send(player, "<dark_red><bold>Schematics <reset><dark_gray>-----------");
send(player, "<red>/LoParkour schematic wand <dark_gray>- <gray>Get the wand");
send(player, "<red>/LoParkour schematic pos1 <dark_gray>- <gray>Set position 1");
send(player, "<red>/LoParkour schematic pos2 <dark_gray>- <gray>Set position 2");
send(player, "<red>/LoParkour schematic save <n> <dark_gray>- <gray>Save selection");
send(player, "<red>/LoParkour schematic paste <n> <dark_gray>- <gray>Paste schematic");
send(player, "<red>/LoParkour schematic list <dark_gray>- <gray>List loaded schematics");
send(player, "<red>/LoParkour schematic reload <dark_gray>- <gray>Reload schematics");
send(player, "");
}
/** Called for 2-arg schematic commands (wand/pos1/pos2/list/reload) */
public static void handleSubcommand(String sub, CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
Location playerLoc = player.getLocation();
Location[] sel = selections.get(player);
switch (sub.toLowerCase()) {
case "wand" -> {
player.getInventory().addItem(WAND.clone());
send(player, "<dark_red><bold>Schematics <reset><gray>Wand given. Left/Right click to set positions.");
}
case "pos1" -> setPos(player, sel, playerLoc, 0);
case "pos2" -> setPos(player, sel, playerLoc, 1);
case "list" -> {
send(player, "<dark_red><bold>Schematics <reset><gray>Loaded schematics:");
LoParkour.getSchematicManager().getAllSchematics().forEach((name, s) ->
send(player, "<gray>- <red>" + name + " <dark_gray>(" + s.getMetadata().getDifficulty() + ")"));
}
case "reload" -> {
if (!cooldowns.cooldown(sender, "schematic-reload", 2500)) return;
LoParkour.getSchematicManager().reload();
send(player, "<dark_red><bold>Schematics <reset><gray>Reloaded.");
}
}
}
/** Called for 3-arg schematic commands (save <n>, paste <n>) */
public static void handleSubcommandWithName(String sub, String name, CommandSender sender,
Player player, PlayerCommandHandler cooldowns) {
switch (sub.toLowerCase()) {
case "save" -> handleSave(name, sender, player, cooldowns);
case "paste" -> handlePaste(name, sender, player);
}
}
// ── internal ───────────────────────────────────────────────────────────────
private static void setPos(Player player, Location[] existing, Location loc, int index) {
String label = index == 0 ? "1" : "2";
send(player, LoParkour.PREFIX + "Position " + label + " set to " + Locations.toString(loc, true));
Location[] updated = existing != null ? existing.clone() : new Location[]{null, null};
updated[index] = loc;
selections.put(player, updated);
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 handleSave(String name, CommandSender sender, Player player, PlayerCommandHandler cooldowns) {
if (!cooldowns.cooldown(sender, "schematic-save", 2500)) return;
// Validate schematic name to prevent path traversal
if (!isValidSchematicName(name)) {
send(player, "<dark_red><bold>Schematics <reset><red>Invalid name. Use only letters, numbers, hyphens and underscores.");
return;
}
Location[] sel = selections.get(player);
if (sel == null || sel[0] == null || sel[1] == null) {
send(player, "<dark_red><bold>Schematics <reset><red>Selection incomplete. Set both positions first.");
return;
}
try {
int w = Math.abs(sel[1].getBlockX() - sel[0].getBlockX()) + 1;
int h = Math.abs(sel[1].getBlockY() - sel[0].getBlockY()) + 1;
int l = Math.abs(sel[1].getBlockZ() - sel[0].getBlockZ()) + 1;
LPSchematic schematic = new LPSchematicBuilder(name, player.getName(), 0.5, w, h, l)
.fromWorld(player.getWorld(), sel[0], sel[1])
.build();
LoParkour.getSchematicManager().saveSchematic(schematic);
send(player, "<dark_red><bold>Schematics <reset><gray>Saved as <red>" + name + "<gray>!");
} catch (Exception e) {
send(player, "<dark_red><bold>Schematics <reset><red>Failed: " + e.getMessage());
}
}
/**
* Validates schematic name to prevent path traversal and filesystem issues.
* Only allows alphanumeric characters, hyphens, and underscores.
*/
private static boolean isValidSchematicName(String name) {
if (name == null || name.isEmpty() || name.length() > 64) {
return false;
}
return name.matches("^[a-zA-Z0-9_-]+$");
}
private static void handlePaste(String name, CommandSender sender, Player player) {
// Validate schematic name to prevent path traversal
if (!isValidSchematicName(name)) {
send(player, "<dark_red><bold>Schematics <reset><red>Invalid name. Use only letters, numbers, hyphens and underscores.");
return;
}
LPSchematic s = LoParkour.getSchematicManager().getSchematic(name);
if (s == null) {
send(sender, LoParkour.PREFIX + "Schematic '" + name + "' not found.");
return;
}
s.paste(player.getLocation(), player.getWorld());
send(sender, LoParkour.PREFIX + "Pasted " + name + ".");
}
private static void send(CommandSender sender, String msg) {
sender.sendMessage(ColorUtil.color(msg));
}
}

View file

@ -1,15 +1,17 @@
package dev.loki.loparkour.command;
package dev.loki.loparkour.command.admin;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.mode.Mode;
import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourUser;
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.ColorUtil;
import dev.loki.loparkour.util.text.ColorUtil;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.OfflinePlayer;
@ -27,8 +29,6 @@ import java.util.UUID;
*/
public class AdminCommandHandler {
private static final String COOLDOWN_HINT = "<gray>Please wait before using this again.";
private final PlayerCommandHandler base;
public AdminCommandHandler(PlayerCommandHandler base) {
@ -61,18 +61,18 @@ public class AdminCommandHandler {
if (target.equalsIgnoreCase("everyone")) {
Bukkit.getOnlinePlayers().forEach(p -> Modes.DEFAULT.create(p));
send(sender, LoParkour.PREFIX + "Force joined everyone!");
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_join_everyone"));
return;
}
if (target.equalsIgnoreCase("nearest")) {
Player closest = findNearest(sender);
if (closest == null) return;
Modes.DEFAULT.create(closest);
send(sender, LoParkour.PREFIX + "Force joined " + closest.getName() + "!");
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 + "Player not online!"); return; }
if (other == null) { send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.player_not_online")); return; }
Modes.DEFAULT.create(other);
}
@ -83,27 +83,27 @@ public class AdminCommandHandler {
if (target.equalsIgnoreCase("everyone")) {
ParkourPlayer.getPlayers().forEach(ParkourUser::leave);
send(sender, LoParkour.PREFIX + "Force kicked everyone!");
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.force_leave_everyone"));
return;
}
Player other = Bukkit.getPlayer(target);
if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; }
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 + "Player isn't playing!"); return; }
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, COOLDOWN_HINT)) return;
if (!base.cooldown(sender, "reset", 2500, Locales.getString(sender, "admin.cooldown"))) return;
if (target.equalsIgnoreCase("everyone")) {
Registry.getModes().stream()
.map(Mode::getLeaderboard)
.filter(lb -> lb != null)
.forEach(lb -> { lb.resetAll(); lb.write(true); });
send(sender, LoParkour.PREFIX + "Reset all high scores.");
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.reset_all"));
return;
}
@ -115,7 +115,7 @@ public class AdminCommandHandler {
.filter(lb -> lb != null)
.forEach(lb -> { lb.remove(uuid); lb.write(true); });
send(sender, LoParkour.PREFIX + "Reset high score of " + name + ".");
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.reset_player").formatted(name));
}
// ── recoverinventory ───────────────────────────────────────────────────────
@ -123,13 +123,13 @@ public class AdminCommandHandler {
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 + "Player not online!"); return; }
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 + "Recovered inventory of " + other.getName() + ".");
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.recover_inventory").formatted(other.getName()));
} else {
send(sender, LoParkour.PREFIX + "<red>No saved inventory found for " + other.getName() + ".");
send(sender, LoParkour.PREFIX + Locales.getString(sender, "admin.recover_inventory_missing").formatted(other.getName()));
}
});
}

View file

@ -1,13 +1,14 @@
package dev.loki.loparkour.command;
package dev.loki.loparkour.command.player;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.util.ColorUtil;
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
import dev.loki.loparkour.config.core.Config;
import dev.loki.loparkour.config.locale.Locales;
import dev.loki.loparkour.menu.core.Menus;
import dev.loki.loparkour.menu.core.ParkourOption;
import dev.loki.loparkour.mode.base.Modes;
import dev.loki.loparkour.player.core.ParkourUser;
import dev.loki.loparkour.util.text.ColorUtil;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
@ -80,34 +81,38 @@ public class PlayerCommandHandler {
private void handleReload(@NotNull CommandSender sender, @Nullable Player player) {
if (!cooldown(sender, "reload", 2500)) return;
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
send(sender, Locales.getString(player, "other.no_do"));
send(sender, Locales.getString(sender, "other.no_do"));
return;
}
Config.reload(false);
send(sender, LoParkour.PREFIX + "Reloading config files... (locales are loading asynchronously)");
dev.loki.loparkour.adaptive.bootstrap.AdaptiveServices.reload();
send(sender, LoParkour.PREFIX + Locales.getString(sender, "commands.reload"));
}
public void sendHelp(@NotNull CommandSender sender) {
send(sender, "");
send(sender, "<dark_gray><strikethrough>---------------<reset> " + LoParkour.NAME + " <dark_gray><strikethrough>---------------<reset>");
send(sender, Locales.getString(sender, "commands.help.header"));
send(sender, "");
send(sender, "<gray>/parkour <dark_gray>- Main command");
send(sender, Locales.getString(sender, "commands.help.line_parkour"));
if (sender.hasPermission(ParkourOption.JOIN.permission)) {
send(sender, "<gray>/parkour join [mode/player] <dark_gray>- Join a mode");
send(sender, "<gray>/parkour leave <dark_gray>- Leave parkour");
send(sender, Locales.getString(sender, "commands.help.line_join"));
send(sender, Locales.getString(sender, "commands.help.line_leave"));
}
if (sender.hasPermission(ParkourOption.MAIN.permission)) {
send(sender, Locales.getString(sender, "commands.help.line_menu"));
}
if (sender.hasPermission(ParkourOption.PLAY.permission)) {
send(sender, Locales.getString(sender, "commands.help.line_play"));
}
if (sender.hasPermission(ParkourOption.LEADERBOARDS.permission)) {
send(sender, Locales.getString(sender, "commands.help.line_leaderboard"));
}
if (sender.hasPermission(ParkourOption.MAIN.permission))
send(sender, "<gray>/parkour menu <dark_gray>- Open the menu");
if (sender.hasPermission(ParkourOption.PLAY.permission))
send(sender, "<gray>/parkour play <dark_gray>- Mode selection menu");
if (sender.hasPermission(ParkourOption.LEADERBOARDS.permission))
send(sender, "<gray>/parkour leaderboard [type] <dark_gray>- Leaderboard");
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
send(sender, "<gray>/LoParkour schematic <dark_gray>- Schematic tools");
send(sender, "<gray>/LoParkour reload <dark_gray>- Reload configs");
send(sender, "<gray>/LoParkour reset <everyone/player> <dark_gray>- Reset scores");
send(sender, "<gray>/LoParkour forcejoin/forceleave <target> <dark_gray>- Force join/leave");
send(sender, "<gray>/LoParkour recoverinventory <player> <dark_gray>- Recover inventory");
send(sender, Locales.getString(sender, "commands.help.line_schematic"));
send(sender, Locales.getString(sender, "commands.help.line_reload"));
send(sender, Locales.getString(sender, "commands.help.line_reset"));
send(sender, Locales.getString(sender, "commands.help.line_force"));
send(sender, Locales.getString(sender, "commands.help.line_recover"));
}
send(sender, "");
}
@ -117,7 +122,7 @@ public class PlayerCommandHandler {
}
/**
* @param whenBlocked message sent (MiniMessage color) if still on cooldown; may be null for silent block
* @param whenBlocked message sent ({@code &#RRGGBB} / {@code &} colors) if still on cooldown; may be null for silent block
*/
public boolean cooldown(CommandSender sender, String key, long millis, @Nullable String whenBlocked) {
String fullKey = sender.getName() + ":" + key;

View file

@ -0,0 +1,271 @@
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.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;
/**
* Admin schematic tools: wand, create, convert, list, paste, reload.
*/
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();
}
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);
}
@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;
}
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);
}
public static void clearWandCache() {
cachedWand = null;
}
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, "");
}
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);
}
}
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 -> {
}
}
}
/** {@code schematic create <name> <difficulty>} */
public static void handleCreateWithDifficulty(
String name,
String difficultyRaw,
CommandSender sender,
Player player,
PlayerCommandHandler cooldowns
) {
if (!cooldowns.cooldown(sender, "schematic-create", 2500)) {
return;
}
handleCreate(name, difficultyRaw, sender, player);
}
/** {@code create <difficulty>} or {@code schematic create <difficulty>} */
public static void handleCreateDifficultyOnly(
String difficultyRaw,
CommandSender sender,
Player player,
PlayerCommandHandler cooldowns
) {
if (!cooldowns.cooldown(sender, "schematic-create", 2500)) {
return;
}
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_-]+$");
}
@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));
}
}