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

View file

@ -92,7 +92,7 @@ world:
# -= World name =-
# The world in which parkour will take place
name: 'witp'
name: 'loparkour'
# -= Delete world on reload =-
# Deletes the world on reload/restart of the server.

View file

@ -54,6 +54,13 @@ advanced:
# -= Spawn island options =-
# Options for the spawn island
island:
# -= Island schematic =-
# The name of the schematic file to use for the spawn island
# This schematic should be placed in the schematics-new folder
# If not specified or not found, no island will be generated
schematic-name: "island"
spawn:
# -= Player block =-

View file

@ -14,7 +14,7 @@ play:
item:
material: "sugar_cane"
name: "&#00D4FF<bold>Play"
name: "&#00D4FF&lPlay"
lore: ""
single:
@ -22,31 +22,46 @@ play:
item:
material: "ender_pearl"
name: "&#00D4FF<bold>Single-player"
name: "&#00D4FF&lSingle-player"
lore: ""
default:
material: "barrel"
name: "&#00D4FF<bold>Default"
name: "&#00D4FF&lDefault"
lore: "&#404040The default gamemode."
speedrun:
material: "clock"
name: "&#00D4FF&lSpeedrun"
lore: "&#404040Race against time! Blocks||&#404040disappear after you step on them."
gravity-shift:
material: "feather"
name: "&#00D4FF&lGravity Shift"
lore: "&#404040Gravity changes direction||&#404040as you parkour!"
hardcore:
material: "skeleton_skull"
name: "&#00D4FF&lHardcore"
lore: "&#404040One mistake and you're out!||&#404040No second chances."
spectator:
name: "&#FFFFFFSpectator"
join: "&#00D4FFYou are now a spectator. You can use \"/parkour\" to leave/change your gamemode."
action_bar: "&#00D4FFYou are a spectator &#A0A0A0• &#00D4FFUse \"/parkour\" to leave."
other_join: "<dark_green><bold>+ <reset>&#A0A0A0%s started spectating you"
other_leave: "&#FF4444<bold>- <reset>&#A0A0A0%s stopped spectating you"
other_join: "&#006400&l+ &r&#A0A0A0%s started spectating you"
other_leave: "&#FF4444&l- &r&#A0A0A0%s stopped spectating you"
item:
material: "glass"
name: "&#00D4FF<bold>Spectator"
name: "&#00D4FF&lSpectator"
lore: ""
head:
material: ""
name: "&#00D4FF<bold>%s"
name: "&#00D4FF&l%s"
lore: ""
community:
@ -54,7 +69,7 @@ community:
item:
material: "oak_boat"
name: "&#00D4FF<bold>Community"
name: "&#00D4FF&lCommunity"
lore: ""
leaderboards:
@ -62,18 +77,18 @@ community:
item:
material: "gold_nugget"
name: "&#00D4FF<bold>Leaderboards"
name: "&#00D4FF&lLeaderboards"
lore: ""
sort:
material: "bookshelf"
name: "&#00D4FF<bold>Sorted by %s"
name: "&#00D4FF&lSorted by %s"
lore: ""
values: ["score", "time", "difficulty"]
head:
material: ""
name: "&#00D4FF<bold>#%r - %p"
name: "&#00D4FF&l#%r - %p"
lore: "&#00D4FFScore: &#A0A0A0%s||&#00D4FFTime: &#A0A0A0%t||&#00D4FFDifficulty: &#A0A0A0%d&#404040/1.0"
settings:
@ -81,7 +96,7 @@ settings:
item:
material: "comparator"
name: "&#00D4FF<bold>Settings"
name: "&#00D4FF&lSettings"
lore: ""
lang:
@ -89,13 +104,13 @@ settings:
item:
material: "writable_book"
name: "&#00D4FF<bold>Language"
lore: "&#404040Currently: &#00D4FF<bold>%s"
name: "&#00D4FF&lLanguage"
lore: "&#404040Currently: &#00D4FF&l%s"
chat:
material: "crossbow"
name: "&#00D4FF<bold>Chat"
lore: "&#404040Currently: &#00D4FF<bold>%s||&#404040Select who you can chat with"
name: "&#00D4FF&lChat"
lore: "&#404040Currently: &#00D4FF&l%s||&#404040Select who you can chat with"
values: ["lobby only", "players only", "public"]
formats:
@ -104,19 +119,19 @@ settings:
parkour_settings:
enabled: "&#00D4FF<bold>✔"
disabled: "&#FF4444<bold>✘"
enabled: "&#00D4FF&l✔"
disabled: "&#FF4444&l✘"
item:
material: "redstone"
name: "&#00D4FF<bold>Parkour Settings"
name: "&#00D4FF&lParkour Settings"
lore: "&#404040Change the way your parkour appears."
items:
no_change:
material: "barrier"
name: "<dark_red><bold>You cant change this right now!"
name: "&#8B0000&lYou cant change this right now!"
lore: "&#A0A0A0You have a score above 0,||&#A0A0A0which means you cant change this setting."
styles:
@ -124,29 +139,29 @@ settings:
style_item:
material: ""
name: "&#FFFFFF<bold>%s"
name: "&#FFFFFF&l%s"
lore: "&#00D4FFCategory: &#A0A0A0%s"
item:
material: "crafting_table"
name: "&#00D4FF<bold>Style"
name: "&#00D4FF&lStyle"
lore: "&#00D4FFCurrently: &#A0A0A0%s||&#A0A0A0Choose which blocks will be used."
leads:
material: "feather"
name: "&#00D4FF<bold>Lead"
name: "&#00D4FF&lLead"
lore: "&#00D4FFCurrently: &#A0A0A0%s blocks||&#A0A0A0How many blocks will||&#A0A0A0be generated in front of you."
time:
material: "clock"
name: "&#00D4FF<bold>Time"
name: "&#00D4FF&lTime"
lore: "&#00D4FFCurrently: &#A0A0A0%s||&#A0A0A0Your current visual time."
schematics:
material: ""
name: "&#00D4FF<bold>Schematic difficulty"
name: "&#00D4FF&lSchematic difficulty"
lore: "&#00D4FFCurrently: &#A0A0A0%s||&#A0A0A0The difficulty of schematics"
values: ["&#FF4444off", "&#00D4FFeasy", "<yellow>medium", "&#00D4FFhard", "<dark_red>very hard"]
values: ["&#FF4444off", "&#00D4FFeasy", "&#FFFF00medium", "&#00D4FFhard", "&#8B0000very hard"]
scoreboard:
material: ""
@ -159,10 +174,10 @@ settings:
name: "Show fall message"
lore: "&#A0A0A0Currently: %s||&#A0A0A0If enabled shows a message when you fall||&#A0A0A0with extra info."
divider: "&#404040<strikethrough>-----------------------------------"
score: "&#00D4FF<bold>Score <reset>&#FFFFFF%s"
time: "&#00D4FF<bold>Time <reset>&#FFFFFF%s"
high_score: "&#00D4FF<bold>High-score <reset>&#FFFFFF%s"
divider: "&#404040&m-----------------------------------"
score: "&#00D4FF&lScore &r&#FFFFFF%s"
time: "&#00D4FF&lTime &r&#FFFFFF%s"
high_score: "&#00D4FF&lHigh-score &r&#FFFFFF%s"
formats:
miss: "&#A0A0A0You missed your high score by %s points!"
@ -192,18 +207,18 @@ settings:
lobby:
name: "&#FFFFFFLobby"
other_join: '<dark_green><bold>+ <reset>&#A0A0A0%s joined your lobby'
other_leave: '&#FF4444<bold>- <reset>&#A0A0A0%s left your lobby'
other_join: '&#006400&l+ &r&#A0A0A0%s joined your lobby'
other_leave: '&#FF4444&l- &r&#A0A0A0%s left your lobby'
item:
material: "iron_ingot"
name: "&#00D4FF<bold>Your Lobby"
name: "&#00D4FF&lYour Lobby"
lore: ""
visibility:
material: "firework_rocket"
name: "&#00D4FF<bold>Visibility"
lore: "&#404040Currently: &#00D4FF<bold>%s||&#404040Select who can join your lobby."
name: "&#00D4FF&lVisibility"
lore: "&#404040Currently: &#00D4FF&l%s||&#404040Select who can join your lobby."
values: ["private", "invite-only", "public"]
player_management:
@ -216,27 +231,27 @@ lobby:
item:
material: "cake"
name: "&#00D4FF<bold>Manage Players"
name: "&#00D4FF&lManage Players"
lore: "&#404040Kick or mute players here."
head:
material: ""
name: "&#00D4FF<bold>%s"
name: "&#00D4FF&l%s"
lore: ""
top: "&#404040Muted in lobby||"
bottom: "&#00D4FFLeft click&#404040 to kick from lobby||&#00D4FFRight click&#404040 to (un)mute in lobby"
scoreboard:
title: "&#00D4FF<bold>LoParkour"
title: "&#00D4FF&lLoParkour"
lines:
- "<blue>"
- "&#0000FF"
- "&#00D4FFScore &#FFFFFF%score%"
- "&#00D4FFTime &#FFFFFF%time%"
- "&#00D4FFHighscore &#FFFFFF%high_score%"
- "&#FF4444"
- "&#00D4FF<bold>server.ip"
- "&#00D4FF&lserver.ip"
other:
@ -246,20 +261,21 @@ other:
quit:
material: "barrier"
name: "&#00D4FF<bold>Quit"
name: "&#00D4FF&lQuit"
lore: ""
close:
material: "arrow"
name: "&#00D4FF<bold>Close"
name: "&#00D4FF&lClose"
lore: ""
iep:
material: "elytra"
name: "&#00D4FF<bold>Flying"
name: "&#00D4FF&lFlying"
lore: "&#A0A0A0Parkour with elytra!"
modes:
elytra:
name: "&#00D4FF<bold>Elytra"
description: "&#A0A0A0Fly through rings!||&#A0A0A0Use fireworks to boost"
material: "elytra"
name: "&#00D4FF&lElytra"
lore: "&#A0A0A0Fly through rings!||&#A0A0A0Use fireworks to boost"

