fix and migrate 70%

This commit is contained in:
loki 2026-03-05 20:28:25 +01:00
parent 8705cad578
commit d1dcf6ab57
138 changed files with 3736 additions and 4735 deletions

View file

@ -1,645 +1,7 @@
package dev.loki.loparkour;
import org.bukkit.Bukkit;
import java.util.ArrayList;
import dev.loki.loparkour.util.Item;
import dev.loki.loparkour.util.ParticleData;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.mode.Mode;
import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.mode.MultiMode;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.player.data.InventoryData;
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
import dev.loki.loparkour.schematic.lpschem.LPSchematicBuilder;
import dev.loki.loparkour.schematic.lpschem.SchematicConverter;
import dev.loki.loparkour.session.Session;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.TabCompleter;
import dev.loki.loparkour.util.ParticleUtil;
import dev.loki.loparkour.util.Locations;
import dev.loki.loparkour.util.ColorUtil;
import org.bukkit.*;
import org.bukkit.command.BlockCommandSender;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.BoundingBox;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
@SuppressWarnings("deprecation")
public class Command implements CommandExecutor, TabCompleter {
public static final HashMap<Player, Location[]> selections = new HashMap<>();
private static final ItemStack WAND = new Item(Material.GOLDEN_AXE, "<red><bold>LPSchematic Wand")
.lore("<gray>Left click: first position", "<gray>Right click: second position")
.build();
@Override
public boolean onCommand(CommandSender sender, org.bukkit.command.Command command, String label, String[] args) {
Player player = null;
if (sender instanceof Player) {
player = (Player) sender;
}
switch (args.length) {
case 0 -> handle0Args(sender, player);
case 1 -> handle1Args(args[0], sender, player);
case 2 -> handle2Args(args[0], args[1], sender, player);
case 3 -> handle3Args(args[0], args[1], args[2], sender, player);
}
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, org.bukkit.command.Command command, String alias, String[] args) {
List<String> completions = new ArrayList<>();
switch (args.length) {
case 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.add("schematic");
completions.add("reload");
completions.add("forcejoin");
completions.add("forceleave");
completions.add("reset");
completions.add("recoverinventory");
}
return completions(args[0], completions);
}
case 2 -> {
if (args[0].equalsIgnoreCase("reset") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
completions.add("everyone");
for (ParkourPlayer pp : ParkourPlayer.getPlayers()) {
completions.add(pp.getName());
}
} else if (args[0].equalsIgnoreCase("join") && sender.hasPermission(ParkourOption.JOIN.permission)) {
for (ParkourPlayer pp : ParkourPlayer.getPlayers()) {
completions.add(pp.getName());
}
} else if (args[0].equalsIgnoreCase("schematic") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
completions.addAll(Arrays.asList("wand", "pos1", "pos2", "save", "paste"));
} else if (args[0].equalsIgnoreCase("forcejoin") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
completions.add("nearest");
completions.add("everyone");
for (Player pl : Bukkit.getOnlinePlayers()) {
completions.add(pl.getName());
}
} else if (args[0].equalsIgnoreCase("forceleave") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
completions.add("everyone");
for (Player pl : Bukkit.getOnlinePlayers()) {
completions.add(pl.getName());
}
} else if (args[0].equalsIgnoreCase("recoverinventory") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
for (Player pl : Bukkit.getOnlinePlayers()) {
completions.add(pl.getName());
}
}
return completions(args[1], completions);
}
default -> {
return Collections.emptyList();
}
}
}
private void handle0Args(@NotNull CommandSender sender, @Nullable Player player) {
if (player != null && ParkourOption.MAIN.mayPerform(player)) {
// TODO: Migrate to LoLib GUI
// Menus.*.open(player);
player.sendMessage("§cМеню временно недоступно во время миграции");
return;
}
sendHelpMessages(sender);
}
private void sendHelpMessages(CommandSender sender) {
send(sender, "");
send(sender, "<dark_gray><strikethrough>---------------<reset> %s <dark_gray><strikethrough>---------------<reset>".formatted(LoParkour.NAME));
send(sender, "");
send(sender, "<gray>/parkour <dark_gray>- Main command");
if (sender.hasPermission(ParkourOption.JOIN.permission)) {
send(sender, "<gray>/parkour join [mode/player] <dark_gray>- Join the default mode or specify one.");
send(sender, "<gray>/parkour leave <dark_gray>- Leave the game on this server");
}
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>- Open the leaderboard of a mode");
}
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
send(sender, "<gray>/LoParkour schematic <dark_gray>- Create a schematic");
send(sender, "<gray>/LoParkour reload <dark_gray>- Reloads the messages-v3.yml file");
send(sender, "<gray>/LoParkour reset <everyone/player> <dark_gray>- Resets all high scores. <red>This can't be recovered!");
send(sender, "<gray>/LoParkour forcejoin <everyone/nearest/player> <dark_gray>- Forces a specific player, the nearest or everyone to join");
send(sender, "<gray>/LoParkour forceleave <everyone/nearest/player> <dark_gray>- Forces a specific player, the nearest or everyone to leave");
send(sender, "<gray>/LoParkour recoverinventory <player> <dark_gray>- Recover a player's saved inventory. <red>Useful for recovering data after server crashes or errors when leaving.");
}
send(sender, "");
}
private void handle1Args(@NotNull String arg, @NotNull CommandSender sender, @Nullable Player player) {
switch (arg.toLowerCase()) {
case "help" -> sendHelpMessages(sender);
case "reload" -> {
if (!cooldown(sender, "reload", 2500)) {
return;
}
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
send(sender, Locales.getString(player, "other.no_do"));
return;
}
Config.reload(false);
send(sender, "%sReloaded config files.".formatted(LoParkour.PREFIX));
}
}
if (player == null) {
return;
}
switch (arg.toLowerCase()) {
case "join" -> {
if (!cooldown(sender, "join", 2500)) {
return;
}
if (!ParkourOption.JOIN.mayPerform(player)) {
send(sender, Locales.getString(player, "other.no_do"));
return;
}
ParkourUser user = ParkourUser.getUser(player);
if (user != null) {
return;
}
Modes.DEFAULT.create(player);
}
case "play" -> {
if (ParkourOption.PLAY.mayPerform(player)) {
// TODO: Migrate to LoLib GUI
// Menus.*.open(player);
player.sendMessage("§cМеню временно недоступно во время миграции");
}
}
case "leave" -> {
if (!cooldown(sender, "leave", 2500)) {
return;
}
ParkourUser.leave(player);
}
case "menu", "main" -> {
if (!ParkourOption.MAIN.mayPerform(player)) {
// TODO: Migrate to LoLib GUI
// Menus.*.open(player);
player.sendMessage("§cМеню временно недоступно во время миграции");
}
}
case "leaderboard" -> player.performCommand("LoParkour leaderboard invalid");
case "schematic" -> {
if (!player.hasPermission(ParkourOption.ADMIN.permission)) { // default players shouldn't have access even if perms are disabled
send(sender, Locales.getString(player, "other.no_do"));
return;
}
send(player, "");
send(player, "<red>/LoParkour schematic wand <dark_gray>- <gray>Get the schematic wand");
send(player, "<red>/LoParkour schematic pos1 <dark_gray>- <gray>Set the first position of your selection");
send(player, "<red>/LoParkour schematic pos2 <dark_gray>- <gray>Set the second position of your selection");
send(player, "<red>/LoParkour schematic save <name> <dark_gray>- <gray>Save selection to .lpschem format");
send(player, "<red>/LoParkour schematic paste <name> <dark_gray>- <gray>Paste a .lpschem schematic");
send(player, "<red>/LoParkour schematic list <dark_gray>- <gray>List all loaded schematics");
send(player, "<red>/LoParkour schematic reload <dark_gray>- <gray>Reload all schematics");
send(player, "<red>/LoParkour schematic convert <dark_gray>- <gray>Convert old schematics to .lpschem");
send(player, "");
send(player, "<dark_gray><underlined>Have any questions or need help? Join the Discord.");
}
}
}
private void handle2Args(@NotNull String arg1, @NotNull String arg2, @NotNull CommandSender sender, @Nullable Player player) {
switch (arg1.toLowerCase()) {
case "forcejoin" -> {
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
return;
}
if (arg2.equalsIgnoreCase("everyone")) {
Bukkit.getOnlinePlayers().forEach(other -> Modes.DEFAULT.create(other));
send(sender, LoParkour.PREFIX + "Successfully force joined everyone!");
return;
}
if (arg2.equalsIgnoreCase("nearest")) {
Player closest = null;
double distance = Double.MAX_VALUE;
// if player is found get location from player
// if no player is found, get location from command block
// if no command block is found, return null
Location from = sender instanceof Player ? ((Player) sender).getLocation() : (sender instanceof BlockCommandSender ? ((BlockCommandSender) sender).getBlock().getLocation() : null);
if (from == null || from.getWorld() == null) {
return;
}
// get the closest player
for (Player p : from.getWorld().getPlayers()) {
double d = p.getLocation().distance(from);
if (d < distance) {
distance = d;
closest = p;
}
}
// no closest player found
if (closest == null) {
return;
}
send(sender, LoParkour.PREFIX + "Successfully force joined " + closest.getName() + "!");
Modes.DEFAULT.create(closest);
return;
}
Player other = Bukkit.getPlayer(arg2);
if (other == null) {
send(sender, LoParkour.PREFIX + "That player isn't online!");
return;
}
Modes.DEFAULT.create(other);
}
case "forceleave" -> {
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
return;
}
if (arg2.equalsIgnoreCase("everyone")) {
ParkourPlayer.getPlayers().forEach(ParkourUser::leave);
send(sender, LoParkour.PREFIX + "Successfully force kicked everyone!");
return;
}
Player other = Bukkit.getPlayer(arg2);
if (other == null) {
send(sender, LoParkour.PREFIX + "That player isn't online!");
return;
}
ParkourUser user = ParkourUser.getUser(other);
if (user == null) {
send(sender, LoParkour.PREFIX + "That player isn't currently playing!");
return;
}
ParkourUser.leave(user);
}
case "reset" -> {
if (!sender.hasPermission(ParkourOption.ADMIN.permission) || !cooldown(sender, "reset", 2500)) {
return;
}
if (arg2.equalsIgnoreCase("everyone")) {
for (Mode mode : Registry.getModes()) {
Leaderboard leaderboard = mode.getLeaderboard();
if (leaderboard == null) {
continue;
}
leaderboard.resetAll();
leaderboard.write(true);
}
send(sender, LoParkour.PREFIX + "Successfully reset all high scores in memory and the files.");
return;
}
String name = null;
UUID uuid = null;
// Check online players
Player online = Bukkit.getPlayerExact(arg2);
if (online != null) {
name = online.getName();
uuid = online.getUniqueId();
}
// Check uuid
if (arg2.contains("-")) {
uuid = UUID.fromString(arg2);
}
// Check offline player
if (uuid == null) {
OfflinePlayer offline = Bukkit.getOfflinePlayer(arg2);
name = offline.getName();
uuid = offline.getUniqueId();
}
UUID finalUuid = uuid;
String finalName = name;
for (Mode mode : Registry.getModes()) {
Leaderboard leaderboard = mode.getLeaderboard();
if (leaderboard == null) {
continue;
}
leaderboard.remove(finalUuid);
leaderboard.write(true);
}
send(sender, LoParkour.PREFIX + "Successfully reset the high score of " + finalName + " in memory and the files.");
}
case "recoverinventory" -> {
if (!cooldown(sender, "recoverinventory", 2500) || !sender.hasPermission(ParkourOption.ADMIN.permission)) {
return;
}
Player other = Bukkit.getPlayer(arg2);
if (other == null) {
send(sender, LoParkour.PREFIX + "That player isn't online!");
return;
}
new InventoryData(other).load(result -> {
if (result != null) {
send(sender, "%sSuccessfully recovered the inventory of %s from their file".formatted(LoParkour.PREFIX, other.getName()));
send(sender, "%sGiving %s their items now...".formatted(LoParkour.PREFIX, other.getName()));
} else {
send(sender, "%s<red>There was an error recovering the inventory of %s from their file".formatted(LoParkour.PREFIX, other.getName()));
send(sender, "%s%s has no saved inventory or there was an error. Check the console.".formatted(LoParkour.PREFIX, other.getName()));
}
});
}
}
if (player == null) {
return;
}
switch (arg1) {
case "join" -> {
if (!cooldown(sender, "join", 2500) || !ParkourOption.JOIN.mayPerform(player)) {
return;
}
Mode mode = Registry.getMode(arg2);
if (mode != null) {
mode.create(player);
return;
}
Player other = Bukkit.getPlayer(arg2);
if (other == null) {
send(sender, "%sUnknown player! Try typing the name again.".formatted(LoParkour.PREFIX)); // could not find, so go to default
return;
}
ParkourPlayer parkourPlayer = ParkourPlayer.getPlayer(other);
if (parkourPlayer == null) {
send(sender, "%sUnknown player! Try typing the name again.".formatted(LoParkour.PREFIX)); // could not find, so go to default
return;
}
ParkourUser user = ParkourUser.getUser(player);
Session session = parkourPlayer.session;
if (user != null && user.session == session) { // already in same session
return;
}
if (session.isAcceptingPlayers()) {
((MultiMode) session.generator.getMode()).join(player, session);
} else {
Modes.SPECTATOR.create(player, session);
}
}
case "leaderboard" -> {
if (!ParkourOption.LEADERBOARDS.mayPerform(player)) {
send(sender, Locales.getString(player, "other.no_do"));
return;
}
Mode mode = Registry.getMode(arg2.toLowerCase());
// if found gamemode is null, return to default
if (mode == null) {
// TODO: Migrate to LoLib GUI
// Menus.*.open(player);
player.sendMessage("§cМеню временно недоступно во время миграции");
} else {
// TODO: Migrate to LoLib GUI
// Menus.SINGLE_LEADERBOARD.open(player, mode, Leaderboard.Sort.SCORE);
player.sendMessage("§cМеню временно недоступно во время миграции");
}
}
case "schematic" -> {
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
send(sender, Locales.getString(player, "other.no_do"));
return;
}
Location playerLocation = player.getLocation();
Location[] existingSelection = selections.get(player);
switch (arg2.toLowerCase()) {
case "wand" -> {
player.getInventory().addItem(WAND);
send(player, "<dark_gray>----------- <dark_red><bold>Schematics <reset><dark_gray>-----------");
send(player, "<gray><red>Left click<gray> -> set first position | <red>Right click<gray> -> set second position");
send(player, "<gray>If you can't place a block and need to set a position mid-air, use <dark_gray>/LoParkour schematic pos1/pos2 <gray>instead.");
}
case "pos1" -> {
send(player, "%sPosition 1 was set to %s".formatted(LoParkour.PREFIX, Locations.toString(playerLocation, true)));
if (existingSelection == null) {
selections.put(player, new Location[]{playerLocation, null});
return;
}
selections.put(player, new Location[]{playerLocation, existingSelection[1]});
ParticleUtil.box(BoundingBox.of(playerLocation, existingSelection[1]), player.getWorld(), Particle.END_ROD, player, 0.2);
}
case "pos2" -> {
send(player, "%sPosition 2 was set to %s".formatted(LoParkour.PREFIX, Locations.toString(playerLocation, true)));
if (existingSelection == null) {
selections.put(player, new Location[]{null, playerLocation});
return;
}
selections.put(player, new Location[]{existingSelection[0], playerLocation});
ParticleUtil.box(BoundingBox.of(existingSelection[0], playerLocation), player.getWorld(), Particle.END_ROD, player, 0.2);
}
case "save" -> {
if (!cooldown(sender, "LoParkour save schematic", 2500)) {
return;
}
if (existingSelection == null || existingSelection[0] == null || existingSelection[1] == null) {
send(player, "<dark_red><bold>Schematics <reset><gray>Your schematic isn't complete yet. Make sure you've set the first and second position.");
return;
}
String code = UUID.randomUUID().toString().split("-")[0];
send(player, ("<dark_red><bold>Schematics <reset><gray>Your schematic is being saved. It will use code <red>'%s'<gray>. " + "You can change the code to whatever you like. " + "Don't forget to add this schematic to <dark_gray>schematics.yml<gray>.").formatted(code));
// TODO: LPSchematic.save(file, pos1, pos2);
}
case "list" -> {
send(player, "<dark_red><bold>Schematics <reset><gray>Loaded schematics:");
LoParkour.getSchematicManager().getAllSchematics().forEach((name, schem) -> {
send(player, "<gray>- <red>" + name + " <dark_gray>(" + schem.getMetadata().getDifficulty() + ")");
});
}
case "reload" -> {
if (!cooldown(sender, "LoParkour reload schematics", 2500)) {
return;
}
LoParkour.getSchematicManager().reload();
send(player, "<dark_red><bold>Schematics <reset><gray>Reloaded all schematics.");
}
case "convert" -> {
if (!cooldown(sender, "LoParkour convert schematics", 5000)) {
return;
}
send(player, "<dark_red><bold>Schematics <reset><gray>Converting old schematics to .lpschem format...");
SchematicConverter.convertAll();
send(player, "<dark_red><bold>Schematics <reset><gray>Conversion complete! Check console for details.");
}
}
}
}
}
private void handle3Args(@NotNull String arg1, @NotNull String arg2, @NotNull String arg3, @NotNull CommandSender sender, @Nullable Player player) {
if (player == null) {
return;
}
if (arg1.equalsIgnoreCase("schematic")) {
if (!player.hasPermission(ParkourOption.ADMIN.permission)) {
return;
}
switch (arg2.toLowerCase()) {
case "save" -> {
if (!cooldown(sender, "LoParkour save schematic", 2500)) {
return;
}
Location[] selection = selections.get(player);
if (selection == null || selection[0] == null || selection[1] == null) {
send(player, "<dark_red><bold>Schematics <reset><gray>Your schematic isn't complete yet. Make sure you've set the first and second position.");
return;
}
try {
int width = Math.abs(selection[1].getBlockX() - selection[0].getBlockX()) + 1;
int height = Math.abs(selection[1].getBlockY() - selection[0].getBlockY()) + 1;
int length = Math.abs(selection[1].getBlockZ() - selection[0].getBlockZ()) + 1;
LPSchematic schematic = new LPSchematicBuilder(arg3, player.getName(), 0.5, width, height, length)
.fromWorld(player.getWorld(), selection[0], selection[1])
.build();
LoParkour.getSchematicManager().saveSchematic(schematic);
send(player, "<dark_red><bold>Schematics <reset><gray>Saved schematic as <red>" + arg3 + "<gray>!");
} catch (Exception e) {
send(player, "<dark_red><bold>Schematics <reset><gray>Failed to save schematic: " + e.getMessage());
e.printStackTrace();
}
}
case "paste" -> {
LPSchematic lpSchematic = LoParkour.getSchematicManager().getSchematic(arg3);
if (lpSchematic != null) {
lpSchematic.paste(player.getLocation(), player.getWorld());
send(sender, "%sPasted .lpschem schematic %s".formatted(LoParkour.PREFIX, arg3));
return;
}
// TODO: Implement schematic loading with LoLib
LPSchematic oldSchematic = null;
if (oldSchematic == null) {
send(sender, "%sCouldn't find %s".formatted(LoParkour.PREFIX, arg3));
return;
}
// oldSchematic.paste(player.getLocation(), player.getWorld());
send(sender, "%sPasted old schematic %s".formatted(LoParkour.PREFIX, arg3));
}
}
}
}
private void send(CommandSender sender, String message) {
sender.sendMessage(ColorUtil.color(message));
}
private final Map<String, Long> cooldowns = new HashMap<>();
private boolean cooldown(CommandSender sender, String key, long millis) {
String fullKey = sender.getName() + ":" + key;
long now = System.currentTimeMillis();
Long last = cooldowns.get(fullKey);
if (last != null && now - last < millis) {
return false;
}
cooldowns.put(fullKey, now);
return true;
}
private List<String> completions(String arg, List<String> completions) {
return completions.stream()
.filter(s -> s.toLowerCase().startsWith(arg.toLowerCase()))
.toList();
}
}
/**
* @deprecated Replaced by {@link LoParkourCommand}. Delete this file.
*/
@Deprecated
class Command extends LoParkourCommand {}

View file

@ -1,328 +1,12 @@
package dev.loki.loparkour;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import dev.loki.loparkour.util.ParticleData;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.session.Session;
import dev.loki.loparkour.world.World;
import org.bukkit.event.Listener;
import dev.loki.loparkour.util.ParticleUtil;
import dev.loki.loparkour.util.Locations;
import dev.loki.loparkour.util.ColorUtil;
import io.papermc.lib.PaperLib;
import org.bukkit.*;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.*;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.util.BoundingBox;
import org.jetbrains.annotations.ApiStatus;
import java.time.Duration;
import java.time.Instant;
import java.util.NoSuchElementException;
/**
* Internal event handler
* @deprecated Split into:
* - {@link dev.loki.loparkour.listener.PlayerConnectionListener}
* - {@link dev.loki.loparkour.listener.ParkourRestrictionListener}
* - {@link dev.loki.loparkour.listener.SchematicWandListener}
*
* Delete this file after verifying compilation.
*/
@ApiStatus.Internal
public class Events implements Listener {
@EventHandler
public void chat(AsyncPlayerChatEvent event) {
if (!Option.OPTIONS_ENABLED.get(ParkourOption.CHAT)) {
return;
}
Player player = event.getPlayer();
ParkourUser user = ParkourUser.getUser(player);
if (user == null) {
return;
}
Session session = user.session;
if (session.isMuted(user)) {
return;
}
event.setCancelled(true);
switch (user.chatType) {
case LOBBY_ONLY -> session.getUsers().forEach(other -> other.sendTranslated("settings.chat.formats.lobby", player.getName(), event.getMessage()));
case PLAYERS_ONLY -> session.getPlayers().forEach(other -> other.sendTranslated("settings.chat.formats.players", player.getName(), event.getMessage()));
default -> event.setCancelled(false);
}
}
@EventHandler(priority = EventPriority.LOWEST)
public void join(PlayerJoinEvent event) {
Player player = event.getPlayer();
if (Config.CONFIG.getBoolean("bungeecord.enabled")) {
Modes.DEFAULT.create(player);
return;
}
if (!player.getWorld().equals(World.getWorld())) {
return;
}
org.bukkit.World fallback = Bukkit.getWorld(Config.CONFIG.getString("world.fall-back"));
if (fallback != null) {
PaperLib.teleportAsync(player, fallback.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
return;
}
PaperLib.teleportAsync(player, Bukkit.getWorlds().stream()
.filter(world -> !world.equals(World.getWorld()))
.findAny()
.orElseThrow(() -> new NoSuchElementException("No fallback world was found!"))
.getSpawnLocation());
}
@EventHandler
public void leave(PlayerQuitEvent event) {
ParkourUser user = ParkourUser.getUser(event.getPlayer());
if (user == null) {
return;
}
ParkourUser.unregister(user, true, false, true);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void command(PlayerCommandPreprocessEvent event) {
if (!Config.CONFIG.getBoolean("focus-mode.enabled")) {
return;
}
ParkourUser user = ParkourUser.getUser(event.getPlayer());
if (user == null) {
return;
}
String command = event.getMessage().toLowerCase();
if (Config.CONFIG.getStringList("focus-mode.whitelist").stream().anyMatch(c -> command.contains(c.toLowerCase()))) {
return;
}
user.sendTranslated("other.no_do");
event.setCancelled(true);
}
@EventHandler
public void interactWand(PlayerInteractEvent event) {
Action action = event.getAction();
Player player = event.getPlayer();
ItemStack item = player.getInventory().getItemInMainHand();
if (!player.hasPermission("LoParkour.admin") || item.getItemMeta() == null || !item.getItemMeta().getDisplayName().contains("LPSchematic Wand") || event.getClickedBlock() == null || event.getHand() != EquipmentSlot.HAND) {
return;
}
Location location = event.getClickedBlock().getLocation();
Location[] existingSelection = Command.selections.get(player);
event.setCancelled(true);
switch (action) {
case LEFT_CLICK_BLOCK -> {
send(player, LoParkour.PREFIX + "Position 1 was set to " + Locations.toString(location, true));
if (existingSelection == null) {
Command.selections.put(player, new Location[]{location, null});
return;
}
Command.selections.put(player, new Location[]{location, existingSelection[1]});
ParticleUtil.box(BoundingBox.of(location, existingSelection[1]), player.getWorld(), Particle.END_ROD, player, 0.2);
}
case RIGHT_CLICK_BLOCK -> {
send(player, LoParkour.PREFIX + "Position 2 was set to " + Locations.toString(location, true));
if (existingSelection == null) {
Command.selections.put(player, new Location[]{null, location});
return;
}
Command.selections.put(player, new Location[]{existingSelection[0], location});
ParticleUtil.box(BoundingBox.of(existingSelection[0], location), player.getWorld(), Particle.END_ROD, player, 0.2);
}
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void interact(PlayerInteractEvent event) {
Player player = event.getPlayer();
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp == null) {
return;
}
boolean type = event.getClickedBlock() != null && (event.getClickedBlock().getType() == Material.DISPENSER || event.getClickedBlock().getType() == Material.DROPPER || event.getClickedBlock().getType() == Material.HOPPER);
if (event.getAction() == Action.RIGHT_CLICK_BLOCK && type && event.getHand() == EquipmentSlot.HAND) {
event.setCancelled(true);
player.closeInventory();
return;
}
boolean action = (event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK) && event.getHand() == EquipmentSlot.HAND;
if (!action) {
return;
}
Material held = getHeldItem(player).getType();
Material play = Locales.getItem(player, "play.item").build().getType();
Material community = Locales.getItem(player, "community.item").build().getType();
Material settings = Locales.getItem(player, "settings.item").build().getType();
Material lobby = Locales.getItem(player, "lobby.item").build().getType();
Material quit = Locales.getItem(player, "other.quit").build().getType();
event.setCancelled(true);
if (held == play) {
// TODO: Migrate to LoLib GUI
// Menus.*.open(player);
player.sendMessage("§cМеню временно недоступно во время миграции");
} else if (held == community) {
// TODO: Migrate to LoLib GUI
// Menus.*.open(player);
player.sendMessage("§cМеню временно недоступно во время миграции");
} else if (held == settings) {
// TODO: Migrate to LoLib GUI
// Menus.*.open(player);
player.sendMessage("§cМеню временно недоступно во время миграции");
} else if (held == lobby) {
// TODO: Migrate to LoLib GUI
// Menus.*.open(player);
player.sendMessage("§cМеню временно недоступно во время миграции");
} else if (held == quit) {
ParkourUser.leave(player);
} else {
if (!Config.CONFIG.getBoolean("options.disable-inventory-blocks")) {
event.setCancelled(false);
}
}
}
private ItemStack getHeldItem(Player player) {
PlayerInventory inventory = player.getInventory();
return inventory.getItemInMainHand().getType() == Material.AIR ? inventory.getItemInOffHand() : inventory.getItemInMainHand();
}
@EventHandler
public void switchWorld(PlayerChangedWorldEvent event) {
Player player = event.getPlayer();
ParkourUser user = ParkourUser.getUser(player);
org.bukkit.World parkour = World.getWorld();
boolean isAdmin = Config.CONFIG.getBoolean("permissions.enabled") ? ParkourOption.ADMIN.mayPerform(player) : player.isOp();
if (player.getWorld() == parkour && user == null && !isAdmin && player.getTicksLived() > 20) {
Bukkit.getWorlds().stream()
.filter(world -> !world.equals(parkour))
.findAny()
.ifPresent(world -> PaperLib.teleportAsync(player, world.getSpawnLocation()));
return;
}
if (event.getFrom() == parkour && user != null && Duration.between(user.joined, Instant.now()).toMillis() > 100) {
ParkourUser.unregister(user, true, false, false);
}
}
@EventHandler
public void onDrop(PlayerDropItemEvent event) {
handleRestriction(event.getPlayer(), event);
}
@EventHandler
public void onPlace(BlockPlaceEvent event) {
handleRestriction(event.getPlayer(), event);
}
@EventHandler
public void onBreak(BlockBreakEvent event) {
handleRestriction(event.getPlayer(), event);
}
@EventHandler
public void damage(EntityDamageEvent event) {
if (!(event.getEntity() instanceof Player player)) {
return;
}
handleRestriction(player, event);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void inventory(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player) || event.getInventory().getType() == InventoryType.CRAFTING) {
return;
}
handleRestriction(player, event);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void spectate(PlayerTeleportEvent event) {
if (event.getCause() != PlayerTeleportEvent.TeleportCause.SPECTATE) {
return;
}
handleRestriction(event.getPlayer(), event);
}
// @EventHandler(priority = EventPriority.HIGHEST)
// public void mount(EntityMountEvent event) {
// if (!(event.getEntity() instanceof Player player)) {
// return;
// }
//
// handleRestriction(player, event);
// }
private void handleRestriction(Player player, Cancellable event) {
if (!ParkourUser.isUser(player)) {
return;
}
event.setCancelled(true);
}
private void send(CommandSender sender, String message) {
sender.sendMessage(ColorUtil.color(message));
}
}
@Deprecated
class Events {}

View file

@ -19,6 +19,7 @@ import dev.loki.loparkour.schematic.lpschem.LPSchematicManager;
import dev.loki.loparkour.storage.Storage;
import dev.loki.loparkour.world.World;
import dev.lolib.core.LoPlugin;
import dev.lolib.scheduler.Scheduler;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.bstats.bukkit.Metrics;
@ -28,6 +29,8 @@ import org.bstats.charts.SingleLineChart;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Main class of LoParkour
@ -39,7 +42,10 @@ public final class LoParkour extends LoPlugin {
public static final String NAME = "&#FF6464<bold>LoParkour<reset>";
public static final String PREFIX = NAME + " &#404040» &#A0A0A0";
private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();
private static final Gson gson = new GsonBuilder()
.setPrettyPrinting()
.excludeFieldsWithoutExposeAnnotation()
.create();
public static Gson getGson() {
return gson;
@ -87,14 +93,9 @@ public final class LoParkour extends LoPlugin {
@Override
public void onEnable() {
instance = this;
super.onEnable();
}
@Override
public void enable() {
instance = this; // must be first — Config enum accesses instance inside reload()
loadConfigs();
loadSchematics();
registerModes();
@ -110,8 +111,77 @@ public final class LoParkour extends LoPlugin {
private void loadSchematics() {
schematicManager = new LPSchematicManager();
// Create default island schematic if it doesn't exist
File schematicsFolder = getInFolder("schematics-new");
File islandFile = new File(schematicsFolder, "island.lpschem");
if (!islandFile.exists()) {
try {
createDefaultIslandSchematic(islandFile);
getLogger().info("Created default island schematic");
} catch (Exception ex) {
getLogger().severe("Failed to create default island schematic: " + ex.getMessage());
ex.printStackTrace();
}
}
schematicManager.loadAll();
}
private void createDefaultIslandSchematic(File file) throws Exception {
// Create a larger 9x5x9 island for better visibility and safety
List<String> palette = new ArrayList<>();
palette.add("minecraft:air");
palette.add("minecraft:stone");
palette.add("minecraft:diamond_block");
palette.add("minecraft:emerald_block");
int width = 9, height = 5, length = 9;
int[] blocks = new int[width * height * length];
// Fill everything with air first
for (int i = 0; i < blocks.length; i++) {
blocks[i] = 0;
}
// Create a solid stone platform at y=1 (7x7 platform)
for (int z = 1; z < 8; z++) {
for (int x = 1; x < 8; x++) {
int index = x + (z * width) + (1 * width * length);
blocks[index] = 1; // stone
}
}
// Place diamond block (player spawn) at center
int centerX = 4, centerZ = 4;
int diamondIndex = centerX + (centerZ * width) + (1 * width * length);
blocks[diamondIndex] = 2; // diamond_block - player spawns here
// Place emerald block (parkour start) to the right of spawn
int emeraldIndex = (centerX + 1) + (centerZ * width) + (1 * width * length);
blocks[emeraldIndex] = 3; // emerald_block - parkour starts here
dev.loki.loparkour.schematic.lpschem.SchematicMetadata metadata =
new dev.loki.loparkour.schematic.lpschem.SchematicMetadata("island", "LoParkour", 0.0);
metadata.addTag("spawn");
metadata.addTag("island");
dev.loki.loparkour.schematic.lpschem.SchematicDimensions dimensions =
new dev.loki.loparkour.schematic.lpschem.SchematicDimensions(width, height, length);
// Markers point to the blocks where player spawns and parkour begins
dev.loki.loparkour.schematic.lpschem.SchematicMarkers markers =
new dev.loki.loparkour.schematic.lpschem.SchematicMarkers(
new dev.loki.loparkour.schematic.lpschem.SchematicMarkers.Vector3i(centerX, 1, centerZ),
new dev.loki.loparkour.schematic.lpschem.SchematicMarkers.Vector3i(centerX + 1, 1, centerZ)
);
dev.loki.loparkour.schematic.lpschem.LPSchematic schematic =
new dev.loki.loparkour.schematic.lpschem.LPSchematic(metadata, dimensions, palette, blocks, markers);
schematic.save(file);
}
private void registerModes() {
Registry.register(new DefaultMode());
@ -140,14 +210,31 @@ public final class LoParkour extends LoPlugin {
}
private void setupWorld() {
if (Config.CONFIG.getBoolean("joining")) {
if (!Config.CONFIG.getBoolean("joining")) {
return;
}
// World creation MUST happen on the main thread (Bukkit API requirement)
try {
loLogger().info("Creating parkour world...");
World.create();
loLogger().info("Parkour world created successfully!");
} catch (Exception ex) {
loLogger().error("Failed to create parkour world", ex);
}
}
private void registerEventsAndCommands() {
getServer().getPluginManager().registerEvents(new Events(), this);
getCommand("LoParkour").setExecutor(new Command());
dev.lolib.gui.GUIManager.init(this); // required for LoLib GUI click handling
var pm = getServer().getPluginManager();
pm.registerEvents(new dev.loki.loparkour.listener.PlayerConnectionListener(), this);
pm.registerEvents(new dev.loki.loparkour.listener.ParkourRestrictionListener(), this);
pm.registerEvents(new dev.loki.loparkour.listener.SchematicWandListener(), this);
LoParkourCommand cmd = new LoParkourCommand();
getCommand("LoParkour").setExecutor(cmd);
getCommand("LoParkour").setTabCompleter(cmd);
}
private void setupMetrics() {

View file

@ -1,11 +1,9 @@
package dev.loki.loparkour;
import org.bukkit.Bukkit;
import java.util.ArrayList;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.command.AdminCommandHandler;
import dev.loki.loparkour.command.PlayerCommandHandler;
import dev.loki.loparkour.command.SchematicCommandHandler;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.menu.Menus;
@ -15,613 +13,170 @@ import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.mode.MultiMode;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.player.data.InventoryData;
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
import dev.loki.loparkour.schematic.lpschem.LPSchematicBuilder;
import dev.loki.loparkour.session.Session;
import dev.loki.loparkour.util.ColorUtil;
import dev.loki.loparkour.util.ParticleUtil;
import dev.loki.loparkour.util.Locations;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.TabCompleter;
import org.bukkit.*;
import org.bukkit.command.BlockCommandSender;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.BoundingBox;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Main command router for /LoParkour and /parkour.
* Delegates to {@link PlayerCommandHandler}, {@link AdminCommandHandler},
* and {@link SchematicCommandHandler}.
*/
@SuppressWarnings("deprecation")
public class LoParkourCommand implements CommandExecutor, TabCompleter {
public static final HashMap<Player, Location[]> selections = new HashMap<>();
private static final ItemStack WAND = createWand();
private static ItemStack createWand() {
ItemStack wand = new ItemStack(Material.GOLDEN_AXE);
var meta = wand.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")
));
wand.setItemMeta(meta);
}
return wand;
}
private final PlayerCommandHandler player = new PlayerCommandHandler();
private final AdminCommandHandler admin = new AdminCommandHandler(player);
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
Player player = null;
if (sender instanceof Player) {
player = (Player) sender;
}
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
Player p = sender instanceof Player ? (Player) sender : null;
switch (args.length) {
case 0 -> handle0Args(sender, player);
case 1 -> handle1Args(args[0], sender, player);
case 2 -> handle2Args(args[0], args[1], sender, player);
case 3 -> handle3Args(args[0], args[1], args[2], sender, player);
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);
}
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
List<String> completions = new ArrayList<>();
switch (args.length) {
case 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.add("schematic");
completions.add("reload");
completions.add("forcejoin");
completions.add("forceleave");
completions.add("reset");
completions.add("recoverinventory");
}
return completions(args[0], completions);
}
case 2 -> {
if (args[0].equalsIgnoreCase("reset") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
completions.add("everyone");
for (ParkourPlayer pp : ParkourPlayer.getPlayers()) {
completions.add(pp.getName());
}
} else if (args[0].equalsIgnoreCase("join") && sender.hasPermission(ParkourOption.JOIN.permission)) {
for (ParkourPlayer pp : ParkourPlayer.getPlayers()) {
completions.add(pp.getName());
}
} else if (args[0].equalsIgnoreCase("schematic") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
completions.addAll(Arrays.asList("wand", "pos1", "pos2", "save", "paste"));
} else if (args[0].equalsIgnoreCase("forcejoin") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
completions.add("nearest");
completions.add("everyone");
// ── 2-arg routing ──────────────────────────────────────────────────────────
for (Player pl : Bukkit.getOnlinePlayers()) {
completions.add(pl.getName());
}
} else if (args[0].equalsIgnoreCase("forceleave") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
completions.add("everyone");
for (Player pl : Bukkit.getOnlinePlayers()) {
completions.add(pl.getName());
}
} else if (args[0].equalsIgnoreCase("recoverinventory") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
for (Player pl : Bukkit.getOnlinePlayers()) {
completions.add(pl.getName());
}
}
return completions(args[1], completions);
}
default -> {
return Collections.emptyList();
}
}
}
private void handle0Args(@NotNull CommandSender sender, @Nullable Player player) {
if (player != null && ParkourOption.MAIN.mayPerform(player)) {
// TODO: Migrate to LoLib GUI
// Menus.MAIN.open(player);
player.sendMessage("§cМеню временно недоступно во время миграции");
return;
}
sendHelpMessages(sender);
}
private void sendHelpMessages(CommandSender sender) {
send(sender, "");
send(sender, "<dark_gray><strikethrough>---------------<reset> %s <dark_gray><strikethrough>---------------<reset>".formatted(LoParkour.NAME));
send(sender, "");
send(sender, "<gray>/parkour <dark_gray>- Main command");
if (sender.hasPermission(ParkourOption.JOIN.permission)) {
send(sender, "<gray>/parkour join [mode/player] <dark_gray>- Join the default mode or specify one.");
send(sender, "<gray>/parkour leave <dark_gray>- Leave the game on this server");
}
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>- Open the leaderboard of a mode");
}
if (sender.hasPermission(ParkourOption.ADMIN.permission)) {
send(sender, "<gray>/LoParkour schematic <dark_gray>- Create a schematic");
send(sender, "<gray>/LoParkour reload <dark_gray>- Reloads the messages-v3.yml file");
send(sender, "<gray>/LoParkour reset <everyone/player> <dark_gray>- Resets all high scores. <red>This can't be recovered!");
send(sender, "<gray>/LoParkour forcejoin <everyone/nearest/player> <dark_gray>- Forces a specific player, the nearest or everyone to join");
send(sender, "<gray>/LoParkour forceleave <everyone/nearest/player> <dark_gray>- Forces a specific player, the nearest or everyone to leave");
send(sender, "<gray>/LoParkour recoverinventory <player> <dark_gray>- Recover a player's saved inventory. <red>Useful for recovering data after server crashes or errors when leaving.");
}
send(sender, "");
}
private void handle1Args(@NotNull String arg, @NotNull CommandSender sender, @Nullable Player player) {
switch (arg.toLowerCase()) {
case "help" -> sendHelpMessages(sender);
case "reload" -> {
if (!cooldown(sender, "reload", 2500)) {
return;
}
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
send(sender, Locales.getString(player, "other.no_do"));
return;
}
Config.reload(false);
send(sender, "%sReloaded config files.".formatted(LoParkour.PREFIX));
}
}
if (player == null) {
return;
}
switch (arg.toLowerCase()) {
case "join" -> {
if (!cooldown(sender, "join", 2500)) {
return;
}
if (!ParkourOption.JOIN.mayPerform(player)) {
send(sender, Locales.getString(player, "other.no_do"));
return;
}
ParkourUser user = ParkourUser.getUser(player);
if (user != null) {
return;
}
Modes.DEFAULT.create(player);
}
case "play" -> {
if (ParkourOption.PLAY.mayPerform(player)) {
// TODO: Migrate to LoLib GUI
// Menus.PLAY.open(player);
player.sendMessage("§cМеню временно недоступно во время миграции");
}
}
case "leave" -> {
if (!cooldown(sender, "leave", 2500)) {
return;
}
ParkourUser.leave(player);
}
case "menu", "main" -> {
if (!ParkourOption.MAIN.mayPerform(player)) {
// TODO: Migrate to LoLib GUI
// Menus.MAIN.open(player);
player.sendMessage("§cМеню временно недоступно во время миграции");
}
}
case "leaderboard" -> player.performCommand("LoParkour leaderboard invalid");
case "schematic" -> {
if (!player.hasPermission(ParkourOption.ADMIN.permission)) { // default players shouldn't have access even if perms are disabled
send(sender, Locales.getString(player, "other.no_do"));
return;
}
send(player, "");
send(player, "<red>/LoParkour schematic wand <dark_gray>- <gray>Get the schematic wand");
send(player, "<red>/LoParkour schematic pos1 <dark_gray>- <gray>Set the first position of your selection");
send(player, "<red>/LoParkour schematic pos2 <dark_gray>- <gray>Set the second position of your selection");
send(player, "<red>/LoParkour schematic save <name> <dark_gray>- <gray>Save selection to .lpschem format");
send(player, "<red>/LoParkour schematic paste <name> <dark_gray>- <gray>Paste a .lpschem schematic");
send(player, "<red>/LoParkour schematic list <dark_gray>- <gray>List all loaded schematics");
send(player, "<red>/LoParkour schematic reload <dark_gray>- <gray>Reload all schematics");
send(player, "");
send(player, "<dark_gray><underlined>Have any questions or need help? Join the Discord.");
}
}
}
private void handle2Args(@NotNull String arg1, @NotNull String arg2, @NotNull CommandSender sender, @Nullable Player player) {
switch (arg1.toLowerCase()) {
case "forcejoin" -> {
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
return;
}
if (arg2.equalsIgnoreCase("everyone")) {
Bukkit.getOnlinePlayers().forEach(other -> Modes.DEFAULT.create(other));
send(sender, LoParkour.PREFIX + "Successfully force joined everyone!");
return;
}
if (arg2.equalsIgnoreCase("nearest")) {
Player closest = null;
double distance = Double.MAX_VALUE;
// if player is found get location from player
// if no player is found, get location from command block
// if no command block is found, return null
Location from = sender instanceof Player ? ((Player) sender).getLocation() : (sender instanceof BlockCommandSender ? ((BlockCommandSender) sender).getBlock().getLocation() : null);
if (from == null || from.getWorld() == null) {
return;
}
// get the closest player
for (Player p : from.getWorld().getPlayers()) {
double d = p.getLocation().distance(from);
if (d < distance) {
distance = d;
closest = p;
}
}
// no closest player found
if (closest == null) {
return;
}
send(sender, LoParkour.PREFIX + "Successfully force joined " + closest.getName() + "!");
Modes.DEFAULT.create(closest);
return;
}
Player other = Bukkit.getPlayer(arg2);
if (other == null) {
send(sender, LoParkour.PREFIX + "That player isn't online!");
return;
}
Modes.DEFAULT.create(other);
}
case "forceleave" -> {
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
return;
}
if (arg2.equalsIgnoreCase("everyone")) {
ParkourPlayer.getPlayers().forEach(ParkourUser::leave);
send(sender, LoParkour.PREFIX + "Successfully force kicked everyone!");
return;
}
Player other = Bukkit.getPlayer(arg2);
if (other == null) {
send(sender, LoParkour.PREFIX + "That player isn't online!");
return;
}
ParkourUser user = ParkourUser.getUser(other);
if (user == null) {
send(sender, LoParkour.PREFIX + "That player isn't currently playing!");
return;
}
ParkourUser.leave(user);
}
case "reset" -> {
if (!sender.hasPermission(ParkourOption.ADMIN.permission) || !cooldown(sender, "reset", 2500)) {
return;
}
if (arg2.equalsIgnoreCase("everyone")) {
for (Mode mode : Registry.getModes()) {
Leaderboard leaderboard = mode.getLeaderboard();
if (leaderboard == null) {
continue;
}
leaderboard.resetAll();
leaderboard.write(true);
}
send(sender, LoParkour.PREFIX + "Successfully reset all high scores in memory and the files.");
return;
}
String name = null;
UUID uuid = null;
// Check online players
Player online = Bukkit.getPlayerExact(arg2);
if (online != null) {
name = online.getName();
uuid = online.getUniqueId();
}
// Check uuid
if (arg2.contains("-")) {
uuid = UUID.fromString(arg2);
}
// Check offline player
if (uuid == null) {
OfflinePlayer offline = Bukkit.getOfflinePlayer(arg2);
name = offline.getName();
uuid = offline.getUniqueId();
}
UUID finalUuid = uuid;
String finalName = name;
for (Mode mode : Registry.getModes()) {
Leaderboard leaderboard = mode.getLeaderboard();
if (leaderboard == null) {
continue;
}
leaderboard.remove(finalUuid);
leaderboard.write(true);
}
send(sender, LoParkour.PREFIX + "Successfully reset the high score of " + finalName + " in memory and the files.");
}
case "recoverinventory" -> {
if (!cooldown(sender, "recoverinventory", 2500) || !sender.hasPermission(ParkourOption.ADMIN.permission)) {
return;
}
Player other = Bukkit.getPlayer(arg2);
if (other == null) {
send(sender, LoParkour.PREFIX + "That player isn't online!");
return;
}
new InventoryData(other).load(result -> {
if (result != null) {
send(sender, "%sSuccessfully recovered the inventory of %s from their file".formatted(LoParkour.PREFIX, other.getName()));
send(sender, "%sGiving %s their items now...".formatted(LoParkour.PREFIX, other.getName()));
} else {
send(sender, "%s<red>There was an error recovering the inventory of %s from their file".formatted(LoParkour.PREFIX, other.getName()));
send(sender, "%s%s has no saved inventory or there was an error. Check the console.".formatted(LoParkour.PREFIX, other.getName()));
}
});
}
}
if (player == null) {
return;
}
switch (arg1) {
case "join" -> {
if (!cooldown(sender, "join", 2500) || !ParkourOption.JOIN.mayPerform(player)) {
return;
}
Mode mode = Registry.getMode(arg2);
if (mode != null) {
mode.create(player);
return;
}
Player other = Bukkit.getPlayer(arg2);
if (other == null) {
send(sender, "%sUnknown player! Try typing the name again.".formatted(LoParkour.PREFIX)); // could not find, so go to default
return;
}
ParkourPlayer parkourPlayer = ParkourPlayer.getPlayer(other);
if (parkourPlayer == null) {
send(sender, "%sUnknown player! Try typing the name again.".formatted(LoParkour.PREFIX)); // could not find, so go to default
return;
}
ParkourUser user = ParkourUser.getUser(player);
Session session = parkourPlayer.session;
if (user != null && user.session == session) { // already in same session
return;
}
if (session.isAcceptingPlayers()) {
((MultiMode) session.generator.getMode()).join(player, session);
} else {
Modes.SPECTATOR.create(player, session);
}
}
case "leaderboard" -> {
if (!ParkourOption.LEADERBOARDS.mayPerform(player)) {
send(sender, Locales.getString(player, "other.no_do"));
return;
}
Mode mode = Registry.getMode(arg2.toLowerCase());
// if found gamemode is null, return to default
if (mode == null) {
// TODO: Migrate to LoLib GUI
// Menus.LEADERBOARDS.open(player);
player.sendMessage("§cМеню временно недоступно во время миграции");
} else {
// TODO: Migrate to LoLib GUI
// Menus.SINGLE_LEADERBOARD.open(player, mode, Leaderboard.Sort.SCORE);
player.sendMessage("§cМеню временно недоступно во время миграции");
}
}
case "schematic" -> {
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) {
send(sender, Locales.getString(player, "other.no_do"));
return;
}
Location playerLocation = player.getLocation();
Location[] existingSelection = selections.get(player);
switch (arg2.toLowerCase()) {
case "wand" -> {
player.getInventory().addItem(WAND);
send(player, "<dark_gray>----------- <dark_red><bold>Schematics <reset><dark_gray>-----------");
send(player, "<gray><red>Left click<gray> -> set first position | <red>Right click<gray> -> set second position");
send(player, "<gray>If you can't place a block and need to set a position mid-air, use <dark_gray>/LoParkour schematic pos1/pos2 <gray>instead.");
}
case "pos1" -> {
send(player, "%sPosition 1 was set to %s".formatted(LoParkour.PREFIX, Locations.toString(playerLocation, true)));
if (existingSelection == null) {
selections.put(player, new Location[]{playerLocation, null});
return;
}
selections.put(player, new Location[]{playerLocation, existingSelection[1]});
ParticleUtil.box(BoundingBox.of(playerLocation, existingSelection[1]), player.getWorld(), Particle.END_ROD, player, 0.2);
}
case "pos2" -> {
send(player, "%sPosition 2 was set to %s".formatted(LoParkour.PREFIX, Locations.toString(playerLocation, true)));
if (existingSelection == null) {
selections.put(player, new Location[]{null, playerLocation});
return;
}
selections.put(player, new Location[]{existingSelection[0], playerLocation});
ParticleUtil.box(BoundingBox.of(existingSelection[0], playerLocation), player.getWorld(), Particle.END_ROD, player, 0.2);
}
case "save" -> {
if (!cooldown(sender, "LoParkour save schematic", 2500)) {
return;
}
if (existingSelection == null || existingSelection[0] == null || existingSelection[1] == null) {
send(player, "<dark_red><bold>Schematics <reset><gray>Your schematic isn't complete yet. Make sure you've set the first and second position.");
return;
}
String code = UUID.randomUUID().toString().split("-")[0];
send(player, ("<dark_red><bold>Schematics <reset><gray>Your schematic is being saved. It will use code <red>'%s'<gray>. " + "You can change the code to whatever you like. " + "Don't forget to add this schematic to <dark_gray>schematics.yml<gray>.").formatted(code));
// Old vilib schematic save - deprecated, use .lpschem format instead
send(player, "<red>Warning: Old schematic format is deprecated. Use '/loparkour schematic save <name>' for .lpschem format.");
}
case "list" -> {
send(player, "<dark_red><bold>Schematics <reset><gray>Loaded schematics:");
LoParkour.getSchematicManager().getAllSchematics().forEach((name, schem) -> {
send(player, "<gray>- <red>" + name + " <dark_gray>(" + schem.getMetadata().getDifficulty() + ")");
});
}
case "reload" -> {
if (!cooldown(sender, "LoParkour reload schematics", 2500)) {
return;
}
LoParkour.getSchematicManager().reload();
send(player, "<dark_red><bold>Schematics <reset><gray>Reloaded all schematics.");
}
}
}
}
}
private void handle3Args(@NotNull String arg1, @NotNull String arg2, @NotNull String arg3, @NotNull CommandSender sender, @Nullable Player player) {
if (player == null) {
return;
}
if (arg1.equalsIgnoreCase("schematic")) {
if (!player.hasPermission(ParkourOption.ADMIN.permission)) {
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;
}
}
switch (arg2.toLowerCase()) {
case "save" -> {
if (!cooldown(sender, "LoParkour save schematic", 2500)) {
return;
}
if (p == null) return;
Location[] selection = selections.get(player);
if (selection == null || selection[0] == null || selection[1] == null) {
send(player, "<dark_red><bold>Schematics <reset><gray>Your schematic isn't complete yet. Make sure you've set the first and second position.");
return;
}
try {
int width = Math.abs(selection[1].getBlockX() - selection[0].getBlockX()) + 1;
int height = Math.abs(selection[1].getBlockY() - selection[0].getBlockY()) + 1;
int length = Math.abs(selection[1].getBlockZ() - selection[0].getBlockZ()) + 1;
LPSchematic schematic = new LPSchematicBuilder(arg3, player.getName(), 0.5, width, height, length)
.fromWorld(player.getWorld(), selection[0], selection[1])
.build();
LoParkour.getSchematicManager().saveSchematic(schematic);
send(player, "<dark_red><bold>Schematics <reset><gray>Saved schematic as <red>" + arg3 + "<gray>!");
} catch (Exception e) {
send(player, "<dark_red><bold>Schematics <reset><gray>Failed to save schematic: " + e.getMessage());
e.printStackTrace();
}
}
case "paste" -> {
LPSchematic lpSchematic = LoParkour.getSchematicManager().getSchematic(arg3);
if (lpSchematic != null) {
lpSchematic.paste(player.getLocation(), player.getWorld());
send(sender, "%sPasted .lpschem schematic %s".formatted(LoParkour.PREFIX, arg3));
return;
}
send(sender, "%sCouldn't find %s. Only .lpschem format is supported.".formatted(LoParkour.PREFIX, arg3));
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);
}
}
}
private void send(CommandSender sender, String message) {
sender.sendMessage(ColorUtil.color(message));
// ── 3-arg routing ──────────────────────────────────────────────────────────
private void handle3(String a1, String a2, String a3, CommandSender sender, @Nullable Player p) {
if (p == null) return;
if (!a1.equalsIgnoreCase("schematic")) return;
if (!p.hasPermission(ParkourOption.ADMIN.permission)) return;
SchematicCommandHandler.handleSubcommandWithName(a2, a3, sender, p, player);
}
private List<String> completions(String arg, List<String> options) {
String lower = arg.toLowerCase();
return options.stream()
.filter(option -> option.toLowerCase().startsWith(lower))
.toList();
// ── 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 + "Unknown player."); return; }
ParkourPlayer pp = ParkourPlayer.getPlayer(other);
if (pp == null) { send(sender, LoParkour.PREFIX + "That player isn't playing."); return; }
ParkourUser user = ParkourUser.getUser(p);
Session session = pp.session;
if (user != null && user.session == session) return;
if (session.isAcceptingPlayers()) {
((MultiMode) session.generator.getMode()).join(p, session);
} else {
Modes.SPECTATOR.create(p, session);
}
}
private boolean cooldown(CommandSender sender, String key, long millis) {
// Simple cooldown implementation - can be enhanced later
return true;
// ── 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", "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)) {
ParkourPlayer.getPlayers().forEach(pp -> completions.add(pp.getName()));
} else if (a1.equals("schematic") && sender.hasPermission(ParkourOption.ADMIN.permission)) {
completions.addAll(Arrays.asList("wand", "pos1", "pos2", "save", "paste", "list", "reload"));
} 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);
}
return Collections.emptyList();
}
// ── 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

@ -27,14 +27,10 @@ public final class Registry {
*/
public static void register(@NotNull Mode mode) {
modes.add(mode);
LoParkour.log("Registered mode %s".formatted(mode.getName()));
}
public static void register(@NotNull Style style) {
styles.add(style);
LoParkour.log("Registered style %s".formatted(style.getName()));
}
/**

View file

@ -0,0 +1,162 @@
package dev.loki.loparkour.command;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.leaderboard.Leaderboard;
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.player.data.InventoryData;
import dev.loki.loparkour.util.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.
*/
public class AdminCommandHandler {
private final PlayerCommandHandler base;
public AdminCommandHandler(PlayerCommandHandler base) {
this.base = base;
}
public void handle(@NotNull String arg1, @NotNull String arg2,
@NotNull CommandSender sender, @Nullable Player player) {
if (!sender.hasPermission(ParkourOption.ADMIN.permission)) return;
switch (arg1.toLowerCase()) {
case "forcejoin" -> handleForceJoin(arg2, sender);
case "forceleave" -> handleForceLeave(arg2, sender);
case "reset" -> handleReset(arg2, sender);
case "recoverinventory" -> handleRecoverInventory(arg2, sender);
}
if (player == null) return;
if (arg1.equals("schematic")) {
SchematicCommandHandler.handleSubcommand(arg2, sender, player, base);
}
}
// ── forcejoin ──────────────────────────────────────────────────────────────
private void handleForceJoin(String target, CommandSender sender) {
if (target.equalsIgnoreCase("everyone")) {
Bukkit.getOnlinePlayers().forEach(p -> Modes.DEFAULT.create(p));
send(sender, LoParkour.PREFIX + "Force joined 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() + "!");
return;
}
Player other = Bukkit.getPlayer(target);
if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; }
Modes.DEFAULT.create(other);
}
// ── forceleave ─────────────────────────────────────────────────────────────
private void handleForceLeave(String target, CommandSender sender) {
if (target.equalsIgnoreCase("everyone")) {
ParkourPlayer.getPlayers().forEach(ParkourUser::leave);
send(sender, LoParkour.PREFIX + "Force kicked everyone!");
return;
}
Player other = Bukkit.getPlayer(target);
if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; }
ParkourUser user = ParkourUser.getUser(other);
if (user == null) { send(sender, LoParkour.PREFIX + "Player isn't playing!"); return; }
ParkourUser.leave(user);
}
// ── reset ──────────────────────────────────────────────────────────────────
private void handleReset(String target, CommandSender sender) {
if (!base.cooldown(sender, "reset", 2500)) 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.");
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 + "Reset high score of " + name + ".");
}
// ── recoverinventory ───────────────────────────────────────────────────────
private void handleRecoverInventory(String target, CommandSender sender) {
if (!base.cooldown(sender, "recoverinventory", 2500)) return;
Player other = Bukkit.getPlayer(target);
if (other == null) { send(sender, LoParkour.PREFIX + "Player not online!"); return; }
new InventoryData(other).load(result -> {
if (result != null) {
send(sender, LoParkour.PREFIX + "Recovered inventory of " + other.getName() + ".");
} else {
send(sender, LoParkour.PREFIX + "<red>No saved inventory found for " + 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);
}
private UUID resolveUUID(String input) {
Player online = Bukkit.getPlayerExact(input);
if (online != null) return online.getUniqueId();
if (input.contains("-")) return UUID.fromString(input);
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));
}
}

View file

@ -0,0 +1,121 @@
package dev.loki.loparkour.command;
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 org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* Handles all player-facing /parkour sub-commands.
* Admin commands are in {@link AdminCommandHandler}.
*/
public class PlayerCommandHandler {
private final Map<String, Long> cooldowns = new HashMap<>();
public void handleNoArgs(@NotNull CommandSender sender, @Nullable Player player) {
if (player != null && ParkourOption.MAIN.mayPerform(player)) {
Menus.MAIN.open(player);
} else {
sendHelp(sender);
}
}
public void handle(@NotNull String arg, @NotNull CommandSender sender, @Nullable Player player) {
switch (arg.toLowerCase()) {
case "help" -> sendHelp(sender);
case "reload" -> handleReload(sender, player);
}
if (player == null) return;
switch (arg.toLowerCase()) {
case "join" -> {
if (!cooldown(sender, "join", 2500)) return;
if (!ParkourOption.JOIN.mayPerform(player)) {
send(sender, Locales.getString(player, "other.no_do"));
return;
}
if (ParkourUser.getUser(player) != null) return;
Modes.DEFAULT.create(player);
}
case "leave" -> {
if (!cooldown(sender, "leave", 2500)) return;
ParkourUser.leave(player);
}
case "play" -> {
if (ParkourOption.PLAY.mayPerform(player)) Menus.PLAY.open(player);
}
case "menu", "main" -> {
if (ParkourOption.MAIN.mayPerform(player)) Menus.MAIN.open(player);
}
case "leaderboard" -> player.performCommand("LoParkour leaderboard invalid");
case "schematic" -> {
if (!player.hasPermission(ParkourOption.ADMIN.permission)) {
send(sender, Locales.getString(player, "other.no_do"));
return;
}
SchematicCommandHandler.sendHelp(player);
}
}
}
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"));
return;
}
Config.reload(false);
send(sender, LoParkour.PREFIX + "Reloaded config files.");
}
public void sendHelp(@NotNull CommandSender sender) {
send(sender, "");
send(sender, "<dark_gray><strikethrough>---------------<reset> " + LoParkour.NAME + " <dark_gray><strikethrough>---------------<reset>");
send(sender, "");
send(sender, "<gray>/parkour <dark_gray>- Main command");
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");
}
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, "");
}
public boolean cooldown(CommandSender sender, String key, long millis) {
String fullKey = sender.getName() + ":" + key;
long now = System.currentTimeMillis();
Long last = cooldowns.get(fullKey);
if (last != null && now - last < millis) return false;
cooldowns.put(fullKey, now);
return true;
}
private void send(CommandSender sender, String message) {
sender.sendMessage(ColorUtil.color(message));
}
}

View file