View file

@ -13,7 +13,7 @@ play:
item:
material: "sugar_cane"
name: "&#00D4FF<bold>Играть"
name: "&#00D4FF&lИграть"
lore: ""
single:
@ -21,31 +21,46 @@ play:
item:
material: "ender_pearl"
name: "&#00D4FF<bold>Одиночная игра"
name: "&#00D4FF&lОдиночная игра"
lore: ""
default:
material: "barrel"
name: "&#00D4FF<bold>Обычный"
name: "&#00D4FF&lОбычный"
lore: "&#404040Стандартный режим игры."
speedrun:
material: "clock"
name: "&#00D4FF&lСпидран"
lore: "&#404040Гонка на время! Блоки||&#404040исчезают после того, как вы на них наступите."
gravity-shift:
material: "feather"
name: "&#00D4FF&lСмена гравитации"
lore: "&#404040Гравитация меняет направление||&#404040во время паркура!"
hardcore:
material: "skeleton_skull"
name: "&#00D4FF&lХардкор"
lore: "&#404040Одна ошибка и всё! Нет||&#404040второго шанса."
spectator:
name: "&#FFFFFFНаблюдатель"
join: "&#00D4FFТеперь вы наблюдатель. Используйте \"/parkour\" чтобы выйти/сменить режим."
action_bar: "&#00D4FFВы наблюдатель &#A0A0A0• &#00D4FFИспользуйте \"/parkour\" чтобы выйти."
other_join: "<dark_green><bold>+ <reset>&#A0A0A0%s начал наблюдать за вами"
other_leave: "&#FF4444<bold>- <reset>&#A0A0A0%s перестал наблюдать за вами"
other_join: "&#006400&l+ &r&#A0A0A0%s начал наблюдать за вами"
other_leave: "&#FF4444&l- &r&#A0A0A0%s перестал наблюдать за вами"
item:
material: "glass"
name: "&#00D4FF<bold>Наблюдатель"
name: "&#00D4FF&lНаблюдатель"
lore: ""
head:
material: ""
name: "&#00D4FF<bold>%s"
name: "&#00D4FF&l%s"
lore: ""
community:
@ -53,7 +68,7 @@ community:
item:
material: "oak_boat"
name: "&#00D4FF<bold>Сообщество"
name: "&#00D4FF&lСообщество"
lore: ""
leaderboards:
@ -61,18 +76,18 @@ community:
item:
material: "gold_nugget"
name: "&#00D4FF<bold>Таблица лидеров"
name: "&#00D4FF&lТаблица лидеров"
lore: ""
sort:
material: "bookshelf"
name: "&#00D4FF<bold>Сортировка по %s"
name: "&#00D4FF&lСортировка по %s"
lore: ""
values: ["счёту", "времени", "сложности"]
head:
material: ""
name: "&#00D4FF<bold>#%r - %p"
name: "&#00D4FF&l#%r - %p"
lore: "&#00D4FFСчёт: &#A0A0A0%s||&#00D4FFВремя: &#A0A0A0%t||&#00D4FFСложность: &#A0A0A0%d&#404040/1.0"
settings:
@ -80,7 +95,7 @@ settings:
item:
material: "comparator"
name: "&#00D4FF<bold>Настройки"
name: "&#00D4FF&lНастройки"
lore: ""
lang:
@ -88,13 +103,13 @@ settings:
item:
material: "writable_book"
name: "&#00D4FF<bold>Язык"
lore: "&#404040Текущий: &#00D4FF<bold>%s"
name: "&#00D4FF&lЯзык"
lore: "&#404040Текущий: &#00D4FF&l%s"
chat:
material: "crossbow"
name: "&#00D4FF<bold>Чат"
lore: "&#404040Текущий: &#00D4FF<bold>%s||&#404040Выберите с кем вы можете общаться"
name: "&#00D4FF&lЧат"
lore: "&#404040Текущий: &#00D4FF&l%s||&#404040Выберите с кем вы можете общаться"
values: ["только лобби", "только игроки", "публичный"]
formats:
@ -103,19 +118,19 @@ settings:
parkour_settings:
enabled: "&#00D4FF<bold>✔"
disabled: "&#FF4444<bold>✘"
enabled: "&#00D4FF&l✔"
disabled: "&#FF4444&l✘"
item:
material: "redstone"
name: "&#00D4FF<bold>Настройки паркура"
name: "&#00D4FF&lНастройки паркура"
lore: "&#404040Измените внешний вид вашего паркура."
items:
no_change:
material: "barrier"
name: "<dark_red><bold>Вы не можете изменить это сейчас!"
name: "&#8B0000&lВы не можете изменить это сейчас!"
lore: "&#A0A0A0У вас счёт больше 0,||&#A0A0A0поэтому вы не можете изменить эту настройку."
styles:
@ -123,29 +138,29 @@ settings:
style_item:
material: ""
name: "&#FFFFFF<bold>%s"
name: "&#FFFFFF&l%s"
lore: "&#00D4FFКатегория: &#A0A0A0%s"
item:
material: "crafting_table"
name: "&#00D4FF<bold>Стиль"
name: "&#00D4FF&lСтиль"
lore: "&#00D4FFТекущий: &#A0A0A0%s||&#A0A0A0Выберите какие блоки будут использоваться."
leads:
material: "feather"
name: "&#00D4FF<bold>Дистанция"
name: "&#00D4FF&lДистанция"
lore: "&#00D4FFТекущая: &#A0A0A0%s блоков||&#A0A0A0Сколько блоков будет||&#A0A0A0сгенерировано впереди вас."
time:
material: "clock"
name: "&#00D4FF<bold>Время"
name: "&#00D4FF&lВремя"
lore: "&#00D4FFТекущее: &#A0A0A0%s||&#A0A0A0Ваше текущее визуальное время."
schematics:
material: ""
name: "&#00D4FF<bold>Сложность схематик"
name: "&#00D4FF&lСложность схематик"
lore: "&#00D4FFТекущая: &#A0A0A0%s||&#A0A0A0Сложность схематик"
values: ["&#FF4444выкл", "&#00D4FFлёгкая", "<yellow>средняя", "&#00D4FFсложная", "<dark_red>очень сложная"]
values: ["&#FF4444выкл", "&#00D4FFлёгкая", "&#FFFF00средняя", "&#00D4FFсложная", "&#8B0000очень сложная"]
scoreboard:
material: ""
@ -158,10 +173,10 @@ settings:
name: "Показывать сообщение о падении"
lore: "&#A0A0A0Текущее: %s||&#A0A0A0Если включено, показывает сообщение при падении||&#A0A0A0с дополнительной информацией."
divider: "&#404040<strikethrough>-----------------------------------"
score: "&#00D4FF<bold>Счёт <reset>&#FFFFFF%s"
time: "&#00D4FF<bold>Время <reset>&#FFFFFF%s"
high_score: "&#00D4FF<bold>Рекорд <reset>&#FFFFFF%s"
divider: "&#404040&m-----------------------------------"
score: "&#00D4FF&lСчёт &r&#FFFFFF%s"
time: "&#00D4FF&lВремя &r&#FFFFFF%s"
high_score: "&#00D4FF&lРекорд &r&#FFFFFF%s"
formats:
miss: "&#A0A0A0Вы не дотянули до рекорда на %s очков!"
@ -191,18 +206,18 @@ settings:
lobby:
name: "&#FFFFFFЛобби"
other_join: '<dark_green><bold>+ <reset>&#A0A0A0%s присоединился к вашему лобби'
other_leave: '&#FF4444<bold>- <reset>&#A0A0A0%s покинул ваше лобби'
other_join: '&#006400&l+ &r&#A0A0A0%s присоединился к вашему лобби'
other_leave: '&#FF4444&l- &r&#A0A0A0%s покинул ваше лобби'
item:
material: "iron_ingot"
name: "&#00D4FF<bold>Ваше лобби"
name: "&#00D4FF&lВаше лобби"
lore: ""
visibility:
material: "firework_rocket"
name: "&#00D4FF<bold>Видимость"
lore: "&#404040Текущая: &#00D4FF<bold>%s||&#404040Выберите кто может присоединиться к вашему лобби."
name: "&#00D4FF&lВидимость"
lore: "&#404040Текущая: &#00D4FF&l%s||&#404040Выберите кто может присоединиться к вашему лобби."
values: ["приватное", "только по приглашению", "публичное"]
player_management:
@ -215,27 +230,27 @@ lobby:
item:
material: "cake"
name: "&#00D4FF<bold>Управление игроками"
name: "&#00D4FF&lУправление игроками"
lore: "&#404040Кикайте или мутьте игроков здесь."
head:
material: ""
name: "&#00D4FF<bold>%s"
name: "&#00D4FF&l%s"
lore: ""
top: "&#404040Замучен в лобби||"
bottom: "&#00D4FFЛевый клик&#404040 чтобы кикнуть из лобби||&#00D4FFПравый клик&#404040 чтобы замутить/размутить в лобби"
scoreboard:
title: "&#00D4FF<bold>LoParkour"
title: "&#00D4FF&lLoParkour"
lines:
- "<blue>"
- "&#0000FF"
- "&#00D4FFСчёт &#FFFFFF%score%"
- "&#00D4FFВремя &#FFFFFF%time%"
- "&#00D4FFРекорд &#FFFFFF%high_score%"
- "&#FF4444"
- "&#00D4FF<bold>server.ip"
- "&#00D4FF&lserver.ip"
other:
@ -245,20 +260,21 @@ other:
quit:
material: "barrier"
name: "&#00D4FF<bold>Выход"
name: "&#00D4FF&lВыход"
lore: ""
close:
material: "arrow"
name: "&#00D4FF<bold>Закрыть"
name: "&#00D4FF&lЗакрыть"
lore: ""
iep:
material: "elytra"
name: "&#00D4FF<bold>Полёт"
name: "&#00D4FF&lПолёт"
lore: "&#A0A0A0Паркур с элитрами!"
modes:
elytra:
name: "&#00D4FF<bold>Элитры"
description: "&#A0A0A0Летайте через кольца!||&#A0A0A0Используйте фейерверки для ускорения"
material: "elytra"
name: "&#00D4FF&lЭлитры"
lore: "&#A0A0A0Летайте через кольца!||&#A0A0A0Используйте фейерверки для ускорения"