@ -0,0 +1,149 @@
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;
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());
}
}
private static void handlePaste(String name, CommandSender sender, Player player) {
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

@ -75,8 +75,6 @@ public enum Config {
Locales.init();
Schematics.init();
Option.init(initialLoad);
LoParkour.log("Loaded all config files");
}
/**

View file

@ -0,0 +1,115 @@
package dev.loki.loparkour.config;
import dev.loki.loparkour.LoParkour;
/**
* Generation and jump-validation settings. Populated by {@link Option#init}.
*/
public final class GenerationOptions {
// Block type chances
public static double TYPE_NORMAL;
public static double TYPE_SPECIAL;
public static double TYPE_SCHEMATICS;
// Special block chances
public static double SPECIAL_ICE;
public static double SPECIAL_SLAB;
public static double SPECIAL_PANE;
public static double SPECIAL_FENCE;
// Distance weights
public static double NORMAL_DISTANCE_1;
public static double NORMAL_DISTANCE_2;
public static double NORMAL_DISTANCE_3;
public static double NORMAL_DISTANCE_4;
// Height weights
public static double NORMAL_HEIGHT_1;
public static double NORMAL_HEIGHT_0;
public static double NORMAL_HEIGHT_NEG1;
public static double NORMAL_HEIGHT_NEG2;
// World bounds
public static int MAX_Y;
public static int MIN_Y;
// Jump validation
public static boolean JUMP_VALIDATION_ENABLED;
public static double MAX_JUMP_DISTANCE;
public static double MAX_HORIZONTAL_DISTANCE;
public static double MAX_VERTICAL_UP;
public static double MAX_VERTICAL_DOWN;
// Jump types
public static boolean JUMP_TYPES_ENABLED;
public static java.util.Map<String, Boolean> JUMP_TYPE_ENABLED;
public static java.util.Map<String, Double> JUMP_TYPE_CHANCE;
// Memory optimisation
public static int BLOCK_CLEANUP_DISTANCE;
public static int CLEANUP_INTERVAL;
// Ghost mode
public static boolean GHOST_MODE_ENABLED;
public static int GHOST_SHOW_TOP;
public static double GHOST_TRANSPARENCY;
static void init() {
// Jump validation
JUMP_VALIDATION_ENABLED = Config.CONFIG.getBoolean("jump-validation.enabled");
MAX_JUMP_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-distance");
MAX_HORIZONTAL_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-horizontal");
MAX_VERTICAL_UP = Config.CONFIG.getDouble("jump-validation.max-vertical-up");
MAX_VERTICAL_DOWN = Config.CONFIG.getDouble("jump-validation.max-vertical-down");
// Jump types
JUMP_TYPES_ENABLED = Config.CONFIG.getBoolean("jump-types.enabled");
JUMP_TYPE_ENABLED = new java.util.HashMap<>();
JUMP_TYPE_CHANCE = new java.util.HashMap<>();
for (String type : Config.CONFIG.getChildren("jump-types.types")) {
String path = "jump-types.types." + type;
JUMP_TYPE_ENABLED.put(type, Config.CONFIG.getBoolean(path + ".enabled"));
JUMP_TYPE_CHANCE.put(type, Config.CONFIG.getDouble(path + ".chance"));
}
// Memory
BLOCK_CLEANUP_DISTANCE = Config.CONFIG.getInt("memory.block-cleanup-distance");
CLEANUP_INTERVAL = Config.CONFIG.getInt("memory.cleanup-interval");
// Ghost
GHOST_MODE_ENABLED = Config.CONFIG.getBoolean("ghost-mode.enabled");
GHOST_SHOW_TOP = Config.CONFIG.getInt("ghost-mode.show-top");
GHOST_TRANSPARENCY = Config.CONFIG.getDouble("ghost-mode.transparency");
// Generation chances
TYPE_NORMAL = Config.GENERATION.getInt("generation.type.normal") / 100.0;
TYPE_SPECIAL = Config.GENERATION.getInt("generation.type.special") / 100.0;
TYPE_SCHEMATICS = Config.GENERATION.getInt("generation.type.schematic") / 100.0;
SPECIAL_ICE = Config.GENERATION.getInt("generation.special.ice") / 100.0;
SPECIAL_SLAB = Config.GENERATION.getInt("generation.special.slab") / 100.0;
SPECIAL_PANE = Config.GENERATION.getInt("generation.special.pane") / 100.0;
SPECIAL_FENCE = Config.GENERATION.getInt("generation.special.fence") / 100.0;
NORMAL_DISTANCE_1 = Config.GENERATION.getInt("generation.normal.distance.1") / 100.0;
NORMAL_DISTANCE_2 = Config.GENERATION.getInt("generation.normal.distance.2") / 100.0;
NORMAL_DISTANCE_3 = Config.GENERATION.getInt("generation.normal.distance.3") / 100.0;
NORMAL_DISTANCE_4 = Config.GENERATION.getInt("generation.normal.distance.4") / 100.0;
NORMAL_HEIGHT_1 = Config.GENERATION.getInt("generation.normal.height.1") / 100.0;
NORMAL_HEIGHT_0 = Config.GENERATION.getInt("generation.normal.height.0") / 100.0;
NORMAL_HEIGHT_NEG1 = Config.GENERATION.getInt("generation.normal.height.-1") / 100.0;
NORMAL_HEIGHT_NEG2 = Config.GENERATION.getInt("generation.normal.height.-2") / 100.0;
MAX_Y = Config.GENERATION.getInt("generation.settings.max-y");
MIN_Y = Config.GENERATION.getInt("generation.settings.min-y");
if (MIN_Y >= MAX_Y) {
MIN_Y = 100; MAX_Y = 200;
LoParkour.getPlugin().getLogger().severe("min-y >= max-y in generation.yml — using defaults 100/200.");
}
}
private GenerationOptions() {}
}

View file

@ -69,9 +69,6 @@ public class Locales {
if (files != null && files.length == 0) {
plugin.saveResource("locales/en.yml", false);
plugin.saveResource("locales/ru.yml", false);
plugin.saveResource("locales/nl.yml", false);
plugin.saveResource("locales/fr.yml", false);
plugin.saveResource("locales/zh_cn.yml", false);
}
// get all files in locales folder
@ -241,7 +238,13 @@ public class Locales {
idx++;
}
Item item = new Item(Material.getMaterial(material.toUpperCase()), name);
Material mat = Material.getMaterial(material.toUpperCase());
if (mat == null) {
LoParkour.getPlugin().getLogger().warning("Invalid material '%s' for locale path '%s', using STONE as fallback".formatted(material, path));
mat = Material.STONE;
}
Item item = new Item(mat, name);
if (!lore.isEmpty()) {
item.lore(lore.split("\\|\\|"));

View file

@ -1,17 +1,11 @@
package dev.loki.loparkour.config;
import org.bukkit.Bukkit;
import java.util.ArrayList;
import dev.loki.loparkour.util.ParticleData;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.style.RandomStyle;
import dev.loki.loparkour.style.Style;
import dev.loki.loparkour.util.ParticleData;
import org.bukkit.*;
import org.bukkit.block.BlockFace;
import org.bukkit.configuration.file.FileConfiguration;
@ -21,303 +15,198 @@ import java.util.*;
import java.util.function.BiFunction;
/**
* Class for variables required in generating without accessing the file a lot (constants)
* Top-level config constants used throughout the plugin.
*
* Heavy sub-sections are delegated to focused classes:
* <ul>
* <li>{@link SqlOptions} – SQL connection settings</li>
* <li>{@link ParticleOptions} – particle &amp; sound settings</li>
* <li>{@link GenerationOptions} – generation &amp; jump-validation settings</li>
* </ul>
*
* Those classes expose their own static fields. Option re-exports the most
* commonly accessed ones via forwarding fields so existing call-sites compile
* without changes.
*/
public class Option {
// ── General ───────────────────────────────────────────────────────────────
public static double BORDER_SIZE;
public static List<Integer> POSSIBLE_LEADS;
// Advanced settings
public static BlockFace HEADING;
public static Map<ParkourOption, Boolean> OPTIONS_ENABLED;
public static Map<ParkourOption, String> OPTIONS_DEFAULTS;
public static Map<ParkourOption, String> OPTIONS_DEFAULTS;
public static Location GO_BACK_LOC;
public static void init(boolean firstLoad) {
initSql();
initEnums();
initGeneration();
initStyles("styles.list", Config.CONFIG.fileConfiguration, RandomStyle::new)
.forEach(Registry::register);
// ── Forwarded from sub-classes (backwards compat) ─────────────────────────
/** @see SqlOptions#SQL */
public static boolean SQL;
/** @see ParticleOptions#SHAPE */
public static ParticleShape PARTICLE_SHAPE;
/** @see ParticleOptions#SOUND_TYPE */
public static Sound SOUND_TYPE;
/** @see ParticleOptions#SOUND_PITCH */
public static int SOUND_PITCH;
/** @see ParticleOptions#SOUND_VOLUME */
public static int SOUND_VOLUME;
/** @see ParticleOptions#PARTICLE_TYPE */
public static Particle PARTICLE_TYPE;
/** @see ParticleOptions#PARTICLE_DATA */
public static ParticleData<?> PARTICLE_DATA;
/** @see GenerationOptions */
public static double TYPE_NORMAL, TYPE_SPECIAL, TYPE_SCHEMATICS;
public static double SPECIAL_ICE, SPECIAL_SLAB, SPECIAL_PANE, SPECIAL_FENCE;
public static double NORMAL_DISTANCE_1, NORMAL_DISTANCE_2, NORMAL_DISTANCE_3, NORMAL_DISTANCE_4;
public static double NORMAL_HEIGHT_1, NORMAL_HEIGHT_0, NORMAL_HEIGHT_NEG1, NORMAL_HEIGHT_NEG2;
public static int MAX_Y, MIN_Y;
public static int BLOCK_CLEANUP_DISTANCE, CLEANUP_INTERVAL;
public static boolean GHOST_MODE_ENABLED;
public static int GHOST_SHOW_TOP;
public static double GHOST_TRANSPARENCY;
// ── init ──────────────────────────────────────────────────────────────────
public static void init(boolean firstLoad) {
SqlOptions.init();
ParticleOptions.init();
GenerationOptions.init();
syncForwardedFields();
initGeneral(firstLoad);
initOptions();
initStyles("styles.list", Config.CONFIG.fileConfiguration, RandomStyle::new)
.forEach(Registry::register);
}
/** Copy sub-class fields to this class so existing code still compiles. */
private static void syncForwardedFields() {
SQL = SqlOptions.SQL;
PARTICLE_SHAPE = ParticleOptions.SHAPE;
SOUND_TYPE = ParticleOptions.SOUND_TYPE;
SOUND_PITCH = ParticleOptions.SOUND_PITCH;
SOUND_VOLUME = ParticleOptions.SOUND_VOLUME;
PARTICLE_TYPE = ParticleOptions.PARTICLE_TYPE;
PARTICLE_DATA = ParticleOptions.PARTICLE_DATA;
TYPE_NORMAL = GenerationOptions.TYPE_NORMAL;
TYPE_SPECIAL = GenerationOptions.TYPE_SPECIAL;
TYPE_SCHEMATICS = GenerationOptions.TYPE_SCHEMATICS;
SPECIAL_ICE = GenerationOptions.SPECIAL_ICE;
SPECIAL_SLAB = GenerationOptions.SPECIAL_SLAB;
SPECIAL_PANE = GenerationOptions.SPECIAL_PANE;
SPECIAL_FENCE = GenerationOptions.SPECIAL_FENCE;
NORMAL_DISTANCE_1 = GenerationOptions.NORMAL_DISTANCE_1;
NORMAL_DISTANCE_2 = GenerationOptions.NORMAL_DISTANCE_2;
NORMAL_DISTANCE_3 = GenerationOptions.NORMAL_DISTANCE_3;
NORMAL_DISTANCE_4 = GenerationOptions.NORMAL_DISTANCE_4;
NORMAL_HEIGHT_1 = GenerationOptions.NORMAL_HEIGHT_1;
NORMAL_HEIGHT_0 = GenerationOptions.NORMAL_HEIGHT_0;
NORMAL_HEIGHT_NEG1 = GenerationOptions.NORMAL_HEIGHT_NEG1;
NORMAL_HEIGHT_NEG2 = GenerationOptions.NORMAL_HEIGHT_NEG2;
MAX_Y = GenerationOptions.MAX_Y;
MIN_Y = GenerationOptions.MIN_Y;
BLOCK_CLEANUP_DISTANCE = GenerationOptions.BLOCK_CLEANUP_DISTANCE;
CLEANUP_INTERVAL = GenerationOptions.CLEANUP_INTERVAL;
GHOST_MODE_ENABLED = GenerationOptions.GHOST_MODE_ENABLED;
GHOST_SHOW_TOP = GenerationOptions.GHOST_SHOW_TOP;
GHOST_TRANSPARENCY = GenerationOptions.GHOST_TRANSPARENCY;
}
// ── General init ──────────────────────────────────────────────────────────
private static void initGeneral(boolean firstLoad) {
GO_BACK_LOC = parseLocation(Config.CONFIG.getString("bungeecord.go-back"));
String[] axes = Config.CONFIG.getString("bungeecord.go-back-axes").split(",");
GO_BACK_LOC.setPitch(Float.parseFloat(axes[0]));
GO_BACK_LOC.setYaw(Float.parseFloat(axes[1]));
// General settings
// Options
List<ParkourOption> options = new ArrayList<>(Arrays.asList(ParkourOption.values()));
// exceptions
options.remove(ParkourOption.JOIN);
options.remove(ParkourOption.ADMIN);
// =====================================
OPTIONS_DEFAULTS = new HashMap<>();
OPTIONS_ENABLED = new HashMap<>();
String prefix = "default-values";
for (ParkourOption option : options) {
String parent = "%s.%s".formatted(prefix, option.path);
// register enabled value
OPTIONS_ENABLED.put(option, Config.CONFIG.getBoolean("%s.enabled".formatted(parent)));
// register default value
if (!Config.CONFIG.isPath("%s.default".formatted(parent))) {
continue;
}
Object value = Config.CONFIG.get("%s.default".formatted(parent));
if (value != null) {
OPTIONS_DEFAULTS.put(option, String.valueOf(value));
}
}
// =====================================
// Config stuff
POSSIBLE_LEADS = Config.CONFIG.getIntList("options.leads.amount");
for (int lead : new ArrayList<>(POSSIBLE_LEADS)) {
if (lead < 1 || lead > 128) {
LoParkour.getPlugin().getLogger().severe("Invalid lead: %d. Should be above 1 and below 128.".formatted(lead));
LoParkour.getPlugin().getLogger().severe("Invalid lead: %d. Must be 1–128.".formatted(lead));
POSSIBLE_LEADS.remove((Object) lead);
}
}
// Generation
String heading = Config.GENERATION.getString("advanced.island.parkour.heading");
switch (heading.toLowerCase()) {
case "north" -> HEADING = BlockFace.NORTH;
case "south" -> HEADING = BlockFace.SOUTH;
case "west" -> HEADING = BlockFace.WEST;
case "east" -> HEADING = BlockFace.EAST;
default -> LoParkour.getPlugin().getLogger().severe("Invalid heading: %s".formatted(heading));
}
// Scoring
HEADING = switch (heading.toLowerCase()) {
case "north" -> BlockFace.NORTH;
case "south" -> BlockFace.SOUTH;
case "west" -> BlockFace.WEST;
case "east" -> BlockFace.EAST;
default -> {
LoParkour.getPlugin().getLogger().severe("Invalid heading: " + heading);
yield BlockFace.NORTH;
}
};
if (firstLoad) {
BORDER_SIZE = Config.GENERATION.getDouble("advanced.border-size");
SQL = Config.CONFIG.getBoolean("sql.enabled");
}
}
private static Vector stringToVector(String direction) {
return switch (direction.toLowerCase()) {
case "north" -> new Vector(0, 0, -1);
case "south" -> new Vector(0, 0, 1);
case "west" -> new Vector(-1, 0, 0);
default -> new Vector(1, 0, 0); // east
};
private static void initOptions() {
List<ParkourOption> options = new ArrayList<>(Arrays.asList(ParkourOption.values()));
options.remove(ParkourOption.JOIN);
options.remove(ParkourOption.ADMIN);
OPTIONS_DEFAULTS = new HashMap<>();
OPTIONS_ENABLED = new HashMap<>();
for (ParkourOption option : options) {
String parent = "default-values." + option.path;
OPTIONS_ENABLED.put(option, Config.CONFIG.getBoolean(parent + ".enabled"));
if (Config.CONFIG.isPath(parent + ".default")) {
Object value = Config.CONFIG.get(parent + ".default");
if (value != null) OPTIONS_DEFAULTS.put(option, String.valueOf(value));
}
}
}
// ── Helpers ───────────────────────────────────────────────────────────────
private static Location parseLocation(String location) {
String[] values = location.replaceAll("[()]", "").replaceAll("[, ]", " ").split(" ");
World world = Bukkit.getWorld(values[3]);
if (world == null) {
world = Bukkit.getWorlds().get(0);
}
return new Location(world, Double.parseDouble(values[0]), Double.parseDouble(values[1]), Double.parseDouble(values[2]));
if (world == null) world = Bukkit.getWorlds().get(0);
return new Location(world, Double.parseDouble(values[0]),
Double.parseDouble(values[1]), Double.parseDouble(values[2]));
}
public static ParticleShape PARTICLE_SHAPE;
public static Sound SOUND_TYPE;
public static int SOUND_PITCH;
public static int SOUND_VOLUME;
public static Particle PARTICLE_TYPE;
public static ParticleData<?> PARTICLE_DATA;
private static void initEnums() {
String value = Config.CONFIG.getString("particles.sound-type").toUpperCase();
try {
SOUND_TYPE = Sound.valueOf(value);
} catch (IllegalArgumentException ex) {
SOUND_TYPE = Sound.valueOf("BLOCK_NOTE_PLING");
LoParkour.getPlugin().getLogger().severe("Invalid sound: %s".formatted(value));
}
value = Config.CONFIG.getString("particles.particle-type");
try {
PARTICLE_TYPE = Particle.valueOf(value);
} catch (IllegalArgumentException ex) {
PARTICLE_TYPE = Particle.valueOf("SPELL_INSTANT");
LoParkour.getPlugin().getLogger().severe("Invalid particle type: %s".formatted(value));
}
SOUND_PITCH = Config.CONFIG.getInt("particles.sound-pitch");
SOUND_VOLUME = Config.CONFIG.getInt("particles.sound-volume");
PARTICLE_SHAPE = ParticleShape.valueOf(Config.CONFIG.getString("particles.particle-shape").toUpperCase());
PARTICLE_DATA = new ParticleData<>(PARTICLE_TYPE, null, 10);
}
public enum ParticleShape {
DOT, CIRCLE, BOX
}
// --------------------------------------------------------------
// MySQL
public static boolean SQL;
public static int SQL_PORT;
public static String SQL_URL;
public static String SQL_DB;
public static String SQL_USERNAME;
public static String SQL_PASSWORD;
public static String SQL_PREFIX;
private static void initSql() {
SQL_PORT = Config.CONFIG.getInt("sql.port");
SQL_DB = Config.CONFIG.getString("sql.database");
SQL_URL = Config.CONFIG.getString("sql.url");
SQL_USERNAME = Config.CONFIG.getString("sql.username");
SQL_PASSWORD = Config.CONFIG.getString("sql.password");
SQL_PREFIX = Config.CONFIG.getString("sql.prefix");
}
// --------------------------------------------------------------
// Jump Validation
public static boolean JUMP_VALIDATION_ENABLED;
public static double MAX_JUMP_DISTANCE;
public static double MAX_HORIZONTAL_DISTANCE;
public static double MAX_VERTICAL_UP;
public static double MAX_VERTICAL_DOWN;
// Jump Types
public static boolean JUMP_TYPES_ENABLED;
public static Map<String, Boolean> JUMP_TYPE_ENABLED;
public static Map<String, Double> JUMP_TYPE_CHANCE;
// Memory Optimization
public static int BLOCK_CLEANUP_DISTANCE;
public static int CLEANUP_INTERVAL;
// Ghost Mode
public static boolean GHOST_MODE_ENABLED;
public static int GHOST_SHOW_TOP;
public static double GHOST_TRANSPARENCY;
// --------------------------------------------------------------
// Generation
public static double TYPE_NORMAL;
public static double TYPE_SPECIAL;
public static double TYPE_SCHEMATICS;
public static double SPECIAL_ICE;
public static double SPECIAL_SLAB;
public static double SPECIAL_PANE;
public static double SPECIAL_FENCE;
public static double NORMAL_DISTANCE_1;
public static double NORMAL_DISTANCE_2;
public static double NORMAL_DISTANCE_3;
public static double NORMAL_DISTANCE_4;
public static double NORMAL_HEIGHT_1;
public static double NORMAL_HEIGHT_0;
public static double NORMAL_HEIGHT_NEG1;
public static double NORMAL_HEIGHT_NEG2;
public static int MAX_Y;
public static int MIN_Y;
private static void initGeneration() {
// Jump validation
JUMP_VALIDATION_ENABLED = Config.CONFIG.getBoolean("jump-validation.enabled");
MAX_JUMP_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-distance");
MAX_HORIZONTAL_DISTANCE = Config.CONFIG.getDouble("jump-validation.max-horizontal");
MAX_VERTICAL_UP = Config.CONFIG.getDouble("jump-validation.max-vertical-up");
MAX_VERTICAL_DOWN = Config.CONFIG.getDouble("jump-validation.max-vertical-down");
// Jump types
JUMP_TYPES_ENABLED = Config.CONFIG.getBoolean("jump-types.enabled");
JUMP_TYPE_ENABLED = new HashMap<>();
JUMP_TYPE_CHANCE = new HashMap<>();
for (String type : Config.CONFIG.getChildren("jump-types.types")) {
String path = "jump-types.types." + type;
JUMP_TYPE_ENABLED.put(type, Config.CONFIG.getBoolean(path + ".enabled"));
JUMP_TYPE_CHANCE.put(type, Config.CONFIG.getDouble(path + ".chance"));
}
// Memory optimization
BLOCK_CLEANUP_DISTANCE = Config.CONFIG.getInt("memory.block-cleanup-distance");
CLEANUP_INTERVAL = Config.CONFIG.getInt("memory.cleanup-interval");
// Ghost mode
GHOST_MODE_ENABLED = Config.CONFIG.getBoolean("ghost-mode.enabled");
GHOST_SHOW_TOP = Config.CONFIG.getInt("ghost-mode.show-top");
GHOST_TRANSPARENCY = Config.CONFIG.getDouble("ghost-mode.transparency");
TYPE_NORMAL = Config.GENERATION.getInt("generation.type.normal") / 100.0;
TYPE_SPECIAL = Config.GENERATION.getInt("generation.type.schematic") / 100.0;
TYPE_SCHEMATICS = Config.GENERATION.getInt("generation.type.special") / 100.0;
SPECIAL_ICE = Config.GENERATION.getInt("generation.special.ice") / 100.0;
SPECIAL_SLAB = Config.GENERATION.getInt("generation.special.slab") / 100.0;
SPECIAL_PANE = Config.GENERATION.getInt("generation.special.pane") / 100.0;
SPECIAL_FENCE = Config.GENERATION.getInt("generation.special.fence") / 100.0;
NORMAL_DISTANCE_1 = Config.GENERATION.getInt("generation.normal.distance.1") / 100.0;
NORMAL_DISTANCE_2 = Config.GENERATION.getInt("generation.normal.distance.2") / 100.0;
NORMAL_DISTANCE_3 = Config.GENERATION.getInt("generation.normal.distance.3") / 100.0;
NORMAL_DISTANCE_4 = Config.GENERATION.getInt("generation.normal.distance.4") / 100.0;
NORMAL_HEIGHT_1 = Config.GENERATION.getInt("generation.normal.height.1") / 100.0;
NORMAL_HEIGHT_0 = Config.GENERATION.getInt("generation.normal.height.0") / 100.0;
NORMAL_HEIGHT_NEG1 = Config.GENERATION.getInt("generation.normal.height.-1") / 100.0;
NORMAL_HEIGHT_NEG2 = Config.GENERATION.getInt("generation.normal.height.-2") / 100.0;
MAX_Y = Config.GENERATION.getInt("generation.settings.max-y");
MIN_Y = Config.GENERATION.getInt("generation.settings.min-y");
if (MIN_Y >= MAX_Y) {
MIN_Y = 100;
MAX_Y = 200;
LoParkour.getPlugin().getLogger().severe("Provided minimum y is the same or larger than maximum y! - check your generation.yml file");
}
}
// --------------------------------------------------------------
public static Set<Style> initStyles(String path, FileConfiguration config, BiFunction<String, List<Material>, Style> fn) {
var styles = new HashSet<Style>();
for (String style : Locales.getChildren(config, path, false)) {
styles.add(fn.apply(style,
config.getStringList("%s.%s".formatted(path, style)).stream()
.map(name -> {
var material = Material.getMaterial(name.toUpperCase());
if (material == null) {
LoParkour.getPlugin().getLogger().severe("Invalid material %s in style %s".formatted(name, style));
return Material.STONE;
}
return material;
})
.toList()));
config.getStringList("%s.%s".formatted(path, style)).stream()
.map(name -> {
Material m = Material.getMaterial(name.toUpperCase());
if (m == null) {
LoParkour.getPlugin().getLogger().severe("Invalid material %s in style %s".formatted(name, style));
return Material.STONE;
}
return m;
})
.toList()));
}
return styles;
}
// ── Inner types ───────────────────────────────────────────────────────────
public enum ParticleShape { DOT, CIRCLE, BOX }
}

View file

@ -0,0 +1,44 @@
package dev.loki.loparkour.config;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.util.ParticleData;
import org.bukkit.Particle;
import org.bukkit.Sound;
/**
* Particle and sound settings. Populated by {@link Option#init}.
*/
public final class ParticleOptions {
public static Option.ParticleShape SHAPE;
public static Particle PARTICLE_TYPE;
public static ParticleData<?> PARTICLE_DATA;
public static Sound SOUND_TYPE;
public static int SOUND_PITCH;
public static int SOUND_VOLUME;
static void init() {
String sound = Config.CONFIG.getString("particles.sound-type").toUpperCase();
try {
SOUND_TYPE = Sound.valueOf(sound);
} catch (IllegalArgumentException ex) {
SOUND_TYPE = Sound.valueOf("BLOCK_NOTE_PLING");
LoParkour.getPlugin().getLogger().severe("Invalid sound: " + sound);
}
String particle = Config.CONFIG.getString("particles.particle-type");
try {
PARTICLE_TYPE = Particle.valueOf(particle);
} catch (IllegalArgumentException ex) {
PARTICLE_TYPE = Particle.valueOf("SPELL_INSTANT");
LoParkour.getPlugin().getLogger().severe("Invalid particle: " + particle);
}
SOUND_PITCH = Config.CONFIG.getInt("particles.sound-pitch");
SOUND_VOLUME = Config.CONFIG.getInt("particles.sound-volume");
SHAPE = Option.ParticleShape.valueOf(Config.CONFIG.getString("particles.particle-shape").toUpperCase());
PARTICLE_DATA = new ParticleData<>(PARTICLE_TYPE, null, 10);
}
private ParticleOptions() {}
}

View file

@ -0,0 +1,27 @@
package dev.loki.loparkour.config;
/**
* SQL connection settings. Populated by {@link Option#init}.
*/
public final class SqlOptions {
public static boolean SQL;
public static int PORT;
public static String URL;
public static String DB;
public static String USERNAME;
public static String PASSWORD;
public static String PREFIX;
static void init() {
SQL = Config.CONFIG.getBoolean("sql.enabled");
PORT = Config.CONFIG.getInt("sql.port");
DB = Config.CONFIG.getString("sql.database");
URL = Config.CONFIG.getString("sql.url");
USERNAME = Config.CONFIG.getString("sql.username");
PASSWORD = Config.CONFIG.getString("sql.password");
PREFIX = Config.CONFIG.getString("sql.prefix");
}
private SqlOptions() {}
}

View file

@ -0,0 +1,202 @@
package dev.loki.loparkour.generator;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.api.event.ParkourBlockGenerateEvent;
import dev.loki.loparkour.api.event.ParkourSchematicGenerateEvent;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.style.Style;
import dev.loki.loparkour.util.Probs;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Fence;
import org.bukkit.block.data.type.GlassPane;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
/**
* Handles block selection, placement, and schematic generation.
* Extracted from {@link ParkourGenerator}.
*/
public class BlockPlacer {
private final ParkourGenerator g;
public BlockPlacer(ParkourGenerator generator) {
this.g = generator;
}
// ── Public entry points ────────────────────────────────────────────────────
/** Generates one block or schematic ahead. */
public void generate() {
if (g.waitForSchematicCompletion) return;
Map<ParkourGenerator.BlockGenerationType, Double> chances = new HashMap<>(g.defaultChances);
if (g.schematicCooldown > 0
|| g.generatorOptions.contains(GeneratorOption.DISABLE_SCHEMATICS)
|| g.profile.get("schematicDifficulty").asDouble() == 0.0
|| !g.schematicBlocks.isEmpty()) {
chances.remove(ParkourGenerator.BlockGenerationType.SCHEMATIC);
}
if (!g.profile.get("useSpecialBlocks").asBoolean()) {
chances.remove(ParkourGenerator.BlockGenerationType.SPECIAL);
}
if (chances.isEmpty()) chances.put(ParkourGenerator.BlockGenerationType.DEFAULT, 1.0);
ParkourGenerator.BlockGenerationType jump = Probs.random(chances);
if (jump == ParkourGenerator.BlockGenerationType.SCHEMATIC && tryGenerateSchematic()) return;
placeNormalBlock(jump);
}
/** Generates {@code amount+1} blocks. */
public void generate(int amount) {
for (int i = 0; i <= amount; i++) generate();
}
/** Seeds the history and generates the initial lead. */
public void generateFirst(Location spawn, Location blockSpawn) {
g.playerSpawn = spawn;
g.lastStandingPlayerLocation = spawn;
g.blockSpawn = blockSpawn;
g.history.add(blockSpawn.getBlock());
generate(g.profile.get("blockLead").asInt());
}
/** Clears schematic blocks and resets cooldown. */
public void deleteSchematic() {
if (!g.deleteSchematic) return;
g.schematicBlocks.forEach(b -> b.setType(Material.AIR));
g.schematicBlocks.clear();
g.deleteSchematic = false;
g.schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
}
// ── Block selection ────────────────────────────────────────────────────────
protected List<Block> selectBlocks() {
int height = Probs.random(g.heightChances);
int distance = Probs.random(g.distanceChances);
return List.of(selectNext(g.getLatest(), distance, height));
}
protected Block selectNext(Block current, int distance, int height) {
JumpDirector director = new JumpDirector(
BoundingBox.of(g.zone[0], g.zone[1]), g.getLatest().getLocation().toVector());
g.heading = director.getRecommendedHeading(g.heading);
height = director.getRecommendedHeight(height);
switch (g.getLatest().getType()) {
case SMOOTH_QUARTZ_SLAB -> height = Math.min(height, 0);
case GLASS_PANE -> distance = Math.min(distance, 3);
}
if (height > 0) distance = Math.max(distance - height, 1);
double sd = g.generatorOptions.contains(GeneratorOption.REDUCE_RANDOM_BLOCK_SELECTION_ANGLE) ? 0.5 : 1;
int randomOffset = new JumpOffsetGenerator(height, distance).getRandomOffset(0, sd);
Vector offset = g.heading.clone().multiply(distance + 1).setY(height);
if (offset.getX() == 0) offset.setX(randomOffset);
else offset.setZ(randomOffset);
offset.rotateAroundY(angleInY(g.heading, Option.HEADING.getDirection()));
return current.getLocation().add(offset).getBlock();
}
protected BlockData selectBlockData() {
Style style = Registry.getStyle(g.profile.get("style").value());
if (style == null) {
g.profile.set("style", Registry.getStyles().stream().findFirst().orElseThrow().getName());
return selectBlockData();
}
return style.getNext().createBlockData();
}
// ── Internal ───────────────────────────────────────────────────────────────
private void placeNormalBlock(ParkourGenerator.BlockGenerationType jump) {
List<Block> blocks = selectBlocks();
if (blocks.isEmpty()) {
LoParkour.getPlugin().getLogger().severe("No blocks to generate!");
return;
}
List<Block> placed = new ArrayList<>();
for (Block block : blocks) {
BlockData data = (jump == ParkourGenerator.BlockGenerationType.SPECIAL
&& !g.generatorOptions.contains(GeneratorOption.DISABLE_SPECIAL))
? Probs.random(g.specialChances)
: selectBlockData();
if (data instanceof Fence) block = block.getLocation().subtract(0, 1, 0).getBlock();
block.setBlockData(data, data instanceof Fence || data instanceof GlassPane);
placed.add(block);
}
new ParkourBlockGenerateEvent(placed, g, g.player).call();
g.effects.particles(placed);
g.effects.sound(placed);
g.history.addAll(placed);
g.schematicCooldown--;
}
private boolean tryGenerateSchematic() {
var manager = LoParkour.getSchematicManager();
if (manager == null) return false;
var all = manager.getAllSchematics();
if (all.isEmpty()) return false;
double diff = g.profile.get("schematicDifficulty").asDouble();
var candidates = all.values().stream()
.filter(s -> s.getMetadata() != null && Math.abs(s.getMetadata().getDifficulty() - diff) <= 0.26)
.toList();
var pool = candidates.isEmpty() ? new ArrayList<>(all.values()) : candidates;
var schematic = pool.get(new java.util.Random().nextInt(pool.size()));
Location origin = g.getLatest().getLocation().add(g.heading.clone().multiply(2));
List<Block> placed = rotatedPaste(schematic, origin);
if (placed.isEmpty()) return false;
g.schematicBlocks.addAll(placed);
g.history.addAll(placed);
g.waitForSchematicCompletion = true;
g.deleteSchematic = false;
new ParkourSchematicGenerateEvent(schematic, g, g.player).call();
return true;
}
private @NotNull List<Block> rotatedPaste(
dev.loki.loparkour.schematic.lpschem.LPSchematic schematic, Location location) {
int[] rotations = {0, 90, 180, 270};
int rotation = rotations[new java.util.Random().nextInt(rotations.length)];
org.bukkit.World world = location.getWorld();
if (world == null) return List.of();
if (rotation == 0) return schematic.paste(location, world);
try {
var t = new dev.loki.loparkour.schematic.lpschem.SchematicTransformer(schematic);
return t.rotate(rotation).paste(location, world);
} catch (Exception ex) {
LoParkour.getPlugin().getLogger().log(Level.WARNING, "Could not rotate schematic", ex);
return schematic.paste(location, world);
}
}
private double angleInY(Vector a, Vector b) {
return Math.atan2(a.getX() * b.getZ() - a.getZ() * b.getX(), a.dot(b));
}
}

View file

@ -0,0 +1,120 @@
package dev.loki.loparkour.generator;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.style.Style;
import dev.loki.loparkour.util.Probs;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Selects next parkour blocks (SRP - Single Responsibility)
*/
public class BlockSelector {
private final Profile profile;
private final Location[] zone;
private final Vector initialHeading;
private final Map<Integer, Double> heightChances = new HashMap<>();
private final Map<Integer, Double> distanceChances = new HashMap<>();
private Vector heading;
public BlockSelector(Profile profile, Location[] zone, Vector initialHeading) {
this.profile = profile;
this.zone = zone;
this.heading = initialHeading.clone();
this.initialHeading = initialHeading.clone();
calculateChances();
}
private void calculateChances() {
heightChances.put(1, Option.NORMAL_HEIGHT_1);
heightChances.put(0, Option.NORMAL_HEIGHT_0);
heightChances.put(-1, Option.NORMAL_HEIGHT_NEG1);
heightChances.put(-2, Option.NORMAL_HEIGHT_NEG2);
distanceChances.put(1, Option.NORMAL_DISTANCE_1);
distanceChances.put(2, Option.NORMAL_DISTANCE_2);
distanceChances.put(3, Option.NORMAL_DISTANCE_3);
distanceChances.put(4, Option.NORMAL_DISTANCE_4);
}
public List<Block> selectBlocks(Block latest) {
int height = Probs.random(heightChances);
int distance = Probs.random(distanceChances);
return List.of(selectNext(latest, distance, height));
}
private Block selectNext(Block current, int distance, int height) {
JumpDirector director = new JumpDirector(
BoundingBox.of(zone[0], zone[1]),
current.getLocation().toVector()
);
heading = director.getRecommendedHeading(heading);
height = director.getRecommendedHeight(height);
// Ensure special blocks are possible
switch (current.getType()) {
case SMOOTH_QUARTZ_SLAB -> height = Math.min(height, 0);
case GLASS_PANE -> distance = Math.min(distance, 3);
}
if (height > 0) {
distance = Math.max(distance - height, 1);
}
int randomOffset = new JumpOffsetGenerator(height, distance).getRandomOffset(0, 1);
Vector offset = heading.clone()
.multiply(distance + 1)
.setY(height);
if (offset.getX() == 0) {
offset.setX(randomOffset);
} else {
offset.setZ(randomOffset);
}
offset.rotateAroundY(angleInY(heading, initialHeading));
return current.getLocation().add(offset).getBlock();
}
public BlockData selectBlockData() {
Style style = Registry.getStyle(profile.get("style").value());
if (style == null) {
profile.set("style", Registry.getStyles().stream()
.findFirst()
.orElseThrow()
.getName());
return selectBlockData();
}
return style.getNext().createBlockData();
}
private double angleInY(Vector a, Vector b) {
double det = a.getX() * b.getZ() - a.getZ() * b.getX();
return Math.atan2(det, a.dot(b));
}
public Vector getHeading() {
return heading;
}
public void resetHeading() {
heading = initialHeading.clone();
}
}

View file

@ -0,0 +1,83 @@
package dev.loki.loparkour.generator;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourSpectator;
import dev.loki.loparkour.util.ParticleUtil;
import org.bukkit.Location;
import org.bukkit.block.Block;
import java.util.List;
/**
* Handles particle and sound effects for the parkour generator.
* Extracted from ParkourGenerator to keep it focused on generation logic.
*/
public class EffectManager {
private final ParkourGenerator generator;
public EffectManager(ParkourGenerator generator) {
this.generator = generator;
}
/**
* Spawns particles around the given blocks based on the configured shape.
*/
public void particles(List<Block> blocks) {
if (!generator.profile.get("particles").asBoolean()) return;
List<Location> locations = blocks.stream().map(Block::getLocation).toList();
Location max = locations.stream().reduce((a, b) -> {
double x = Math.max(a.getX(), b.getX());
double y = Math.max(a.getY(), b.getY());
double z = Math.max(a.getZ(), b.getZ());
return new Location(a.getWorld(), x, y, z);
}).orElse(null);
Location min = locations.stream().reduce((a, b) -> {
double x = Math.min(a.getX(), b.getX());
double y = Math.min(a.getY(), b.getY());
double z = Math.min(a.getZ(), b.getZ());
return new Location(a.getWorld(), x, y, z);
}).orElse(null);
if (max == null || min == null || max.getWorld() == null) return;
List<ParkourPlayer> viewers = generator.getPlayers();
switch (Option.PARTICLE_SHAPE) {
case DOT -> {
Location center = min.clone().add(
(max.getX() - min.getX()) / 2,
(max.getY() - min.getY()) / 2,
(max.getZ() - min.getZ()) / 2
);
viewers.forEach(p -> p.player.spawnParticle(
Option.PARTICLE_TYPE, center, 5, 0.1, 0.1, 0.1, 0,
Option.PARTICLE_DATA != null ? Option.PARTICLE_DATA.data() : null
));
}
case CIRCLE -> ParticleUtil.circle(
min.clone().add(0.5, 0.5, 0.5), max.getWorld(), Option.PARTICLE_TYPE,
viewers.stream().map(p -> p.player).toList(), 0.5, 8
);
case BOX -> ParticleUtil.box(
org.bukkit.util.BoundingBox.of(min, max), max.getWorld(), Option.PARTICLE_TYPE,
viewers.isEmpty() ? null : viewers.get(0).player, 0.3
);
}
}
/**
* Plays the configured sound at the first block's location.
*/
public void sound(List<Block> blocks) {
if (!generator.profile.get("sound").asBoolean() || blocks.isEmpty()) return;
Location loc = blocks.get(0).getLocation();
generator.getPlayers().forEach(p ->
p.player.playSound(loc, Option.SOUND_TYPE, Option.SOUND_VOLUME, Option.SOUND_PITCH));
generator.getSpectators().forEach(s ->
s.player.playSound(loc, Option.SOUND_TYPE, Option.SOUND_VOLUME, Option.SOUND_PITCH));
}
}

View file

@ -0,0 +1,182 @@
package dev.loki.loparkour.generator;
import dev.lolib.scheduler.Scheduler;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.api.event.ParkourFallEvent;
import dev.loki.loparkour.api.event.ParkourScoreEvent;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.leaderboard.Score;
import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourSpectator;
import dev.loki.loparkour.reward.Rewards;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.jetbrains.annotations.Nullable;
import java.time.Instant;
import java.util.Iterator;
/**
* Handles parkour lifecycle: tick, fall, reset, cleanup.
*/
public class GeneratorLifecycle {
private final ParkourGenerator generator;
private dev.lolib.scheduler.ScheduledTask cleanupTask;
public GeneratorLifecycle(ParkourGenerator generator) {
this.generator = generator;
}
public void startTick() {
generator.task = Scheduler.get(LoParkour.getPlugin()).runTimer(this::tick, 0, 1);
cleanupTask = Scheduler.get(LoParkour.getPlugin()).runTimer(this::cleanupDistantBlocks, 0, Option.CLEANUP_INTERVAL);
}
public void tick() {
if (generator.state.stopped) {
generator.task.cancel();
if (cleanupTask != null) cleanupTask.cancel();
return;
}
generator.getPlayers().forEach(p -> {
updateVisualTime(p, p.selectedTime);
p.updateScoreboard(generator);
p.player.setSaturation(20);
});
generator.getSpectators().forEach(ParkourSpectator::update);
if (generator.player.getLocation().getWorld() != generator.state.lastStandingPlayerLocation.getWorld()) return;
if (generator.player.getLocation().subtract(generator.state.lastStandingPlayerLocation).getY() < -10) {
fall();
return;
}
Block below = blockBelow();
if (below == null) return;
handleSchematicEndBlock(below);
if (!generator.state.history.contains(below)) return;
int idx = generator.state.history.indexOf(below);
int delta = idx - generator.state.lastPositionIndexPlayer;
if (delta <= 0) return;
generator.state.lastStandingPlayerLocation = generator.player.getLocation();
int lead = generator.profile.get("blockLead").asInt();
if (generator.state.history.size() - idx <= lead) {
generator.generate(lead - (generator.state.history.size() - idx));
}
generator.state.lastPositionIndexPlayer = idx;
removeTrailBlocks(idx);
cleanupDistantBlocks();
generator.placer.deleteSchematic();
int pts = Config.CONFIG.getBoolean("scoring.all-points") ? delta : 1;
for (int i = 0; i < pts; i++) score();
if (generator.state.start == null) generator.state.start = Instant.now();
}
private void handleSchematicEndBlock(Block below) {
if (generator.state.schematicBlocks.contains(below)
&& below.getType() == Material.RED_WOOL
&& !generator.state.deleteSchematic) {
for (int i = 0; i < generator.profile.get("schematicDifficulty").asDouble() * 15; i++) {
score();
}
generator.state.waitForSchematicCompletion = false;
generator.state.schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
generator.generate(generator.profile.get("blockLead").asInt());
generator.state.deleteSchematic = true;
}
}
private @Nullable Block blockBelow() {
Location loc = generator.player.getLocation().subtract(0, 1, 0);
Block b = loc.getBlock();
if (b.getType() == Material.AIR) {
if (loc.subtract(0, 0.5, 0).getBlock().getType() == Material.AIR) return null;
b = loc.getBlock();
}
return b;
}
private void removeTrailBlocks(int idx) {
for (int i = idx - ParkourGenerator.BLOCK_TRAIL - 1; i >= idx - 4 * ParkourGenerator.BLOCK_TRAIL; i--) {
if (i > 0) generator.state.history.get(i).setType(Material.AIR);
}
}
private void updateVisualTime(ParkourPlayer p, int selectedTime) {
int t = 18000 + selectedTime;
if (t >= 24000) t -= 24000;
p.player.setPlayerTime(t, false);
}
protected void score() {
generator.state.score++;
generator.state.totalScore++;
checkRewards();
new ParkourScoreEvent(generator.player).call();
}
private void checkRewards() {
if (!Rewards.REWARDS_ENABLED || generator.state.score == 0) return;
if (Rewards.SCORE_REWARDS.containsKey(generator.state.score)) {
Rewards.SCORE_REWARDS.get(generator.state.score).forEach(r -> r.execute(generator.player, generator.getMode()));
}
int intervalScore = Config.CONFIG.getBoolean("scoring.rewards-use-total-score")
? generator.state.totalScore
: generator.state.score;
for (int interval : Rewards.INTERVAL_REWARDS.keySet()) {
if (intervalScore % interval == 0) {
Rewards.INTERVAL_REWARDS.get(interval).forEach(r -> r.execute(generator.player, generator.getMode()));
}
}
String key = Integer.toString(generator.state.score);
if (Rewards.ONE_TIME_REWARDS.containsKey(generator.state.score)
&& !generator.player.collectedRewards.contains(key)) {
Rewards.ONE_TIME_REWARDS.get(generator.state.score).forEach(r -> r.execute(generator.player, generator.getMode()));
generator.player.collectedRewards.add(key);
}
}
protected void fall() {
new ParkourFallEvent(generator.player).call();
generator.reset(true);
}
protected void cleanupDistantBlocks() {
if (generator.state.history.size() < Option.BLOCK_CLEANUP_DISTANCE * 2) return;
Location loc = generator.player.getLocation();
int removed = 0;
Iterator<Block> it = generator.state.history.iterator();
while (it.hasNext()) {
Block b = it.next();
if (b.getLocation().distance(loc) > Option.BLOCK_CLEANUP_DISTANCE) {
b.setType(Material.AIR, false);
it.remove();
removed++;
} else break;
}
if (removed > 0) {
generator.state.lastPositionIndexPlayer = Math.max(0, generator.state.lastPositionIndexPlayer - removed);
}
}
}

View file

@ -0,0 +1,64 @@
package dev.loki.loparkour.generator;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.util.Vector;
import java.time.Instant;
import java.util.*;
/**
* Holds all state data for parkour generation.
* No logic - just data and getters/setters.
*/
public class GeneratorState {
// Score state
public int score = 0;
public int totalScore = 0;
public int schematicCooldown;
public boolean stopped = false;
// Location state
public Location blockSpawn;
public Location playerSpawn;
public Location lastStandingPlayerLocation;
public Vector heading;
// Time state
public Instant start;
// Block history
public List<Block> history = new LinkedList<>();
public int lastPositionIndexPlayer = -1;
// Schematic state
public boolean deleteSchematic = false;
public boolean waitForSchematicCompletion = false;
public List<Block> schematicBlocks = new ArrayList<>();
// Chance maps
public final Map<Integer, Double> distanceChances = new HashMap<>();
public final Map<Integer, Double> heightChances = new HashMap<>();
public final Map<BlockData, Double> specialChances = new HashMap<>();
public final Map<ParkourGenerator.BlockGenerationType, Double> defaultChances = new HashMap<>();
public GeneratorState() {}
public void resetScore() {
score = 0;
start = null;
}
public void clearHistory() {
history.clear();
lastPositionIndexPlayer = 0;
}
public void resetSchematicState() {
waitForSchematicCompletion = false;
deleteSchematic = true;
schematicBlocks.clear();
}
}

View file

@ -0,0 +1,8 @@
package dev.loki.loparkour.generator;
/**
* @deprecated Tick logic was merged back into {@link ParkourGenerator#tick()}.
* Delete this file.
*/
@Deprecated
class GeneratorTick {}

View file

@ -47,11 +47,11 @@ public final class Island {
*/
public void build(Location location) {
if (schematic == null) {
LoParkour.getPlugin().getLogger().warning("Cannot build island - schematic is null!");
return;
}
LoParkour.log("Building island");
blocks = new ArrayList<>(); // TODO: schematic.paste(location, world)
blocks = schematic.paste(location, location.getWorld());
Material playerMaterial = Material.getMaterial(Config.GENERATION.getString("advanced.island.spawn.player-block").toUpperCase());
Material parkourMaterial = Material.getMaterial(Config.GENERATION.getString("advanced.island.parkour.begin-block").toUpperCase());
@ -60,14 +60,18 @@ public final class Island {
Block player = blocks.stream().filter(block -> block.getType() == playerMaterial).findAny().orElseThrow();
Block parkour = blocks.stream().filter(block -> block.getType() == parkourMaterial).findAny().orElseThrow();
player.setType(Material.AIR);
parkour.setType(Material.AIR);
player.setType(Material.STONE);
parkour.setType(Material.STONE);
Location ps = player.getLocation().add(0.5, 0, 0.5);
Location ps = player.getLocation().add(0.5, 1.0, 0.5);
ps.setYaw(Config.GENERATION.getInt("advanced.island.spawn.yaw"));
ps.setPitch(Config.GENERATION.getInt("advanced.island.spawn.pitch"));
session.generator.generateFirst(ps, parkour.getLocation().subtract(session.generator.heading).subtract(0, 1, 0));
// First parkour block should be generated FROM the parkour start block
// in the direction of heading, not AT the parkour start block
Location parkourStart = parkour.getLocation().add(session.generator.heading);
session.generator.generateFirst(ps, parkourStart);
session.generator.startTick();
session.getPlayers().forEach(pp -> pp.setup(ps));
} catch (NoSuchElementException ex) {

View file

@ -1,16 +1,9 @@
package dev.loki.loparkour.generator;
import java.util.ArrayList;
import dev.loki.loparkour.util.ParticleData;
import dev.lolib.scheduler.Scheduler;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.api.event.ParkourBlockGenerateEvent;
import dev.loki.loparkour.api.event.ParkourFallEvent;
import dev.loki.loparkour.api.event.ParkourSchematicGenerateEvent;
import dev.loki.loparkour.api.event.ParkourScoreEvent;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Option;
@ -22,23 +15,13 @@ import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourSpectator;
import dev.loki.loparkour.reward.Rewards;
import dev.loki.loparkour.schematic.lpschem.LPSchematic;
import dev.loki.loparkour.session.Session;
import dev.loki.loparkour.style.Style;
import dev.loki.loparkour.world.Divider;
import dev.loki.loparkour.util.ParticleUtil;
import dev.loki.loparkour.util.Locations;
import dev.loki.loparkour.util.Probs;
import dev.lolib.scheduler.Scheduler;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.type.Fence;
import org.bukkit.block.data.type.GlassPane;
import dev.lolib.scheduler.ScheduledTask;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -49,98 +32,102 @@ import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* The class that generates the parkour, which each {@link ParkourPlayer} has.
* Coordinates parkour generation, ticking, scoring, and reset.
*
* @author loki
* @since 5.0.0
* Heavy lifting is delegated to:
* <ul>
* <li>{@link BlockPlacer} – block selection &amp; placement</li>
* <li>{@link EffectManager} – particles &amp; sounds</li>
* </ul>
*/
public class ParkourGenerator {
public static final int BLOCK_TRAIL = 2;
// ── State ──────────────────────────────────────────────────────────────────
public int score = 0;
public int totalScore = 0;
public int schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
public int schematicCooldown;
public boolean stopped = false;
public Location[] zone;
public ParkourPlayer player;
public dev.lolib.scheduler.ScheduledTask task;
private dev.lolib.scheduler.ScheduledTask cleanupTask;
public Location blockSpawn;
/**
* Where the player spawns on reset
*/
public Location playerSpawn;
/**
* Instant when run started.
*/
public Instant start;
/**
* The direction of the parkour
*/
public Vector heading = Option.HEADING.getDirection();
/**
* Generator options
*/
public final List<GeneratorOption> generatorOptions;
/**
* The {@link Session} associated with this Generator.
*/
public final Session session;
/**
* This Generator's {@link Profile}.
*/
public final Profile profile = new Profile();
public final Island island;
public final Map<Integer, Double> distanceChances = new HashMap<>();
public final Map<Integer, Double> heightChances = new HashMap<>();
public final Map<BlockData, Double> specialChances = new HashMap<>();
public final Map<BlockGenerationType, Double> defaultChances = new HashMap<>();
protected boolean deleteSchematic = false;
protected boolean waitForSchematicCompletion = false;
protected Location lastStandingPlayerLocation;
protected List<Block> schematicBlocks = new ArrayList<>();
protected int lastPositionIndexPlayer = -1;
protected List<Block> history = new LinkedList<>();
// Chance maps – populated by calculateChances()
public final Map<Integer, Double> distanceChances = new HashMap<>();
public final Map<Integer, Double> heightChances = new HashMap<>();
public final Map<BlockData, Double> specialChances = new HashMap<>();
public final Map<BlockGenerationType, Double> defaultChances = new HashMap<>();
public ParkourGenerator(@NotNull Session session, @Nullable dev.loki.loparkour.schematic.lpschem.LPSchematic schematic, GeneratorOption... generatorOptions) {
// Schematic state
public boolean deleteSchematic = false;
public boolean waitForSchematicCompletion = false;
public List<Block> schematicBlocks = new ArrayList<>();
// History
public List<Block> history = new LinkedList<>();
public int lastPositionIndexPlayer = -1;
public Location lastStandingPlayerLocation;
// Delegates
public final EffectManager effects;
public final BlockPlacer placer;
// ── Constructors ───────────────────────────────────────────────────────────
public ParkourGenerator(@NotNull Session session, @Nullable LPSchematic schematic,
GeneratorOption... generatorOptions) {
this.session = session;
this.generatorOptions = Arrays.asList(generatorOptions);
player = session.getPlayers().get(0);
island = new Island(session, schematic);
zone = Divider.toSelection(session);
this.player = session.getPlayers().get(0);
this.island = new Island(session, schematic);
this.zone = Divider.toSelection(session);
this.schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
this.effects = new EffectManager(this);
this.placer = new BlockPlacer(this);
calculateChances();
}
public ParkourGenerator(@NotNull Session session, GeneratorOption... generatorOptions) {
// TODO: Implement schematic loading with LoLib
this(session, null, generatorOptions);
this(session, loadIslandSchematic(), generatorOptions);
}
public void overrideProfile() { }
private static LPSchematic loadIslandSchematic() {
String name = Config.GENERATION.getString("advanced.island.schematic-name");
if (name == null || name.isEmpty()) return null;
var manager = LoParkour.getSchematicManager();
if (manager == null) return null;
LPSchematic s = manager.getSchematic(name);
if (s == null) LoParkour.getPlugin().getLogger()
.warning("Island schematic '%s' not found!".formatted(name));
return s;
}
public void overrideProfile() {}
// ── Chances ────────────────────────────────────────────────────────────────
protected void calculateChances() {
defaultChances.clear();
defaultChances.put(BlockGenerationType.DEFAULT, Option.TYPE_NORMAL);
defaultChances.put(BlockGenerationType.DEFAULT, Option.TYPE_NORMAL);
defaultChances.put(BlockGenerationType.SCHEMATIC, Option.TYPE_SCHEMATICS);
defaultChances.put(BlockGenerationType.SPECIAL, Option.TYPE_SPECIAL);
defaultChances.put(BlockGenerationType.SPECIAL, Option.TYPE_SPECIAL);
heightChances.clear();
heightChances.put(1, Option.NORMAL_HEIGHT_1);
heightChances.put(0, Option.NORMAL_HEIGHT_0);
heightChances.put( 1, Option.NORMAL_HEIGHT_1);
heightChances.put( 0, Option.NORMAL_HEIGHT_0);
heightChances.put(-1, Option.NORMAL_HEIGHT_NEG1);
heightChances.put(-2, Option.NORMAL_HEIGHT_NEG2);
@ -151,208 +138,47 @@ public class ParkourGenerator {
distanceChances.put(4, Option.NORMAL_DISTANCE_4);
specialChances.clear();
specialChances.put(Material.PACKED_ICE.createBlockData(), Option.SPECIAL_ICE);
specialChances.put(Material.PACKED_ICE.createBlockData(), Option.SPECIAL_ICE);
specialChances.put(Material.SMOOTH_QUARTZ_SLAB.createBlockData("[type=bottom]"), Option.SPECIAL_SLAB);
specialChances.put(Material.GLASS_PANE.createBlockData(), Option.SPECIAL_PANE);
specialChances.put(Material.OAK_FENCE.createBlockData(), Option.SPECIAL_FENCE);
specialChances.put(Material.GLASS_PANE.createBlockData(), Option.SPECIAL_PANE);
specialChances.put(Material.OAK_FENCE.createBlockData(), Option.SPECIAL_FENCE);
}
/**
* Generates particles around blocks.
*
* @param blocks The blocks.
*/
protected void particles(List<Block> blocks) {
if (!profile.get("particles").asBoolean()) {
return;
}
// ── Generation (delegates) ─────────────────────────────────────────────────
ParticleData<?> data = Option.PARTICLE_DATA;
List<Location> locations = blocks.stream().map(Block::getLocation).toList();
Location max = locations.stream().reduce(Locations::max).orElseThrow();
Location min = locations.stream().reduce(Locations::min).orElseThrow();
Location center = min.clone().add(max.clone().subtract(min));
public void generate() { placer.generate(); }
public void generate(int amount) { placer.generate(amount); }
// display particle
switch (Option.PARTICLE_SHAPE) {
case DOT -> {} // TODO: ParticleUtil.draw
case CIRCLE -> {} // TODO: ParticleUtil.circle
case BOX -> {} // TODO: ParticleUtil.box
}
public void generateFirst(Location spawn, Location block) {
placer.generateFirst(spawn, block);
}
/**
* Generates sound around blocks.
*
* @param blocks The blocks.
*/
protected void sound(List<Block> blocks) {
if (!profile.get("sound").asBoolean()) {
return;
}
// play sound
getPlayers().forEach(viewer -> viewer.player.playSound(blocks.get(0).getLocation(), Option.SOUND_TYPE, Option.SOUND_VOLUME, Option.SOUND_PITCH));
getSpectators().forEach(viewer -> viewer.player.playSound(blocks.get(0).getLocation(), Option.SOUND_TYPE, Option.SOUND_VOLUME, Option.SOUND_PITCH));
}
protected BlockData selectBlockData() {
Style style = Registry.getStyle(profile.get("style").value());
if (style == null) {
profile.set("style", Registry.getStyles().stream()
.findFirst()
.orElseThrow()
.getName());
return selectBlockData();
}
return style.getNext().createBlockData();
}
protected List<Block> selectBlocks() {
int height = Probs.random(heightChances);
int distance = Probs.random(distanceChances);
return List.of(selectNext(getLatest(), distance, height));
}
// Selects the next block that will continue the parkour.
// This is done by choosing a random value for the sideways movement.
// Based on this sideways movement, a value for forward movement will be chosen.
// This is done to ensure players are able to complete the jump.
protected Block selectNext(Block current, int distance, int height) {
JumpDirector director = new JumpDirector(BoundingBox.of(zone[0], zone[1]), getLatest().getLocation().toVector());
heading = director.getRecommendedHeading(heading);
height = director.getRecommendedHeight(height);
// ensure special is possible
switch (getLatest().getType()) {
case SMOOTH_QUARTZ_SLAB -> height = Math.min(height, 0);
case GLASS_PANE -> distance = Math.min(distance, 3);
}
if (height > 0) {
distance = Math.max(distance - height, 1);
}
double mean = 0;
double standardDeviation = generatorOptions.contains(GeneratorOption.REDUCE_RANDOM_BLOCK_SELECTION_ANGLE) ? 0.5 : 1;
int randomOffset = new JumpOffsetGenerator(height, distance).getRandomOffset(mean, standardDeviation);
Vector offset = heading.clone()
.multiply(distance + 1)
.setY(height);
if (offset.getX() == 0) {
offset.setX(randomOffset);
} else {
offset.setZ(randomOffset);
}
// rotate offset to match heading
offset.rotateAroundY(angleInY(heading, Option.HEADING.getDirection()));
return current.getLocation().add(offset).getBlock();
}
protected void score() {
score++;
totalScore++;
checkRewards();
new ParkourScoreEvent(player).call();
}
private void checkRewards() {
if (!Rewards.REWARDS_ENABLED || score == 0) {
return;
}
// check generic score rewards
if (Rewards.SCORE_REWARDS.containsKey(score)) {
Rewards.SCORE_REWARDS.get(score).forEach(s -> s.execute(player, getMode()));
}
// gets the correct type of score to check based on the config option
int intervalScore = Config.CONFIG.getBoolean("scoring.rewards-use-total-score") ? totalScore : score;
for (int interval : Rewards.INTERVAL_REWARDS.keySet()) {
if (intervalScore % interval != 0) {
continue;
}
Rewards.INTERVAL_REWARDS.get(interval).forEach(s -> s.execute(player, getMode()));
}
if (Rewards.ONE_TIME_REWARDS.containsKey(score) && !player.collectedRewards.contains(Integer.toString(score))) {
Rewards.ONE_TIME_REWARDS.get(score).forEach(s -> s.execute(player, getMode()));
player.collectedRewards.add(Integer.toString(score));
}
}
protected void fall() {
new ParkourFallEvent(player).call();
reset(true);
}
public void menu(ParkourPlayer player) {
// TODO: Migrate to LoLib GUI system
// Menus.PARKOUR_SETTINGS.open(player);
player.sendTranslated("other.menu-unavailable");
}
// ── Tick ───────────────────────────────────────────────────────────────────
public void startTick() {
task = Scheduler.get(LoParkour.getPlugin()).runTimer(this::tick, 0, 1);
task = Scheduler.get(LoParkour.getPlugin()).runTimer(this::tick, 0, 1);
cleanupTask = Scheduler.get(LoParkour.getPlugin()).runTimer(this::cleanupDistantBlocks, 0, Option.CLEANUP_INTERVAL);
}
/**
* Starts the check
*/
public void tick() {
if (stopped) {
task.cancel();
if (cleanupTask != null) {
cleanupTask.cancel();
}
return;
}
if (stopped) { task.cancel(); if (cleanupTask != null) cleanupTask.cancel(); return; }
getPlayers().forEach(other -> {
updateVisualTime(other, other.selectedTime);
other.updateScoreboard(this);
other.player.setSaturation(20);
getPlayers().forEach(p -> {
updateVisualTime(p, p.selectedTime);
p.updateScoreboard(this);
p.player.setSaturation(20);
});
getSpectators().forEach(ParkourSpectator::update);
if (player.getLocation().getWorld() != lastStandingPlayerLocation.getWorld()) {
return;
}
if (player.getLocation().getWorld() != lastStandingPlayerLocation.getWorld()) return;
if (player.getLocation().subtract(lastStandingPlayerLocation).getY() < -10) { fall(); return; }
if (player.getLocation().subtract(lastStandingPlayerLocation).getY() < -10) { // fall check
LoParkour.log("Player %s is falling".formatted(player.getName()));
fall();
return;
}
Location belowPlayer = player.getLocation().subtract(0, 1, 0);
Block blockBelowPlayer = belowPlayer.getBlock(); // Get the block below
if (blockBelowPlayer.getType() == Material.AIR) {
if (belowPlayer.subtract(0, 0.5, 0).getBlock().getType() == Material.AIR) {
return;
}
blockBelowPlayer = belowPlayer.getBlock();
}
if (schematicBlocks.contains(blockBelowPlayer) && blockBelowPlayer.getType() == Material.RED_WOOL && !deleteSchematic) { // Structure deletion check
for (int i = 0; i < profile.get("schematicDifficulty").asDouble() * 15; i++) {
score();
}
Block below = blockBelow();
if (below == null) return;
// Schematic end block
if (schematicBlocks.contains(below) && below.getType() == Material.RED_WOOL && !deleteSchematic) {
for (int i = 0; i < profile.get("schematicDifficulty").asDouble() * 15; i++) score();
waitForSchematicCompletion = false;
schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
generate(profile.get("blockLead").asInt());
@ -360,365 +186,194 @@ public class ParkourGenerator {
return;
}
if (!history.contains(blockBelowPlayer)) {
return; // player is on an unknown block
}
if (!history.contains(below)) return;
int currentIndex = history.indexOf(blockBelowPlayer); // current index of the player
int deltaFromLast = currentIndex - lastPositionIndexPlayer;
if (deltaFromLast <= 0) { // the player is actually making progress and not going backwards (current index is higher than the previous)
return;
}
int idx = history.indexOf(below);
int delta = idx - lastPositionIndexPlayer;
if (delta <= 0) return;
lastStandingPlayerLocation = player.getLocation();
int blockLead = profile.get("blockLead").asInt();
int lead = profile.get("blockLead").asInt();
if (history.size() - idx <= lead) generate(lead - (history.size() - idx));
lastPositionIndexPlayer = idx;
int deltaCurrentTotal = history.size() - currentIndex; // delta between current index and total
if (deltaCurrentTotal <= blockLead) {
generate(blockLead - deltaCurrentTotal); // generate the remaining amount so it will match
}
lastPositionIndexPlayer = currentIndex;
for (int i = currentIndex - BLOCK_TRAIL - 1; i >= currentIndex - 4 * BLOCK_TRAIL; i--) {
if (i <= 0) {
continue;
}
history.get(i).setType(Material.AIR);
// Remove trail blocks behind player
for (int i = idx - BLOCK_TRAIL - 1; i >= idx - 4 * BLOCK_TRAIL; i--) {
if (i > 0) history.get(i).setType(Material.AIR);
}
cleanupDistantBlocks();
placer.deleteSchematic();
deleteSchematic();
int pts = Config.CONFIG.getBoolean("scoring.all-points") ? delta : 1;
for (int i = 0; i < pts; i++) score();
for (int i = 0; i < (Config.CONFIG.getBoolean("scoring.all-points") ? deltaFromLast : 1); i++) { // score the difference
score();
if (start == null) start = Instant.now();
}
private @Nullable Block blockBelow() {
Location loc = player.getLocation().subtract(0, 1, 0);
Block b = loc.getBlock();
if (b.getType() == Material.AIR) {
if (loc.subtract(0, 0.5, 0).getBlock().getType() == Material.AIR) return null;
b = loc.getBlock();
}
return b;
}
private void updateVisualTime(ParkourPlayer p, int selectedTime) {
int t = 18000 + selectedTime;
if (t >= 24000) t -= 24000;
p.player.setPlayerTime(t, false);
}
// ── Score ──────────────────────────────────────────────────────────────────
protected void score() {
score++;
totalScore++;
checkRewards();
new ParkourScoreEvent(player).call();
}
private void checkRewards() {
if (!Rewards.REWARDS_ENABLED || score == 0) return;
if (Rewards.SCORE_REWARDS.containsKey(score))
Rewards.SCORE_REWARDS.get(score).forEach(r -> r.execute(player, getMode()));
int intervalScore = Config.CONFIG.getBoolean("scoring.rewards-use-total-score") ? totalScore : score;
for (int interval : Rewards.INTERVAL_REWARDS.keySet()) {
if (intervalScore % interval == 0)
Rewards.INTERVAL_REWARDS.get(interval).forEach(r -> r.execute(player, getMode()));
}
if (start == null) { // start stopwatch when first point is achieved
start = Instant.now();
String key = Integer.toString(score);
if (Rewards.ONE_TIME_REWARDS.containsKey(score) && !player.collectedRewards.contains(key)) {
Rewards.ONE_TIME_REWARDS.get(score).forEach(r -> r.execute(player, getMode()));
player.collectedRewards.add(key);
}
}
// updates the player time
protected void updateVisualTime(ParkourPlayer player, int selectedTime) {
int newTime = 18000 + selectedTime;
if (newTime >= 24000) {
newTime -= 24000;
}
// ── Fall / Reset ───────────────────────────────────────────────────────────
player.player.setPlayerTime(newTime, false);
protected void fall() {
new ParkourFallEvent(player).call();
reset(true);
}
/**
* Resets the parkour. If regenerate is false, this generator is stopped and the island is destroyed.
*
* @param regenerate True if parkour should regenerate, false if not.
*/
public void reset(boolean regenerate) {
LoParkour.log("Resetting generator, regenerate = %s".formatted(regenerate));
stopped = !regenerate;
if (!regenerate && task == null) {
LoParkour.getPlugin().getLogger().warning("## Incomplete joining setup.");
LoParkour.getPlugin().getLogger().warning("## There has probably been an error somewhere. Please report this error!");
LoParkour.getPlugin().getLogger().warning("## You don't have to report this warning.");
}
if (!regenerate && task == null)
LoParkour.getPlugin().getLogger().warning("## Incomplete joining setup — report this!");
lastPositionIndexPlayer = 0;
if (!history.isEmpty()) {
history.remove(0);
history.forEach(block -> block.setType(Material.AIR, false));
history.forEach(b -> b.setType(Material.AIR, false));
history.clear();
}
waitForSchematicCompletion = false;
deleteSchematic = true;
deleteSchematic();
placer.deleteSchematic();
Leaderboard leaderboard = getMode().getLeaderboard();
int record = leaderboard != null ? leaderboard.get(player.getUUID()).score() : 0;
Leaderboard lb = getMode().getLeaderboard();
int record = lb != null ? lb.get(player.getUUID()).score() : 0;
if (profile.get("showFallMessage").asBoolean()) {
String message;
int number = 0;
if (score == record) {
message = "settings.parkour_settings.items.fall_message.formats.tied";
} else if (score > record) {
number = score - record;
message = "settings.parkour_settings.items.fall_message.formats.beat";
} else {
number = record - score;
message = "settings.parkour_settings.items.fall_message.formats.miss";
}
for (ParkourPlayer players : getPlayers()) {
players.sendTranslated("settings.parkour_settings.items.fall_message.divider");
players.sendTranslated("settings.parkour_settings.items.fall_message.score", Integer.toString(score));
players.sendTranslated("settings.parkour_settings.items.fall_message.time", getFormattedTime());
players.sendTranslated("settings.parkour_settings.items.fall_message.high_score", Integer.toString(record));
players.sendTranslated(message, Integer.toString(number));
players.sendTranslated("settings.parkour_settings.items.fall_message.divider");
}
}
if (leaderboard != null && score > record) {
if (profile.get("showFallMessage").asBoolean()) sendFallMessage(record);
if (lb != null && score > record)
registerScore(getDetailedTime(), Double.toString(getDifficultyScore()).substring(0, 3), score);
}
score = 0;
start = null;
heading = Option.HEADING.getDirection();
if (regenerate) { // generate back the blocks
if (regenerate) {
player.teleport(playerSpawn);
generateFirst(playerSpawn, blockSpawn);
return;
}
island.destroy();
if (getPlayers().isEmpty())
getSpectators().forEach(s -> Modes.DEFAULT.create(s.player));
}
if (getPlayers().isEmpty()) {
getSpectators().forEach(spectator -> Modes.DEFAULT.create(spectator.player));
private void sendFallMessage(int record) {
String key;
int number = 0;
if (score == record) key = "settings.parkour_settings.items.fall_message.formats.tied";
else if (score > record) { key = "settings.parkour_settings.items.fall_message.formats.beat"; number = score - record; }
else { key = "settings.parkour_settings.items.fall_message.formats.miss"; number = record - score; }
for (ParkourPlayer p : getPlayers()) {
p.sendTranslated("settings.parkour_settings.items.fall_message.divider");
p.sendTranslated("settings.parkour_settings.items.fall_message.score", Integer.toString(score));
p.sendTranslated("settings.parkour_settings.items.fall_message.time", getFormattedTime());
p.sendTranslated("settings.parkour_settings.items.fall_message.high_score", Integer.toString(record));
p.sendTranslated(key, Integer.toString(number));
p.sendTranslated("settings.parkour_settings.items.fall_message.divider");
}
}
protected void registerScore(String time, String difficulty, int score) {
Leaderboard leaderboard = getMode().getLeaderboard();
if (leaderboard == null) {
return;
}
getPlayers().forEach(player -> leaderboard.put(player.getUUID(), new Score(player.getName(), time, difficulty, score)));
Leaderboard lb = getMode().getLeaderboard();
if (lb == null) return;
getPlayers().forEach(p -> lb.put(p.getUUID(), new Score(p.getName(), time, difficulty, score)));
}
private void deleteSchematic() {
if (!deleteSchematic) {
return;
}
LoParkour.log("Deleting schematic");
// ── Cleanup ────────────────────────────────────────────────────────────────
schematicBlocks.forEach(block -> block.setType(Material.AIR));
schematicBlocks.clear();
deleteSchematic = false;
schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
}
/**
* Generates the next parkour block or schematic.
*/
public void generate() {
if (waitForSchematicCompletion) {
return;
}
Map<BlockGenerationType, Double> chances = new HashMap<>(defaultChances);
if (schematicCooldown > 0 || generatorOptions.contains(GeneratorOption.DISABLE_SCHEMATICS) || profile.get("schematicDifficulty").asDouble() == 0.0 || !schematicBlocks.isEmpty()) {
chances.remove(BlockGenerationType.SCHEMATIC);
}
if (!profile.get("useSpecialBlocks").asBoolean()) {
chances.remove(BlockGenerationType.SPECIAL);
}
if (chances.isEmpty()) {
chances.put(BlockGenerationType.DEFAULT, 1.0);
}
BlockGenerationType jump = Probs.random(chances);
if (jump == BlockGenerationType.SCHEMATIC) {
LoParkour.log("Generating schematic jump at index %s".formatted(history.size() + 1));
double difficulty = profile.get("schematicDifficulty").asDouble();
// TODO: Implement schematic generation with LoLib
LoParkour.getPlugin().getLogger().warning("LPSchematic generation temporarily disabled during migration");
return;
}
LoParkour.log("Generating normal jump at index %s".formatted(history.size() + 1));
List<Block> blocks = selectBlocks();
if (blocks.isEmpty()) {
LoParkour.getPlugin().getLogger().severe("Error while trying to generate parkour: No blocks to generate found");
return;
}
List<Block> movedBlocks = new ArrayList<>();
for (Block block : blocks) {
BlockData data = (jump == BlockGenerationType.SPECIAL && !generatorOptions.contains(GeneratorOption.DISABLE_SPECIAL)) ? Probs.random(specialChances) : selectBlockData();
if (data instanceof Fence) {
block = block.getLocation().subtract(0, 1, 0).getBlock();
}
block.setBlockData(data, data instanceof Fence || data instanceof GlassPane);
movedBlocks.add(block);
}
new ParkourBlockGenerateEvent(movedBlocks, this, player).call();
particles(movedBlocks);
sound(movedBlocks);
history.addAll(movedBlocks);
schematicCooldown--;
}
private @NotNull List<Block> rotatedPaste(dev.loki.loparkour.schematic.lpschem.LPSchematic schematic, Location location) {
// TODO: Implement schematic rotation with LoLib
return new ArrayList<>();
}
private double angleInY(Vector a, Vector b) {
double det = a.getX() * b.getZ() - a.getZ() * b.getX();
return Math.atan2(det, a.dot(b));
}
protected Block getLatest() {
return history.get(history.size() - 1);
}
// Очистка блоков на большом расстоянии от игрока для экономии памяти
protected void cleanupDistantBlocks() {
if (history.size() < Option.BLOCK_CLEANUP_DISTANCE * 2) {
return;
}
Location playerLoc = player.getLocation();
if (history.size() < Option.BLOCK_CLEANUP_DISTANCE * 2) return;
Location loc = player.getLocation();
int removed = 0;
Iterator<Block> iterator = history.iterator();
while (iterator.hasNext()) {
Block block = iterator.next();
if (block.getLocation().distance(playerLoc) > Option.BLOCK_CLEANUP_DISTANCE) {
block.setType(Material.AIR, false);
iterator.remove();
Iterator<Block> it = history.iterator();
while (it.hasNext()) {
Block b = it.next();
if (b.getLocation().distance(loc) > Option.BLOCK_CLEANUP_DISTANCE) {
b.setType(Material.AIR, false);
it.remove();
removed++;
} else {
break;
}
}
if (removed > 0) {
lastPositionIndexPlayer = Math.max(0, lastPositionIndexPlayer - removed);
} else break;
}
if (removed > 0) lastPositionIndexPlayer = Math.max(0, lastPositionIndexPlayer - removed);
}
private double getDifficulty(String fileName) {
String path = "difficulty.%s".formatted(fileName.split("[-.]")[1]);
// ── Misc ───────────────────────────────────────────────────────────────────
if (!Config.SCHEMATICS.isPath(path)) {
return 1.0; // todo remove
}
public void menu(ParkourPlayer player) { Menus.PARKOUR_SETTINGS.open(player); }
return Config.SCHEMATICS.getDouble(path);
}
public Block getLatest() { return history.get(history.size() - 1); }
/**
* Generates a specific amount of blocks ahead of the player
*
* @param amount The amount
*/
public void generate(int amount) {
for (int i = 0; i < amount + 1; i++) {
generate();
}
}
/**
* Generates the first few blocks (which come off the spawn island)
*
* @param spawn The spawn of the player
* @param block The location used to begin the parkour of off
*/
public void generateFirst(Location spawn, Location block) {
LoParkour.log("First generation");
playerSpawn = spawn;
lastStandingPlayerLocation = spawn;
blockSpawn = block;
history.add(blockSpawn.getBlock());
generate(profile.get("blockLead").asInt());
}
/**
* Calculates a score between 0 (inclusive) and 1 (inclusive) to determine how difficult it was for
* the player to achieve this score using their settings.
*/
public double getDifficultyScore() {
double score = 0;
if (profile.get("useSpecialBlocks").asBoolean()) score += 0.5;
if (profile.get("schematicDifficulty").asDouble() > 0) {
if (profile.get("schematicDifficulty").asDouble() <= 0.25) score += 0.2;
else if (profile.get("schematicDifficulty").asDouble() <= 0.5) score += 0.3;
else if (profile.get("schematicDifficulty").asDouble() <= 0.75) score += 0.4;
else if (profile.get("schematicDifficulty").asDouble() <= 1.0) score += 0.5;
}
return score;
double s = 0;
if (profile.get("useSpecialBlocks").asBoolean()) s += 0.5;
double d = profile.get("schematicDifficulty").asDouble();
if (d > 0) s += d <= 0.25 ? 0.2 : d <= 0.5 ? 0.3 : d <= 0.75 ? 0.4 : 0.5;
return s;
}
/**
* @return The time in custom format.
*/
public String getFormattedTime() {
return getTime(Config.CONFIG.getString("options.time.score-format"));
}
/**
* @return The current detailed duration of the run.
*/
public String getDetailedTime() {
return getTime("mm:ss:SSS");
}
public String getFormattedTime() { return getTime(Config.CONFIG.getString("options.time.score-format")); }
public String getDetailedTime() { return getTime("mm:ss:SSS"); }
private String getTime(String format) {
var timeMs = Instant.now().minusMillis(start != null ? start.toEpochMilli() : Instant.now().toEpochMilli());
var ms = Instant.now().minusMillis(start != null ? start.toEpochMilli() : Instant.now().toEpochMilli());
try {
return DateTimeFormatter.ofPattern(format)
.withZone(ZoneOffset.UTC)
.format(timeMs);
return DateTimeFormatter.ofPattern(format).withZone(ZoneOffset.UTC).format(ms);
} catch (IllegalArgumentException ex) {
LoParkour.getPlugin().getLogger().severe("Invalid score time format %s".formatted(Config.CONFIG.getString("options.time.score-format")) + " - " + ex.getMessage());
LoParkour.getPlugin().getLogger().severe("Invalid time format: " + format);
return "";
}
}
/**
* @return This generator's mode.
*/
public Mode getMode() {
return Modes.DEFAULT;
}
public Mode getMode() { return Modes.DEFAULT; }
/**
* @return The players in this session.
*/
@NotNull
public List<ParkourPlayer> getPlayers() {
return session.getPlayers();
}
@NotNull public List<ParkourPlayer> getPlayers() { return session.getPlayers(); }
@NotNull public List<ParkourSpectator> getSpectators() { return session.getSpectators(); }
/**
* @return The spectators in this session.
*/
@NotNull
public List<ParkourSpectator> getSpectators() {
return session.getSpectators();
}
public enum BlockGenerationType {
DEFAULT, SCHEMATIC, SPECIAL
}
public enum BlockGenerationType { DEFAULT, SCHEMATIC, SPECIAL }
}

View file

@ -0,0 +1,126 @@
package dev.loki.loparkour.generator;
import dev.loki.loparkour.api.event.ParkourScoreEvent;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.leaderboard.Score;
import dev.loki.loparkour.mode.Mode;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.reward.Rewards;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* Manages scoring and rewards (SRP - Single Responsibility)
*/
public class ScoreManager {
private final ParkourPlayer player;
private final Mode mode;
private final Profile profile;
private int score = 0;
private int totalScore = 0;
private Instant start;
public ScoreManager(ParkourPlayer player, Mode mode, Profile profile) {
this.player = player;
this.mode = mode;
this.profile = profile;
}
public void addScore(int points) {
for (int i = 0; i < points; i++) {
score++;
totalScore++;
checkRewards();
new ParkourScoreEvent(player).call();
}
if (start == null) {
start = Instant.now();
}
}
private void checkRewards() {
if (!Rewards.REWARDS_ENABLED || score == 0) {
return;
}
if (Rewards.SCORE_REWARDS.containsKey(score)) {
Rewards.SCORE_REWARDS.get(score).forEach(s -> s.execute(player, mode));
}
int intervalScore = Config.CONFIG.getBoolean("scoring.rewards-use-total-score") ? totalScore : score;
for (int interval : Rewards.INTERVAL_REWARDS.keySet()) {
if (intervalScore % interval != 0) continue;
Rewards.INTERVAL_REWARDS.get(interval).forEach(s -> s.execute(player, mode));
}
if (Rewards.ONE_TIME_REWARDS.containsKey(score) &&
!player.collectedRewards.contains(Integer.toString(score))) {
Rewards.ONE_TIME_REWARDS.get(score).forEach(s -> s.execute(player, mode));
player.collectedRewards.add(Integer.toString(score));
}
}
public void reset() {
score = 0;
start = null;
}
public void registerScore(List<ParkourPlayer> players) {
Leaderboard leaderboard = mode.getLeaderboard();
if (leaderboard == null) return;
String time = getDetailedTime();
String difficulty = Double.toString(getDifficultyScore()).substring(0, Math.min(3, Double.toString(getDifficultyScore()).length()));
players.forEach(p -> leaderboard.put(p.getUUID(), new Score(p.getName(), time, difficulty, score)));
}
public String getFormattedTime() {
return getTime(Config.CONFIG.getString("options.time.score-format"));
}
public String getDetailedTime() {
return getTime("mm:ss:SSS");
}
private String getTime(String format) {
var timeMs = Instant.now().minusMillis(start != null ? start.toEpochMilli() : Instant.now().toEpochMilli());
try {
return DateTimeFormatter.ofPattern(format)
.withZone(ZoneOffset.UTC)
.format(timeMs);
} catch (IllegalArgumentException ex) {
return "";
}
}
private double getDifficultyScore() {
double score = 0;
if (profile.get("useSpecialBlocks").asBoolean()) score += 0.5;
double schematicDiff = profile.get("schematicDifficulty").asDouble();
if (schematicDiff > 0) {
if (schematicDiff <= 0.25) score += 0.2;
else if (schematicDiff <= 0.5) score += 0.3;
else if (schematicDiff <= 0.75) score += 0.4;
else score += 0.5;
}
return score;
}
public int getScore() {
return score;
}
public int getTotalScore() {
return totalScore;
}
}

View file

@ -39,8 +39,9 @@ public class Leaderboard {
Storage.init(mode);
// read all data
read(true);
// Defer initial read until storage is ready (SQL connects async — reading before
// connection is established would return empty results and lose all scores).
Storage.runWhenReady(() -> read(true));
var interval = Config.CONFIG.getInt("storage-update-interval");
@ -48,10 +49,8 @@ public class Leaderboard {
// read/write all data every x seconds after x seconds to allow time for reading/writing
Scheduler.get(LoParkour.getPlugin()).runTimerAsync(() -> {
if (Config.CONFIG.getBoolean("joining")) {
LoParkour.log("Periodic saving of leaderboard data of %s".formatted(mode));
write(true);
} else {
LoParkour.log("Periodic reading of leaderboard data of %s".formatted(mode));
read(true);
}
}, interval * 20, interval * 20);
@ -61,8 +60,6 @@ public class Leaderboard {
* Writes all scores to the leaderboard file associated with this leaderboard
*/
public void write(boolean async) {
LoParkour.log("Saving leaderboard data of %s".formatted(mode));
run(() -> Storage.writeScores(mode, scores), async);
}
@ -70,8 +67,6 @@ public class Leaderboard {
* Reads all scores from the leaderboard file
*/
public void read(boolean async) {
LoParkour.log("Reading leaderboard data of %s".formatted(mode));
run(() -> {
scores.clear();
scores.putAll(Storage.readScores(mode));

View file

@ -0,0 +1,133 @@
package dev.loki.loparkour.listener;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.session.Session;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.player.*;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
/**
* Handles hotbar item interactions, chat routing, focus mode, and
* general restrictions (drop/place/break/damage) for parkour players.
*/
public class ParkourRestrictionListener implements Listener {
@EventHandler
public void onChat(AsyncPlayerChatEvent event) {
if (!Option.OPTIONS_ENABLED.get(ParkourOption.CHAT)) return;
Player player = event.getPlayer();
ParkourUser user = ParkourUser.getUser(player);
if (user == null) return;
Session session = user.session;
if (session.isMuted(user)) return;
event.setCancelled(true);
switch (user.chatType) {
case LOBBY_ONLY -> session.getUsers().forEach(u -> u.sendTranslated("settings.chat.formats.lobby", player.getName(), event.getMessage()));
case PLAYERS_ONLY -> session.getPlayers().forEach(u -> u.sendTranslated("settings.chat.formats.players", player.getName(), event.getMessage()));
default -> event.setCancelled(false);
}
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onCommand(PlayerCommandPreprocessEvent event) {
if (!Config.CONFIG.getBoolean("focus-mode.enabled")) return;
ParkourUser user = ParkourUser.getUser(event.getPlayer());
if (user == null) return;
String cmd = event.getMessage().toLowerCase();
if (Config.CONFIG.getStringList("focus-mode.whitelist").stream().anyMatch(cmd::contains)) return;
user.sendTranslated("other.no_do");
event.setCancelled(true);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onInteract(PlayerInteractEvent event) {
Player player = event.getPlayer();
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp == null) return;
// Block container right-click
boolean isContainer = event.getClickedBlock() != null && switch (event.getClickedBlock().getType()) {
case DISPENSER, DROPPER, HOPPER -> true;
default -> false;
};
if (event.getAction() == org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK
&& isContainer && event.getHand() == org.bukkit.inventory.EquipmentSlot.HAND) {
event.setCancelled(true);
player.closeInventory();
return;
}
boolean isRightClick = (event.getAction() == org.bukkit.event.block.Action.RIGHT_CLICK_AIR
|| event.getAction() == org.bukkit.event.block.Action.RIGHT_CLICK_BLOCK)
&& event.getHand() == org.bukkit.inventory.EquipmentSlot.HAND;
if (!isRightClick) return;
org.bukkit.Material held = heldItem(player).getType();
event.setCancelled(true);
if (held == Locales.getItem(player, "play.item").build().getType()) Menus.PLAY.open(player);
else if (held == Locales.getItem(player, "community.item").build().getType()) Menus.COMMUNITY.open(player);
else if (held == Locales.getItem(player, "settings.item").build().getType()) Menus.SETTINGS.open(player);
else if (held == Locales.getItem(player, "lobby.item").build().getType()) Menus.LOBBY.open(player);
else if (held == Locales.getItem(player, "other.quit").build().getType()) ParkourUser.leave(player);
else if (!Config.CONFIG.getBoolean("options.disable-inventory-blocks")) event.setCancelled(false);
}
@EventHandler(priority = EventPriority.LOW)
public void onInventoryClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) return;
if (event.getInventory().getType() == InventoryType.CHEST) return; // LoLib GUI
if (event.getInventory().getType() == InventoryType.CRAFTING) return; // own inv
restrict(player, event);
}
@EventHandler
public void onDrop(PlayerDropItemEvent event) { restrict(event.getPlayer(), event); }
@EventHandler
public void onPlace(BlockPlaceEvent event) { restrict(event.getPlayer(), event); }
@EventHandler
public void onBreak(BlockBreakEvent event) { restrict(event.getPlayer(), event); }
@EventHandler
public void onDamage(EntityDamageEvent event) {
if (event.getEntity() instanceof Player p) restrict(p, event);
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onSpectate(PlayerTeleportEvent event) {
if (event.getCause() == PlayerTeleportEvent.TeleportCause.SPECTATE)
restrict(event.getPlayer(), event);
}
private void restrict(Player player, Cancellable event) {
if (ParkourUser.isUser(player)) event.setCancelled(true);
}
private ItemStack heldItem(Player player) {
PlayerInventory inv = player.getInventory();
return inv.getItemInMainHand().getType() == org.bukkit.Material.AIR
? inv.getItemInOffHand()
: inv.getItemInMainHand();
}
}

View file

@ -0,0 +1,82 @@
package dev.loki.loparkour.listener;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.world.World;
import io.papermc.lib.PaperLib;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerChangedWorldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import java.time.Duration;
import java.time.Instant;
import java.util.NoSuchElementException;
/**
* Handles join / quit / world-change events.
*/
public class PlayerConnectionListener implements Listener {
@EventHandler(priority = EventPriority.LOWEST)
public void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
if (Config.CONFIG.getBoolean("bungeecord.enabled")) {
Modes.DEFAULT.create(player);
return;
}
if (!player.getWorld().equals(World.getWorld())) return;
org.bukkit.World fallback = Bukkit.getWorld(Config.CONFIG.getString("world.fall-back"));
if (fallback != null) {
PaperLib.teleportAsync(player, fallback.getSpawnLocation(), PlayerTeleportEvent.TeleportCause.PLUGIN);
return;
}
PaperLib.teleportAsync(player, Bukkit.getWorlds().stream()
.filter(w -> !w.equals(World.getWorld()))
.findAny()
.orElseThrow(() -> new NoSuchElementException("No fallback world found!"))
.getSpawnLocation());
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
ParkourUser user = ParkourUser.getUser(event.getPlayer());
if (user == null) return;
ParkourUser.unregister(user, true, false, true);
}
@EventHandler
public void onWorldChange(PlayerChangedWorldEvent event) {
Player player = event.getPlayer();
ParkourUser user = ParkourUser.getUser(player);
org.bukkit.World parkour = World.getWorld();
boolean isAdmin = Config.CONFIG.getBoolean("permissions.enabled")
? ParkourOption.ADMIN.mayPerform(player)
: player.isOp();
if (player.getWorld() == parkour && user == null && !isAdmin && player.getTicksLived() > 20) {
Bukkit.getWorlds().stream()
.filter(w -> !w.equals(parkour))
.findAny()
.ifPresent(w -> PaperLib.teleportAsync(player, w.getSpawnLocation()));
return;
}
if (event.getFrom() == parkour && user != null
&& Duration.between(user.joined, Instant.now()).toMillis() > 100) {
ParkourUser.unregister(user, true, false, false);
}
}
}

View file

@ -0,0 +1,57 @@
package dev.loki.loparkour.listener;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.command.SchematicCommandHandler;
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.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.BoundingBox;
/**
* Handles LPSchematic wand clicks (left/right click on blocks).
*/
public class SchematicWandListener implements Listener {
@EventHandler
public void onWandInteract(PlayerInteractEvent event) {
Player player = event.getPlayer();
ItemStack item = player.getInventory().getItemInMainHand();
if (!player.hasPermission("LoParkour.admin")) return;
if (item.getItemMeta() == null || !item.getItemMeta().getDisplayName().contains("LPSchematic Wand")) return;
if (event.getClickedBlock() == null || event.getHand() != EquipmentSlot.HAND) return;
Location loc = event.getClickedBlock().getLocation();
Location[] sel = SchematicCommandHandler.selections.get(player);
event.setCancelled(true);
if (event.getAction() == Action.LEFT_CLICK_BLOCK) {
Location[] updated = new Location[]{loc, sel != null ? sel[1] : null};
SchematicCommandHandler.selections.put(player, updated);
send(player, LoParkour.PREFIX + "Position 1 set to " + Locations.toString(loc, true));
if (updated[1] != null)
ParticleUtil.box(BoundingBox.of(loc, updated[1]), player.getWorld(), Particle.END_ROD, player, 0.2);
} else if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
Location[] updated = new Location[]{sel != null ? sel[0] : null, loc};
SchematicCommandHandler.selections.put(player, updated);
send(player, LoParkour.PREFIX + "Position 2 set to " + Locations.toString(loc, true));
if (updated[0] != null)
ParticleUtil.box(BoundingBox.of(updated[0], loc), player.getWorld(), Particle.END_ROD, player, 0.2);
}
}
private void send(CommandSender sender, String msg) {
sender.sendMessage(ColorUtil.color(msg));
}
}

View file

@ -0,0 +1,101 @@
package dev.loki.loparkour.menu;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.util.ColorUtil;
import dev.lolib.gui.InventoryGUI;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
/**
* Base class for all LoParkour menus.
*
* Real LoLib API:
* InventoryGUI.create().title(String).rows(n).fill(item).setItem(slot, item, Consumer<InventoryClickEvent>).open(player)
* ScrollableGUI.create().title(String).rows(n).addItem(item, Consumer<InventoryClickEvent>).scrollUpButton(item,slot).scrollDownButton(item,slot).open(player)
*
* cancelAllClicks = true by default — items cannot be taken out automatically.
* Handlers receive Consumer<InventoryClickEvent>, event is already cancelled before handler runs.
*/
public abstract class LPMenu {
public abstract void open(@NotNull Player player);
protected String locale(@NotNull Player player) {
var user = dev.loki.loparkour.player.ParkourUser.getUser(player);
return user != null ? user.locale : Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
}
protected ItemStack filler() {
ItemStack item = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(" ");
item.setItemMeta(meta);
}
return item;
}
protected ItemStack backItem() {
ItemStack item = new ItemStack(Material.RED_DYE);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(ColorUtil.color("<#DE1F1F><bold>«"));
item.setItemMeta(meta);
}
return item;
}
protected ItemStack nextItem() {
ItemStack item = new ItemStack(Material.LIME_DYE);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(ColorUtil.color("<#0DCB07><bold>»"));
item.setItemMeta(meta);
}
return item;
}
protected ItemStack closeButton(@NotNull Player player) {
return localeItem(player, "other.close");
}
protected ItemStack localeItem(@NotNull Player player, @NotNull String key) {
return Locales.getItem(player, key).build();
}
protected ItemStack localeItem(@NotNull Player player, @NotNull String key, String... replace) {
return Locales.getItem(player, key, replace).build();
}
protected ItemStack localeItem(@NotNull String locale, @NotNull String key) {
return Locales.getItem(locale, key).build();
}
protected ItemStack localeItem(@NotNull String locale, @NotNull String key, String... replace) {
return Locales.getItem(locale, key, replace).build();
}
/**
* Create a base GUI filled with filler items.
* Uses .fill() — only fills empty slots, so setItem calls after this override correctly.
*/
protected InventoryGUI baseGui(String title, int rows) {
return InventoryGUI.create()
.title(ColorUtil.color(title))
.rows(rows)
.fill(filler());
}
protected int[] distributeEvenly(int count, int start, int end) {
int[] result = new int[count];
if (count == 0) return result;
for (int i = 0; i < count; i++) {
result[i] = start + Math.round((float) i * (end - start) / Math.max(count - 1, 1));
}
return result;
}
}

View file

@ -2,28 +2,57 @@ package dev.loki.loparkour.menu;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.util.ColorUtil;
import dev.lolib.gui.InventoryGUI;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
public class MainMenu extends DynamicMenu {
import java.util.ArrayList;
import java.util.List;
// TODO: Migrate to LoLib GUI system
/*
public MainMenu() {
registerMainItem(1, 0, (player, user) -> Locales.getItem(player, "play.item").click(event -> Menus.PLAY.open(event.getPlayer())), player -> ParkourOption.PLAY.mayPerform(player) && Config.CONFIG.getBoolean("joining"));
registerMainItem(1, 1, (player, user) -> Locales.getItem(player, "community.item").click(event -> Menus.COMMUNITY.open(event.getPlayer())), ParkourOption.COMMUNITY::mayPerform);
registerMainItem(1, 2, (player, user) -> Locales.getItem(player, "settings.item").click(event -> Menus.SETTINGS.open(event.getPlayer())), player -> ParkourOption.SETTINGS.mayPerform(player) && ParkourUser.isUser(player));
registerMainItem(1, 3, (player, user) -> Locales.getItem(player, "lobby.item").click(event -> Menus.LOBBY.open(event.getPlayer())), player -> ParkourOption.LOBBY.mayPerform(player) && ParkourUser.isUser(player));
registerMainItem(1, 4, (player, user) -> Locales.getItem(player, "other.quit").click(event -> ParkourPlayer.leave(player)), player -> ParkourOption.QUIT.mayPerform(player) && ParkourUser.isUser(player));
public class MainMenu extends LPMenu {
@Override
public void open(@NotNull Player player) {
String locale = locale(player);
String title = Locales.getString(locale, "main.name");
List<ItemStack> visibleItems = new ArrayList<>();
List<Runnable> actions = new ArrayList<>();
if (ParkourOption.PLAY.mayPerform(player) && Config.CONFIG.getBoolean("joining")) {
visibleItems.add(localeItem(player, "play.item"));
actions.add(() -> Menus.PLAY.open(player));
}
if (ParkourOption.COMMUNITY.mayPerform(player)) {
visibleItems.add(localeItem(player, "community.item"));
actions.add(() -> Menus.COMMUNITY.open(player));
}
if (ParkourOption.SETTINGS.mayPerform(player) && ParkourUser.isUser(player)) {
visibleItems.add(localeItem(player, "settings.item"));
actions.add(() -> Menus.SETTINGS.open(player));
}
if (ParkourOption.LOBBY.mayPerform(player) && ParkourUser.isUser(player)) {
visibleItems.add(localeItem(player, "lobby.item"));
actions.add(() -> Menus.LOBBY.open(player));
}
if (ParkourOption.QUIT.mayPerform(player) && ParkourUser.isUser(player)) {
visibleItems.add(localeItem(player, "other.quit"));
actions.add(() -> ParkourUser.leave(player));
}
int count = visibleItems.size();
int[] slots = distributeEvenly(count, 9, 17);
InventoryGUI gui = baseGui(title, 3);
for (int i = 0; i < count; i++) {
final int idx = i;
gui = gui.setItem(slots[i], visibleItems.get(i), e -> actions.get(idx).run());
}
gui.open(player);
}
*/
// TODO: Migrate to LoLib GUI system
/*
public void open(Player player) {
display(player, new Menu(3, Locales.getString(player, "main.name"))
.distributeRowsEvenly());
}
*/
}

View file

@ -1,29 +1,22 @@
package dev.loki.loparkour.menu.community;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.menu.DynamicMenu;
import dev.loki.loparkour.menu.LPMenu;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* The menu for all community-related things
*/
public class CommunityMenu extends DynamicMenu {
public class CommunityMenu extends LPMenu {
// TODO: Migrate to LoLib GUI system
/*
public CommunityMenu() {
registerMainItem(1, 1, (player, user) -> Locales.getItem(player, "community.leaderboards.item").click(event -> Menus.LEADERBOARDS.open(event.getPlayer())), ParkourOption.LEADERBOARDS::mayPerform);
registerMainItem(2, 10, (player, user) -> Locales.getItem(player, "other.close").click(event -> event.getPlayer().closeInventory()), player -> true);
@Override
public void open(@NotNull Player player) {
String title = Locales.getString(locale(player), "community.name");
baseGui(title, 3)
.setItem(13, localeItem(player, "community.leaderboards.item"),
e -> { if (ParkourOption.LEADERBOARDS.mayPerform(player)) Menus.LEADERBOARDS.open(player); })
.setItem(22, closeButton(player), e -> player.closeInventory())
.open(player);
}
*/
// TODO: Migrate to LoLib GUI system
/*
public void open(Player player) {
display(player, new Menu(3, Locales.getString(player, "community.name"))
.distributeRowsEvenly());
}
*/
}

View file

@ -2,55 +2,55 @@ package dev.loki.loparkour.menu.community;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.menu.LPMenu;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.mode.Mode;
import dev.loki.loparkour.player.ParkourUser;
import org.bukkit.Material;
import dev.loki.loparkour.util.ColorUtil;
import dev.lolib.gui.ScrollableGUI;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* Leaderboards menu
*/
public class LeaderboardsMenu {
public class LeaderboardsMenu extends LPMenu {
// TODO: Migrate to LoLib GUI system
/*
public void open(Player player) {
ParkourUser user = ParkourUser.getUser(player);
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
@Override
public void open(@NotNull Player player) {
String locale = locale(player);
String title = Locales.getString(locale, ParkourOption.LEADERBOARDS.path + ".name");
PagedMenu menu = new PagedMenu(3, Locales.getString(player, "%s.name".formatted(ParkourOption.LEADERBOARDS.path)));
List<Mode> modes = new ArrayList<>();
List<ItemStack> items = new ArrayList<>();
Mode latest = null;
List<MenuItem> items = new ArrayList<>();
for (Mode mode : Registry.getModes()) {
Leaderboard leaderboard = mode.getLeaderboard();
Item item = mode.getItem(locale);
if (leaderboard == null || item == null) {
continue;
}
items.add(item.clone().click(event -> Menus.SINGLE_LEADERBOARD.open(player, mode, leaderboard.sort)));
latest = mode;
if (mode.getLeaderboard() == null) continue;
var item = mode.getItem(locale);
if (item == null) continue;
modes.add(mode);
items.add(item.build());
}
if (items.size() == 1) {
Menus.SINGLE_LEADERBOARD.open(player, latest, Leaderboard.Sort.SCORE);
if (modes.size() == 1) {
Menus.SINGLE_LEADERBOARD.open(player, modes.get(0), Leaderboard.Sort.SCORE);
return;
}
// menu.displayRows(0, 1)
// .addToDisplay(items)
// .nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> menu.page(1)))
// .prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> menu.page(-1)))
// .item(22, Locales.getItem(player, "other.close").click(event -> Menus.COMMUNITY.open(event.getPlayer())))
// .open(player);
ScrollableGUI gui = ScrollableGUI.create()
.title(ColorUtil.color(title))
.rows(3)
.scrollUpButton(backItem(), 0)
.scrollDownButton(nextItem(), 8);
for (int i = 0; i < items.size(); i++) {
final Mode mode = modes.get(i);
gui = gui.addItem(items.get(i),
e -> Menus.SINGLE_LEADERBOARD.open(player, mode, Leaderboard.Sort.SCORE));
}
gui.open(player);
}
*/
}

View file

@ -1,116 +1,84 @@
package dev.loki.loparkour.menu.community;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.leaderboard.Score;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.LPMenu;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.mode.Mode;
import dev.loki.loparkour.player.ParkourUser;
import org.bukkit.Bukkit;
import dev.loki.loparkour.util.ColorUtil;
import dev.lolib.gui.ScrollableGUI;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* Menu for a single leaderboard
*/
public class SingleLeaderboardMenu {
public class SingleLeaderboardMenu extends LPMenu {
// TODO: Migrate to LoLib GUI system
/*
public void open(Player player, Mode mode, Leaderboard.Sort sort) {
public void open(@NotNull Player player, @NotNull Mode mode, @NotNull Leaderboard.Sort sort) {
Leaderboard leaderboard = mode.getLeaderboard();
if (leaderboard == null) return;
if (leaderboard == null) {
return;
String locale = locale(player);
String title = Locales.getString(locale, ParkourOption.LEADERBOARDS.path + ".name");
List<ItemStack> items = new ArrayList<>();
int rank = 0;
for (Map.Entry<UUID, Score> entry : leaderboard.sort(sort).entrySet()) {
rank++;
Score score = entry.getValue();
if (score == null) continue;
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
if (rank <= 20 && !ParkourUser.isBedrockPlayer(player)) {
OfflinePlayer op = org.bukkit.Bukkit.getOfflinePlayer(entry.getKey());
if (op.getName() != null && !op.getName().startsWith(".")) {
SkullMeta meta = (SkullMeta) skull.getItemMeta();
if (meta != null) { meta.setOwningPlayer(op); skull.setItemMeta(meta); }
}
}
var base = Locales.getItem(locale, ParkourOption.LEADERBOARDS.path + ".head");
int r = rank;
base.modifyName(n -> n.replace("%r", String.valueOf(r))
.replace("%s", String.valueOf(score.score()))
.replace("%p", score.name())
.replace("%t", score.time())
.replace("%d", score.difficulty()))
.modifyLore(l -> l.replace("%r", String.valueOf(r))
.replace("%s", String.valueOf(score.score()))
.replace("%p", score.name())
.replace("%t", score.time())
.replace("%d", score.difficulty()));
ItemMeta built = base.build().getItemMeta();
if (built != null) skull.setItemMeta(built);
items.add(skull);
}
// init vars
// var user = ParkourUser.getUser(player);
// var locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
// var menu = new PagedMenu(3, Locales.getString(player, "%s.name".formatted(ParkourOption.LEADERBOARDS.path)));
// var items = new ArrayList<MenuItem>();
// var base = Locales.getItem(player, "%s.head".formatted(ParkourOption.LEADERBOARDS.path));
// for (Map.Entry<UUID, Score> entry : leaderboard.sort(sort).entrySet()) {
// int rank = items.size() + 1;
// var uuid = entry.getKey();
// var score = entry.getValue();
// if (score == null) {
// continue;
// }
// Item item = base.clone().material(Material.PLAYER_HEAD)
// .modifyName(name -> name.replace("%r", Integer.toString(rank))
// .replace("%s", Integer.toString(score.score()))
// .replace("%p", score.name())
// .replace("%t", score.time())
// .replace("%d", score.difficulty()))
// .modifyLore(line -> line.replace("%r", Integer.toString(rank))
// .replace("%s", Integer.toString(score.score()))
// .replace("%p", score.name())
// .replace("%t", score.time())
// .replace("%d", score.difficulty()));
// Player head gathering
// ItemStack stack = item.build();
// if there are more than 36 players, don't show the heads to avoid server crashing
// and bedrock has no player skull support
// if (rank <= 20 && !ParkourUser.isBedrockPlayer(player)) {
// OfflinePlayer op = Bukkit.getOfflinePlayer(uuid);
// if (op.getName() != null && !op.getName().startsWith(".")) { // bedrock players' names with geyser start with a .
// SkullMeta meta = (SkullMeta) stack.getItemMeta();
// if (meta != null) {
// SkullSetter.setPlayerHead(op, meta);
// item.meta(meta);
// }
// }
// }
// if (uuid.equals(player.getUniqueId())) {
// menu.item(21, item.clone());
// }
// items.add(item);
// }
// List<String> values = Locales.getStringList(locale, "%s.sort.values".formatted(ParkourOption.LEADERBOARDS.path));
// if (values.size() != 3) {
// LoParkour.logging().stack("Error while trying to get locales for sort values: not enough sort values present",
// "check your %s locale file".formatted(locale), new IllegalArgumentException());
// }
// String name = switch (sort) {
// case SCORE -> values.get(0);
// case TIME -> values.get(1);
// case DIFFICULTY -> values.get(2);
// };
// get next sorting type
// var next = switch (sort) {
// case SCORE -> Leaderboard.Sort.TIME;
// case TIME -> Leaderboard.Sort.DIFFICULTY;
// default -> Leaderboard.Sort.SCORE;
// };
// menu.displayRows(0, 1)
// .addToDisplay(items)
// .nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> menu.page(1)))
// .prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> menu.page(-1)))
// .item(22, Locales.getItem(player, ParkourOption.LEADERBOARDS.path + ".sort", name.toLowerCase()).click(event -> open(player, mode, next)))
// .item(23, Locales.getItem(player, "other.close").click(event -> {
// List<Mode> modes = Registry.getModes()
// .stream()
// .filter(m -> m.getLeaderboard() != null && m.getItem(locale) != null)
// .toList();
// if (modes.size() == 1) {
// Menus.COMMUNITY.open(player);
// return;
// }
// Menus.LEADERBOARDS.open(event.getPlayer());
// }))
// .open(player);
ScrollableGUI gui = ScrollableGUI.create()
.title(ColorUtil.color(title))
.rows(3)
.scrollUpButton(backItem(), 0)
.scrollDownButton(nextItem(), 8);
for (ItemStack item : items) {
gui = gui.addItem(item, e -> { /* view only */ });
}
gui.open(player);
}
*/
@Override
public void open(@NotNull Player player) { /* use open(player, mode, sort) */ }
}

View file

@ -1,83 +1,77 @@
package dev.loki.loparkour.menu.lobby;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.menu.DynamicMenu;
import dev.loki.loparkour.menu.LPMenu;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.session.Session;
import dev.loki.loparkour.util.ColorUtil;
import dev.lolib.gui.InventoryGUI;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class LobbyMenu extends DynamicMenu {
public class LobbyMenu extends LPMenu {
// TODO: Migrate to LoLib GUI system
/*
public LobbyMenu() {
registerMainItem(1, 0, (player, user) -> Locales.getItem(player, "lobby.player_management.item").click(event -> Menus.PLAYER_MANAGEMENT.open(player)), player -> {
ParkourUser user = ParkourUser.getUser(player);
@Override
public void open(@NotNull Player player) {
ParkourUser user = ParkourUser.getUser(player);
if (user == null) return;
return ParkourOption.PLAYER_MANAGEMENT.mayPerform(player) && user instanceof ParkourPlayer && user.session.getPlayers().get(0) == user;
});
String locale = user.locale;
String title = Locales.getString(locale, "lobby.name");
registerMainItem(1, 1, (player, user) -> {
if (user == null) {
return null;
}
boolean isHost = !user.session.getPlayers().isEmpty()
&& user.session.getPlayers().get(0) == user;
List<String> values = Locales.getStringList(user.locale, "lobby.visibility.values");
InventoryGUI gui = baseGui(title, 3);
return new SliderItem().initial(switch (user.session.getVisibility()) {
case PUBLIC -> 0;
case ID_ONLY -> 1;
case PRIVATE -> 2;
}).add(0, Locales.getItem(player, "lobby.visibility").modifyLore(lore -> lore.replace("%s", values.get(2))), event -> { // public
ParkourUser u = ParkourUser.getUser(event.getPlayer());
if (ParkourOption.PLAYER_MANAGEMENT.mayPerform(player) && isHost) {
gui = gui.setItem(11, localeItem(player, "lobby.player_management.item"),
e -> Menus.PLAYER_MANAGEMENT.open(player));
}
if (u != null) {
u.session.setVisibility(Session.Visibility.PUBLIC);
}
return true;
}).add(1, Locales.getItem(player, "lobby.visibility").modifyLore(lore -> lore.replace("%s", values.get(1))), event -> { // id only
ParkourUser u = ParkourUser.getUser(event.getPlayer());
if (u != null) {
u.session.setVisibility(Session.Visibility.ID_ONLY);
}
return true;
}).add(2, Locales.getItem(player, "lobby.visibility").modifyLore(lore -> lore.replace("%s", values.get(0))), event -> { // private
ParkourUser u = ParkourUser.getUser(event.getPlayer());
if (u != null) {
u.session.setVisibility(Session.Visibility.PRIVATE);
}
return true;
if (ParkourOption.VISIBILITY.mayPerform(player) && isHost) {
gui = gui.setItem(13, visibilityItem(user), e -> {
Session.Visibility next = switch (user.session.getVisibility()) {
case PUBLIC -> Session.Visibility.ID_ONLY;
case ID_ONLY -> Session.Visibility.PRIVATE;
case PRIVATE -> Session.Visibility.PUBLIC;
};
user.session.setVisibility(next);
open(player);
});
}, player -> {
ParkourUser user = ParkourUser.getUser(player);
}
return ParkourOption.VISIBILITY.mayPerform(player) && user instanceof ParkourPlayer && user.session.getPlayers().get(0) == user;
});
registerMainItem(2, 10, (player, user) -> Locales.getItem(player, "other.close").click(event -> event.getPlayer().closeInventory()), player -> true);
gui.setItem(22, closeButton(player), e -> player.closeInventory())
.open(player);
}
*/
/**
* Opens the main menu.
*
* @param player The player to open the menu to
*/
// TODO: Migrate to LoLib GUI system
/*
public void open(Player player) {
display(player, new Menu(3, Locales.getString(player, "lobby.name"))
.distributeRowsEvenly());
private ItemStack visibilityItem(ParkourUser user) {
String locale = user.locale;
List<String> values = Locales.getStringList(locale, "lobby.visibility.values");
String label = switch (user.session.getVisibility()) {
case PUBLIC -> values.size() > 2 ? values.get(2) : "Public";
case ID_ONLY -> values.size() > 1 ? values.get(1) : "ID Only";
case PRIVATE -> !values.isEmpty() ? values.get(0) : "Private";
};
Material mat = switch (user.session.getVisibility()) {
case PUBLIC -> Material.LIME_STAINED_GLASS_PANE;
case ID_ONLY -> Material.YELLOW_STAINED_GLASS_PANE;
case PRIVATE -> Material.RED_STAINED_GLASS_PANE;
};
ItemStack item = new ItemStack(mat);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(ColorUtil.color(
Locales.getString(locale, "lobby.visibility") + " §7» §f" + label));
item.setItemMeta(meta);
}
return item;
}
*/
}

View file

@ -1,116 +1,78 @@
package dev.loki.loparkour.menu.lobby;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.menu.LPMenu;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.session.Session;
import dev.loki.loparkour.util.ColorUtil;
import dev.lolib.gui.InventoryGUI;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Menu for managing players
*/
public class PlayerManagementMenu {
public class PlayerManagementMenu extends LPMenu {
// TODO: Migrate to LoLib GUI system
/*
public void open(Player p) {
if (p == null) {
return;
}
// ParkourPlayer viewer = ParkourPlayer.getPlayer(p);
// if (viewer == null) {
// return;
// }
// Session session = viewer.session;
// PagedMenu menu = new PagedMenu(3, Locales.getString(viewer.locale, "lobby.player_management.name"));
// add(menu, viewer, session.getPlayers().stream().map(player -> (ParkourUser) player).toList());
// add(menu, viewer, session.getSpectators().stream().map(player -> (ParkourUser) player).toList());
// menu.displayRows(0, 1)
// .prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> menu.page(-1)))
// .nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> menu.page(1)))
// .item(22, Locales.getItem(viewer.locale, "other.close").click(event -> Menus.LOBBY.open(event.getPlayer())))
// .open(p);
}
*/
@Override
public void open(@NotNull Player player) {
ParkourPlayer viewer = ParkourPlayer.getPlayer(player);
if (viewer == null) return;
// TODO: Migrate to LoLib GUI system
/*
private void add(PagedMenu menu, ParkourUser viewer, Collection<ParkourUser> users) {
Session session = viewer.session;
var session = viewer.session;
String locale = viewer.locale;
String title = Locales.getString(locale, "lobby.player_management.name");
for (ParkourUser other : users) {
if (other == viewer) {
continue;
List<ParkourUser> others = new ArrayList<>();
others.addAll(session.getPlayers().stream().filter(p -> p != viewer).toList());
others.addAll(session.getSpectators());
InventoryGUI gui = baseGui(title, 3)
.setItem(22, closeButton(player), e -> Menus.LOBBY.open(player));
for (int i = 0; i < Math.min(others.size(), 7); i++) {
final int idx = i;
final ParkourUser other = others.get(i);
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
if (!ParkourUser.isBedrockPlayer(other.player)
&& other.getName() != null && !other.getName().startsWith(".")) {
SkullMeta meta = (SkullMeta) skull.getItemMeta();
if (meta != null) { meta.setOwningPlayer(other.player); skull.setItemMeta(meta); }
}
Item item = Locales.getItem(viewer.locale, "lobby.player_management.head", other.getName());
item.material(Material.PLAYER_HEAD);
boolean muted = session.isMuted(other);
List<String> lore = new ArrayList<>();
if (muted) {
// add top
lore.addAll(List.of(Locales.getString(viewer.locale, "lobby.player_management.head.top").split("\\|\\|")));
ItemMeta displayMeta = skull.getItemMeta();
if (displayMeta != null) {
displayMeta.setDisplayName(ColorUtil.color("<yellow>" + other.getName() + (muted ? " §c[muted]" : "")));
displayMeta.setLore(List.of(
ColorUtil.color("§7Left-click §fto kick"),
ColorUtil.color("§7Right-click §f" + (muted ? "to unmute" : "to mute"))
));
skull.setItemMeta(displayMeta);
}
// add bottom
lore.addAll(List.of(Locales.getString(viewer.locale, "lobby.player_management.head.bottom").split("\\|\\|")));
// Player head gathering
item.material(Material.PLAYER_HEAD).lore(lore).click(event -> {
ClickType click = event.event().getClick();
switch (click) {
case LEFT -> {
Modes.DEFAULT.create(other.player);
other.sendTranslated("lobby.player_management.kicked");
viewer.sendTranslated("lobby.player_management.advice");
open(viewer.player);
}
case RIGHT -> {
session.toggleMute(other);
if (!muted) {
other.sendTranslated("lobby.player_management.muted");
} else {
other.sendTranslated("lobby.player_management.unmuted");
}
open(viewer.player);
}
gui = gui.setItem(10 + idx, skull, e -> {
if (e.getClick() == ClickType.LEFT) {
Modes.DEFAULT.create(other.player);
other.sendTranslated("lobby.player_management.kicked");
viewer.sendTranslated("lobby.player_management.advice");
} else if (e.getClick() == ClickType.RIGHT) {
session.toggleMute(other);
boolean nowMuted = session.isMuted(other);
other.sendTranslated(nowMuted ? "lobby.player_management.muted" : "lobby.player_management.unmuted");
}
open(player);
});
ItemStack stack = item.build(); // Updating meta requires building
stack.setType(Material.PLAYER_HEAD);
// bedrock has no player skull support
if (menu.getTotalToDisplay().size() <= 36 && !ParkourUser.isBedrockPlayer(other.player)) {
if (other.getName() != null && !other.getName().startsWith(".")) { // bedrock players' names with geyser start with a .
SkullMeta meta = (SkullMeta) stack.getItemMeta();
if (meta != null) {
SkullSetter.setPlayerHead(other.player, meta);
item.meta(meta);
}
}
}
menu.addToDisplay(List.of(item));
}
gui.open(player);
}
*/
}

View file

@ -1,43 +1,25 @@
package dev.loki.loparkour.menu.play;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.menu.DynamicMenu;
import dev.loki.loparkour.menu.LPMenu;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import org.bukkit.Bukkit;
import dev.lolib.gui.InventoryGUI;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* The menu where players can join modes
*/
public class PlayMenu extends DynamicMenu {
public class PlayMenu extends LPMenu {
// TODO: Migrate to LoLib GUI system
/*
public PlayMenu() {
registerMainItem(1, 0, (player, user) -> Locales.getItem(player, "play.single.item")
.click(event -> Menus.SINGLE.open(event.getPlayer())),
ParkourOption.SINGLE::mayPerform);
@Override
public void open(@NotNull Player player) {
String title = Locales.getString(locale(player), "play.name");
registerMainItem(1, 1, (player, user) -> Locales.getItem(player, "other.iep")
.click(event -> event.getPlayer().performCommand("iep play")),
player -> Bukkit.getPluginManager().isPluginEnabled("IEP"));
registerMainItem(1, 6, (player, user) -> Locales.getItem(player, "play.spectator.item")
.click(event -> Menus.SPECTATOR.open(event.getPlayer())),
ParkourOption.SPECTATOR::mayPerform);
registerMainItem(2, 0, (player, user) -> Locales.getItem(player, "other.close")
.click(event -> event.getPlayer().closeInventory()),
player -> true);
baseGui(title, 3)
.setItem(11, localeItem(player, "play.single.item"),
e -> { if (ParkourOption.SINGLE.mayPerform(player)) Menus.SINGLE.open(player); })
.setItem(15, localeItem(player, "play.spectator.item"),
e -> { if (ParkourOption.SPECTATOR.mayPerform(player)) Menus.SPECTATOR.open(player); })
.setItem(22, closeButton(player), e -> player.closeInventory())
.open(player);
}
*/
// TODO: Migrate to LoLib GUI system
/*
public void open(Player player) {
display(player, new Menu(3, Locales.getString(player, "play.name"))
.distributeRowsEvenly());
}
*/
}

View file

@ -3,65 +3,66 @@ package dev.loki.loparkour.menu.play;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.menu.LPMenu;
import dev.loki.loparkour.mode.Mode;
import dev.loki.loparkour.mode.MultiMode;
import dev.loki.loparkour.player.ParkourUser;
import org.bukkit.Material;
import dev.loki.loparkour.util.ColorUtil;
import dev.lolib.gui.ScrollableGUI;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
/**
* The menu to select single player modes
*/
public class SingleMenu {
public class SingleMenu extends LPMenu {
// TODO: Migrate to LoLib GUI system
/*
public void open(Player player) {
ParkourUser user = ParkourUser.getUser(player);
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
@Override
public void open(@NotNull Player player) {
String locale = locale(player);
List<Mode> modes = Registry.getModes();
List<Mode> availableModes = new ArrayList<>();
List<ItemStack> items = new ArrayList<>();
List<MenuItem> items = new ArrayList<>();
List<Mode> modeSet = new ArrayList<>();
for (Mode mode : modes) {
boolean permissions = Config.CONFIG.getBoolean("permissions.enabled") && !player.hasPermission("LoParkour.gamemode." + mode.getName());
Item item = mode.getItem(locale);
if (permissions || mode instanceof MultiMode || item == null) {
continue;
}
modeSet.add(mode);
items.add(item.clone().click(event -> {
if (user == null || Duration.between(user.joined, Instant.now()).toSeconds() > 3) {
mode.create(player);
}
}));
for (Mode mode : Registry.getModes()) {
boolean blocked = Config.CONFIG.getBoolean("permissions.enabled")
&& !player.hasPermission("LoParkour.gamemode." + mode.getName());
if (blocked || mode instanceof MultiMode) continue;
var item = mode.getItem(locale);
if (item == null) continue;
availableModes.add(mode);
items.add(item.build());
}
if (modeSet.size() == 1) {
modeSet.get(0).create(player);
if (availableModes.size() == 1) {
availableModes.get(0).create(player);
return;
}
// PagedMenu mode = new PagedMenu(3, Locales.getString(player, "play.single.name"));
// mode.displayRows(0, 1)
// .addToDisplay(items)
// .nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> mode.page(1)))
// .prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> mode.page(-1)))
// .item(22, Locales.getItem(player, "other.close").click(event -> Menus.PLAY.open(event.getPlayer())))
// .open(player);
}
*/
String title = Locales.getString(locale, "play.single.name");
ScrollableGUI gui = ScrollableGUI.create()
.title(ColorUtil.color(title))
.rows(3)
.scrollUpButton(backItem(), 0)
.scrollDownButton(nextItem(), 8);
for (int i = 0; i < items.size(); i++) {
final Mode mode = availableModes.get(i);
gui = gui.addItem(items.get(i), e -> {
// Prevent multiple clicks - check if player is already in parkour
if (ParkourUser.getUser(player) != null) {
return; // Already in parkour, ignore click
}
player.closeInventory();
mode.create(player);
});
}
gui.open(player);
}
}

View file

@ -1,83 +1,60 @@
package dev.loki.loparkour.menu.play;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.menu.LPMenu;
import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.session.Session;
import dev.loki.loparkour.util.ColorUtil;
import dev.loki.loparkour.world.Divider;
import dev.lolib.gui.ScrollableGUI;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* The menu to select other players to spectate
*/
public class SpectatorMenu {
public class SpectatorMenu extends LPMenu {
// TODO: Migrate to LoLib GUI system
/*
public void open(Player player) {
@Override
public void open(@NotNull Player player) {
String locale = locale(player);
ParkourUser user = ParkourUser.getUser(player);
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
String title = Locales.getString(locale, "play.spectator.name");
PagedMenu spectator = new PagedMenu(3, Locales.getString(player, "play.spectator.name"));
List<MenuItem> display = new ArrayList<>();
List<Session> sessions = new ArrayList<>();
List<ItemStack> items = new ArrayList<>();
for (Session session : Divider.sections.keySet()) {
if (!session.isAcceptingSpectators()) { // only showcase sessions with spectators enabled
continue;
}
if (user != null && session == user.session) { // don't let player join their own session
continue;
}
if (session.getPlayers().isEmpty()) { // weird but possible
continue;
}
if (!session.isAcceptingSpectators()) continue;
if (user != null && session == user.session) continue;
if (session.getPlayers().isEmpty()) continue;
var pp = session.getPlayers().get(0);
Item item = Locales.getItem(locale, "play.spectator.head", pp.getName());
// Player head gathering
item.material(Material.PLAYER_HEAD);
ItemStack stack = item.build(); // Updating meta requires building
stack.setType(Material.PLAYER_HEAD);
// bedrock has no player skull support
if (!ParkourUser.isBedrockPlayer(player)) {
if (pp.getName() != null && !pp.getName().startsWith(".")) { // bedrock players' names with geyser start with a .
SkullMeta meta = (SkullMeta) stack.getItemMeta();
if (meta != null) {
SkullSetter.setPlayerHead(pp.player, meta);
item.meta(meta);
}
}
ItemStack skull = new ItemStack(Material.PLAYER_HEAD);
if (!ParkourUser.isBedrockPlayer(player)
&& pp.getName() != null && !pp.getName().startsWith(".")) {
SkullMeta meta = (SkullMeta) skull.getItemMeta();
if (meta != null) { meta.setOwningPlayer(pp.player); skull.setItemMeta(meta); }
}
item.click(event -> Modes.SPECTATOR.create(player, session));
display.add(item);
sessions.add(session);
items.add(skull);
}
spectator.displayRows(0, 1)
.addToDisplay(display)
.nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> spectator.page(1)))
.prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> spectator.page(-1)))
.item(22, Locales.getItem(player, "other.close").click(event -> Menus.PLAY.open(event.getPlayer())))
.open(player);
ScrollableGUI gui = ScrollableGUI.create()
.title(ColorUtil.color(title))
.rows(3)
.scrollUpButton(backItem(), 0)
.scrollDownButton(nextItem(), 8);
for (int i = 0; i < items.size(); i++) {
final Session session = sessions.get(i);
gui = gui.addItem(items.get(i), e -> Modes.SPECTATOR.create(player, session));
}
gui.open(player);
}
*/
}

View file

@ -1,42 +1,55 @@
package dev.loki.loparkour.menu.settings;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.menu.LPMenu;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.util.ColorUtil;
import dev.lolib.gui.InventoryGUI;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class LangMenu {
public class LangMenu extends LPMenu {
/**
* Opens the language menu
*
* @param user The ParkourPlayer instance
*/
// TODO: Migrate to LoLib GUI system
/*
public void open(ParkourPlayer user) {
if (user == null) {
return;
}
// PagedMenu style = new PagedMenu(3, Locales.getString(user.locale, "settings.lang.name"));
// List<MenuItem> items = new ArrayList<>();
// for (String lang : Locales.locales.keySet()) {
// Item item = new Item(Material.PAPER, "<#238681><bold>" + Locales.getString(lang, "name"));
// items.add(item.click(event -> {
// user.locale = lang;
// user._locale = lang;
// Menus.SETTINGS.open(event.getPlayer());
// }));
// }
// style.displayRows(0, 1).addToDisplay(items)
// .nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> style.page(1)))
// .prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> style.page(-1)))
// .item(22, Locales.getItem(user.locale, "other.close").click(event -> Menus.SETTINGS.open(event.getPlayer())))
// .open(user.player);
public void open(@NotNull ParkourPlayer user) {
if (user == null) return;
open(user.player);
}
*/
@Override
public void open(@NotNull Player player) {
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp == null) return;
String locale = pp.locale;
String title = Locales.getString(locale, "settings.lang.name");
List<String> langs = new ArrayList<>(Locales.locales.keySet());
InventoryGUI gui = baseGui(title, 3);
for (int i = 0; i < Math.min(langs.size(), 7); i++) {
final String lang = langs.get(i);
String langName = Locales.getString(lang, "name");
ItemStack item = new ItemStack(Material.PAPER);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(ColorUtil.color("&#238681&l" + langName));
item.setItemMeta(meta);
}
gui = gui.setItem(10 + i, item, e -> {
pp.locale = lang;
pp._locale = lang;
Menus.SETTINGS.open(player);
});
}
gui.open(player);
}
}

View file

@ -1,351 +1,138 @@
package dev.loki.loparkour.menu.settings;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.menu.DynamicMenu;
import dev.loki.loparkour.menu.LPMenu;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.style.RandomStyle;
import dev.loki.loparkour.style.Style;
import org.bukkit.ChatColor;
import dev.loki.loparkour.util.ColorUtil;
import dev.lolib.gui.InventoryGUI;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Handles all menu-related activities
*
* @author loki
* @since v3.0.0
*/
public class ParkourSettingsMenu extends DynamicMenu {
public class ParkourSettingsMenu extends LPMenu {
// TODO: Migrate to LoLib GUI system
/*
public ParkourSettingsMenu(ParkourOption... disabled) {
// ---------- top row ----------
// styles
registerMainItem(1, 0, (p, user) -> Locales.getItem(p, ParkourOption.STYLES.path + ".item", user instanceof ParkourPlayer player ? player.style : null).click(event -> {
if (!(user instanceof ParkourPlayer player)) {
return;
}
openStyleMenu(player);
}), player -> checkOptions(player, ParkourOption.STYLES, disabled));
// leads
registerMainItem(1, 1, (p, user) -> {
Item displayItem = Locales.getItem(p, ParkourOption.LEADS.path);
if (!(user instanceof ParkourPlayer player)) {
return displayItem;
}
List<Integer> leads = Option.POSSIBLE_LEADS;
SliderItem item = new SliderItem().initial(leads.indexOf(player.blockLead)); // initial value of the player
int slot = 0;
for (int value : leads) {
item.add(slot, displayItem.clone()
.amount(value)
.modifyLore(line -> line.replace("%s", Integer.toString(value))),
event2 -> handleSettingChange(player, () -> player.blockLead = value));
slot++;
}
return item;
}, player -> checkOptions(player, ParkourOption.LEADS, disabled));
// schematics
registerMainItem(1, 8, (p, user) -> {
Item item = Locales.getItem(p, ParkourOption.SCHEMATICS.path);
if (!(user instanceof ParkourPlayer player)) {
return item;
}
List<Double> difficulties = List.of(0.0, 0.25, 0.5, 0.75, 1.0);
List<String> values = Locales.getStringList(user.locale, "%s.values".formatted(ParkourOption.SCHEMATICS.path));
if (!difficulties.contains(player.schematicDifficulty)) {
player.schematicDifficulty = difficulties.get(0);
}
return new SliderItem()
.initial(difficulties.indexOf(player.schematicDifficulty))
.add(0, item.clone()
.material(Material.RED_STAINED_GLASS_PANE)
.modifyLore(line -> line.replace("%s", values.get(0))),
event -> handleScoreSettingChange(player, event, () -> player.schematicDifficulty = 0.0))
.add(1, item.clone()
.material(Material.LIME_STAINED_GLASS_PANE)
.modifyLore(line -> line.replace("%s", values.get(1))),
event -> handleScoreSettingChange(player, event, () -> player.schematicDifficulty = 0.25))
.add(2, item.clone()
.material(Material.YELLOW_STAINED_GLASS_PANE)
.modifyLore(line -> line.replace("%s", values.get(2))),
event -> handleScoreSettingChange(player, event, () -> player.schematicDifficulty = 0.5))
.add(3, item.clone()
.material(Material.ORANGE_STAINED_GLASS_PANE)
.modifyLore(line -> line.replace("%s", values.get(3))),
event -> handleScoreSettingChange(player, event, () -> player.schematicDifficulty = 0.75))
.add(4, item.clone()
.material(Material.SKELETON_SKULL)
.modifyLore(line -> line.replace("%s", values.get(4))),
event -> handleScoreSettingChange(player, event, () -> player.schematicDifficulty = 1.0));
}, player -> checkOptions(player, ParkourOption.SCHEMATICS, disabled));
// time
registerMainItem(1, 9, (p, user) -> {
Item item = Locales.getItem(p, ParkourOption.TIME.path);
if (!(user instanceof ParkourPlayer player)) {
return item;
}
// Tick times start at 6:00 and total is 24.000.
// Source: https://minecraft.fandom.com/wiki/Daylight_cycle?file=Day_Night_Clock_24h.png
List<Integer> times = Arrays.asList(0, 6000, 12000, 18000); // 00:00 -> 6:00 -> 12:00 -> 18:00
var format = Config.CONFIG.getInt("options.time.format");
return new SliderItem()
.initial(times.indexOf(player.selectedTime))
.add(0, item.clone()
.modifyLore(line -> line.replace("%s", format == 12 ? "12:00 AM" : "00:00")),
event -> handleSettingChange(player, () -> player.selectedTime = 0))
.add(1, item.clone()
.modifyLore(line -> line.replace("%s", format == 12 ? "6:00 AM" : "6:00")),
event -> handleSettingChange(player, () -> player.selectedTime = 6000))
.add(2, item.clone()
.modifyLore(line -> line.replace("%s", format == 12 ? "12:00 PM" : "12:00")),
event -> handleSettingChange(player, () -> player.selectedTime = 12000))
.add(3, item.clone()
.modifyLore(line -> line.replace("%s", format == 12 ? "6:00 PM" : "18:00")),
event -> handleSettingChange(player, () -> player.selectedTime = 18000));
}, player -> checkOptions(player, ParkourOption.TIME, disabled));
// ---------- bottom row ----------
// show scoreboard
registerMainItem(2, 0, (p, user) -> {
Item item = Locales.getItem(p, ParkourOption.SCOREBOARD.path);
if (!(user instanceof ParkourPlayer player)) {
return item;
}
return new SliderItem()
.initial(player.showScoreboard ? 0 : 1)
.add(0, item.clone()
.material(Material.LIME_STAINED_GLASS_PANE)
.modifyName(name -> "<#0DCB07><bold>" + ChatColor.stripColor(name))
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, true))),
event -> handleSettingChange(player, () -> {
player.showScoreboard = true;
player.board = new FastBoard(p);
}))
.add(1, item.clone()
.material(Material.RED_STAINED_GLASS_PANE)
.modifyName(name -> "<red><bold>" + ChatColor.stripColor(name))
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, false))),
event -> handleSettingChange(player, () -> {
player.showScoreboard = false;
if (player.board != null && !player.board.isDeleted()) {
player.board.delete();
}
}));
}, player -> checkOptions(player, ParkourOption.SCOREBOARD, disabled) && Boolean.parseBoolean(Option.OPTIONS_DEFAULTS.get(ParkourOption.SCOREBOARD)));
// show fall message
registerMainItem(2, 1, (p, user) -> {
Item item = Locales.getItem(p, ParkourOption.FALL_MESSAGE.path + ".item");
if (!(user instanceof ParkourPlayer player)) {
return item;
}
return new SliderItem()
.initial(player.showFallMessage ? 0 : 1)
.add(0, item.clone()
.material(Material.LIME_STAINED_GLASS_PANE)
.modifyName(name -> "<#0DCB07><bold>" + ChatColor.stripColor(name))
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, true))),
event -> handleSettingChange(player, () -> player.showFallMessage = true))
.add(1, item.clone()
.material(Material.RED_STAINED_GLASS_PANE)
.modifyName(name -> "<red><bold>" + ChatColor.stripColor(name))
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, false))),
event -> handleSettingChange(player, () -> player.showFallMessage = false));
}, player -> checkOptions(player, ParkourOption.FALL_MESSAGE, disabled));
// show sound
registerMainItem(2, 2, (p, user) -> {
Item item = Locales.getItem(p, ParkourOption.PARTICLES.path);
if (!(user instanceof ParkourPlayer player)) {
return item;
}
return new SliderItem()
.initial(player.particles ? 0 : 1)
.add(0, item.clone().material(Material.LIME_STAINED_GLASS_PANE)
.modifyName(name -> "<#0DCB07><bold>" + ChatColor.stripColor(name))
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, true))),
event -> handleSettingChange(player, () -> player.particles = true))
.add(1, item.clone()
.material(Material.RED_STAINED_GLASS_PANE)
.modifyName(name -> "<red><bold>" + ChatColor.stripColor(name))
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, false))),
event -> handleSettingChange(player, () -> player.particles = false));
}, player -> checkOptions(player, ParkourOption.PARTICLES, disabled));
// show sound
registerMainItem(2, 3, (p, user) -> {
Item item = Locales.getItem(p, ParkourOption.SOUND.path);
if (!(user instanceof ParkourPlayer player)) {
return item;
}
return new SliderItem()
.initial(player.sound ? 0 : 1)
.add(0, item.clone()
.material(Material.LIME_STAINED_GLASS_PANE)
.modifyName(name -> "<#0DCB07><bold>" + ChatColor.stripColor(name))
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, true))),
event -> handleSettingChange(player, () -> player.sound = true))
.add(1, item.clone()
.material(Material.RED_STAINED_GLASS_PANE)
.modifyName(name -> "<red><bold>" + ChatColor.stripColor(name))
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, false))),
event -> handleSettingChange(player, () -> player.sound = false));
}, player -> checkOptions(player, ParkourOption.SOUND, disabled));
// show special blocks
registerMainItem(2, 4, (p, user) -> {
Item item = Locales.getItem(p, ParkourOption.SPECIAL_BLOCKS.path);
if (!(user instanceof ParkourPlayer player)) {
return item;
}
return new SliderItem()
.initial(player.useSpecialBlocks ? 0 : 1)
.add(0, item.clone()
.material(Material.LIME_STAINED_GLASS_PANE)
.modifyName(name -> "<#0DCB07><bold>" + ChatColor.stripColor(name))
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, true))),
event -> handleScoreSettingChange(player, event, () -> player.useSpecialBlocks = true))
.add(1, item.clone()
.material(Material.RED_STAINED_GLASS_PANE)
.modifyName(name -> "<red><bold>" + ChatColor.stripColor(name))
.modifyLore(line -> line.replace("%s", getBooleanSymbol(user, false))),
event -> handleScoreSettingChange(player, event, () -> player.useSpecialBlocks = false));
}, player -> checkOptions(player, ParkourOption.SPECIAL_BLOCKS, disabled));
// Always allow closing of the menu
registerMainItem(3, 10,
(player, user) -> Locales.getItem(player, "other.close")
.click(event -> Menus.SETTINGS.open(event.getPlayer())),
player -> true);
public void open(@NotNull ParkourPlayer user) {
if (user == null) return;
open(user.player);
}
*/
/**
* Shows the main menu to a valid ParkourPlayer instance
*
* @param user The ParkourPlayer
*/
// TODO: Migrate to LoLib GUI system
/*
public void open(ParkourPlayer user) {
display(user.player, new Menu(4, Locales.getString(user.locale, "settings.name"))
.distributeRowEvenly(0, 1, 2, 3)
.item(27, Locales.getItem(user.locale, "other.close").click(event -> Menus.SETTINGS.open(event.getPlayer()))));
@Override
public void open(@NotNull Player player) {
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp == null) return;
String locale = pp.locale;
String title = Locales.getString(locale, "settings.name");
baseGui(title, 4)
.setItem(10, toggleItem(locale, "particles", pp.particles), e -> {
pp.particles = !pp.particles;
pp.updateGeneratorSettings(pp.session.generator);
open(pp);
})
.setItem(11, toggleItem(locale, "sound", pp.sound), e -> {
pp.sound = !pp.sound;
pp.updateGeneratorSettings(pp.session.generator);
open(pp);
})
.setItem(12, toggleItem(locale, "special_blocks", pp.useSpecialBlocks), e -> {
if (pp.session.generator.score == 0) {
pp.useSpecialBlocks = !pp.useSpecialBlocks;
pp.updateGeneratorSettings(pp.session.generator);
}
open(pp);
})
.setItem(13, toggleItem(locale, "fall_message", pp.showFallMessage), e -> {
pp.showFallMessage = !pp.showFallMessage;
pp.updateGeneratorSettings(pp.session.generator);
open(pp);
})
.setItem(14, toggleItem(locale, "scoreboard", pp.showScoreboard), e -> {
pp.showScoreboard = !pp.showScoreboard;
pp.updateGeneratorSettings(pp.session.generator);
open(pp);
})
.setItem(19, localeItem(player, "settings.parkour_settings.styles.item"),
e -> openStyleMenu(pp))
.setItem(20, schemDiffItem(pp), e -> {
if (pp.session.generator.score == 0) {
List<Double> diffs = List.of(0.0, 0.25, 0.5, 0.75, 1.0);
int idx = diffs.indexOf(pp.schematicDifficulty);
pp.schematicDifficulty = diffs.get((idx + 1) % diffs.size());
pp.updateGeneratorSettings(pp.session.generator);
}
open(pp);
})
.setItem(31, closeButton(player), e -> Menus.SETTINGS.open(player))
.open(player);
}
*/
/**
* Opens the style menu
*
* @param player The ParkourPlayer instance
*/
// TODO: Migrate to LoLib GUI system
/*
public void openStyleMenu(ParkourPlayer player) {
// init menu
PagedMenu menu = new PagedMenu(3, Locales.getString(player.locale, ParkourOption.STYLES.path + ".name"));
private ItemStack toggleItem(String locale, String key, boolean value) {
Material mat = value ? Material.LIME_STAINED_GLASS_PANE : Material.RED_STAINED_GLASS_PANE;
String symbol = Locales.getString(locale, "settings.parkour_settings." + (value ? "enabled" : "disabled"));
String label = Locales.getString(locale, "settings.parkour_settings." + key + ".item");
ItemStack item = new ItemStack(mat);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(ColorUtil.color(label + " §7[" + symbol + "§7]"));
item.setItemMeta(meta);
}
return item;
}
List<MenuItem> items = new ArrayList<>();
private ItemStack schemDiffItem(ParkourPlayer pp) {
List<Double> diffs = List.of(0.0, 0.25, 0.5, 0.75, 1.0);
List<String> values = Locales.getStringList(pp.locale, "settings.parkour_settings.schematics.values");
int idx = Math.max(0, diffs.indexOf(pp.schematicDifficulty));
String label = idx < values.size() ? values.get(idx) : String.valueOf(pp.schematicDifficulty);
Material[] mats = {Material.RED_STAINED_GLASS_PANE, Material.LIME_STAINED_GLASS_PANE,
Material.YELLOW_STAINED_GLASS_PANE, Material.ORANGE_STAINED_GLASS_PANE, Material.SKELETON_SKULL};
ItemStack item = new ItemStack(idx < mats.length ? mats[idx] : Material.PAPER);
ItemMeta meta = item.getItemMeta();
if (meta != null) {
meta.setDisplayName(ColorUtil.color("§eSchematics §7» §f" + label));
item.setItemMeta(meta);
}
return item;
}
private void openStyleMenu(ParkourPlayer pp) {
Player player = pp.player;
String locale = pp.locale;
String title = Locales.getString(locale, "settings.parkour_settings.styles.name");
List<Style> styles = new ArrayList<>();
InventoryGUI gui = baseGui(title, 3);
int slot = 10;
for (Style style : Registry.getStyles()) {
var name = style.getName();
if (slot > 16) break;
String name = style.getName();
String perm = ParkourOption.STYLES.permission + "." + name.toLowerCase().replace(" ", ".");
if (Config.CONFIG.getBoolean("permissions.per-style") && !player.hasPermission(perm)) continue;
var perm = "%s.%s".formatted(ParkourOption.STYLES.permission, name.toLowerCase());
if (Config.CONFIG.getBoolean("permissions.per-style") && !player.player.hasPermission(perm.replace(" ", "."))) {
continue;
}
ItemStack item = new ItemStack(style.getNext());
ItemMeta meta = item.getItemMeta();
if (meta != null) { meta.setDisplayName(ColorUtil.color("&#A020F0" + name)); item.setItemMeta(meta); }
var category = style instanceof RandomStyle ? "random" : "incremental";
items.add(Locales.getItem(player.player, ParkourOption.STYLES.path + ".style_item", name, category)
.material(style.getNext())
.click(event -> {
player.style = name;
player.updateGeneratorSettings(player.session.generator);
open(player);
}));
final Style s = style;
gui = gui.setItem(slot++, item, e -> {
pp.style = s.getName();
pp.updateGeneratorSettings(pp.session.generator);
open(pp);
});
}
menu.displayRows(0, 1)
.addToDisplay(items)
.nextPage(26, new Item(Material.LIME_DYE, "<#0DCB07><bold>»").click(event -> menu.page(1)))
.prevPage(18, new Item(Material.RED_DYE, "<#DE1F1F><bold>«").click(event -> menu.page(-1)))
.item(22, Locales.getItem(player.locale, "other.close").click(event -> open(player)))
.open(player.player);
}
*/
// TODO: Migrate to LoLib GUI system
/*
private boolean handleSettingChange(ParkourPlayer player, Runnable onAllowed) {
onAllowed.run();
player.updateGeneratorSettings(player.session.generator);
return true;
}
*/
// TODO: Migrate to LoLib GUI system
/*
private boolean handleScoreSettingChange(ParkourPlayer player, MenuClickEvent event, Runnable onAllowed) {
if (player.session.generator.score == 0) {
return handleSettingChange(player, onAllowed);
}
event.menu().item(event.slot(), new TimedItem(Locales.getItem(player.locale, "settings.parkour_settings.items.no_change").click((event1) -> {}), event, LoParkour.getPlugin()).stay(5 * 20));
event.menu().updateItem(event.slot());
return false;
}
*/
// replaces true/false with a checkmark and cross
private String getBooleanSymbol(ParkourUser user, boolean value) {
return value ? Locales.getString(user.player, "settings.parkour_settings.enabled") : Locales.getString(user.player, "settings.parkour_settings.disabled");
}
// check if option is allowed to be displayed
private boolean checkOptions(Player player, ParkourOption option, ParkourOption[] disabled) {
return !Arrays.asList(disabled).contains(option) && option.mayPerform(player);
gui.open(player);
}
}

View file

@ -1,79 +1,40 @@
package dev.loki.loparkour.menu.settings;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.menu.DynamicMenu;
import dev.loki.loparkour.menu.LPMenu;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.session.Session;
import dev.lolib.gui.InventoryGUI;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.List;
public class SettingsMenu extends LPMenu {
/**
* The menu for settings
*/
public class SettingsMenu extends DynamicMenu {
@Override
public void open(@NotNull Player player) {
String locale = locale(player);
String title = Locales.getString(locale, "settings.name");
// TODO: Migrate to LoLib GUI system
/*
public SettingsMenu() {
registerMainItem(1, 0, (player, user) -> Locales.getItem(player, "settings.parkour_settings.item").click(event -> {
ParkourPlayer pp = ParkourPlayer.getPlayer(event.getPlayer());
InventoryGUI gui = baseGui(title, 3);
if (pp != null) {
pp.session.generator.menu(pp);
}
}), player -> ParkourOption.PARKOUR_SETTINGS.mayPerform(player) && ParkourPlayer.isPlayer(player));
registerMainItem(1, 1, (player, user) -> Locales.getItem(player, "settings.lang.item", user != null ? Locales.getString(user.locale, "name") : "?").click(event -> Menus.LANG.open(ParkourPlayer.getPlayer(event.getPlayer()))), player -> ParkourOption.LANG.mayPerform(player) && ParkourUser.isUser(player));
registerMainItem(1, 2, (player, user) -> {
// user has to be not-null to see this item
assert user != null;
List<String> values = Locales.getStringList(user.locale, "settings.chat.values");
return new SliderItem().initial(switch (user.chatType) {
case LOBBY_ONLY -> 0;
case PLAYERS_ONLY -> 1;
case PUBLIC -> 2;
}).add(0, Locales.getItem(player, "settings.chat").modifyLore(lore -> lore.replace("%s", values.get(0))), event -> { // lobby only
ParkourUser u = ParkourUser.getUser(event.getPlayer());
if (u != null) {
u.chatType = Session.ChatType.LOBBY_ONLY;
}
return true;
}).add(1, Locales.getItem(player, "settings.chat").modifyLore(lore -> lore.replace("%s", values.get(1))), event -> { // players only
ParkourUser u = ParkourUser.getUser(event.getPlayer());
if (u != null) {
u.chatType = Session.ChatType.PLAYERS_ONLY;
}
return true;
}).add(2, Locales.getItem(player, "settings.chat").modifyLore(lore -> lore.replace("%s", values.get(2))), event -> { // public
ParkourUser u = ParkourUser.getUser(event.getPlayer());
if (u != null) {
u.chatType = Session.ChatType.PUBLIC;
}
return true;
if (ParkourOption.PARKOUR_SETTINGS.mayPerform(player) && ParkourPlayer.isPlayer(player)) {
gui = gui.setItem(11, localeItem(player, "settings.parkour_settings.item"), e -> {
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp != null) pp.session.generator.menu(pp);
});
}, player -> ParkourOption.CHAT.mayPerform(player) && ParkourUser.isUser(player));
}
registerMainItem(2, 0, (player, user) -> Locales.getItem(player, "other.close").click(event -> event.getPlayer().closeInventory()), player -> true);
}
*/
if (ParkourOption.LANG.mayPerform(player) && ParkourUser.isUser(player)) {
String langName = Locales.getString(locale, "name");
gui = gui.setItem(13, localeItem(locale, "settings.lang.item", langName), e -> {
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp != null) Menus.LANG.open(pp);
});
}
// TODO: Migrate to LoLib GUI system
/*
public void open(Player player) {
display(player, new Menu(3, Locales.getString(player, "settings.name")).distributeRowsEvenly());
gui.setItem(22, closeButton(player), e -> player.closeInventory())
.open(player);
}
*/
}

View file

@ -37,9 +37,7 @@ public class SpectatorMode implements Mode {
@Override
public void create(Player player) {
// TODO: Migrate to LoLib GUI
// Menus.SPECTATOR.open(player);
player.sendMessage("§cМеню временно недоступно во время миграции");
Menus.SPECTATOR.open(player);
}
public void create(Player player, Session session) {

View file

@ -114,7 +114,7 @@ public class ParkourPlayer extends ParkourUser {
*/
public static @Nullable ParkourPlayer getPlayer(@NotNull Player player) {
return getPlayers().stream()
.filter(other -> other.getUUID() == player.getUniqueId())
.filter(other -> other.getUUID().equals(player.getUniqueId()))
.findAny()
.orElse(null);
}
@ -130,8 +130,6 @@ public class ParkourPlayer extends ParkourUser {
@Override
public void unregister() {
LoParkour.log("Unregistering player %s".formatted(player.getName()));
if (session.generator != null &&
session.generator.getMode() != null &&
session.generator.getMode() instanceof MultiMode mode) {
@ -199,19 +197,14 @@ public class ParkourPlayer extends ParkourUser {
}
public void setup(Location to) {
LoParkour.log("Setting up player %s".formatted(player.getName()));
if (to != null) {
teleport(to);
}
player.setGameMode(GameMode.ADVENTURE);
// -= Inventory =-
if (Config.CONFIG.getBoolean("options.inventory-handling")) {
Scheduler.get(LoParkour.getPlugin()).runLater(() -> {
LoParkour.log("Setting up inventory for player %s".formatted(player.getName()));
player.getInventory().clear();
List<dev.loki.loparkour.util.Item> items = new ArrayList<>();

View file

@ -19,9 +19,6 @@ import dev.loki.loparkour.player.data.PreviousData;
import dev.loki.loparkour.session.Session;
import dev.loki.loparkour.storage.Storage;
import dev.loki.loparkour.world.Divider;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.Objective;
import org.bukkit.scoreboard.DisplaySlot;
import dev.loki.loparkour.util.ColorUtil;
import io.papermc.lib.PaperLib;
import me.clip.placeholderapi.PlaceholderAPI;
@ -29,6 +26,10 @@ import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.ChannelNotRegisteredException;
import org.bukkit.scoreboard.DisplaySlot;
import org.bukkit.scoreboard.Objective;
import org.bukkit.scoreboard.Scoreboard;
import org.bukkit.scoreboard.Team;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -46,28 +47,17 @@ import java.util.stream.Collectors;
*/
public abstract class ParkourUser {
/**
* Registers a player. This registers the player internally.
* This automatically unregisters the player if it is already registered.
*
* @param player The player
* @return the ParkourPlayer instance of the newly joined player
*/
public static @NotNull ParkourPlayer register(@NotNull Player player, @NotNull Session session) {
PreviousData data = null;
ParkourUser existing = getUser(player);
if (existing != null) {
LoParkour.log("Registering player %s with existing data".formatted(player.getName()));
data = existing.previousData;
unregister(existing, false, false, false);
} else {
LoParkour.log("Registering player %s".formatted(player.getName()));
}
ParkourPlayer pp = new ParkourPlayer(player, session, data);
// stats
joinCount++;
new ParkourJoinEvent(pp).call();
@ -75,48 +65,28 @@ public abstract class ParkourUser {
return pp;
}
/**
* This is the same as {@link #leave(ParkourUser)}, but instead for a Bukkit player instance.
*
* @param player The Bukkit player instance that will be removed from the game if the player is active.
* @see #leave(ParkourUser)
*/
public static void leave(@NotNull Player player) {
ParkourUser user = getUser(player);
if (user == null) {
return;
}
if (user == null) return;
leave(user);
}
/**
* Forces user to leave. Follows behaviour of /parkour leave.
*
* @param user The user.
*/
public static void leave(@NotNull ParkourUser user) {
unregister(user, true, true, false);
}
/**
* Unregisters a Parkour user instance.
*
* @param user The user to unregister.
* @param restorePreviousData Whether to restore the data from before the player joined the parkour.
* @param kickIfBungee Whether to kick the player if Bungeecord mode is enabled.
*/
public static void unregister(@NotNull ParkourUser user, boolean restorePreviousData, boolean kickIfBungee, boolean urgent) {
new ParkourLeaveEvent(user).call();
LoParkour.log("Unregistering player %s, restorePreviousData = %s, kickIfBungee = %s".formatted(user.getName(), restorePreviousData, kickIfBungee));
try {
user.unregister();
// TODO: Implement scoreboard deletion with LoLib
// if (user.board != null) {
// user.board.delete();
// }
} catch (Exception ex) { // safeguard to prevent people from losing data
// Reset scoreboard to main
if (user.board != null) {
user.player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
user.board = null;
}
} catch (Exception ex) {
LoParkour.getPlugin().getLogger().log(java.util.logging.Level.SEVERE,
"Error while trying to make player " + user.getName() + " leave", ex);
user.send("<red><bold>There was an error while trying to handle leaving.");
@ -143,7 +113,6 @@ public abstract class ParkourUser {
}
}
// Sends a player to a BungeeCord server. server is the server name.
private static void sendPlayerToServer(Player player, String server) {
ByteArrayDataOutput out = ByteStreams.newDataOutput();
out.writeUTF("Connect");
@ -152,79 +121,38 @@ public abstract class ParkourUser {
try {
player.sendPluginMessage(LoParkour.getPlugin(), "BungeeCord", out.toByteArray());
} catch (ChannelNotRegisteredException ex) {
LoParkour.getPlugin().getLogger().severe("Error while trying to send %s to server %s. This server is not registered.".formatted(player.getName(), server) + " - " + ex.getMessage());
LoParkour.getPlugin().getLogger().severe(
"Error while trying to send %s to server %s. - %s".formatted(player.getName(), server, ex.getMessage()));
player.kickPlayer("Couldn't move you to %s. Please rejoin.".formatted(server));
}
}
/**
* @param player The player.
* @return True when this player is a {@link ParkourUser}, false if not.
*/
public static boolean isUser(@Nullable Player player) {
return player != null && getUsers().stream().anyMatch(other -> other.player == player);
}
/**
* @param player The player.
* @return player as a {@link ParkourUser}, null if not found.
*/
public static @Nullable ParkourUser getUser(@NotNull Player player) {
return getUsers().stream()
.filter(other -> other.getUUID() == player.getUniqueId())
.filter(other -> other.getUUID().equals(player.getUniqueId()))
.findAny()
.orElse(null);
}
/**
* @return Set with all users.
*/
public static Set<ParkourUser> getUsers() {
return Divider.sections.keySet().stream()
.flatMap(session -> session.getUsers().stream())
.collect(Collectors.toSet());
}
/**
* This user's locale
*/
@NotNull
public String locale = Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
// ─── Fields ───────────────────────────────────────────────────────────────
/**
* This user's scoreboard
*/
public Scoreboard board;
/**
* This user's PreviousData
*/
@NotNull
public PreviousData previousData;
/**
* The selected {@link Session.ChatType}
*/
@NotNull public String locale = Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
@Nullable public Scoreboard board;
@NotNull public PreviousData previousData;
public Session.ChatType chatType = Session.ChatType.PUBLIC;
/**
* The {@link Session} this user is in.
*/
public final Session session;
/**
* The Bukkit player instance associated with this user.
*/
public final Player player;
/**
* The {@link Instant} when the player joined.
*/
public final Instant joined;
/**
* The amount of players that have joined while the plugin has been enabled.
*/
public static int joinCount;
public ParkourUser(@NotNull Player player, @NotNull Session session, @Nullable PreviousData previousData) {
@ -235,62 +163,72 @@ public abstract class ParkourUser {
if (Boolean.parseBoolean(Option.OPTIONS_DEFAULTS.get(ParkourOption.SCOREBOARD))) {
this.board = Bukkit.getScoreboardManager().getNewScoreboard();
// Create the sidebar objective immediately and assign to player
Objective obj = this.board.registerNewObjective("lp_sidebar", "dummy",
ColorUtil.color(Locales.getString(locale, "scoreboard.title")));
obj.setDisplaySlot(DisplaySlot.SIDEBAR);
player.setScoreboard(this.board);
}
}
/**
* Unregisters this user.
*/
protected abstract void unregister();
/**
* Teleports the player asynchronously.
*
* @param to Where the player will be teleported to
*/
public void teleport(@NotNull Location to) {
PaperLib.teleportAsync(player, to);
player.teleport(to);
}
/**
* Sends a message.
*
* @param message The message
*/
public void send(String message) {
player.sendMessage(ColorUtil.color(message));
}
/**
* Sends a translated message
*
* @param key The translation key
* @param format Any objects that may be given to the formatting of the string.
*/
public void sendTranslated(String key, Object... format) {
send(Locales.getString(locale, key).formatted(format));
}
/**
* Updates the scoreboard for the specified generator.
*
* @param generator The generator.
* Updates the sidebar scoreboard for the given generator state.
* Uses vanilla Bukkit scoreboard API — Team prefix trick for coloured lines.
*/
public void updateScoreboard(ParkourGenerator generator) {
// board can be null a few ticks after on player leave
if (board == null || false || !generator.profile.get("showScoreboard").asBoolean()) {
if (board == null || !generator.profile.get("showScoreboard").asBoolean()) {
return;
}
Leaderboard leaderboard = generator.getMode().getLeaderboard();
Score top = leaderboard == null ? new Score("?", "?", "?", 0) : leaderboard.getScoreAtRank(1);
Score high = leaderboard == null ? new Score("?", "?", "?", 0) : leaderboard.get(getUUID());
if (top == null) {
top = new Score("?", "?", "?", 0);
if (top == null) top = new Score("?", "?", "?", 0);
// Update title
Objective obj = board.getObjective("lp_sidebar");
if (obj == null) {
obj = board.registerNewObjective("lp_sidebar", "dummy",
ColorUtil.color(replace(Locales.getString(locale, "scoreboard.title"), top, high, generator)));
obj.setDisplaySlot(DisplaySlot.SIDEBAR);
} else {
obj.setDisplayName(ColorUtil.color(
replace(Locales.getString(locale, "scoreboard.title"), top, high, generator)));
}
// board.updateTitle(replace(Locales.getString(locale, "scoreboard.title"), top, high, generator));
// board.updateLines(replace(Locales.getStringList(locale, "scoreboard.lines"), top, high, generator));
// Update lines using Team prefix trick (each line = unique entry).
// We need globally-unique entries: use a combination of colour codes that gives
// up to 32 unique strings (§0§0, §0§1, … §1§0 …). This safely covers any
// reasonable scoreboard length.
List<String> lines = replace(Locales.getStringList(locale, "scoreboard.lines"), top, high, generator);
String[] codes = {"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"};
for (int i = 0; i < lines.size(); i++) {
String entry = "§" + codes[i / codes.length % codes.length] + "§" + codes[i % codes.length];
String teamName = "lp_line_" + i;
Team team = board.getTeam(teamName);
if (team == null) {
team = board.registerNewTeam(teamName);
team.addEntry(entry);
}
team.setPrefix(ColorUtil.color(lines.get(i)));
obj.getScore(entry).setScore(lines.size() - i);
}
}
private List<String> replace(List<String> s, Score top, Score high, ParkourGenerator generator) {
@ -302,45 +240,21 @@ public abstract class ParkourUser {
.replace("%score%", Integer.toString(generator.score))
.replace("%time%", generator.getFormattedTime())
.replace("%difficulty%", Double.toString(generator.getDifficultyScore()))
.replace("%top_score%", Integer.toString(top.score()))
.replace("%top_player%", top.name())
.replace("%top_time%", top.time())
.replace("%high_score%", Integer.toString(high.score()))
.replace("%high_score_time%", high.time()));
}
// translate papi
private String translate(Player player, String string) {
return LoParkour.getPlaceholderHook() == null ? string : PlaceholderAPI.setPlaceholders(player, string);
}
/**
* @return The player's uuid
*/
public UUID getUUID() {
return player.getUniqueId();
}
public UUID getUUID() { return player.getUniqueId(); }
public Location getLocation() { return player.getLocation(); }
public String getName() { return player.getName(); }
/**
* @return The player's location
*/
public Location getLocation() {
return player.getLocation();
}
/**
* @return The player's name
*/
public String getName() {
return player.getName();
}
/**
* @param player The player
* @return true if the player is a Bedrock player, false if not.
*/
public static boolean isBedrockPlayer(Player player) {
return Bukkit.getPluginManager().isPluginEnabled("floodgate") && FloodgateHook.isBedrockPlayer(player);
}

View file

@ -31,30 +31,14 @@ public class Schematics {
FOLDER.mkdirs();
}
File[] files = FOLDER.listFiles((dir, name) -> name.contains("parkour-") || name.contains("spawn-island"));
File[] files = FOLDER.listFiles((dir, name) -> name.endsWith(".lpschem"));
if (files == null || files.length == 0) {
download();
init();
LoParkour.log("No schematics found in " + FOLDER.getAbsolutePath());
return;
}
try {
// TODO: Migrate to LoLib schematics
// dev.efnilite.vilib.schematic.Schematics.addFromFiles(LoParkour.getPlugin(), files);
} catch (Exception ex) {
LoParkour.getPlugin().getLogger().log(java.util.logging.Level.SEVERE,
"Error while trying to load schematics", ex);
}
LoParkour.log("Found " + files.length + " schematic files");
});
}
private static void download() {
LoParkour.log("Downloading schematics");
List<String> schematics = new ArrayList<>();
schematics.addAll(Arrays.asList(SPAWN_SCHEMATICS));
schematics.addAll(Config.SCHEMATICS.getChildren("difficulty", false).stream().map("parkour-%s"::formatted).toList());
schematics.forEach(file -> LoParkour.getPlugin().saveResource("schematics/%s".formatted(file), true));
}
}

View file

@ -168,6 +168,16 @@ public class LPSchematic {
this.logic = logic;
}
/** Raw block palette-index array. Do not modify directly. */
public int[] getBlockArray() {
return blocks;
}
/** The palette of block data strings. Do not modify directly. */
public List<String> getPalette() {
return palette;
}
private static class SchematicData {
int format_version;
SchematicMetadata metadata;

View file

@ -1,89 +0,0 @@
package dev.loki.loparkour.schematic.lpschem;
import dev.loki.loparkour.LoParkour;
import org.bukkit.Material;
import org.bukkit.block.data.BlockData;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.Map;
public class SchematicConverter {
public static LPSchematic fromVilib(@NotNull LPSchematic oldSchematic, @NotNull String name, @NotNull String author) {
var dimensions = oldSchematic.getDimensions();
// TODO: Fix LPSchematic API
int width = 10; // dimensions.getWidth();
int height = 10; // dimensions.getHeight();
int length = 10; // dimensions.getLength();
LPSchematicBuilder builder = new LPSchematicBuilder(name, author, 0.5, width, height, length);
// TODO: Fix LPSchematic API - vilib code commented out
// var blocks = oldSchematic.getBlocks();
// for (Map.Entry<Location, BlockData> entry : blocks.entrySet()) {
// var pos = entry.getKey();
// BlockData data = entry.getValue();
//
// if (data != null && data.getMaterial() != Material.AIR) {
// int x = (int) pos.getX();
// int y = (int) pos.getY();
// int z = (int) pos.getZ();
//
// builder.setBlock(x, y, z, data);
//
// if (data.getMaterial() == Material.LIME_WOOL) {
// builder.setStart(x, y, z);
// } else if (data.getMaterial() == Material.RED_WOOL) {
// builder.setEnd(x, y, z);
// }
// }
// }
if (!hasMarkers(builder)) {
builder.setStart(0, 0, 0);
builder.setEnd(width - 1, 0, length - 1);
}
return builder.build();
}
private static boolean hasMarkers(LPSchematicBuilder builder) {
try {
builder.build();
return true;
} catch (IllegalStateException e) {
return false;
}
}
public static void convertAll() {
LoParkour.log("Starting conversion of old schematics to .lpschem format");
int converted = 0;
int failed = 0;
// TODO: Fix vilib schematic conversion - vilib code commented out
// try {
// for (String name : dev.efnilite.vilib.schematic.dev.loki.loparkour.schematic.Schematics.getSchematicNames(LoParkour.getPlugin())) {
// try {
// LPSchematic oldSchematic = dev.efnilite.vilib.schematic.dev.loki.loparkour.schematic.Schematics.getSchematic(LoParkour.getPlugin(), name);
// if (oldSchematic != null) {
// LPSchematic newSchematic = fromVilib(oldSchematic, name, "Converted");
// LoParkour.getSchematicManager().saveSchematic(newSchematic);
// converted++;
// LoParkour.log("Converted: " + name);
// }
// } catch (Exception e) {
// failed++;
// LoParkour.getPlugin().getLogger().severe("Failed to convert schematic: " + name);
// e.printStackTrace();
// }
// }
// } catch (Exception e) {
// LoParkour.getPlugin().getLogger().severe("Error during schematic conversion");
// e.printStackTrace();
// }
LoParkour.log("Conversion complete: " + converted + " converted, " + failed + " failed");
}
}

View file

@ -0,0 +1,145 @@
package dev.loki.loparkour.schematic.lpschem;
import org.bukkit.Bukkit;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Directional;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* Rotates a {@link LPSchematic} by 90/180/270 degrees around the Y axis.
* Preserves block facing where possible via {@link Directional} BlockData.
*/
public class SchematicTransformer {
private final LPSchematic source;
public SchematicTransformer(@NotNull LPSchematic source) {
this.source = source;
}
/**
* Returns a new rotated schematic. The original is not modified.
*
* @param degrees 90, 180, or 270 (clockwise from above).
*/
@NotNull
public LPSchematic rotate(int degrees) {
degrees = ((degrees % 360) + 360) % 360;
if (degrees == 0) return source;
SchematicDimensions dim = source.getDimensions();
int origW = dim.width;
int origH = dim.height;
int origL = dim.length;
// New dimensions after rotation
int newW = (degrees == 180) ? origW : origL;
int newL = (degrees == 180) ? origL : origW;
int[] origBlocks = getBlocks();
List<String> palette = getPalette();
int[] rotated = new int[origW * origH * origL];
for (int y = 0; y < origH; y++) {
for (int z = 0; z < origL; z++) {
for (int x = 0; x < origW; x++) {
int origIdx = x + (z * origW) + (y * origW * origL);
int paletteIdx = origBlocks[origIdx];
int nx, nz;
switch (degrees) {
case 90 -> { nx = origL - 1 - z; nz = x; }
case 180 -> { nx = origW - 1 - x; nz = origL - 1 - z; }
case 270 -> { nx = z; nz = origW - 1 - x; }
default -> { nx = x; nz = z; }
}
int newIdx = nx + (nz * newW) + (y * newW * newL);
rotated[newIdx] = paletteIdx;
}
}
}
// Rotate palette entries (block facing)
List<String> rotatedPalette = rotatePalette(palette, degrees);
SchematicDimensions newDim = new SchematicDimensions(newW, origH, newL);
// Rotate markers
SchematicMarkers origMarkers = source.getMarkers();
SchematicMarkers newMarkers = rotateMarkers(origMarkers, origW, origL, newW, newL, degrees);
return new LPSchematic(source.getMetadata(), newDim, rotatedPalette, rotated, newMarkers);
}
// ── Internals ────────────────────────────────────────────────────────────
private int[] getBlocks() {
return source.getBlockArray();
}
private List<String> getPalette() {
return new ArrayList<>(source.getPalette());
}
private List<String> rotatePalette(List<String> palette, int degrees) {
List<String> result = new ArrayList<>(palette.size());
for (String blockStr : palette) {
result.add(rotateBlockData(blockStr, degrees));
}
return result;
}
private String rotateBlockData(String blockDataStr, int degrees) {
try {
BlockData bd = Bukkit.createBlockData(blockDataStr);
if (bd instanceof Directional dir) {
for (int i = 0; i < degrees / 90; i++) {
dir.setFacing(rotateDirection(dir.getFacing()));
}
return dir.getAsString();
}
return blockDataStr;
} catch (Exception e) {
return blockDataStr;
}
}
private org.bukkit.block.BlockFace rotateDirection(org.bukkit.block.BlockFace face) {
return switch (face) {
case NORTH -> org.bukkit.block.BlockFace.EAST;
case EAST -> org.bukkit.block.BlockFace.SOUTH;
case SOUTH -> org.bukkit.block.BlockFace.WEST;
case WEST -> org.bukkit.block.BlockFace.NORTH;
default -> face;
};
}
private SchematicMarkers rotateMarkers(SchematicMarkers m,
int origW, int origL,
int newW, int newL,
int degrees) {
var start = rotateVec(m.getStart(), origW, origL, degrees);
var end = rotateVec(m.getEnd(), origW, origL, degrees);
SchematicMarkers result = new SchematicMarkers(start, end);
for (var cp : m.getCheckpoints()) {
result.addCheckpoint(rotateVec(cp, origW, origL, degrees));
}
return result;
}
private SchematicMarkers.Vector3i rotateVec(SchematicMarkers.Vector3i v,
int origW, int origL, int degrees) {
return switch (degrees) {
case 90 -> new SchematicMarkers.Vector3i(origL - 1 - v.z, v.y, v.x);
case 180 -> new SchematicMarkers.Vector3i(origW - 1 - v.x, v.y, origL - 1 - v.z);
case 270 -> new SchematicMarkers.Vector3i(v.z, v.y, origW - 1 - v.x);
default -> new SchematicMarkers.Vector3i(v.x, v.y, v.z);
};
}
}

View file

@ -74,12 +74,6 @@ public class Session {
Function<Session, Boolean> isAcceptingPlayers,
Function<Session, Boolean> isAcceptingSpectators,
Player... players) {
LoParkour.log("Creating session");
if (players != null) {
LoParkour.log("Players in session: %s".formatted(Arrays.stream(players).map(Player::getName).toList()));
}
Session session = new Session();
if (isAcceptingPlayers != null) session.isAcceptingPlayers = isAcceptingPlayers;
@ -128,8 +122,6 @@ public class Session {
*/
public void addPlayers(ParkourPlayer... toAdd) {
for (ParkourPlayer player : toAdd) {
LoParkour.log("Adding player %s to session".formatted(player.getName()));
for (ParkourPlayer to : getPlayers()) {
to.send(Locales.getString(player.locale, "lobby.other_join").formatted(player.getName()));
}
@ -145,8 +137,6 @@ public class Session {
*/
public void removePlayers(ParkourPlayer... toRemove) {
for (ParkourPlayer player : toRemove) {
LoParkour.log("Removing player %s from session".formatted(player.getName()));
users.remove(player.getUUID());
}
@ -180,8 +170,6 @@ public class Session {
*/
public void addSpectators(ParkourSpectator... spectators) {
for (ParkourSpectator spectator : spectators) {
LoParkour.log("Adding spectator %s to session".formatted(spectator.getName()));
for (ParkourPlayer player : getPlayers()) {
player.sendTranslated("play.spectator.other_join", spectator.getName());
}
@ -197,8 +185,6 @@ public class Session {
*/
public void removeSpectators(ParkourSpectator... spectators) {
for (ParkourSpectator spectator : spectators) {
LoParkour.log("Removing spectator %s from session".formatted(spectator.getName()));
for (ParkourPlayer player : getPlayers()) {
player.sendTranslated("play.spectator.other_leave", spectator.getName());
}

View file

@ -22,6 +22,18 @@ public class Storage {
}
}
/**
* If SQL is enabled, defers the callback until the connection is ready.
* If SQL is disabled (disk), calls the callback immediately.
*/
public static void runWhenReady(Runnable callback) {
if (Option.SQL) {
StorageSQL.runWhenConnected(callback);
} else {
callback.run();
}
}
public static void close() {
if (Option.SQL) {
StorageSQL.close();

View file

@ -81,6 +81,12 @@ class StorageDisk {
try (FileReader reader = new FileReader(getPlayerFile(player))) {
ParkourPlayer from = LoParkour.getGson().fromJson(reader, ParkourPlayer.class);
if (from == null) {
LoParkour.getPlugin().getLogger().warning("Player data file for %s is empty or corrupted, using defaults".formatted(player.getName()));
player.setSettings(new HashMap<>());
return;
}
Map<String, Object> settings = new HashMap<>();
settings.put("style", from.style);

View file

@ -8,14 +8,11 @@ import dev.loki.loparkour.player.ParkourPlayer;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import dev.lolib.utils.Colls;
import java.sql.*;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* MySQL storage manager.
@ -25,13 +22,35 @@ import java.util.UUID;
class StorageSQL {
private static Connection connection;
private static boolean initialized = false;
private static volatile boolean connected = false;
static {
connect();
// Tables queued to create after connection is established
private static final List<String> pendingTableCreations = new CopyOnWriteArrayList<>();
// Callbacks to run after connection is ready (e.g. initial leaderboard reads)
private static final List<Runnable> onConnectCallbacks = new CopyOnWriteArrayList<>();
/**
* Returns true if the SQL connection is ready.
*/
public static boolean isConnected() {
return connected;
}
/**
* Runs the given callback immediately if already connected, or defers it until connect() succeeds.
*/
public static void runWhenConnected(Runnable callback) {
if (connected) {
callback.run();
} else {
onConnectCallbacks.add(callback);
}
}
public static void init(String mode) {
sendUpdate("""
String createTableSql = """
CREATE TABLE IF NOT EXISTS `%s`
(
uuid CHAR(36) NOT NULL PRIMARY KEY,
@ -41,98 +60,89 @@ class StorageSQL {
score INT
)
CHARSET = utf8 ENGINE = InnoDB;
"""
.formatted(getTableName(mode)));
""".formatted(getTableName(mode));
if (!initialized) {
initialized = true;
// Queue this table for creation after connect() finishes
pendingTableCreations.add(createTableSql);
// Connect async — table creation happens inside connect() after connection is ready
Bukkit.getScheduler().runTaskAsynchronously(LoParkour.getPlugin(), StorageSQL::connect);
} else {
// Already connected (or connecting) — send directly
pendingTableCreations.add(createTableSql);
}
}
public static void close() {
try {
connection.close();
LoParkour.log("Closed connection to MySQL");
if (connection != null && !connection.isClosed()) {
connection.close();
LoParkour.log("Closed connection to MySQL");
}
} catch (SQLException ex) {
LoParkour.getPlugin().getLogger().severe("Error while trying to close connection to SQL database" + " - " + ex.getMessage());
LoParkour.getPlugin().getLogger().severe("Error while trying to close connection to SQL database - " + ex.getMessage());
}
}
public static @NotNull Map<UUID, Score> readScores(@NotNull String mode) {
try (ResultSet results = sendQuery(
"""
SELECT * FROM `%s`;
"""
.formatted(getTableName(mode)))) {
if (results == null) {
return new HashMap<>();
String sql = "SELECT * FROM `%s`;".formatted(getTableName(mode));
try (PreparedStatement stmt = prepareStatement(sql)) {
if (stmt == null) return new HashMap<>();
try (ResultSet results = stmt.executeQuery()) {
Map<UUID, Score> scores = new HashMap<>();
while (results.next()) {
scores.put(UUID.fromString(results.getString("uuid")), new Score(
results.getString("name"),
results.getString("time"),
results.getString("difficulty"),
results.getInt("score")));
}
return scores;
}
Map<UUID, Score> scores = new HashMap<>();
while (results.next()) { // advance row
scores.put(UUID.fromString(results.getString("uuid")), new Score(
results.getString("name"),
results.getString("time"),
results.getString("difficulty"),
results.getInt("score")));
}
return scores;
} catch (SQLException ex) {
LoParkour.getPlugin().getLogger().severe("Error while trying to read SQL data of %s".formatted(mode) + " - " + ex.getMessage());
LoParkour.getPlugin().getLogger().severe("Error while trying to read SQL data of %s - %s".formatted(mode, ex.getMessage()));
return new HashMap<>();
}
}
public static void writeScores(@NotNull String mode, @NotNull Map<UUID, Score> scores) {
new HashMap<>(scores).forEach((uuid, score) -> sendUpdate(
"""
INSERT INTO `%s`
(uuid, name, time, difficulty, score)
VALUES ('%s', '%s', '%s', '%s', %d)
ON DUPLICATE KEY UPDATE name = '%s',
time = '%s',
difficulty = '%s',
score = %d;
"""
.formatted(getTableName(mode), uuid.toString(), score.name(), score.time(), score.difficulty(), score.score(),
score.name(), score.time(), score.difficulty(), score.score())));
new HashMap<>(scores).forEach((uuid, score) -> sendUpdate("""
INSERT INTO `%s`
(uuid, name, time, difficulty, score)
VALUES ('%s', '%s', '%s', '%s', %d)
ON DUPLICATE KEY UPDATE name = '%s',
time = '%s',
difficulty = '%s',
score = %d;
""".formatted(getTableName(mode), uuid, score.name(), score.time(), score.difficulty(), score.score(),
score.name(), score.time(), score.difficulty(), score.score())));
}
// returns leaderboard table name
private static String getTableName(String mode) {
return "%sleaderboard-%s".formatted(Option.SQL_PREFIX, mode);
}
public static void readPlayer(@NotNull ParkourPlayer player) {
try (ResultSet results = sendQuery(
"""
SELECT * FROM `%s` WHERE uuid = '%s';
"""
.formatted("%soptions".formatted(Option.SQL_PREFIX), player.getUUID()))) {
String sql = "SELECT * FROM `%soptions` WHERE uuid = '%s';".formatted(Option.SQL_PREFIX, player.getUUID());
try (PreparedStatement stmt = prepareStatement(sql)) {
if (stmt == null) { player.setSettings(new HashMap<>()); return; }
try (ResultSet results = stmt.executeQuery()) {
if (!results.next()) { player.setSettings(new HashMap<>()); return; }
if (results == null) {
player.setSettings(new HashMap<>());
return;
}
boolean hasNext = results.next(); // move cursor
if (!hasNext) {
player.setSettings(new HashMap<>());
return;
}
Map<String, Object> settings = new HashMap<>();
for (String key : ParkourPlayer.PLAYER_COLUMNS.keySet()) {
try {
settings.put(key, results.getObject(key));
} catch (SQLException ex) {
LoParkour.getPlugin().getLogger().severe("Error while trying to read SQL data of %s, option = %s - %s".formatted(player.getName(), key, ex.getMessage()));
Map<String, Object> settings = new HashMap<>();
for (String key : ParkourPlayer.PLAYER_COLUMNS.keySet()) {
try {
settings.put(key, results.getObject(key));
} catch (SQLException ex) {
LoParkour.getPlugin().getLogger().severe(
"Error reading SQL data of %s, key=%s - %s".formatted(player.getName(), key, ex.getMessage()));
}
}
player.setSettings(settings);
}
player.setSettings(settings);
} catch (SQLException ex) {
LoParkour.getPlugin().getLogger().severe("Error while trying to read SQL data of %s".formatted(player.getName()) + " - " + ex.getMessage());
LoParkour.getPlugin().getLogger().severe("Error reading SQL data of %s - %s".formatted(player.getName(), ex.getMessage()));
}
}
@ -141,7 +151,7 @@ class StorageSQL {
String schematicDifficulty = df.format(player.schematicDifficulty);
sendUpdate("""
INSERT INTO `%s`
INSERT INTO `%soptions`
(uuid, style, blockLead, useParticles, useSpecial, showFallMsg, showScoreboard,
selectedTime, collectedRewards, locale, schematicDifficulty, sound)
VALUES ('%s', '%s', %d, %b, %b, %b, %b, %d, '%s', '%s', %s, %b)
@ -156,103 +166,117 @@ class StorageSQL {
locale = '%s',
schematicDifficulty = %s,
sound = %b;
"""
.formatted("%soptions".formatted(Option.SQL_PREFIX), player.getUUID(), player.style, player.blockLead,
player.particles, player.useSpecialBlocks, player.showFallMessage,
player.showScoreboard, player.selectedTime, String.join(",", player.collectedRewards), player.locale,
schematicDifficulty, player.sound,
player.style, player.blockLead,
player.particles, player.useSpecialBlocks, player.showFallMessage,
player.showScoreboard, player.selectedTime, String.join(",", player.collectedRewards), player.locale,
schematicDifficulty, player.sound));
""".formatted(Option.SQL_PREFIX,
player.getUUID(), player.style, player.blockLead,
player.particles, player.useSpecialBlocks, player.showFallMessage,
player.showScoreboard, player.selectedTime, String.join(",", player.collectedRewards),
player.locale, schematicDifficulty, player.sound,
player.style, player.blockLead,
player.particles, player.useSpecialBlocks, player.showFallMessage,
player.showScoreboard, player.selectedTime, String.join(",", player.collectedRewards),
player.locale, schematicDifficulty, player.sound));
}
public static void connect() {
private static void connect() {
try {
LoParkour.log("Connecting to MySQL");
LoParkour.log("Connecting to MySQL...");
try { // load drivers
Class.forName("com.mysql.cj.jdbc.Driver"); // for newer versions
} catch (ClassNotFoundException old) {
Class.forName("com.mysql.jdbc.Driver"); // for older versions
try {
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
Class.forName("com.mysql.jdbc.Driver");
}
connection = DriverManager.getConnection(("jdbc:mysql://%s:%d/%s?allowPublicKeyRetrieval=true" +
"&useSSL=false&useUnicode=true&characterEncoding=utf-8" +
"&autoReconnect=true&maxReconnects=5").formatted(Option.SQL_URL, Option.SQL_PORT, Option.SQL_DB), Option.SQL_USERNAME, Option.SQL_PASSWORD);
connection = DriverManager.getConnection(
("jdbc:mysql://%s:%d/%s?allowPublicKeyRetrieval=true" +
"&useSSL=false&useUnicode=true&characterEncoding=utf-8" +
"&autoReconnect=true&maxReconnects=2&connectTimeout=5000&socketTimeout=5000")
.formatted(Option.SQL_URL, Option.SQL_PORT, Option.SQL_DB),
Option.SQL_USERNAME, Option.SQL_PASSWORD);
sendUpdate("CREATE DATABASE IF NOT EXISTS `%s`;".formatted(Option.SQL_DB));
sendUpdate("USE `%s`;".formatted(Option.SQL_DB));
sendUpdate("CREATE TABLE IF NOT EXISTS `%soptions` (`uuid` CHAR(36) NOT NULL, `time` VARCHAR(8), `style` VARCHAR(32), `blockLead` INT, `useParticles` BOOLEAN, `useDifficulty` BOOLEAN, `useSpecial` BOOLEAN, `showFallMsg` BOOLEAN, `showScoreboard` BOOLEAN, PRIMARY KEY (`uuid`)) ENGINE = InnoDB CHARSET = utf8;".formatted(Option.SQL_PREFIX));
// Base options table
sendUpdate(("CREATE TABLE IF NOT EXISTS `%soptions` " +
"(`uuid` CHAR(36) NOT NULL, `time` VARCHAR(8), `style` VARCHAR(32), `blockLead` INT, " +
"`useParticles` BOOLEAN, `useDifficulty` BOOLEAN, `useSpecial` BOOLEAN, " +
"`showFallMsg` BOOLEAN, `showScoreboard` BOOLEAN, PRIMARY KEY (`uuid`)) " +
"ENGINE = InnoDB CHARSET = utf8;").formatted(Option.SQL_PREFIX));
// v3.0.0
// Migrations
sendUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `time`;".formatted(Option.SQL_PREFIX));
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `selectedTime` INT NOT NULL;".formatted(Option.SQL_PREFIX));
// v3.1.0
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `collectedRewards` MEDIUMTEXT;".formatted(Option.SQL_PREFIX));
// v3.6.0
sendUpdateSuppressed("ALTER TABLE `%s` ADD `locale` VARCHAR(8);".formatted(Option.SQL_PREFIX + "options"));
sendUpdateSuppressed("ALTER TABLE `%s` ADD `schematicDifficulty` DOUBLE;".formatted(Option.SQL_PREFIX + "options"));
// v4.0.0
sendUpdateSuppressed("ALTER TABLE `%s` ADD `sound` BOOLEAN;".formatted(Option.SQL_PREFIX + "options"));
// 5.0.0
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `locale` VARCHAR(8);".formatted(Option.SQL_PREFIX));
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `schematicDifficulty` DOUBLE;".formatted(Option.SQL_PREFIX));
sendUpdateSuppressed("ALTER TABLE `%soptions` ADD `sound` BOOLEAN;".formatted(Option.SQL_PREFIX));
sendUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useDifficulty`;".formatted(Option.SQL_PREFIX));
sendUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useStructure`;".formatted(Option.SQL_PREFIX));
// Now flush all pending table creations (leaderboard tables queued before connection was ready)
for (String sql : pendingTableCreations) {
sendUpdate(sql);
}
pendingTableCreations.clear();
LoParkour.log("Connected to MySQL");
connected = true;
// Run deferred callbacks (e.g. initial leaderboard reads)
for (Runnable cb : onConnectCallbacks) {
try { cb.run(); } catch (Exception e) {
LoParkour.getPlugin().getLogger().severe("Error in SQL onConnect callback: " + e.getMessage());
}
}
onConnectCallbacks.clear();
} catch (Exception ex) {
LoParkour.getPlugin().getLogger().severe("Could not connect to MySQL - check your SQL settings in the config - " + ex.getMessage());
Bukkit.getPluginManager().disablePlugin(LoParkour.getPlugin()); // disable plugin since data handling without db will go horribly wrong
LoParkour.getPlugin().getLogger().severe("Could not connect to MySQL - check your SQL settings - " + ex.getMessage());
LoParkour.getPlugin().getLogger().severe("Disabling SQL storage, using local storage instead");
Option.SQL = false;
}
}
private static void validateConnection() {
try {
if (!connection.isValid(10)) {
if (connection == null || !connection.isValid(2)) {
LoParkour.getPlugin().getLogger().warning("MySQL connection lost, attempting reconnect...");
connect();
}
} catch (Exception ex) {
LoParkour.getPlugin().getLogger().severe("Error while trying to reconnect to MySQL" + " - " + ex.getMessage());
LoParkour.getPlugin().getLogger().severe("Error reconnecting to MySQL - " + ex.getMessage());
Option.SQL = false;
}
}
// send query and get result
private static ResultSet sendQuery(String sql) {
/** Returns a PreparedStatement with proper connection validation. Caller must close it. */
private static PreparedStatement prepareStatement(String sql) {
validateConnection();
if (connection == null) return null;
try {
return connection.prepareStatement(sql).executeQuery();
return connection.prepareStatement(sql);
} catch (SQLException ex) {
LoParkour.getPlugin().getLogger().severe("Error while sending query %s".formatted(sql) + " - " + ex.getMessage());
LoParkour.getPlugin().getLogger().severe("Error preparing statement: %s - %s".formatted(sql, ex.getMessage()));
return null;
}
}
// send update
private static void sendUpdate(String sql) {
validateConnection();
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.executeUpdate();
if (connection == null) return;
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.executeUpdate();
} catch (SQLException ex) {
LoParkour.getPlugin().getLogger().severe("Error while sending query %s".formatted(sql) + " - " + ex.getMessage());
LoParkour.getPlugin().getLogger().severe("Error while sending update: %s - %s".formatted(sql, ex.getMessage()));
}
}
// if query throws an error, ignore it
private static void sendUpdateSuppressed(String sql) {
validateConnection();
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.executeUpdate();
if (connection == null) return;
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.executeUpdate();
} catch (SQLException ignored) {
}
}
}

View file

@ -40,8 +40,6 @@ public class Divider {
var location = toLocation(session);
LoParkour.log("Added session at %s".formatted(Locations.toString(location, true)));
return location;
}
@ -51,8 +49,6 @@ public class Divider {
* @param session The session.
*/
public static void remove(Session session) {
LoParkour.log("Removed session at %s".formatted(Locations.toString(toLocation(session), true)));
sections.remove(session);
}

View file

@ -34,6 +34,7 @@ public class World {
if (world != null) {
LoParkour.getPlugin().getLogger().warning("Crash detected! The parkour world loading twice is not usual behaviour. This only happens after a server crash.");
World.world = world; // ensure static field is populated
}
if (Config.CONFIG.getBoolean("world.delete-on-reload")) {
@ -61,6 +62,10 @@ public class World {
}
private static void setup() {
if (world == null) {
LoParkour.getPlugin().getLogger().severe("Cannot setup world rules — world is null (createWorld failed).");
return;
}
LoParkour.log("Initializing world rules");
world.setGameRule(GameRule.DO_FIRE_TICK, false);