View file

@ -9,7 +9,7 @@
# %player% - the player's name.
# test %player% - execute a console command. Don't add "/"!
# leave:test - executes a command on leave.
# send:<bold>test - sends a message.
# send:&ltest - sends a message.
# vault:-87 - deposits a specific amount of money (Vault required)
# default:test - mode-specific rewards. Mode names are "default", or for IP+ modes the same as the name used in the IP+ config.
#
@ -32,7 +32,7 @@ score-rewards:
interval-rewards:
10:
- "witpreward10"
- "send:<green>Good job!"
- "send:&#00FF00Good job!"
# -= One time rewards =-
# Rewards which players will receive just once.
@ -42,13 +42,13 @@ interval-rewards:
one-time-rewards:
1:
- "send:You made your first jump!"
- "team_survival:send:<gray>Be sure to invite your friends!"
- "duels:send:<gray>Good luck!"
- "team_survival:send:&#808080Be sure to invite your friends!"
- "duels:send:&#808080Good luck!"
2023:
- "leave:currentyear"
- "vault:100"
2024:
- "send:<reset>"
- "send:<red><bold>Time travelling is illegal!"
- "send:<reset>"
- "send:&r"
- "send:&#FF0000&lTime travelling is illegal!"
- "send:&r"
- "vault:-100"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more