Initial commit: LoParkour v1.1.0

This commit is contained in:
loki 2026-02-24 22:07:03 +01:00
commit aecb03ccf8
120 changed files with 9668 additions and 0 deletions

View file

@ -0,0 +1,546 @@
package dev.loki.loparkour;
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.session.Session;
import dev.efnilite.vilib.command.ViCommand;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.particle.ParticleData;
import dev.efnilite.vilib.particle.Particles;
import dev.efnilite.vilib.schematic.Schematic;
import dev.efnilite.vilib.schematic.Schematics;
import dev.efnilite.vilib.util.Locations;
import dev.efnilite.vilib.util.Strings;
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 extends ViCommand {
public static final HashMap<Player, Location[]> selections = new HashMap<>();
private static final ItemStack WAND = new Item(Material.GOLDEN_AXE, "<red><bold>Schematic Wand")
.lore("<gray>Left click: first position", "<gray>Right click: second position")
.build();
@Override
public boolean execute(CommandSender sender, 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> tabComplete(CommandSender sender, 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)) {
Menus.MAIN.open(player);
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)) {
Menus.PLAY.open(player);
}
}
case "leave" -> {
if (!cooldown(sender, "leave", 2500)) {
return;
}
ParkourUser.leave(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)) { // 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 <dark_gray>- <gray>Save your selection to a schematic file");
send(player, "<red>/LoParkour schematic paste <file> <dark_gray>- <gray>Paste a schematic file");
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) {
Menus.LEADERBOARDS.open(player);
} else {
Menus.SINGLE_LEADERBOARD.open(player, mode, Leaderboard.Sort.SCORE);
}
}
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]});
Particles.box(BoundingBox.of(playerLocation, existingSelection[1]), player.getWorld(), new ParticleData<>(Particle.END_ROD, null, 2), 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});
Particles.box(BoundingBox.of(existingSelection[0], playerLocation), player.getWorld(), new ParticleData<>(Particle.END_ROD, null, 2), 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));
Schematic.save(LoParkour.getInFolder("schematics/parkour-%s".formatted(code)), existingSelection[0], existingSelection[1], LoParkour.getPlugin());
}
}
}
}
}
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 (!arg2.equalsIgnoreCase("paste")) {
return;
}
if (!player.hasPermission(ParkourOption.ADMIN.permission)) {
return;
}
Schematic schematic = Schematics.getSchematic(LoParkour.getPlugin(), arg3);
if (schematic == null) {
send(sender, "%sCouldn't find %s".formatted(LoParkour.PREFIX, arg3));
return;
}
schematic.paste(player.getLocation());
send(sender, "%sPasted schematic %s".formatted(LoParkour.PREFIX, arg3));
}
}
private void send(CommandSender sender, String message) {
sender.sendMessage(Strings.colour(message));
}
}

View file

@ -0,0 +1,316 @@
package dev.loki.loparkour;
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 dev.efnilite.vilib.event.EventWatcher;
import dev.efnilite.vilib.particle.ParticleData;
import dev.efnilite.vilib.particle.Particles;
import dev.efnilite.vilib.util.Locations;
import dev.efnilite.vilib.util.Strings;
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
*/
@ApiStatus.Internal
public class Events implements EventWatcher {
@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("Schematic 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]});
Particles.box(BoundingBox.of(location, existingSelection[1]), player.getWorld(), new ParticleData<>(Particle.END_ROD, null, 2), 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});
Particles.box(BoundingBox.of(existingSelection[0], location), player.getWorld(), new ParticleData<>(Particle.END_ROD, null, 2), 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").getMaterial();
Material community = Locales.getItem(player, "community.item").getMaterial();
Material settings = Locales.getItem(player, "settings.item").getMaterial();
Material lobby = Locales.getItem(player, "lobby.item").getMaterial();
Material quit = Locales.getItem(player, "other.quit").getMaterial();
event.setCancelled(true);
if (held == play) {
Menus.PLAY.open(player);
} else if (held == community) {
Menus.COMMUNITY.open(player);
} else if (held == settings) {
Menus.SETTINGS.open(player);
} else if (held == lobby) {
Menus.LOBBY.open(player);
} 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(Strings.colour(message));
}
}

View file

@ -0,0 +1,156 @@
package dev.loki.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.hook.HoloHook;
import dev.loki.loparkour.hook.PAPIHook;
import dev.loki.loparkour.mode.DefaultMode;
import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.mode.SpectatorMode;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.reward.Rewards;
import dev.loki.loparkour.storage.Storage;
import dev.loki.loparkour.world.World;
import dev.efnilite.vilib.ViPlugin;
import dev.efnilite.vilib.bstats.bukkit.Metrics;
import dev.efnilite.vilib.bstats.charts.SimplePie;
import dev.efnilite.vilib.bstats.charts.SingleLineChart;
import dev.efnilite.vilib.inventory.Menu;
import dev.efnilite.vilib.util.Logging;
import dev.efnilite.vilib.util.UpdateChecker;
import org.jetbrains.annotations.Nullable;
import java.io.File;
/**
* Main class of LoParkour
*
* @author loki
*/
public final class LoParkour extends ViPlugin {
public static final String NAME = "&#FF6464<bold>LoParkour<reset>";
public static final String PREFIX = NAME + " &#404040» &#A0A0A0";
private static Logging logging;
private static LoParkour instance;
@Nullable
private static PAPIHook placeholderHook;
public static void log(String message) {
if (Config.CONFIG.getBoolean("debug")) {
logging.info("[Debug] " + message);
}
}
/**
* @param child The file name.
* @return A file from within the plugin folder.
*/
public static File getInFolder(String child) {
return new File(instance.getDataFolder(), child);
}
/**
* @return This plugin's {@link Logging} instance.
*/
public static Logging logging() {
return logging;
}
/**
* @return The plugin instance.
*/
public static LoParkour getPlugin() {
return instance;
}
@Nullable
public static PAPIHook getPlaceholderHook() {
return placeholderHook;
}
@Override
public void onLoad() {
instance = this;
logging = new Logging(this);
}
@Override
public void enable() {
// ----- Configurations -----
Config.reload(true);
// ----- Registry -----
Registry.register(new DefaultMode());
Registry.register(new SpectatorMode());
Modes.init();
Menu.init(this);
// hook with hd / papi after gamemode leaderboards have initialized
if (getServer().getPluginManager().isPluginEnabled("HolographicDisplays")) {
logging.info("Registered Holographic Displays hook");
HoloHook.init();
}
if (getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) {
logging.info("Registered PlaceholderAPI hook");
placeholderHook = new PAPIHook();
placeholderHook.register();
}
if (Config.CONFIG.getBoolean("bungeecord.enabled")) {
getServer().getMessenger().registerOutgoingPluginChannel(this, "BungeeCord");
logging.info("Registered BungeeCord hook");
}
// ----- Worlds -----
if (Config.CONFIG.getBoolean("joining")) {
World.create();
}
// ----- Events -----
registerListener(new Events());
registerCommand("LoParkour", new Command());
// ----- Metrics -----
Metrics metrics = new Metrics(this, 9272);
metrics.addCustomChart(new SimplePie("using_sql", () -> Boolean.toString(Option.SQL)));
metrics.addCustomChart(new SimplePie("using_rewards", () -> Boolean.toString(Rewards.REWARDS_ENABLED)));
metrics.addCustomChart(new SimplePie("locale_count", () -> Integer.toString(Locales.locales.size())));
metrics.addCustomChart(new SingleLineChart("player_joins", () -> {
int joins = ParkourUser.joinCount;
ParkourUser.joinCount = 0;
return joins;
}));
UpdateChecker.check(this, 87226);
}
@Override
public void disable() {
try {
for (ParkourUser user : ParkourUser.getUsers()) {
ParkourUser.leave(user);
}
// write all LoParkour gamemodes
Modes.DEFAULT.getLeaderboard().write(false);
Storage.close();
World.delete();
} catch (Throwable ignored) {
}
}
}

View file

@ -0,0 +1,42 @@
package dev.loki.loparkour.api;
import dev.loki.loparkour.api.event.ParkourBlockGenerateEvent;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourUser;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
/**
* Main API handler.
* <ul>
* <li>For general player handling, please view {@link ParkourUser}.</li>
* <li>For parkour players, please view {@link ParkourPlayer}.</li>
* <li>For player spectating, please view {@link dev.efnilite.LoParkour.player.ParkourSpectator}.</li>
* <li>For events, please view {@link ParkourBlockGenerateEvent} and others in the events package.</li>
* <li>For Sessions, please view {@link dev.efnilite.LoParkour.session.Session}.</li>
* <li>For Schematics, please view {@link dev.efnilite.LoParkour.schematic.Schematic}</li>
* </ul>
*/
@SuppressWarnings("unused")
public class ParkourAPI {
private ParkourAPI() throws IllegalAccessException {
throw new IllegalAccessException("Initializing API class");
}
/**
* @param player The player.
* @return The {@link ParkourPlayer}. Null if not found.
*/
public static @Nullable ParkourPlayer getPlayer(Player player) {
return ParkourPlayer.getPlayer(player);
}
/**
* @param player The player.
* @return The {@link ParkourPlayer}. Null if not found.
*/
public static @Nullable ParkourUser getUser(Player player) {
return ParkourUser.getUser(player);
}
}

View file

@ -0,0 +1,67 @@
package dev.loki.loparkour.api;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.mode.Mode;
import dev.loki.loparkour.style.Style;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.LinkedList;
import java.util.List;
/**
* Registers stuff.
*
* @author loki
* @since 5.0.0
*/
public final class Registry {
private static final LinkedList<Mode> modes = new LinkedList<>();
private static final LinkedList<Style> styles = new LinkedList<>();
/**
* Registers a {@link Mode}.
*
* @param mode The mode.
*/
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()));
}
/**
* @param name The mode name.
* @return The {@link Mode} instance. May be null.
*/
@Nullable
public static Mode getMode(@NotNull String name) {
return modes.stream()
.filter(mode -> mode.getName().equals(name))
.findFirst()
.orElse(null);
}
@Nullable
public static Style getStyle(@NotNull String name) {
return styles.stream()
.filter(style -> style.getName().equals(name))
.findFirst()
.orElse(null);
}
public static List<Style> getStyles() {
return styles;
}
public static List<Mode> getModes() {
return modes;
}
}

View file

@ -0,0 +1,27 @@
package dev.loki.loparkour.api.event;
import dev.loki.loparkour.generator.ParkourGenerator;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.efnilite.vilib.event.EventWrapper;
import org.bukkit.block.Block;
import java.util.List;
/**
* Gets called when a new jump is generated. Read-only.
*
* @author loki
* @since 5.0.0
*/
public class ParkourBlockGenerateEvent extends EventWrapper {
public final List<Block> blocks;
public final ParkourGenerator generator;
public final ParkourPlayer player;
public ParkourBlockGenerateEvent(List<Block> blocks, ParkourGenerator generator, ParkourPlayer player) {
this.blocks = blocks;
this.generator = generator;
this.player = player;
}
}

View file

@ -0,0 +1,16 @@
package dev.loki.loparkour.api.event;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.efnilite.vilib.event.EventWrapper;
/**
* Gets called when a player falls. Read-only.
*/
public class ParkourFallEvent extends EventWrapper {
public final ParkourPlayer player;
public ParkourFallEvent(ParkourPlayer player) {
this.player = player;
}
}

View file

@ -0,0 +1,16 @@
package dev.loki.loparkour.api.event;
import dev.loki.loparkour.player.ParkourUser;
import dev.efnilite.vilib.event.EventWrapper;
/**
* Gets called when a player joins a session. Read-only.
*/
public class ParkourJoinEvent extends EventWrapper {
public final ParkourUser player;
public ParkourJoinEvent(ParkourUser player) {
this.player = player;
}
}

View file

@ -0,0 +1,16 @@
package dev.loki.loparkour.api.event;
import dev.loki.loparkour.player.ParkourUser;
import dev.efnilite.vilib.event.EventWrapper;
/**
* Gets called when a player leaves a session. Read-only.
*/
public class ParkourLeaveEvent extends EventWrapper {
public final ParkourUser player;
public ParkourLeaveEvent(ParkourUser player) {
this.player = player;
}
}

View file

@ -0,0 +1,25 @@
package dev.loki.loparkour.api.event;
import dev.loki.loparkour.generator.ParkourGenerator;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.efnilite.vilib.event.EventWrapper;
import dev.efnilite.vilib.schematic.Schematic;
/**
* Gets called when a new jump is generated. Read-only.
*
* @author loki
* @since 5.0.0
*/
public class ParkourSchematicGenerateEvent extends EventWrapper {
public final Schematic schematic;
public final ParkourGenerator generator;
public final ParkourPlayer player;
public ParkourSchematicGenerateEvent(Schematic schematic, ParkourGenerator generator, ParkourPlayer player) {
this.schematic = schematic;
this.generator = generator;
this.player = player;
}
}

View file

@ -0,0 +1,16 @@
package dev.loki.loparkour.api.event;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.efnilite.vilib.event.EventWrapper;
/**
* Gets called when a point is scored. Read-only.
*/
public class ParkourScoreEvent extends EventWrapper {
public final ParkourPlayer player;
public ParkourScoreEvent(ParkourPlayer player) {
this.player = player;
}
}

View file

@ -0,0 +1,19 @@
package dev.loki.loparkour.api.event;
import dev.loki.loparkour.player.ParkourSpectator;
import dev.efnilite.vilib.event.EventWrapper;
/**
* Gets called when a player starts spectating a session. Read-only.
*
* @author loki
* @since 5.0.0
*/
public class ParkourSpectateEvent extends EventWrapper {
public final ParkourSpectator player;
public ParkourSpectateEvent(ParkourSpectator player) {
this.player = player;
}
}

View file

@ -0,0 +1,199 @@
package dev.loki.loparkour.config;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.reward.Rewards;
import dev.loki.loparkour.schematic.Schematics;
import dev.efnilite.vilib.configupdater.ConfigUpdater;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
/**
* Config management class.
*/
public enum Config {
CONFIG("config.yml", List.of("styles")),
GENERATION("generation.yml", null),
REWARDS("rewards-v2.yml", List.of("score-rewards", "interval-rewards", "one-time-rewards")),
SCHEMATICS("schematics/schematics.yml", List.of("difficulty"));
/**
* The path to this file, incl. plugin folder.
*/
public final File path;
/**
* The name of this file, e.g. config.yml
*/
public final String fileName;
/**
* The sections in the file that will be ignored when updating the keys.
*/
public final List<String> ignoredSections;
/**
* The {@link FileConfiguration} instance associated with this config file.
*/
public FileConfiguration fileConfiguration;
Config(String fileName, @Nullable List<String> ignoredSections) {
this.fileName = fileName;
this.ignoredSections = ignoredSections;
this.path = LoParkour.getInFolder(fileName);
if (!path.exists()) {
LoParkour.getPlugin().saveResource(fileName, false);
}
update();
load();
}
/**
* Reloads all config files.
*/
public static void reload(boolean initialLoad) {
for (Config config : values()) {
config.load();
}
// read config stuff
Rewards.init();
Locales.init();
Schematics.init();
Option.init(initialLoad);
LoParkour.log("Loaded all config files");
}
/**
* Loads the file from disk.
*/
public void load() {
this.fileConfiguration = YamlConfiguration.loadConfiguration(path);
}
/**
* Updates the file so all keys are present.
*/
public void update() {
try {
ConfigUpdater.update(LoParkour.getPlugin(), fileName, path, ignoredSections);
} catch (Exception ex) {
LoParkour.logging().stack("Error while trying to update config file", ex);
}
}
/**
* @param path The path.
* @return True when path exists, false if not.
*/
public boolean isPath(@NotNull String path) {
return fileConfiguration.isSet(path);
}
/**
* @param path The path.
* @return The value at path.
*/
public Object get(@NotNull String path) {
check(path);
return fileConfiguration.get(path);
}
/**
* @param path The path.
* @return The boolean value at path.
*/
public boolean getBoolean(@NotNull String path) {
check(path);
return fileConfiguration.getBoolean(path);
}
/**
* @param path The path.
* @return The int value at path.
*/
public int getInt(@NotNull String path) {
check(path);
return fileConfiguration.getInt(path);
}
/**
* @param path The path.
* @return The double value at path.
*/
public double getDouble(@NotNull String path) {
check(path);
return fileConfiguration.getDouble(path);
}
/**
* @param path The path.
* @return The String value at path.
*/
@NotNull
public String getString(@NotNull String path) {
check(path);
return fileConfiguration.getString(path, "");
}
/**
* @param path The path.
* @return The String list value at path.
*/
@NotNull
public List<String> getStringList(@NotNull String path) {
check(path);
return fileConfiguration.getStringList(path);
}
/**
* @param path The path.
* @return The int list value at path.
*/
@NotNull
public List<Integer> getIntList(@NotNull String path) {
check(path);
return fileConfiguration.getIntegerList(path);
}
/**
* @param path The path.
* @param deep Whether search should include children of children as well.
* @return The children nodes from path.
*/
@NotNull
public List<String> getChildren(@NotNull String path, boolean... deep) {
check(path);
ConfigurationSection section = fileConfiguration.getConfigurationSection(path);
if (section == null) {
return new ArrayList<>();
}
return new ArrayList<>(section.getKeys(deep != null));
}
// checks if the specified path exists to avoid developer error
private void check(@NotNull String path) {
if (!isPath(path)) {
throw new NoSuchElementException("Unknown path %s in %s".formatted(path, fileName));
}
}
}

View file

@ -0,0 +1,257 @@
package dev.loki.loparkour.config;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.player.ParkourUser;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.util.Task;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
/**
* Locale message/item handler.
*/
public class Locales {
// a list of all nodes, used to check against missing nodes
private static List<String> resourceNodes;
/**
* A map of all locales with their respective yml trees
*/
public static final Map<String, FileConfiguration> locales = new HashMap<>();
/**
* Initializes this Locale handler.
*/
public static void init() {
Plugin plugin = LoParkour.getPlugin();
Task.create(plugin).async().execute(() -> {
locales.clear();
FileConfiguration embedded = YamlConfiguration.loadConfiguration(new InputStreamReader(plugin.getResource("locales/en.yml"), StandardCharsets.UTF_8));
// get all nodes from the plugin's english resource, aka the most updated version
resourceNodes = getChildren(embedded, "", true);
File folder = LoParkour.getInFolder("locales");
// download files to locales folder
if (!folder.exists()) {
folder.mkdirs();
}
String[] files = folder.list();
// create non-existent files
if (files != null && files.length == 0) {
plugin.saveResource("locales/en.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
try (Stream<Path> stream = Files.list(folder.toPath())) {
stream.forEach(path -> {
File file = path.toFile();
// get locale from file name
String locale = file.getName().split("\\.")[0];
LoParkour.log("Found locale " + locale);
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
validate(embedded, config, file);
locales.put(locale, config);
});
} catch (Exception ex) {
LoParkour.logging().stack("Error while trying to read locale files", "restart/reload your server", ex);
}
}).run();
}
// validates whether a lang file contains all required keys.
// if it doesn't, automatically add them
private static void validate(FileConfiguration provided, FileConfiguration user, File localPath) {
List<String> userNodes = getChildren(user, "", true);
for (String node : resourceNodes) {
if (userNodes.contains(node)) {
continue;
}
LoParkour.log("Fixing missing config node %s in %s".formatted(node, localPath.getName()));
user.set(node, provided.get(node));
}
LoParkour.log("Validated locale " + localPath.getName());
try {
user.save(localPath);
} catch (IOException ex) {
LoParkour.logging().stack("Error while trying to save fixed config file %s".formatted(localPath), "delete this file and restart your server", ex);
}
}
/**
* Gets a String from the provided path in the provided player's locale.
* If the player is a {@link ParkourUser}, their locale value will be used.
* If not, the default locale will be used.
*
* @param player The player
* @param path The path
* @return a String
*/
public static String getString(Player player, String path) {
ParkourUser user = ParkourUser.getUser(player);
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
return getString(locale, path);
}
/**
* Gets a coloured String from the provided path in the provided locale file
*
* @param locale The locale
* @param path The path
* @return a String
*/
public static String getString(String locale, String path) {
return getValue(locale, config -> config.getString(path), "");
}
/**
* Gets an uncoloured String list from the provided path in the provided locale file
*
* @param locale The locale
* @param path The path
* @return a String list
*/
public static List<String> getStringList(String locale, String path) {
return getValue(locale, config -> config.getStringList(path), Collections.emptyList());
}
private static <T> T getValue(String locale, Function<FileConfiguration, T> f, T def) {
if (locales.isEmpty()) {
return def;
}
FileConfiguration config = locales.get(locale);
return config != null ? f.apply(config) : def;
}
/**
* Returns an item from a json locale file.
* The locale is derived from the player.
* If the player is a {@link ParkourUser}, their locale value will be used.
* If not, the default locale will be used.
*
* @param player The player
* @param path The full path of the item in the locale file
* @return a non-null {@link Item} instance built from the description in the locale file
*/
@NotNull
public static Item getItem(@NotNull Player player, String path, String... replace) {
ParkourUser user = ParkourUser.getUser(player);
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
return getItem(locale, path, replace);
}
private static final Pattern pattern = Pattern.compile("%[a-z]");
/**
* Returns an item from a provided json locale file with possible replacements.
*
* @param locale The locale
* @param path The path in the json file
* @param replace The Strings that will replace any appearances of a String following the regex "%[a-z]"
* @return a non-null {@link Item} instance built from the description in the locale file
*/
@NotNull
public static Item getItem(String locale, String path, String... replace) {
if (locales.isEmpty()) { // during reloading
return new Item(Material.STONE, "");
}
FileConfiguration base = locales.get(locale);
String material = base.getString("%s.material".formatted(path));
String name = base.getString("%s.name".formatted(path));
String lore = base.getString("%s.lore".formatted(path));
int modelId = base.getInt("%s.model_id".formatted(path), -1);
if (material == null) {
material = "";
}
if (name == null) {
name = "";
}
if (lore == null) {
lore = "";
}
int idx = 0;
Matcher matcher = pattern.matcher(name);
while (matcher.find()) {
if (idx == replace.length) {
break;
}
name = name.replaceFirst(matcher.group(), replace[idx]);
idx++;
}
matcher = pattern.matcher(lore);
while (matcher.find()) {
if (idx == replace.length) {
break;
}
lore = lore.replaceFirst(matcher.group(), replace[idx]);
idx++;
}
Item item = new Item(Material.getMaterial(material.toUpperCase()), name);
if (!lore.isEmpty()) {
item.lore(lore.split("\\|\\|"));
}
if (modelId != -1) {
item.modelId(modelId);
}
return item;
}
public static List<String> getChildren(FileConfiguration file, String path, boolean deep) {
ConfigurationSection section = file.getConfigurationSection(path);
return section != null ? new ArrayList<>(section.getKeys(deep)) : Collections.emptyList();
}
}

View file

@ -0,0 +1,264 @@
package dev.loki.loparkour.config;
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.efnilite.vilib.particle.ParticleData;
import org.bukkit.*;
import org.bukkit.block.BlockFace;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.util.Vector;
import java.util.*;
import java.util.function.BiFunction;
/**
* Class for variables required in generating without accessing the file a lot (constants)
*/
public class Option {
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 Location GO_BACK_LOC;
public static void init(boolean firstLoad) {
initSql();
initEnums();
initGeneration();
initStyles("styles.list", Config.CONFIG.fileConfiguration, RandomStyle::new)
.forEach(Registry::register);
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.logging().error("Invalid lead: %d. Should be above 1 and below 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.logging().error("Invalid heading: %s".formatted(heading));
}
// Scoring
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 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]));
}
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.logging().error("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.logging().error("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, 0, 0, 0, 0);
}
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");
}
// --------------------------------------------------------------
// 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() {
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.logging().stack("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.logging().error("Invalid material %s in style %s".formatted(name, style));
return Material.STONE;
}
return material;
})
.toList()));
}
return styles;
}
}

View file

@ -0,0 +1,12 @@
package dev.loki.loparkour.generator;
/**
* Options for {@link ParkourGenerator}s.
*/
public enum GeneratorOption {
DISABLE_SCHEMATICS,
DISABLE_SPECIAL,
REDUCE_RANDOM_BLOCK_SELECTION_ANGLE
}

View file

@ -0,0 +1,89 @@
package dev.loki.loparkour.generator;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.session.Session;
import dev.efnilite.vilib.schematic.Schematic;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.NoSuchElementException;
/**
* Spawn island handler.
*
* @author loki
* @since 5.0.0
*/
public final class Island {
/**
* The session.
*/
public final Session session;
/**
* The schematic.
*/
public final Schematic schematic;
/**
* The blocks that have been affected by the schematic.
*/
public List<Block> blocks;
public Island(@NotNull Session session, @Nullable Schematic schematic) {
this.session = session;
this.schematic = schematic;
}
/**
* Builds the island and teleports the player.
*/
public void build(Location location) {
if (schematic == null) {
return;
}
LoParkour.log("Building island");
blocks = schematic.paste(location.subtract(0, schematic.getDimensions().getY(), 0));
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());
try {
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);
Location ps = player.getLocation().add(0.5, 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));
session.generator.startTick();
session.getPlayers().forEach(pp -> pp.setup(ps));
} catch (NoSuchElementException ex) {
LoParkour.logging().stack("Error while trying to find parkour or player spawn in schematic %s".formatted(schematic.getFile().getName()),
"check if you used the same material as the one in generation.yml", ex);
blocks.forEach(block -> block.setType(Material.AIR));
}
}
/**
* Destroys the island.
*/
public void destroy() {
if (blocks == null) {
return;
}
blocks.forEach(block -> block.setType(Material.AIR, false));
}
}

View file

@ -0,0 +1,137 @@
package dev.loki.loparkour.generator;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
/**
* The JumpDirector provides calculations to ensure parkour blocks stay within the allowed playable area.
*/
public class JumpDirector {
private static final int SAFE_DISTANCE = 5;
private final BoundingBox bb;
private final double[][] progress;
/**
* Constructor.
*
* @param bb The bounding box of the allowed parkour area.
* @param point The point to base the direction on.
*/
public JumpDirector(@NotNull BoundingBox bb, @NotNull Vector point) {
this.bb = bb;
this.progress = calculateParameterization(point);
}
/**
* Calculates the player's position in a parameter form, to make it easier to detect when the player is near the edge of the border.
* Returns a 2-dimensional array where the first array index is used to select the x, y and z (0, 1 and 2 respectively).
*
* @return Array where the first index is tx and second index is borderMarginX (see comments in code for explanation).
*/
private double[][] calculateParameterization(@NotNull Vector point) {
Vector min = bb.getMin();
Vector max = bb.getMax();
// the total dimensions
double dx = max.getX() - min.getX();
double dy = max.getY() - min.getY();
double dz = max.getZ() - min.getZ();
// the relative x, y and z coordinates
// relative being from the min point of the selection zone
double relativeX = point.getX() - min.getX();
double relativeY = point.getY() - min.getY();
double relativeZ = point.getZ() - min.getZ();
// get progress along axes
// tx = 0 means that the player is at the same x coordinate as the min point (origin)
// tx = 1 means that the player is at the same x coordinate as the max point
// everything between is the progress between these two points, relatively speaking
double tx = relativeX / dx;
double ty = relativeY / dy;
double tz = relativeZ / dz;
// the margin until the border
// if tx < borderMarginX, it means the x coordinate is within 'safeDistance' blocks of the border
double borderMarginX = SAFE_DISTANCE / dx;
double borderMarginY = SAFE_DISTANCE / dy;
double borderMarginZ = SAFE_DISTANCE / dz;
return new double[][]{{tx, borderMarginX}, {ty, borderMarginY}, {tz, borderMarginZ}};
}
/**
* Updates the heading to make sure it avoids the border of the selected zone.
* When the most recent block is detected to be within a 5-block radius of the border,
* the heading will automatically be turned around to ensure that the edge does not get
* destroyed.
*
* @return The recommended new heading. Current heading if no modification is needed.
*/
@NotNull
public Vector getRecommendedHeading(Vector current) {
// get x values from progress array
double tx = progress[0][0];
double borderMarginX = progress[0][1];
// get z values from progress array
double tz = progress[2][0];
double borderMarginZ = progress[2][1];
Vector recommendedHeading = new Vector(0, 0, 0);
// check border
if (tx < borderMarginX) {
// x should increase
recommendedHeading = new Vector(1, 0, 1);
} else if (tx > 1 - borderMarginX) {
// x should decrease
recommendedHeading = new Vector(-1, 0, -1);
}
if (tz < borderMarginZ) {
// z should increase
recommendedHeading = new Vector(1, 0, 1);
} else if (tz > 1 - borderMarginZ) {
// z should decrease
recommendedHeading = new Vector(-1, 0, -1);
}
if (recommendedHeading.lengthSquared() == 0) {
return current;
} else {
return recommendedHeading;
}
}
/**
* Updates the height to make sure the player doesn't go below the playable zone.
* If the current height is fine, it will return 0.
* If the current height is within the border margin, it will return a value (1 or -1)
* to make sure the player doesn't go below this value
*
* @return The recommended new height. 0 if no modification is needed.
*/
public int getRecommendedHeight(int current) {
double ty = progress[1][0];
double borderMarginY = progress[1][1];
if (ty < borderMarginY) {
// y should increase
return 1;
} else if (ty > 1 - borderMarginY) {
// y should decrease
return -1;
}
return current;
}
/**
* @return The progress array.
* @see #calculateParameterization(Vector)
*/
public double[][] getProgress() {
return progress;
}
}

View file

@ -0,0 +1,71 @@
package dev.loki.loparkour.generator;
import dev.efnilite.vilib.util.Colls;
import dev.efnilite.vilib.util.Probs;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
/**
* Generates a random offset (the sidestep) for a parkour block.
* Source: <a href="https://i.imgur.com/RPevMfX.png">here</a>.
*/
public class JumpOffsetGenerator {
private final int maxOffset;
/**
* Constructor.
*
* @param y The height difference.
*/
public JumpOffsetGenerator(int y, int distance) {
this.maxOffset = switch (y) {
case 1 -> switch (distance) {
case 1 -> 4;
case 2 -> 3;
default -> 2;
};
case 0 -> switch (distance) {
case 1, 2 -> 4;
case 3 -> 3;
default -> 2;
};
case -1 -> switch (distance) {
case 1, 2 -> 5;
case 3 -> 4;
default -> 3;
};
case -2 -> switch (distance) {
case 1, 2 -> 5;
default -> 4;
};
default -> throw new IllegalArgumentException("Unknown jump height difference %d".formatted(y));
};
}
/**
* @param mean The mean (average) value for the offset. Usually 0 to avoid parkour going only left or right.
* @param standardDeviation The standard deviation.
* @param random The random instance.
* @return A random jump-able offset.
*/
public int getRandomOffset(double mean, double standardDeviation, Random random) {
Map<Integer, Double> distribution = Colls.range(-maxOffset, maxOffset + 1)
.stream()
.collect(Collectors.toMap(offset -> offset, offset -> Probs.normalpdf(mean, standardDeviation, offset)));
return Probs.random(distribution, random);
}
/**
* @param mean The mean (average) value for the offset. Usually 0 to avoid parkour going only left or right.
* @param standardDeviation The standard deviation.
* @return A random jump-able offset.
*/
public int getRandomOffset(double mean, double standardDeviation) {
return getRandomOffset(mean, standardDeviation, ThreadLocalRandom.current());
}
}

View file

@ -0,0 +1,843 @@
package dev.loki.loparkour.generator;
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;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.leaderboard.Score;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.mode.Mode;
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.session.Session;
import dev.loki.loparkour.style.Style;
import dev.loki.loparkour.world.Divider;
import dev.efnilite.vilib.particle.ParticleData;
import dev.efnilite.vilib.particle.Particles;
import dev.efnilite.vilib.schematic.Schematic;
import dev.efnilite.vilib.schematic.Schematics;
import dev.efnilite.vilib.util.Colls;
import dev.efnilite.vilib.util.Locations;
import dev.efnilite.vilib.util.Probs;
import dev.efnilite.vilib.util.Task;
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.scheduler.BukkitTask;
import org.bukkit.util.BoundingBox;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* The class that generates the parkour, which each {@link ParkourPlayer} has.
*
* @author loki
* @since 5.0.0
*/
public class ParkourGenerator {
/**
* The amount of blocks that trail behind the player.
*/
public static final int BLOCK_TRAIL = 2;
/**
* This generator's score
*/
public int score = 0;
/**
* The total score achieved in this Generator instance
*/
public int totalScore = 0;
/**
* The schematic cooldown
*/
public int schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
/**
* Whether this generator has been stopped
*/
public boolean stopped = false;
/**
* The zone in which the parkour can take place. (playable area)
*/
public Location[] zone;
/**
* The player
*/
public ParkourPlayer player;
/**
* The task used in checking the player's current location
*/
public BukkitTask task;
/**
* Where blocks from schematics spawn
*/
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();
/**
* The island instance.
*/
public final Island island;
/**
* The chances of which distance the jump should have
*/
public final Map<Integer, Double> distanceChances = new HashMap<>();
/**
* The chances of which height the jump should have
*/
public final Map<Integer, Double> heightChances = new HashMap<>();
/**
* The chances of which type of special jump
*/
public final Map<BlockData, Double> specialChances = new HashMap<>();
/**
* The chances of default jump types: schematic, 'special' (ice, etc.) or normal
*/
public final Map<JumpType, Double> defaultChances = new HashMap<>();
/**
* Whether the schematic should be deleted on the next jump.
*/
protected boolean deleteSchematic = false;
protected boolean waitForSchematicCompletion = false;
/**
* The last location the player was found standing in
*/
protected Location lastStandingPlayerLocation;
/**
* A list of blocks from the (possibly) spawned structure
*/
protected List<Block> schematicBlocks = new ArrayList<>();
/**
* The player's current position index.
*/
protected int lastPositionIndexPlayer = -1;
/**
* The history of generated blocks. The most recently generated block is the last item in the list.
*/
protected List<Block> history = new ArrayList<>();
/**
* Creates a new ParkourGenerator instance
*
* @param session The session.
* @param schematic The schematic to use for the spawn island.
* @param generatorOptions The options.
*/
public ParkourGenerator(@NotNull Session session, @Nullable Schematic 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);
calculateChances();
}
/**
* Creates a new ParkourGenerator instance.
*
* @param session The session.
* @param generatorOptions The options.
*/
public ParkourGenerator(@NotNull Session session, GeneratorOption... generatorOptions) {
this(session, Schematics.getSchematic(LoParkour.getPlugin(), "spawn-island"), generatorOptions);
}
/**
* Ensures generator preferences in profile can't be overridden by the player changing settings.
*/
public void overrideProfile() { }
/**
* Calculates all chances for every variable.
* Modification is possible in the generator constructor or through external map changes.
*/
protected void calculateChances() {
defaultChances.clear();
defaultChances.put(JumpType.DEFAULT, Option.TYPE_NORMAL);
defaultChances.put(JumpType.SCHEMATIC, Option.TYPE_SCHEMATICS);
defaultChances.put(JumpType.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_NEG1);
heightChances.put(-2, Option.NORMAL_HEIGHT_NEG2);
distanceChances.clear();
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);
specialChances.clear();
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);
}
/**
* Generates particles around blocks.
*
* @param blocks The blocks.
*/
protected void particles(List<Block> blocks) {
if (!profile.get("particles").asBoolean()) {
return;
}
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));
// display particle
switch (Option.PARTICLE_SHAPE) {
case DOT -> Particles.draw(center.add(0.5, 1, 0.5), data.speed(0.4).size(20).offsetX(0.5).offsetY(1).offsetZ(0.5));
case CIRCLE -> Particles.circle(center.add(0.5, 0.5, 0.5), data.size(5), (int) Math.sqrt(blocks.size()), 20);
case BOX -> Particles.box(BoundingBox.of(max, min), player.player.getWorld(), data.size(1), 0.2);
}
}
/**
* 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) {
Menus.PARKOUR_SETTINGS.open(player);
}
public void startTick() {
task = Task.create(LoParkour.getPlugin())
.repeat(1)
.execute(this::tick)
.run();
}
/**
* Starts the check
*/
public void tick() {
if (stopped) {
task.cancel();
return;
}
getPlayers().forEach(other -> {
updateVisualTime(other, other.selectedTime);
other.updateScoreboard(this);
other.player.setSaturation(20);
});
getSpectators().forEach(ParkourSpectator::update);
if (player.getLocation().getWorld() != lastStandingPlayerLocation.getWorld()) {
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();
}
waitForSchematicCompletion = false;
schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
generate(profile.get("blockLead").asInt());
deleteSchematic = true;
return;
}
if (!history.contains(blockBelowPlayer)) {
return; // player is on an unknown block
}
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;
}
lastStandingPlayerLocation = player.getLocation();
int blockLead = profile.get("blockLead").asInt();
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--) {
// avoid setting beginning block to air
if (i <= 0) {
continue;
}
history.get(i).setType(Material.AIR);
}
deleteSchematic();
for (int i = 0; i < (Config.CONFIG.getBoolean("scoring.all-points") ? deltaFromLast : 1); i++) { // score the difference
score();
}
if (start == null) { // start stopwatch when first point is achieved
start = Instant.now();
}
}
// updates the player time
protected void updateVisualTime(ParkourPlayer player, int selectedTime) {
int newTime = 18000 + selectedTime;
if (newTime >= 24000) {
newTime -= 24000;
}
player.player.setPlayerTime(newTime, false);
}
/**
* 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.logging().warn("## Incomplete joining setup.");
LoParkour.logging().warn("## There has probably been an error somewhere. Please report this error!");
LoParkour.logging().warn("## You don't have to report this warning.");
}
lastPositionIndexPlayer = 0;
if (!history.isEmpty()) {
history.remove(0);
history.forEach(block -> block.setType(Material.AIR, false));
history.clear();
}
waitForSchematicCompletion = false;
deleteSchematic = true;
deleteSchematic();
Leaderboard leaderboard = getMode().getLeaderboard();
int record = leaderboard != null ? leaderboard.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) {
registerScore(getDetailedTime(), Double.toString(getDifficultyScore()).substring(0, 3), score);
}
score = 0;
start = null;
heading = Option.HEADING.getDirection();
if (regenerate) { // generate back the blocks
player.teleport(playerSpawn);
generateFirst(playerSpawn, blockSpawn);
return;
}
island.destroy();
if (getPlayers().isEmpty()) {
getSpectators().forEach(spectator -> Modes.DEFAULT.create(spectator.player));
}
}
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)));
}
private void deleteSchematic() {
if (!deleteSchematic) {
return;
}
LoParkour.log("Deleting schematic");
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<JumpType, Double> chances = new HashMap<>(defaultChances);
if (schematicCooldown > 0 || generatorOptions.contains(GeneratorOption.DISABLE_SCHEMATICS) || profile.get("schematicDifficulty").asDouble() == 0.0 || !schematicBlocks.isEmpty()) {
chances.remove(JumpType.SCHEMATIC);
}
if (!profile.get("useSpecialBlocks").asBoolean()) {
chances.remove(JumpType.SPECIAL);
}
if (chances.isEmpty()) {
chances.put(JumpType.DEFAULT, 1.0);
}
JumpType jump = Probs.random(chances);
if (jump == JumpType.SCHEMATIC) {
LoParkour.log("Generating schematic jump at index %s".formatted(history.size() + 1));
double difficulty = profile.get("schematicDifficulty").asDouble();
Schematic schematic = Schematics.getSchematic(LoParkour.getPlugin(),
Colls.random(Schematics.getSchematicNames(LoParkour.getPlugin()).stream()
.filter(name -> name.contains("parkour-") && getDifficulty(name) <= difficulty)
.toList()));
schematicBlocks = rotatedPaste(schematic, selectBlocks().get(0).getLocation());
particles(schematicBlocks);
sound(schematicBlocks);
new ParkourSchematicGenerateEvent(schematic, this, player).call();
if (schematicBlocks.isEmpty()) {
LoParkour.logging().stack("Error while trying to paste schematic %s".formatted(schematic.getFile().getName()), new NoSuchElementException("No schematic blocks found"));
return;
}
schematicCooldown = Config.GENERATION.getInt("advanced.schematic-cooldown");
waitForSchematicCompletion = true;
return;
}
LoParkour.log("Generating normal jump at index %s".formatted(history.size() + 1));
List<Block> blocks = selectBlocks();
if (blocks.isEmpty()) {
LoParkour.logging().stack("Error while trying to generate parkour", new NoSuchElementException("No blocks to generate found"));
return;
}
List<Block> movedBlocks = new ArrayList<>();
for (Block block : blocks) {
BlockData data = (jump == JumpType.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(Schematic schematic, Location location) {
if (schematic == null || location == null) {
return Collections.emptyList();
}
Optional<Vector> optionalStart = schematic.getVectorBlockMap().entrySet().stream()
.filter(entry -> {
if (entry == null || entry.getValue() == null) return false;
return entry.getValue().getMaterial() == Material.LIME_WOOL;
})
.map(Map.Entry::getKey)
.findAny();
Optional<Vector> optionalEnd = schematic.getVectorBlockMap().entrySet().stream()
.filter(entry -> {
if (entry == null || entry.getValue() == null) return false;
return entry.getValue().getMaterial() == Material.RED_WOOL;
})
.map(Map.Entry::getKey)
.findAny();
if (optionalStart.isEmpty()) {
LoParkour.logging().stack("Error while trying to find start of schematic", "check if you placed a lime wool block");
return Collections.emptyList();
}
if (optionalEnd.isEmpty()) {
LoParkour.logging().stack("Error while trying to find end of schematic", "check if you placed a red wool block");
return Collections.emptyList();
}
Vector start = optionalStart.get();
Vector end = optionalEnd.get();
Vector startToEnd = end.clone().subtract(start);
/// snapped vector, supports no rotation if x == z
Vector snapped;
if (startToEnd.equals(heading)) {
snapped = new Vector(0, 0, 0);
} else if (Math.abs(startToEnd.getX()) > Math.abs(startToEnd.getZ())) {
snapped = new Vector(Math.signum(startToEnd.getX()), 0, 0);
} else {
snapped = new Vector(0, 0, Math.signum(startToEnd.getZ()));
}
// the angle between heading and normalized direction of schematic, snapped to 90 deg angles
double snappedAngle = angleInY(heading, snapped);
Location rotatedStart = location.clone().subtract(start.clone().rotateAroundY(snappedAngle));
Vector rotatedStartToEnd = startToEnd.clone().rotateAroundY(snappedAngle);
history.add(location.clone().add(rotatedStartToEnd).subtract(0, 1, 0).getBlock());
return schematic.paste(rotatedStart, snappedAngle); // only yaw
}
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);
}
private double getDifficulty(String fileName) {
String path = "difficulty.%s".formatted(fileName.split("[-.]")[1]);
if (!Config.SCHEMATICS.isPath(path)) {
return 1.0; // todo remove
}
return Config.SCHEMATICS.getDouble(path);
}
/**
* 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;
}
/**
* @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");
}
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) {
LoParkour.logging().stack("Invalid score time format %s".formatted(Config.CONFIG.getString("options.time.score-format")), ex);
return "";
}
}
/**
* @return This generator's mode.
*/
public Mode getMode() {
return Modes.DEFAULT;
}
/**
* @return The players in this session.
*/
@NotNull
public List<ParkourPlayer> getPlayers() {
return session.getPlayers();
}
/**
* @return The spectators in this session.
*/
@NotNull
public List<ParkourSpectator> getSpectators() {
return session.getSpectators();
}
public enum JumpType {
DEFAULT, SCHEMATIC, SPECIAL
}
}

View file

@ -0,0 +1,61 @@
package dev.loki.loparkour.generator;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
* Wrapper class for setting storing.
* Allows different modes to change settings without them being saved as being player-selected.
*/
public class Profile {
/**
* Map of the setting's name and the value.
*/
public final Map<String, ProfileValue> settings = new HashMap<>();
@NotNull
public Profile set(@NotNull String setting, @NotNull String value) {
settings.put(setting, new ProfileValue(value));
return this;
}
@NotNull
public ProfileValue get(@NotNull String setting) {
ProfileValue value = settings.get(setting);
return value == null ? new ProfileValue("") : value;
}
/**
* Represents a setting.
*
* @param value The string value
*/
public record ProfileValue(String value) {
/**
* @return true when the string value is "true", else "false".
*/
public boolean asBoolean() {
return value.equals("true"); // save parsing
}
/**
* @return The value as a double.
*/
public double asDouble() {
return Double.parseDouble(value);
}
/**
* @return The value as an int.
*/
public int asInt() {
return Integer.parseInt(value);
}
}
}

View file

@ -0,0 +1,17 @@
package dev.loki.loparkour.hook;
import org.bukkit.entity.Player;
import org.geysermc.floodgate.api.FloodgateApi;
public class FloodgateHook {
/**
* Whether this player is a bedrock player.
*
* @param player The player.
* @return True if this player is a bedrock player, false if not.
*/
public static boolean isBedrockPlayer(Player player) {
return FloodgateApi.getInstance().isFloodgatePlayer(player.getUniqueId());
}
}

View file

@ -0,0 +1,84 @@
package dev.loki.loparkour.hook;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.leaderboard.Score;
import dev.loki.loparkour.mode.Mode;
import me.filoghost.holographicdisplays.api.HolographicDisplaysAPI;
public class HoloHook {
/**
* Initializes this hook.
*/
public static void init() {
try {
Class.forName("me.filoghost.holographicdisplays.api.HolographicDisplaysAPI");
} catch (Exception ex) {
LoParkour.logging().warn("##");
LoParkour.logging().warn("## LoParkour only supports Holographic Displays v3.0.0 or higher!");
LoParkour.logging().warn("## This hook will now be disabled.");
LoParkour.logging().warn("##");
return;
}
HolographicDisplaysAPI.get(LoParkour.getPlugin()).registerGlobalPlaceholder("ip_leaderboard", 100, argument -> {
if (argument == null) {
return "?";
}
// {ip_leaderboard: default, score, #1}
String[] split = argument.replace(" ", "").split(",");
Mode mode = Registry.getMode(split[0].toLowerCase());
if (mode == null) {
return "?";
}
Leaderboard leaderboard = mode.getLeaderboard();
if (leaderboard == null) {
return "?";
}
String type = split[1].toLowerCase();
String rank = split[2].replace("#", "");
Score score = leaderboard.getScoreAtRank(Integer.parseInt(rank));
if (score == null) {
return "?";
}
return switch (type) {
case "score" -> Integer.toString(score.score());
case "name" -> score.name();
case "time" -> score.time();
case "difficulty" -> score.difficulty();
case "difficulty_string" -> parseDifficulty(score.difficulty());
default -> "?";
};
});
}
private static String parseDifficulty(String string) {
if (string.contains("?")) {
return "?";
}
double difficulty = Double.parseDouble(string);
if (difficulty <= 0.25) {
return "easy";
} else if (difficulty <= 0.5) {
return "medium";
} else if (difficulty <= 0.75) {
return "hard";
} else if (difficulty <= 1) {
return "very hard";
}
return "?";
}
}

View file

@ -0,0 +1,200 @@
package dev.loki.loparkour.hook;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.generator.ParkourGenerator;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.leaderboard.Score;
import dev.loki.loparkour.mode.Mode;
import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourSpectator;
import dev.loki.loparkour.player.ParkourUser;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PAPIHook extends PlaceholderExpansion {
private static final Pattern INFINITE_REGEX = Pattern.compile("(.+)_(\\d+)");
@Override
public @NotNull String getIdentifier() {
return "witp";
}
@Override
public @NotNull String getAuthor() {
return "loki";
}
@Override
public boolean canRegister() {
return true;
}
@Override
public boolean persist() {
return true;
}
@Override
public @NotNull String getVersion() {
return LoParkour.getPlugin().getDescription().getVersion();
}
@Override
public String onPlaceholderRequest(Player player, @NotNull String params) {
// placeholders that don't require a player
switch (params) {
case "version", "ver" -> {
return LoParkour.getPlugin().getDescription().getVersion();
}
case "leader", "record_player" -> {
Score score = Modes.DEFAULT.getLeaderboard().getScoreAtRank(1);
return score != null ? score.name() : "?";
}
case "leader_score", "record_score", "record" -> {
Score score = Modes.DEFAULT.getLeaderboard().getScoreAtRank(1);
return score != null ? Integer.toString(score.score()) : "?";
}
}
if (params.contains("player_rank_")) {
return getInfiniteScore(params.replace("player_rank_", ""), Score::name);
} else if (params.contains("score_rank_")) {
return getInfiniteScore(params.replace("score_rank_", ""), Score::score);
} else if (params.contains("time_rank_")) {
return getInfiniteScore(params.replace("time_rank_", ""), Score::time);
} else if (params.contains("difficulty_rank_")) {
return getInfiniteScore(params.replace("difficulty_rank_", ""), Score::difficulty);
} else if (params.contains("difficulty_string_rank_")) {
return getInfiniteScore(params.replace("difficulty_string_rank_", ""),
score -> parseDifficulty(Double.parseDouble(score.difficulty().contains("?") ? "2" : score.difficulty())));
}
// placeholders that require player
if (player == null) {
return "player doesn't exist";
}
switch (params) {
case "rank" -> {
return Integer.toString(Modes.DEFAULT.getLeaderboard().getRank(player.getUniqueId()));
}
case "highscore", "high_score" -> {
return Integer.toString(Modes.DEFAULT.getLeaderboard().get(player.getUniqueId()).score());
}
case "high_score_time" -> {
return Modes.DEFAULT.getLeaderboard().get(player.getUniqueId()).time();
}
}
ParkourUser user = ParkourUser.getUser(player);
ParkourPlayer pp = null;
if (user instanceof ParkourPlayer) {
pp = (ParkourPlayer) user;
} else if (user instanceof ParkourSpectator) {
pp = ((ParkourSpectator) user).closest;
}
if (pp != null && pp.session.generator != null) {
ParkourGenerator generator = pp.session.generator;
switch (params) {
case "score", "current_score" -> {
return Integer.toString(generator.score);
}
case "time", "current_time" -> {
return generator.getFormattedTime();
}
case "blocklead", "lead" -> {
return Integer.toString(pp.blockLead);
}
case "style" -> {
return pp.style;
}
case "time_pref", "time_preference" -> {
return Integer.toString(pp.selectedTime);
}
case "scoreboard" -> {
return pp.showScoreboard.toString();
}
case "difficulty" -> {
return Double.toString(pp.schematicDifficulty);
}
case "difficulty_string" -> {
return parseDifficulty(pp.schematicDifficulty);
}
default -> {
if (params.contains("score_until_")) {
String replaced = params.replace("score_until_", "");
int interval = Integer.parseInt(replaced);
if (interval > 0) {
return Integer.toString(interval - (generator.totalScore % interval)); // 100 - (5 % 100) = 95
} else {
return "0";
}
}
}
}
}
return null;
}
private String parseDifficulty(double difficulty) {
if (difficulty <= 0.25) {
return "easy";
} else if (difficulty <= 0.5) {
return "medium";
} else if (difficulty <= 0.75) {
return "hard";
} else if (difficulty <= 1) {
return "very hard";
}
return "?";
}
private String getInfiniteScore(String rankData, Function<Score, ?> f) {
int rank;
Leaderboard leaderboard = null;
Matcher matcher = INFINITE_REGEX.matcher(rankData);
// use mode-specific format
// x_mode_rank
if (matcher.matches()) {
String name = matcher.group(1);
rank = Integer.parseInt(matcher.group(2));
Mode mode = Registry.getMode(name);
if (mode != null) {
leaderboard = mode.getLeaderboard();
}
// use generic format
// x_rank
} else {
rank = Integer.parseInt(rankData);
}
if (leaderboard == null) {
leaderboard = Modes.DEFAULT.getLeaderboard();
}
if (rank > 0) {
Score score = leaderboard.getScoreAtRank(rank);
if (score == null) {
return "?";
} else {
return String.valueOf(f.apply(score));
}
} else {
return "?";
}
}
}

View file

@ -0,0 +1,39 @@
package dev.loki.loparkour.hook;
import dev.loki.loparkour.LoParkour;
import net.milkbowl.vault.economy.Economy;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.plugin.RegisteredServiceProvider;
import java.util.NoSuchElementException;
public class VaultHook {
private static Economy economy;
/**
* Deposits amount to the bal of player.
*
* @param player The player.
* @param amount The amount.
*/
public static void deposit(Player player, double amount) {
if (Bukkit.getPluginManager().getPlugin("Vault") == null) {
return;
}
if (economy == null) {
RegisteredServiceProvider<Economy> service = Bukkit.getServicesManager().getRegistration(Economy.class);
if (service == null) {
LoParkour.logging().stack("Error while trying to fetch the Vault economy", new NoSuchElementException("No economy found"));
return;
}
economy = service.getProvider();
}
economy.depositPlayer(player, amount);
}
}

View file

@ -0,0 +1,204 @@
package dev.loki.loparkour.leaderboard;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.storage.Storage;
import dev.efnilite.vilib.util.Task;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* Class for handling leaderboards.
*/
public class Leaderboard {
/**
* The mode that this leaderboard belongs to
*/
public final String mode;
/**
* The way in which items will be sorted.
*/
public final Sort sort;
/**
* A map of all scores for this mode
*/
public final Map<UUID, Score> scores = new LinkedHashMap<>();
public Leaderboard(@NotNull String mode, Sort sort) {
this.mode = mode.toLowerCase();
this.sort = sort;
Storage.init(mode);
// read all data
read(true);
var interval = Config.CONFIG.getInt("storage-update-interval");
// read/write all data every x seconds after x seconds to allow time for reading/writing
Task.create(LoParkour.getPlugin())
.delay(interval * 20)
.repeat(interval * 20)
.async()
.execute(Config.CONFIG.getBoolean("joining") ? () -> {
LoParkour.log("Periodic saving of leaderboard data of %s".formatted(mode));
write(true);
} : () -> {
LoParkour.log("Periodic reading of leaderboard data of %s".formatted(mode));
read(true);
})
.run();
}
/**
* 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);
}
/**
* 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));
sort();
}, async);
}
private void run(Runnable runnable, boolean async) {
if (async) {
Task.create(LoParkour.getPlugin()).async().execute(runnable).run();
} else {
runnable.run();
}
}
/**
* Returns sorted copy of the score map.
* @param sort The sorting method.
* @return A sorted map of scores.
*/
public Map<UUID, Score> sort(Sort sort) {
LinkedHashMap<UUID, Score> sorted = new LinkedHashMap<>();
scores.entrySet().stream()
.sorted((one, two) -> {
switch (sort) {
case SCORE -> {
int scoreComparison = two.getValue().score() - one.getValue().score();
if (scoreComparison != 0) {
return scoreComparison;
} else {
return one.getValue().getTimeMillis() - two.getValue().getTimeMillis();
}
}
case TIME -> {
return one.getValue().getTimeMillis() - two.getValue().getTimeMillis();
}
case DIFFICULTY -> {
return (int) Math.signum(Double.parseDouble(two.getValue().difficulty()) -
Double.parseDouble(one.getValue().difficulty()));
}
default -> throw new IllegalArgumentException("Invalid sort method");
}
})
.forEachOrdered(entry -> sorted.put(entry.getKey(), entry.getValue()));
return sorted;
}
// sorts all scores in the map
private void sort() {
var sorted = sort(sort);
scores.clear();
scores.putAll(sorted);
}
/**
* Registers a new score, overriding the old one
*
* @param uuid The player's uuid
* @param score The {@link Score} instance associated with a player's run
* @return the previous score, if there was one
*/
@Nullable
public Score put(@NotNull UUID uuid, @NotNull Score score) {
Score previous = scores.put(uuid, score);
sort();
return previous;
}
/**
* Resets the score of a player by deleting it from the internal map
*
* @param uuid The UUID
* @return the previous value if one was found
*/
@Nullable
public Score remove(@NotNull UUID uuid) {
return scores.remove(uuid);
}
/**
* Resets all registered scores for this mode
*/
public void resetAll() {
new HashSet<>(scores.keySet()).forEach(this::remove);
}
/**
* @param uuid The {@link UUID} to get.
* @return The {@link Score} associated with the player. If null, returns a {@link Score} instance with "?".
*/
@NotNull
public Score get(@NotNull UUID uuid) {
return scores.getOrDefault(uuid, new Score("?", "?", "?", 0));
}
/**
* @param uuid The uuid
* @return The rank. Starts from 1. Returns 0 if no ranking is found.
*/
public int getRank(@NotNull UUID uuid) {
return new ArrayList<>(scores.keySet()).indexOf(uuid) + 1;
}
/**
* Gets the score at a specified rank.
* Ranks start at 1.
*
* @param rank The rank
* @return the {@link Score} instance, null if one isn't found
*/
@Nullable
public Score getScoreAtRank(int rank) {
if (scores.size() < rank) {
return null;
}
return new ArrayList<>(scores.values()).get(rank - 1);
}
public enum Sort {
SCORE, TIME, DIFFICULTY
}
}

View file

@ -0,0 +1,42 @@
package dev.loki.loparkour.leaderboard;
/**
* Represents a record, used to keep track of the score a player may achieve.
*
* @param name The name of the player
* @param score The score achieved
* @param time The time it took to achieve this score
* @param difficulty The difficulty of this run
*/
public record Score(String name, String time, String difficulty, int score) {
/**
* Gets a {@link Score} instance from a string
*
* @param string The string
* @return a {@link Score} instance based off the provided string
*/
public static Score fromString(String string) {
String[] parts = string.split(",");
return new Score(parts[0], parts[1], parts[2], Integer.parseInt(parts[3]));
}
/**
* @return This score's time in millis.
*/
public int getTimeMillis() {
String[] split = time.split(":");
int m = Integer.parseInt(split[0]);
int s = Integer.parseInt(split[1]);
int ms = Integer.parseInt(split[2]);
return m * 60 * 1000 + s * 1000 + ms;
}
@Override
public String toString() {
return String.format("%s,%s,%s,%s", name, time, difficulty, score);
}
}

View file

@ -0,0 +1,76 @@
package dev.loki.loparkour.menu;
import dev.loki.loparkour.player.ParkourUser;
import dev.efnilite.vilib.inventory.Menu;
import dev.efnilite.vilib.inventory.item.MenuItem;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Predicate;
/**
* A class for menus where items are dynamically displayed; that is, depending on provided conditions.
* These conditions are specified by the param {@code shouldDisplay} in the method {@link #registerMainItem(int, int, BiFunction, Predicate)}.
*/
public abstract class DynamicMenu {
protected final Map<Integer, List<ItemContainer>> registeredItems = new HashMap<>();
/**
* Registers an item that will be displayed in a specific slot if the specified condition is met.
* This will be used to create a context-aware main menu.
*
* @param row The row in which this item will be displayed. Starts from 0 and ends at 5.
* @param id The id of this item. This will be used to determine the positions of each item.
* Lower is more to the left. Can't be lower than 0.
* @param item The item
* @param shouldDisplay Whether this item should be displayed right now.
*/
public void registerMainItem(int row, int id, BiFunction<@NotNull Player, @Nullable ParkourUser, MenuItem> item, Predicate<Player> shouldDisplay) {
if (id < 0 || row < 0 || row > 4) {
return;
}
List<ItemContainer> existing = registeredItems.get(row);
if (existing == null) {
existing = new ArrayList<>();
}
existing.add(new ItemContainer(id, item, shouldDisplay));
registeredItems.put(row, existing);
}
/**
* Opens this dynamic menu to a player
*
* @param player The player
* @param menu The menu
*/
public void display(Player player, Menu menu) {
ParkourUser user = ParkourUser.getUser(player);
for (int row : registeredItems.keySet()) {
int actualSlot = row * 9; // 0, 9, 18, etc.
List<ItemContainer> containers = registeredItems.get(row); // sort by id first
containers.sort(Comparator.comparingInt(container -> container.id));
for (ItemContainer container : containers) {
if (container.predicate.test(player)) { // if item in id passes predicate, display it in the menu
menu.item(actualSlot, container.item.apply(player, user));
actualSlot++;
}
}
}
menu.open(player);
}
/**
* Data class for registered items
*/
protected record ItemContainer(int id, BiFunction<@NotNull Player, @Nullable ParkourUser, MenuItem> item,
Predicate<Player> predicate) {}
}

View file

@ -0,0 +1,24 @@
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.efnilite.vilib.inventory.Menu;
import org.bukkit.entity.Player;
public class MainMenu extends DynamicMenu {
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 void open(Player player) {
display(player, new Menu(3, Locales.getString(player, "main.name"))
.distributeRowsEvenly());
}
}

View file

@ -0,0 +1,39 @@
package dev.loki.loparkour.menu;
import dev.loki.loparkour.menu.community.CommunityMenu;
import dev.loki.loparkour.menu.community.LeaderboardsMenu;
import dev.loki.loparkour.menu.community.SingleLeaderboardMenu;
import dev.loki.loparkour.menu.lobby.LobbyMenu;
import dev.loki.loparkour.menu.lobby.PlayerManagementMenu;
import dev.loki.loparkour.menu.play.PlayMenu;
import dev.loki.loparkour.menu.play.SingleMenu;
import dev.loki.loparkour.menu.play.SpectatorMenu;
import dev.loki.loparkour.menu.settings.LangMenu;
import dev.loki.loparkour.menu.settings.ParkourSettingsMenu;
import dev.loki.loparkour.menu.settings.SettingsMenu;
public class Menus {
// main
public static MainMenu MAIN = new MainMenu();
// play
public static PlayMenu PLAY = new PlayMenu();
public static SingleMenu SINGLE = new SingleMenu();
public static SpectatorMenu SPECTATOR = new SpectatorMenu();
// community
public static CommunityMenu COMMUNITY = new CommunityMenu();
public static LeaderboardsMenu LEADERBOARDS = new LeaderboardsMenu();
public static SingleLeaderboardMenu SINGLE_LEADERBOARD = new SingleLeaderboardMenu();
// settings
public static SettingsMenu SETTINGS = new SettingsMenu();
public static LangMenu LANG = new LangMenu();
public static ParkourSettingsMenu PARKOUR_SETTINGS = new ParkourSettingsMenu();
// lobby
public static LobbyMenu LOBBY = new LobbyMenu();
public static PlayerManagementMenu PLAYER_MANAGEMENT = new PlayerManagementMenu();
}

View file

@ -0,0 +1,81 @@
package dev.loki.loparkour.menu;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Option;
import org.bukkit.permissions.Permissible;
/**
* An enum for all Parkour Menu Options
*/
public enum ParkourOption {
// main
MAIN("main", "LoParkour.main"),
// play
PLAY("play", "LoParkour.play"),
SINGLE("play.single", "LoParkour.play.single"),
SPECTATOR("play.spectator", "LoParkour.play.spectator"),
// community
COMMUNITY("community", "LoParkour.community"),
LEADERBOARDS("community.leaderboards", "LoParkour.community.leaderboards"),
// settings
SETTINGS("settings", "LoParkour.settings"),
PARKOUR_SETTINGS("settings.parkour_settings.item", "LoParkour.settings.parkour_settings"),
STYLES("settings.parkour_settings.items.styles", "LoParkour.settings.styles"),
LEADS("settings.parkour_settings.items.leads", "LoParkour.settings.leads"),
TIME("settings.parkour_settings.items.time", "LoParkour.settings.time"),
SCHEMATICS("settings.parkour_settings.items.schematics", "LoParkour.settings.schematics"),
SCOREBOARD("settings.parkour_settings.items.scoreboard", "LoParkour.settings.show_scoreboard"),
FALL_MESSAGE("settings.parkour_settings.items.fall_message", "LoParkour.settings.fall_message"),
PARTICLES("settings.parkour_settings.items.particles", "LoParkour.settings.particles"),
SOUND("settings.parkour_settings.items.sound", "LoParkour.settings.sound"),
SPECIAL_BLOCKS("settings.parkour_settings.items.special_blocks", "LoParkour.settings.special_blocks"),
LANG("settings.lang", "LoParkour.settings.lang"),
CHAT("settings.chat", "LoParkour.settings.chat"),
// lobby
LOBBY("lobby", "LoParkour.lobby"),
VISIBILITY("lobby.visibility", "LoParkour.lobby.visibility"),
PLAYER_MANAGEMENT("lobby.player_management", "LoParkour.lobby.player_management"),
// other
JOIN("join", "LoParkour.join"),
QUIT("quit", "LoParkour.quit"),
ADMIN("admin", "LoParkour.admin");
/**
* The path in config files for this option.
*/
public final String path;
/**
* The permission for this option.
*/
public final String permission;
ParkourOption(String path, String permission) {
this.path = path;
this.permission = permission;
}
/**
* @param permissible The player
* @return True if the player is allowed to view/perform this option, false if not.
*/
public boolean mayPerform(Permissible permissible) {
boolean value = Option.OPTIONS_ENABLED.getOrDefault(this, true);
if (value) {
if (Config.CONFIG.getBoolean("permissions.enabled")) {
return permissible.hasPermission(permission);
}
return true;
}
return false;
}
}

View file

@ -0,0 +1,24 @@
package dev.loki.loparkour.menu.community;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.menu.DynamicMenu;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import dev.efnilite.vilib.inventory.Menu;
import org.bukkit.entity.Player;
/**
* The menu for all community-related things
*/
public class CommunityMenu extends DynamicMenu {
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);
}
public void open(Player player) {
display(player, new Menu(3, Locales.getString(player, "community.name"))
.distributeRowsEvenly());
}
}

View file

@ -0,0 +1,57 @@
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.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.mode.Mode;
import dev.loki.loparkour.player.ParkourUser;
import dev.efnilite.vilib.inventory.PagedMenu;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.inventory.item.MenuItem;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import java.util.ArrayList;
import java.util.List;
/**
* Leaderboards menu
*/
public class LeaderboardsMenu {
public void open(Player player) {
ParkourUser user = ParkourUser.getUser(player);
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
PagedMenu menu = new PagedMenu(3, Locales.getString(player, "%s.name".formatted(ParkourOption.LEADERBOARDS.path)));
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 (items.size() == 1) {
Menus.SINGLE_LEADERBOARD.open(player, latest, 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);
}
}

View file

@ -0,0 +1,137 @@
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.ParkourOption;
import dev.loki.loparkour.mode.Mode;
import dev.loki.loparkour.player.ParkourUser;
import dev.efnilite.vilib.inventory.PagedMenu;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.inventory.item.MenuItem;
import dev.efnilite.vilib.util.SkullSetter;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
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 void open(Player player, Mode mode, Leaderboard.Sort sort) {
Leaderboard leaderboard = mode.getLeaderboard();
if (leaderboard == null) {
return;
}
// 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);
}
}

View file

@ -0,0 +1,79 @@
package dev.loki.loparkour.menu.lobby;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.menu.DynamicMenu;
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.efnilite.vilib.inventory.Menu;
import dev.efnilite.vilib.inventory.item.SliderItem;
import org.bukkit.entity.Player;
import java.util.List;
public class LobbyMenu extends DynamicMenu {
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);
return ParkourOption.PLAYER_MANAGEMENT.mayPerform(player) && user instanceof ParkourPlayer && user.session.getPlayers().get(0) == user;
});
registerMainItem(1, 1, (player, user) -> {
if (user == null) {
return null;
}
List<String> values = Locales.getStringList(user.locale, "lobby.visibility.values");
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 (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;
});
}, 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);
}
/**
* Opens the main menu.
*
* @param player The player to open the menu to
*/
public void open(Player player) {
display(player, new Menu(3, Locales.getString(player, "lobby.name"))
.distributeRowsEvenly());
}
}

View file

@ -0,0 +1,118 @@
package dev.loki.loparkour.menu.lobby;
import dev.loki.loparkour.config.Locales;
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.efnilite.vilib.inventory.PagedMenu;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.util.SkullSetter;
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.SkullMeta;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Menu for managing players
*/
public class PlayerManagementMenu {
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);
}
private void add(PagedMenu menu, ParkourUser viewer, Collection<ParkourUser> users) {
Session session = viewer.session;
for (ParkourUser other : users) {
if (other == viewer) {
continue;
}
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("\\|\\|")));
}
// 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);
}
}
});
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));
}
}
}

View file

@ -0,0 +1,38 @@
package dev.loki.loparkour.menu.play;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.menu.DynamicMenu;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.menu.ParkourOption;
import dev.efnilite.vilib.inventory.Menu;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
/**
* The menu where players can join modes
*/
public class PlayMenu extends DynamicMenu {
public PlayMenu() {
registerMainItem(1, 0, (player, user) -> Locales.getItem(player, "play.single.item")
.click(event -> Menus.SINGLE.open(event.getPlayer())),
ParkourOption.SINGLE::mayPerform);
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);
}
public void open(Player player) {
display(player, new Menu(3, Locales.getString(player, "play.name"))
.distributeRowsEvenly());
}
}

View file

@ -0,0 +1,68 @@
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.mode.Mode;
import dev.loki.loparkour.mode.MultiMode;
import dev.loki.loparkour.player.ParkourUser;
import dev.efnilite.vilib.inventory.PagedMenu;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.inventory.item.MenuItem;
import org.bukkit.Material;
import org.bukkit.entity.Player;
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 void open(Player player) {
ParkourUser user = ParkourUser.getUser(player);
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
List<Mode> modes = Registry.getModes();
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);
}
}));
}
if (modeSet.size() == 1) {
modeSet.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);
}
}

View file

@ -0,0 +1,84 @@
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.mode.Modes;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.session.Session;
import dev.loki.loparkour.world.Divider;
import dev.efnilite.vilib.inventory.PagedMenu;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.inventory.item.MenuItem;
import dev.efnilite.vilib.util.SkullSetter;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.SkullMeta;
import java.util.ArrayList;
import java.util.List;
/**
* The menu to select other players to spectate
*/
public class SpectatorMenu {
public void open(Player player) {
ParkourUser user = ParkourUser.getUser(player);
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
PagedMenu spectator = new PagedMenu(3, Locales.getString(player, "play.spectator.name"));
List<MenuItem> display = 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;
}
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);
}
}
}
item.click(event -> Modes.SPECTATOR.create(player, session));
display.add(item);
}
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);
}
}

View file

@ -0,0 +1,46 @@
package dev.loki.loparkour.menu.settings;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.menu.Menus;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.efnilite.vilib.inventory.PagedMenu;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.inventory.item.MenuItem;
import org.bukkit.Material;
import java.util.ArrayList;
import java.util.List;
public class LangMenu {
/**
* Opens the language menu
*
* @param user The ParkourPlayer instance
*/
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);
}
}

View file

@ -0,0 +1,344 @@
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.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 dev.efnilite.vilib.fastboard.FastBoard;
import dev.efnilite.vilib.inventory.Menu;
import dev.efnilite.vilib.inventory.MenuClickEvent;
import dev.efnilite.vilib.inventory.PagedMenu;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.inventory.item.MenuItem;
import dev.efnilite.vilib.inventory.item.SliderItem;
import dev.efnilite.vilib.inventory.item.TimedItem;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.entity.Player;
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 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);
}
/**
* Shows the main menu to a valid ParkourPlayer instance
*
* @param user The ParkourPlayer
*/
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()))));
}
/**
* Opens the style menu
*
* @param player The ParkourPlayer instance
*/
public void openStyleMenu(ParkourPlayer player) {
// init menu
PagedMenu menu = new PagedMenu(3, Locales.getString(player.locale, ParkourOption.STYLES.path + ".name"));
List<MenuItem> items = new ArrayList<>();
for (Style style : Registry.getStyles()) {
var name = style.getName();
var perm = "%s.%s".formatted(ParkourOption.STYLES.permission, name.toLowerCase());
if (Config.CONFIG.getBoolean("permissions.per-style") && !player.player.hasPermission(perm.replace(" ", "."))) {
continue;
}
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);
}));
}
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);
}
private boolean handleSettingChange(ParkourPlayer player, Runnable onAllowed) {
onAllowed.run();
player.updateGeneratorSettings(player.session.generator);
return true;
}
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);
}
}

View file

@ -0,0 +1,75 @@
package dev.loki.loparkour.menu.settings;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.menu.DynamicMenu;
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.efnilite.vilib.inventory.Menu;
import dev.efnilite.vilib.inventory.item.SliderItem;
import org.bukkit.entity.Player;
import java.util.List;
/**
* The menu for settings
*/
public class SettingsMenu extends DynamicMenu {
public SettingsMenu() {
registerMainItem(1, 0, (player, user) -> Locales.getItem(player, "settings.parkour_settings.item").click(event -> {
ParkourPlayer pp = ParkourPlayer.getPlayer(event.getPlayer());
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;
});
}, 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);
}
public void open(Player player) {
display(player, new Menu(3, Locales.getString(player, "settings.name")).distributeRowsEvenly());
}
}

View file

@ -0,0 +1,55 @@
package dev.loki.loparkour.mode;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.generator.ParkourGenerator;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.session.Session;
import dev.efnilite.vilib.inventory.item.Item;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* The default parkour mode
*/
public class DefaultMode implements Mode {
private final Leaderboard leaderboard = new Leaderboard(getName(), Leaderboard.Sort.SCORE);
@Override
@NotNull
public String getName() {
return "default";
}
@Override
@Nullable
public Item getItem(String locale) {
return Locales.getItem(locale, "play.single.default");
}
@Override
@NotNull
public Leaderboard getLeaderboard() {
return leaderboard;
}
@Override
public void create(Player player) {
if (!Config.CONFIG.getBoolean("joining")) {
player.sendMessage("Joining is currently disabled.");
return;
}
ParkourPlayer pp = ParkourPlayer.getPlayer(player);
if (pp != null && pp.session.generator != null && pp.session.generator.getMode() instanceof DefaultMode) {
return;
}
player.closeInventory();
Session.create(ParkourGenerator::new, null, null, player);
}
}

View file

@ -0,0 +1,38 @@
package dev.loki.loparkour.mode;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.efnilite.vilib.inventory.item.Item;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Interface for all modes.
* Every registered mode needs to inherit this class, because it needs identifying functions.
*/
public interface Mode {
/**
* Method that gets called when a mode is clicked in the menu or joined using /parkour join.
*
* @param player The player.
*/
void create(Player player);
/**
* @param locale The locale of the menu, used to adjust the name.
* @return The item used in menus to show this mode. If this item is null, the mode won't be displayed.
*/
@Nullable Item getItem(String locale);
/**
* @return The {@link Leaderboard} that belongs to this mode
*/
@Nullable Leaderboard getLeaderboard();
/**
* @return The internal name used for this mode.
*/
@NotNull String getName();
}

View file

@ -0,0 +1,14 @@
package dev.loki.loparkour.mode;
import dev.loki.loparkour.api.Registry;
public class Modes {
public static DefaultMode DEFAULT;
public static SpectatorMode SPECTATOR;
public static void init() {
DEFAULT = (DefaultMode) Registry.getMode("default");
SPECTATOR = (SpectatorMode) Registry.getMode("spectator");
}
}

View file

@ -0,0 +1,29 @@
package dev.loki.loparkour.mode;
import dev.loki.loparkour.session.Session;
import org.bukkit.entity.Player;
/**
* Class for modes featuring multiple players.
*/
public interface MultiMode extends Mode {
/**
* Adds a player to this mode.
*
* @param player The player
*/
void join(Player player, Session session);
/**
* Removes a player from this mode.
*
* @param player The player
*/
void leave(Player player, Session session);
/**
* @return The max player count.
*/
int getMaxPlayers();
}

View file

@ -0,0 +1,67 @@
package dev.loki.loparkour.mode;
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.player.ParkourSpectator;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.session.Session;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.util.Strings;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class SpectatorMode implements Mode {
@Override
public @NotNull String getName() {
return "spectator";
}
@Override
@Nullable
public Item getItem(String locale) {
return null;
}
@Override
@Nullable
public Leaderboard getLeaderboard() {
return null;
}
@Override
public void create(Player player) {
Menus.SPECTATOR.open(player);
}
public void create(Player player, Session session) {
if (!Config.CONFIG.getBoolean("joining")) {
player.sendMessage(Strings.colour("<red><bold>Joining is currently disabled."));
return;
}
if (!ParkourOption.SPECTATOR.mayPerform(player)) {
player.sendMessage(Locales.getString(player, "other.no_do"));
return;
}
ParkourUser user = ParkourUser.getUser(player);
ParkourSpectator spectator;
if (session.getPlayers().isEmpty()) {
return;
}
if (user != null) {
ParkourUser.unregister(user, false, false, false);
spectator = new ParkourSpectator(player, session, user.previousData);
} else {
spectator = new ParkourSpectator(player, session, null);
}
session.addSpectators(spectator);
}
}

View file

@ -0,0 +1,235 @@
package dev.loki.loparkour.player;
import com.google.gson.annotations.Expose;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.generator.ParkourGenerator;
import dev.loki.loparkour.generator.Profile;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.mode.MultiMode;
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 dev.efnilite.vilib.inventory.Menu;
import dev.efnilite.vilib.inventory.item.Item;
import dev.efnilite.vilib.util.Colls;
import dev.efnilite.vilib.util.Task;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.BiConsumer;
/**
* Subclass of {@link ParkourUser}. This class is used for players who are actively playing Parkour in any (default) mode.
*
* @author loki
*/
public class ParkourPlayer extends ParkourUser {
public static final Map<String, OptionContainer> PLAYER_COLUMNS = new HashMap<>();
static {
PLAYER_COLUMNS.put("uuid", new OptionContainer(null, null));
PLAYER_COLUMNS.put("style", new OptionContainer(ParkourOption.STYLES, (player, v) -> player.style = v));
PLAYER_COLUMNS.put("blockLead", new OptionContainer(ParkourOption.LEADS, (player, v) -> player.blockLead = Integer.parseInt(v)));
PLAYER_COLUMNS.put("useParticles", new OptionContainer(ParkourOption.PARTICLES, (player, v) -> player.particles = parseBoolean(v)));
PLAYER_COLUMNS.put("useSpecial", new OptionContainer(ParkourOption.SPECIAL_BLOCKS, (player, v) -> player.useSpecialBlocks = parseBoolean(v)));
PLAYER_COLUMNS.put("showFallMsg", new OptionContainer(ParkourOption.FALL_MESSAGE, (player, v) -> player.showFallMessage = parseBoolean(v)));
PLAYER_COLUMNS.put("showScoreboard", new OptionContainer(ParkourOption.SCOREBOARD, (player, v) -> player.showScoreboard = parseBoolean(v)));
PLAYER_COLUMNS.put("selectedTime", new OptionContainer(ParkourOption.TIME, (player, v) -> player.selectedTime = Integer.parseInt(v)));
PLAYER_COLUMNS.put("collectedRewards", new OptionContainer(null, (player, v) -> {
player.collectedRewards = new ArrayList<>();
if (!v.isEmpty()) {
player.collectedRewards.addAll(Arrays.stream(v.replaceAll("[ \\[\\]]", "").split(","))
.distinct()
.toList());
}
}));
PLAYER_COLUMNS.put("locale", new OptionContainer(ParkourOption.LANG, (player, v) -> {
player._locale = v;
player.locale = v;
}));
PLAYER_COLUMNS.put("schematicDifficulty", new OptionContainer(ParkourOption.SCHEMATICS, (player, v) -> player.schematicDifficulty = Double.parseDouble(v)));
PLAYER_COLUMNS.put("sound", new OptionContainer(ParkourOption.SOUND, (player, v) -> player.sound = parseBoolean(v)));
}
public @Expose Double schematicDifficulty;
public @Expose Integer blockLead;
public @Expose Boolean particles;
public @Expose Boolean sound;
public @Expose Boolean useSpecialBlocks;
public @Expose Boolean showFallMessage;
public @Expose Boolean showScoreboard;
public @Expose Integer selectedTime;
public @Expose String style;
public @Expose String _locale;
public @Expose List<String> collectedRewards;
/**
* Creates a new instance of a ParkourPlayer<br>
* If you are using the API, please use {@link ParkourPlayer#register(Player, Session)} instead
*/
public ParkourPlayer(@NotNull Player player, @NotNull Session session, @Nullable PreviousData previousData) {
super(player, session, previousData);
this._locale = locale;
// generic player settings
player.setFlying(false);
player.setAllowFlight(false);
player.setInvisible(false);
for (PotionEffect effect : player.getActivePotionEffects()) {
player.removePotionEffect(effect.getType());
}
}
private static boolean parseBoolean(String string) {
return string == null
|| string.equals("1") // for MySQL
|| string.equals("true"); // for disk
}
/**
* @param player The player.
* @return True when this player is a {@link ParkourPlayer}, false if not.
*/
public static boolean isPlayer(@Nullable Player player) {
return player != null && getPlayers().stream().anyMatch(other -> other.player == player);
}
/**
* @param player The player.
* @return player as a {@link ParkourPlayer}, null if not found.
*/
public static @Nullable ParkourPlayer getPlayer(@NotNull Player player) {
return getPlayers().stream()
.filter(other -> other.getUUID() == player.getUniqueId())
.findAny()
.orElse(null);
}
/**
* @return List with all players.
*/
public static List<ParkourPlayer> getPlayers() {
return Divider.sections.keySet().stream()
.flatMap(session -> session.getPlayers().stream())
.toList();
}
@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) {
mode.leave(player, session);
}
session.removePlayers(this);
save(LoParkour.getPlugin().isEnabled());
}
/**
* Sets the user's settings. If an item is not included, the setting gets reset.
*
* @param settings The settings map.
*/
public void setSettings(@NotNull Map<String, Object> settings) {
for (String key : PLAYER_COLUMNS.keySet()) {
Object value = settings.get(key);
OptionContainer container = PLAYER_COLUMNS.get(key);
if (container.consumer == null) {
continue;
}
if (value == null || !Option.OPTIONS_ENABLED.getOrDefault(container.option, true)) {
container.consumer.accept(this, Option.OPTIONS_DEFAULTS.getOrDefault(container.option, ""));
continue;
}
container.consumer.accept(this, String.valueOf(value));
}
}
/**
* Forces this player's generator to match the settings of this player.
*/
public void updateGeneratorSettings(ParkourGenerator generator) {
Profile profile = generator.profile;
profile.set("schematicDifficulty", schematicDifficulty.toString())
.set("blockLead", blockLead.toString())
.set("particles", particles.toString())
.set("sound", sound.toString())
.set("useSpecialBlocks", useSpecialBlocks.toString())
.set("showFallMessage", showFallMessage.toString())
.set("showScoreboard", showScoreboard.toString())
.set("selectedTime", selectedTime.toString())
.set("style", style);
generator.overrideProfile();
}
/**
* Saves the player's data to their file
*/
public void save(boolean async) {
Runnable write = () -> Storage.writePlayer(this);
if (async) {
Task.create(LoParkour.getPlugin()).async().execute(write).run();
} else {
write.run();
}
}
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")) {
Task.create(LoParkour.getPlugin()).delay(5).execute(() -> {
LoParkour.log("Setting up inventory for player %s".formatted(player.getName()));
player.getInventory().clear();
List<Item> items = new ArrayList<>();
if (ParkourOption.PLAY.mayPerform(player)) items.add(Locales.getItem(locale, "play.item"));
if (ParkourOption.COMMUNITY.mayPerform(player)) items.add(Locales.getItem(locale, "community.item"));
if (ParkourOption.SETTINGS.mayPerform(player)) items.add(Locales.getItem(locale, "settings.item"));
if (ParkourOption.LOBBY.mayPerform(player)) items.add(Locales.getItem(locale, "lobby.item"));
if (ParkourOption.QUIT.mayPerform(player)) items.add(Locales.getItem(locale, "other.quit"));
List<Integer> slots = Menu.getEvenlyDistributedSlots(items.size());
Colls.range(0, items.size()).forEach(idx -> player.getInventory().setItem(slots.get(idx), items.get(idx).build()));
}).run();
} else {
sendTranslated("other.customize");
}
}
public record OptionContainer(ParkourOption option, BiConsumer<ParkourPlayer, String> consumer) {
}
}

View file

@ -0,0 +1,110 @@
package dev.loki.loparkour.player;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.api.event.ParkourSpectateEvent;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.player.data.PreviousData;
import dev.loki.loparkour.session.Session;
import dev.efnilite.vilib.util.Strings;
import dev.efnilite.vilib.util.Task;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.GameMode;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitTask;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Comparator;
/**
* Class for spectators of a Session.
*
* @author loki
*/
public class ParkourSpectator extends ParkourUser {
private final BukkitTask closestChecker;
/**
* The closest player.
*/
@NotNull
public ParkourPlayer closest;
public ParkourSpectator(@NotNull Player player, @NotNull Session session, @Nullable PreviousData previousData) {
super(player, session, previousData);
this.closest = session.getPlayers().get(0);
new ParkourSpectateEvent(this).call();
Task.create(LoParkour.getPlugin())
.delay(1)
.execute(() -> {
teleport(closest.getLocation());
sendTranslated("play.spectator.join");
player.setGameMode(GameMode.SPECTATOR);
player.setAllowFlight(true);
player.setFlying(true);
if (ParkourUser.isBedrockPlayer(player)) { // bedrock has no spectator mode, so just make the player invisible
player.setInvisible(true);
player.setCollidable(false);
}
})
.run();
closestChecker = Task.create(LoParkour.getPlugin())
.async()
.delay(1)
.repeat(10)
.execute(() -> {
if (session.getPlayers().isEmpty()) {
return;
}
closest = session.getPlayers().stream()
.min(Comparator.comparing(other -> other.getLocation().distanceSquared(player.getLocation()))) // x or x^2 doesn't matter in getting smallest
.orElse(closest);
})
.run();
}
/**
* Updates the spectator's action bar, scoreboard and checks distance.
*/
public void update() {
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(Strings.colour(Locales.getString(player, "play.spectator.action_bar"))));
player.setGameMode(GameMode.SPECTATOR);
updateScoreboard(session.generator);
// spectator is still being teleported to world
if (closest.getLocation().getWorld() != player.getLocation().getWorld()) {
return;
}
if (closest.getLocation().distanceSquared(player.getLocation()) < 100 * 100) { // avoid sqrt
return;
}
teleport(closest.getLocation());
if (player.getGameMode() != GameMode.SPECTATOR) { // if player isn't in spectator or is a bedrock player
return;
}
// if player is a spectator
player.setSpectatorTarget(null);
player.setSpectatorTarget(player.getSpectatorTarget());
}
/**
* Stops the closest checker runnable.
*/
@Override
public void unregister() {
closestChecker.cancel();
session.removeSpectators(this);
player.setInvisible(false);
}
}

View file

@ -0,0 +1,343 @@
package dev.loki.loparkour.player;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.api.event.ParkourJoinEvent;
import dev.loki.loparkour.api.event.ParkourLeaveEvent;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.generator.ParkourGenerator;
import dev.loki.loparkour.hook.FloodgateHook;
import dev.loki.loparkour.leaderboard.Leaderboard;
import dev.loki.loparkour.leaderboard.Score;
import dev.loki.loparkour.menu.ParkourOption;
import dev.loki.loparkour.mode.Mode;
import dev.loki.loparkour.mode.Modes;
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 dev.efnilite.vilib.fastboard.FastBoard;
import dev.efnilite.vilib.util.Strings;
import io.papermc.lib.PaperLib;
import me.clip.placeholderapi.PlaceholderAPI;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.plugin.messaging.ChannelNotRegisteredException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Superclass of every type of player. This encompasses every player currently in the Parkour world.
* This includes active players ({@link ParkourPlayer}) and spectators ({@link ParkourSpectator}).
*
* @author loki
*/
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();
Storage.readPlayer(pp);
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;
}
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();
if (user.board != null && !user.board.isDeleted()) {
user.board.delete();
}
} catch (Exception ex) { // safeguard to prevent people from losing data
LoParkour.logging().stack("Error while trying to make player %s leave".formatted(user.getName()), ex);
user.send("<red><bold>There was an error while trying to handle leaving.");
}
if (restorePreviousData && Config.CONFIG.getBoolean("bungeecord.enabled") && kickIfBungee) {
sendPlayerToServer(user.player, Config.CONFIG.getString("bungeecord.return_server"));
return;
}
if (!restorePreviousData) return;
user.previousData.apply(user.player, urgent);
Mode mode = user.session.generator.getMode();
if (mode == null) {
LoParkour.logging().error("Mode is null for %s".formatted(user.getName()));
mode = Modes.DEFAULT;
}
if (user instanceof ParkourPlayer player) {
Mode finalMode = mode;
user.previousData.onLeave.forEach(r -> r.execute(player, finalMode));
}
}
// 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");
out.writeUTF(server);
try {
player.sendPluginMessage(LoParkour.getPlugin(), "BungeeCord", out.toByteArray());
} catch (ChannelNotRegisteredException ex) {
LoParkour.logging().stack("Error while trying to send %s to server %s. This server is not registered.".formatted(player.getName(), server), ex);
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())
.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);
/**
* This user's scoreboard
*/
public FastBoard board;
/**
* This user's PreviousData
*/
@NotNull
public PreviousData previousData;
/**
* The selected {@link Session.ChatType}
*/
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) {
this.player = player;
this.session = session;
this.joined = Instant.now();
this.previousData = previousData == null ? new PreviousData(player) : previousData;
if (Boolean.parseBoolean(Option.OPTIONS_DEFAULTS.get(ParkourOption.SCOREBOARD))) {
this.board = new FastBoard(player);
}
}
/**
* 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);
}
/**
* Sends a message.
*
* @param message The message
*/
public void send(String message) {
player.sendMessage(Strings.colour(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.
*/
public void updateScoreboard(ParkourGenerator generator) {
// board can be null a few ticks after on player leave
if (board == null || board.isDeleted() || !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);
}
board.updateTitle(replace(Locales.getString(locale, "scoreboard.title"), top, high, generator));
board.updateLines(replace(Locales.getStringList(locale, "scoreboard.lines"), top, high, generator));
}
private List<String> replace(List<String> s, Score top, Score high, ParkourGenerator generator) {
return s.stream().map(line -> replace(line, top, high, generator)).toList();
}
private String replace(String s, Score top, Score high, ParkourGenerator generator) {
return Strings.colour(translate(player, s)
.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();
}
/**
* @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

@ -0,0 +1,106 @@
package dev.loki.loparkour.player.data;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Config;
import dev.efnilite.vilib.util.Task;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.io.BukkitObjectInputStream;
import org.bukkit.util.io.BukkitObjectOutputStream;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
public class InventoryData {
private final File file;
private final Player player;
private Map<Integer, ItemStack> items = new HashMap<>();
public InventoryData(Player player) {
this.player = player;
this.file = LoParkour.getInFolder("inventories/%s".formatted(player.getUniqueId()));
}
/**
* Gives all items to the player.
*/
public void apply() {
player.getInventory().clear();
items.forEach((slot, item) -> player.getInventory().setItem(slot, item));
}
/**
* Loads inventory data from file.
*
* @param onFinish What to do when the async procedure has finished.
*/
public void load(Consumer<@Nullable InventoryData> onFinish) {
if (!file.exists()) {
onFinish.accept(null);
return;
}
Task.create(LoParkour.getPlugin()).async().execute(() -> loadFile(onFinish)).run();
}
@SuppressWarnings("unchecked")
private void loadFile(Consumer<@Nullable InventoryData> onFinish) {
try (BukkitObjectInputStream stream = new BukkitObjectInputStream(new BufferedInputStream(new FileInputStream(file)))) {
items = (Map<Integer, ItemStack>) stream.readObject();
onFinish.accept(this);
} catch (IOException | ClassNotFoundException ex) {
LoParkour.logging().stack("Error while reading inventory of %s from file %s".formatted(player.getName(), file.getName()), ex);
onFinish.accept(null);
}
}
/**
* Saves the inventory to cache, so if the player leaves the player gets their items back
*
* @param toFile Whether the file should be updated.
*/
public void save(boolean toFile) {
int index = 0;
Inventory inventory = this.player.getInventory();
for (ItemStack item : inventory.getContents()) {
if (item != null) {
this.items.put(index, item);
}
index++;
}
String command = Config.CONFIG.getString("options.alt-inventory-saving-command");
if (!command.isEmpty()) {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command.replace("%player%", player.getName()));
}
if (toFile) {
Task.create(LoParkour.getPlugin()).async().execute(this::saveFile).run();
}
}
private void saveFile() {
try {
file.getParentFile().mkdirs();
file.createNewFile();
} catch (IOException ex) {
LoParkour.logging().stack("Error while creating file to save inventory of %s to file %s".formatted(player.getName(), file.getName()), ex);
}
try (ObjectOutputStream stream = new BukkitObjectOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) {
stream.writeObject(items);
stream.flush();
} catch (IOException ex) {
LoParkour.logging().stack("Error while saving inventory of %s to file %s".formatted(player.getName(), file.getName()), ex);
}
}
}

View file

@ -0,0 +1,96 @@
package dev.loki.loparkour.player.data;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Config;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.reward.Reward;
import io.papermc.lib.PaperLib;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Class for storing previous data of players
*/
public class PreviousData {
private final InventoryData inventoryData;
private final int hunger;
private final boolean flying;
private final boolean allowFlight;
private final GameMode gamemode;
private final Location location;
private final Collection<PotionEffect> effects;
/**
* List of all {@link Reward} to execute on leave.
*/
public List<Reward> onLeave = new ArrayList<>();
public PreviousData(@NotNull Player player) {
gamemode = player.getGameMode();
location = player.getLocation();
hunger = player.getFoodLevel();
allowFlight = player.getAllowFlight();
flying = player.isFlying();
effects = player.getActivePotionEffects();
for (PotionEffect effect : effects) {
player.removePotionEffect(effect.getType());
}
if (Config.CONFIG.getBoolean("options.inventory-handling")) {
inventoryData = new InventoryData(player);
inventoryData.save(Config.CONFIG.getBoolean("options.inventory-saving"));
} else {
inventoryData = null;
}
}
public void apply(Player player, boolean urgent) {
var to = Config.CONFIG.getBoolean("bungeecord.go-back-enabled") ? Option.GO_BACK_LOC : location;
if (!urgent)
PaperLib.teleportAsync(player, to).thenRun(() -> apply(player));
else {
player.teleport(to);
apply(player);
}
}
private void apply(Player player) {
try {
player.setFoodLevel(hunger);
player.setGameMode(gamemode);
player.setAllowFlight(allowFlight);
player.setFlying(flying);
for (PotionEffect effect : player.getActivePotionEffects()) {
player.removePotionEffect(effect.getType());
}
for (PotionEffect effect : effects) {
player.addPotionEffect(effect);
}
player.resetPlayerTime();
player.resetPlayerWeather();
player.setVelocity(new Vector(0, 0, 0));
player.setFallDistance(0f);
} catch (Exception ex) { // not the best way to do this... too bad!
LoParkour.logging().stack("Error while recovering stats of %s".formatted(player.getName()), ex);
}
if (inventoryData != null) {
inventoryData.apply();
}
}
}

View file

@ -0,0 +1,71 @@
package dev.loki.loparkour.reward;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.api.Registry;
import dev.loki.loparkour.hook.VaultHook;
import dev.loki.loparkour.mode.Mode;
import dev.loki.loparkour.mode.Modes;
import dev.loki.loparkour.player.ParkourPlayer;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
/**
* A class for handling reward commands, etc.
*/
// todo add cross-server support
// todo mode-specific way of saving one-time-rewards
public record Reward(@NotNull String string) {
/**
* Parses and executes this reward
*
* @param player The player to which to give this reward to
*/
public void execute(@NotNull ParkourPlayer player, @NotNull Mode mode) {
if (string.isEmpty()) {
return;
}
String string = this.string;
Mode rewardMode = Registry.getModes().stream()
.filter(m -> this.string.contains("%s:".formatted(m.getName().toLowerCase())))
.findFirst()
.orElse(Modes.DEFAULT);
if (mode != rewardMode) {
return;
}
string = string.replaceFirst("%s:".formatted(rewardMode.getName().toLowerCase()), "");
// check for placeholders
if (string.toLowerCase().contains("%player%")) {
string = string.replaceAll("%player%", player.getName());
}
// check for extra data
if (string.toLowerCase().contains("leave:")) { // leave:
string = string.replaceFirst("leave:", "");
player.previousData.onLeave.add(new Reward(string));
return;
}
// check for things to perform
if (string.toLowerCase().contains("send:")) {
string = string.replaceFirst("send:", "");
player.send(string);
} else if (string.toLowerCase().contains("vault:")) {
string = string.replaceFirst("vault:", "");
try {
VaultHook.deposit(player.player, Double.parseDouble(string));
} catch (NumberFormatException ex) {
LoParkour.logging().stack("Error while trying to process Vault reward", "check your rewards file for incorrect numbers", ex);
}
} else {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), string);
}
}
}

View file

@ -0,0 +1,79 @@
package dev.loki.loparkour.reward;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Config;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Class that reads the rewards-v2.yml file and puts them in the variables listed below.
*/
public class Rewards {
public static boolean REWARDS_ENABLED;
/**
* A map with all Score-type score rewards.
* The key is the score, and the value are the commands that will be executed once this score is reached.
*/
public static Map<Integer, List<Reward>> SCORE_REWARDS = new HashMap<>();
/**
* A map with all Interval-type score rewards.
* The key is the score, and the value are the commands that will be executed once this score is reached.
*/
public static Map<Integer, List<Reward>> INTERVAL_REWARDS = new HashMap<>();
/**
* A map with all One time-type score rewards.
* The key is the score, and the value are the commands that will be executed once this score is reached.
*/
public static Map<Integer, List<Reward>> ONE_TIME_REWARDS = new HashMap<>();
/**
* Reads the rewards from the rewards-v2.yml file
*/
public static void init() {
// init options
REWARDS_ENABLED = Config.REWARDS.getBoolean("enabled");
if (!REWARDS_ENABLED) {
return;
}
SCORE_REWARDS = parseScores("score-rewards");
INTERVAL_REWARDS = parseScores("interval-rewards");
ONE_TIME_REWARDS = parseScores("one-time-rewards");
}
private static Map<Integer, List<Reward>> parseScores(String path) {
Map<Integer, List<Reward>> rewardMap = new HashMap<>();
for (String score : Config.REWARDS.getChildren(path)) {
// read commands for this score
List<Reward> rewardStrings = Config.REWARDS.getStringList("%s.%s".formatted(path, score)).stream()
.map(Reward::new)
.toList();
try {
int value = Integer.parseInt(score);
if (value < 1) {
LoParkour.logging().stack("Error while trying to read rewards",
"check the rewards file for incorrect numbers",
new IllegalArgumentException("%s is not a valid score".formatted(value)));
continue;
}
rewardMap.put(value, rewardStrings);
} catch (NumberFormatException ex) {
LoParkour.logging().stack("Error while trying to read rewards", "check the rewards file for incorrect numbers", ex);
}
}
return rewardMap;
}
}

View file

@ -0,0 +1,56 @@
package dev.loki.loparkour.schematic;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Config;
import dev.efnilite.vilib.util.Task;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Stores schematics, so they don't have to be read every time.
*/
public class Schematics {
private static final String[] SPAWN_SCHEMATICS = new String[]{
"spawn-island", "spawn-island-duels"
};
private static final File FOLDER = LoParkour.getInFolder("schematics");
/**
* Reads all files.
*/
public static void init() {
Task.create(LoParkour.getPlugin()).async().execute(() -> {
if (!FOLDER.exists()) {
FOLDER.mkdirs();
}
File[] files = FOLDER.listFiles((dir, name) -> name.contains("parkour-") || name.contains("spawn-island"));
if (files == null || files.length == 0) {
download();
init();
return;
}
try {
dev.efnilite.vilib.schematic.Schematics.addFromFiles(LoParkour.getPlugin(), files);
} catch (IOException | ClassNotFoundException ex) {
LoParkour.logging().stack("Error while trying to load schematics", ex);
}
}).run();
}
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

@ -0,0 +1,293 @@
package dev.loki.loparkour.session;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Locales;
import dev.loki.loparkour.generator.ParkourGenerator;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.loki.loparkour.player.ParkourSpectator;
import dev.loki.loparkour.player.ParkourUser;
import dev.loki.loparkour.world.Divider;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.function.Function;
/**
* <p>A session is bound to a {@link Divider} section.
* It manages all players, all spectators, visibility, the generator, etc.</p>
* <p>Iteration 2.</p>
*
* @author loki
* @since 5.0.0
*/
public class Session {
/**
* The spawn location of this session.
*/
private Location spawnLocation;
/**
* The generator.
*/
public ParkourGenerator generator;
/**
* The visibility of this session. Default public.
*/
private Visibility visibility = Visibility.PUBLIC;
/**
* List of muted users.
*/
private final List<ParkourUser> muted = new ArrayList<>();
/**
* List of users.
*/
private final Map<UUID, ParkourUser> users = new HashMap<>();
/**
* Function that takes the current session and returns whether new players should be accepted.
*/
private Function<Session, Boolean> isAcceptingPlayers = session -> false;
/**
* Function that takes the current session and returns whether new spectators should be accepted.
*/
private Function<Session, Boolean> isAcceptingSpectators = session -> session.visibility == Visibility.PUBLIC;
/**
* Creates a new session.
*
* @param generatorFunction The generator function.
* @param isAcceptingPlayers The function that takes the current session and returns whether new players should be accepted.
* @param isAcceptingSpectators The function that takes the current session and returns whether new spectators should be accepted.
* @param players The players.
* @return The session.
*/
public static Session create(Function<Session, ParkourGenerator> generatorFunction,
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;
if (isAcceptingSpectators != null) session.isAcceptingSpectators = isAcceptingSpectators;
List<ParkourPlayer> pps = new ArrayList<>();
if (players != null) {
for (Player player : players) {
ParkourPlayer pp = ParkourUser.register(player, session);
session.addPlayers(pp);
pps.add(pp);
}
}
session.spawnLocation = Divider.add(session);
session.generator = generatorFunction.apply(session);
if (players != null) {
pps.forEach(p -> p.updateGeneratorSettings(session.generator));
}
session.generator.island.build(session.spawnLocation);
return session;
}
/**
* Sets the visibility of this session.
* @param visibility The visibility.
*/
public void setVisibility(Visibility visibility) {
this.visibility = visibility;
}
/**
* @return The visibility of this session.
*/
public Visibility getVisibility() {
return visibility;
}
/**
* Adds provided players to this session's player list.
*
* @param toAdd The players to add.
*/
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()));
}
users.put(player.getUUID(), player);
}
}
/**
* Removes provided players from this session's player list.
*
* @param toRemove The players to remove.
*/
public void removePlayers(ParkourPlayer... toRemove) {
for (ParkourPlayer player : toRemove) {
LoParkour.log("Removing player %s from session".formatted(player.getName()));
users.remove(player.getUUID());
}
List<ParkourPlayer> players = getPlayers();
for (ParkourPlayer player : toRemove) {
for (ParkourPlayer to : players) {
to.send(Locales.getString(player.locale, "lobby.other_leave").formatted(player.getName()));
}
}
if (toRemove.length > 0 && players.isEmpty()) {
generator.reset(false);
Divider.remove(this);
}
}
/**
* @return The players.
*/
public List<ParkourPlayer> getPlayers() {
return users.values().stream()
.filter(user -> user instanceof ParkourPlayer)
.map(user -> (ParkourPlayer) user)
.toList();
}
/**
* Adds provided spectators to this session's spectator list.
*
* @param spectators The spectators to add.
*/
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());
}
users.put(spectator.getUUID(), spectator);
}
}
/**
* Removes provided spectators from this session's spectator list.
*
* @param spectators The spectators to remove.
*/
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());
}
users.remove(spectator.getUUID());
}
}
/**
* @return The spectators.
*/
public List<ParkourSpectator> getSpectators() {
return users.values().stream()
.filter(user -> user instanceof ParkourSpectator)
.map(user -> (ParkourSpectator) user)
.toList();
}
/**
* @return The users.
*/
public List<ParkourUser> getUsers() {
return new ArrayList<>(users.values());
}
/**
* @return the spawn location for this {@link Session}.
*/
@SuppressWarnings("unused")
public Location getSpawnLocation() {
return spawnLocation.clone();
}
/**
* Toggles mute for the specified user.
*
* @param user The user to (un)mute.
*/
public void toggleMute(@NotNull ParkourUser user) {
if (!muted.remove(user)) {
muted.add(user);
}
}
/**
* @param user The user.
* @return True when the user is muted, false if not.
*/
public boolean isMuted(@NotNull ParkourUser user) {
return muted.contains(user);
}
/**
* @return True when players may join this session, false if not.
*/
public boolean isAcceptingPlayers() {
return isAcceptingPlayers.apply(this);
}
/**
* @return True when spectators may join this session, false if not.
*/
public boolean isAcceptingSpectators() {
return isAcceptingSpectators.apply(this);
}
public enum Visibility {
/**
* No-one can join.
*/
PRIVATE,
/**
* Only people with the session id can join.
*/
ID_ONLY,
/**
* Anyone can join.
*/
PUBLIC,
}
/**
* An enum for all available chat types that a player can select while playing
*/
public enum ChatType {
LOBBY_ONLY, PLAYERS_ONLY, PUBLIC
}
}

View file

@ -0,0 +1,84 @@
package dev.loki.loparkour.storage;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.leaderboard.Score;
import dev.loki.loparkour.player.ParkourPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.UUID;
/**
* I/O handling.
*
* @author loki
* @since 5.0.0
*/
public class Storage {
public static void init(String mode) {
if (Option.SQL) {
StorageSQL.init(mode);
}
}
public static void close() {
if (Option.SQL) {
StorageSQL.close();
}
}
/**
* Reads scores.
*
* @param mode The mode.
* @return Map with all scores, unsorted.
*/
public static @NotNull Map<UUID, Score> readScores(@NotNull String mode) {
if (Option.SQL) {
return StorageSQL.readScores(mode);
} else {
return StorageDisk.readScores(mode);
}
}
/**
* Writes scores.
*
* @param mode The mode.
* @param scores The score map.
*/
public static void writeScores(@NotNull String mode, @NotNull Map<UUID, Score> scores) {
if (Option.SQL) {
StorageSQL.writeScores(mode, scores);
} else {
StorageDisk.writeScores(mode, scores);
}
}
/**
* Reads player data and applies changes.
*
* @param player The player.
*/
public static void readPlayer(@NotNull ParkourPlayer player) {
if (Option.SQL) {
StorageSQL.readPlayer(player);
} else {
StorageDisk.readPlayer(player);
}
}
/**
* Writes player data.
*
* @param player The player.
*/
public static void writePlayer(@NotNull ParkourPlayer player) {
if (Option.SQL) {
StorageSQL.writePlayer(player);
} else {
StorageDisk.writePlayer(player);
}
}
}

View file

@ -0,0 +1,133 @@
package dev.loki.loparkour.storage;
import com.google.gson.annotations.Expose;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.leaderboard.Score;
import dev.loki.loparkour.player.ParkourPlayer;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
/**
* Local disk (json) storage manager.
*
* @since 5.0.0
*/
class StorageDisk {
public static @NotNull Map<UUID, Score> readScores(@NotNull String mode) {
File file = getLeaderboardFile(mode);
if (!file.exists()) {
return new HashMap<>();
}
try (FileReader reader = new FileReader(file)) {
LeaderboardContainer read = LoParkour.getGson().fromJson(reader, LeaderboardContainer.class);
if (read == null) {
return new HashMap<>();
}
Map<UUID, String> serialized = new LinkedHashMap<>(read.serialized);
Map<UUID, Score> scores = new HashMap<>();
serialized.forEach((uuid, score) -> scores.put(uuid, Score.fromString(score)));
return scores;
} catch (IOException ex) {
LoParkour.logging().stack("Error while trying to read leaderboard file %s".formatted(mode), ex);
return new HashMap<>();
}
}
public static void writeScores(@NotNull String mode, @NotNull Map<UUID, Score> scores) {
LeaderboardContainer container = new LeaderboardContainer();
scores.forEach((uuid, score) -> container.serialized.put(uuid, score.toString()));
File file = getLeaderboardFile(mode);
createFile(file);
try (FileWriter writer = new FileWriter(file)) {
LoParkour.getGson().toJson(container, writer);
writer.flush();
} catch (IOException ex) {
LoParkour.logging().stack("Error while trying to write to leaderboard file %s".formatted(mode), ex);
}
}
private static File getLeaderboardFile(String mode) {
return LoParkour.getInFolder("leaderboards/%s.json".formatted(mode.toLowerCase()));
}
public static class LeaderboardContainer {
@Expose
public final Map<UUID, String> serialized = new LinkedHashMap<>();
}
public static void readPlayer(@NotNull ParkourPlayer player) {
if (!getPlayerFile(player).exists()) {
player.setSettings(new HashMap<>());
return;
}
try (FileReader reader = new FileReader(getPlayerFile(player))) {
ParkourPlayer from = LoParkour.getGson().fromJson(reader, ParkourPlayer.class);
Map<String, Object> settings = new HashMap<>();
settings.put("style", from.style);
settings.put("blockLead", from.blockLead);
settings.put("useParticles", from.particles);
settings.put("useSpecial", from.useSpecialBlocks);
settings.put("showFallMsg", from.showFallMessage);
settings.put("showScoreboard", from.showScoreboard);
settings.put("selectedTime", from.selectedTime);
settings.put("collectedRewards", from.collectedRewards);
settings.put("locale", from._locale);
settings.put("schematicDifficulty", from.schematicDifficulty);
settings.put("sound", from.sound);
player.setSettings(settings);
} catch (IOException ex) {
LoParkour.logging().stack("Error while trying to read disk data of %s".formatted(player.getName()), ex);
}
}
public static void writePlayer(@NotNull ParkourPlayer player) {
File file = getPlayerFile(player);
createFile(file);
try (FileWriter writer = new FileWriter(file)) {
LoParkour.getGson().toJson(player, writer);
writer.flush();
} catch (IOException ex) {
LoParkour.logging().stack("Error while trying to write disk data of %s to file %s".formatted(player.getName(), file), ex);
}
}
private static void createFile(File file) {
if (file.exists()) {
return;
}
try {
file.getParentFile().mkdirs();
file.createNewFile();
} catch (IOException ex) {
LoParkour.logging().stack("Error while trying to create file %s".formatted(file), ex);
}
}
private static File getPlayerFile(ParkourPlayer player) {
return LoParkour.getInFolder("players/%s.json".formatted(player.getUUID()));
}
}

View file

@ -0,0 +1,257 @@
package dev.loki.loparkour.storage;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.leaderboard.Score;
import dev.loki.loparkour.player.ParkourPlayer;
import dev.efnilite.vilib.util.Colls;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
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;
/**
* MySQL storage manager.
*
* @since 5.0.0
*/
class StorageSQL {
private static Connection connection;
static {
connect();
}
public static void init(String mode) {
sendUpdate("""
CREATE TABLE IF NOT EXISTS `%s`
(
uuid CHAR(36) NOT NULL PRIMARY KEY,
name VARCHAR(16),
time VARCHAR(16),
difficulty VARCHAR(3),
score INT
)
CHARSET = utf8 ENGINE = InnoDB;
"""
.formatted(getTableName(mode)));
}
public static void close() {
try {
connection.close();
LoParkour.log("Closed connection to MySQL");
} catch (SQLException ex) {
LoParkour.logging().stack("Error while trying to close connection to SQL database", ex);
}
}
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<>();
}
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.logging().stack("Error while trying to read SQL data of %s".formatted(mode), ex);
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())));
}
// 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()))) {
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 = Colls.thread(ParkourPlayer.PLAYER_COLUMNS).mapv((key, value) -> {
try {
return results.getObject(key);
} catch (SQLException ex) {
LoParkour.logging().stack("Error while trying to read SQL data of %s, option = %s".formatted(player.getName(), key), ex);
return null;
}
}).get();
player.setSettings(settings);
} catch (SQLException ex) {
LoParkour.logging().stack("Error while trying to read SQL data of %s".formatted(player.getName()), ex);
}
}
public static void writePlayer(@NotNull ParkourPlayer player) {
DecimalFormat df = new DecimalFormat("#.######", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
String schematicDifficulty = df.format(player.schematicDifficulty);
sendUpdate("""
INSERT INTO `%s`
(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)
ON DUPLICATE KEY UPDATE style = '%s',
blockLead = %d,
useParticles = %b,
useSpecial = %b,
showFallMsg = %b,
showScoreboard = %b,
selectedTime = %d,
collectedRewards = '%s',
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));
}
public static void connect() {
try {
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
}
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);
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));
// v3.0.0
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` DROP COLUMN `useDifficulty`;".formatted(Option.SQL_PREFIX));
sendUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useStructure`;".formatted(Option.SQL_PREFIX));
LoParkour.log("Connected to MySQL");
} catch (Exception ex) {
LoParkour.logging().stack("Could not connect to MySQL", "check your SQL settings in the config", ex);
Bukkit.getPluginManager().disablePlugin(LoParkour.getPlugin()); // disable plugin since data handling without db will go horribly wrong
}
}
private static void validateConnection() {
try {
if (!connection.isValid(10)) {
connect();
}
} catch (Exception ex) {
LoParkour.logging().stack("Error while trying to reconnect to MySQL", ex);
}
}
// send query and get result
private static ResultSet sendQuery(String sql) {
validateConnection();
try {
return connection.prepareStatement(sql).executeQuery();
} catch (SQLException ex) {
LoParkour.logging().stack("Error while sending query %s".formatted(sql), ex);
return null;
}
}
// send update
private static void sendUpdate(String sql) {
validateConnection();
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.executeUpdate();
} catch (SQLException ex) {
LoParkour.logging().stack("Error while sending query %s".formatted(sql), ex);
}
}
// if query throws an error, ignore it
private static void sendUpdateSuppressed(String sql) {
validateConnection();
try (PreparedStatement statement = connection.prepareStatement(sql)) {
statement.executeUpdate();
} catch (SQLException ignored) {
}
}
}

View file

@ -0,0 +1,28 @@
package dev.loki.loparkour.style;
import org.bukkit.Material;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public final class RandomStyle implements Style {
private final String name;
private final List<Material> materials;
public RandomStyle(String name, List<Material> materials) {
this.name = name;
this.materials = materials;
}
@Override
public Material getNext() {
return materials.get(ThreadLocalRandom.current().nextInt(materials.size()));
}
@Override
public String getName() {
return name;
}
}

View file

@ -0,0 +1,22 @@
package dev.loki.loparkour.style;
import org.bukkit.Material;
/**
* Represents a parkour style.
*/
public interface Style {
/**
* The material to be used for the next block.
* @return A material.
*/
Material getNext();
/**
* The name of the style.
* @return A name.
*/
String getName();
}

View file

@ -0,0 +1,125 @@
package dev.loki.loparkour.world;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Option;
import dev.loki.loparkour.session.Session;
import dev.efnilite.vilib.util.Locations;
import org.bukkit.Location;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.IntStream;
/**
* <p>Divides the parkour world in sections, each with an active session.</p>
* <p>Iteration 2.</p>
*
* @author loki
* @since 5.0.0
*/
public class Divider {
/**
* Map with all session ids map to the session instances.
*/
public static final Map<Session, Integer> sections = new HashMap<>();
/**
* Associates a session to a specific section.
*
* @param session The session.
*/
public static synchronized Location add(Session session) {
// attempts to get the closest available section to the center
var missing = IntStream.range(0, sections.size() + 1)
.filter(i -> !sections.containsValue(i))
.findFirst()
.orElseThrow();
sections.put(session, missing);
var location = toLocation(session);
LoParkour.log("Added session at %s".formatted(Locations.toString(location, true)));
return location;
}
/**
* Disassociates a session from a specific section.
*
* @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);
}
/**
* @param session The session.
* @return The location at the center of section n.
*/
private static Location toLocation(Session session) {
int[] xz = spiralAt(sections.get(session));
return new Location(World.getWorld(),
xz[0] * Option.BORDER_SIZE,
(Option.MAX_Y + Option.MIN_Y) / 2.0,
xz[1] * Option.BORDER_SIZE);
}
/**
* @param session The session.
* @return Array where the first item is the smallest location and second item is the largest.
*/
public static Location[] toSelection(Session session) {
Location center = toLocation(session);
// get the min and max locations
Location max = center.clone().add(Option.BORDER_SIZE / 2, 0, Option.BORDER_SIZE / 2);
Location min = center.clone().subtract(Option.BORDER_SIZE / 2, 0, Option.BORDER_SIZE / 2);
max.setY(Option.MAX_Y);
min.setY(Option.MIN_Y);
return new Location[]{min, max};
}
/**
* Gets a spiral
*
* @param n The number of value
* @return the coords of this value
*/
// https://math.stackexchange.com/a/163101
private static int[] spiralAt(int n) {
if (n < 0) {
throw new IllegalArgumentException("Invalid n bound: %d".formatted(n));
}
n++; // one-index
int k = (int) Math.ceil((Math.sqrt(n) - 1) / 2);
int t = 2 * k + 1;
int m = t * t;
t--;
if (n > m - t) {
return new int[]{k - (m - n), -k};
} else {
m -= t;
}
if (n > m - t) {
return new int[]{-k, -k + (m - n)};
} else {
m -= t;
}
if (n > m - t) {
return new int[]{-k + (m - n), k};
} else {
return new int[]{k, k - (m - n - t)};
}
}
}

View file

@ -0,0 +1,126 @@
package dev.loki.loparkour.world;
import dev.loki.loparkour.LoParkour;
import dev.loki.loparkour.config.Config;
import dev.efnilite.vilib.util.VoidGenerator;
import org.bukkit.*;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.stream.Stream;
public class World {
private static String name;
private static org.bukkit.World world;
/**
* Creates a new world and sets all according settings in it.
*/
public static void create() {
name = Config.CONFIG.getString("world.name");
var world = Bukkit.getWorld(name);
if (!Config.CONFIG.getBoolean("joining")) {
return;
}
if (world != null) {
LoParkour.logging().warn("Crash detected! The parkour world loading twice is not usual behaviour. This only happens after a server crash.");
}
if (Config.CONFIG.getBoolean("world.delete-on-reload")) {
deleteWorld();
}
createWorld();
setup();
}
private static void createWorld() {
LoParkour.log("Creating Spigot world");
try {
WorldCreator creator = new WorldCreator(name)
.generateStructures(false)
.type(WorldType.NORMAL)
.generator(VoidGenerator.getGenerator()) // to fix No keys in MapLayer etc.
.environment(org.bukkit.World.Environment.NORMAL);
world = Bukkit.createWorld(creator);
} catch (Exception ex) {
LoParkour.logging().stack("Error while trying to create the parkour world", "delete the parkour world folder and restart the server", ex);
}
}
private static void setup() {
LoParkour.log("Initializing world rules");
world.setGameRule(GameRule.DO_FIRE_TICK, false);
world.setGameRule(GameRule.DO_MOB_SPAWNING, false);
world.setGameRule(GameRule.DO_TILE_DROPS, false);
world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false);
world.setGameRule(GameRule.DO_WEATHER_CYCLE, false);
world.setGameRule(GameRule.LOG_ADMIN_COMMANDS, false);
world.setGameRule(GameRule.KEEP_INVENTORY, true);
world.setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
world.getWorldBorder().setCenter(0, 0);
world.getWorldBorder().setSize(10_000_000);
world.setDifficulty(Difficulty.PEACEFUL);
world.setClearWeatherDuration(1000000);
world.setAutoSave(false);
}
/**
* Deletes the world.
*/
public static void delete() {
if (!Config.CONFIG.getBoolean("world.delete-on-reload") || !Config.CONFIG.getBoolean("joining")) {
return;
}
LoParkour.log("Deleting world");
getWorld().getPlayers().forEach(player -> player.kickPlayer("Server is restarting"));
deleteWorld();
}
private static void deleteWorld() {
LoParkour.log("Deleting Spigot world");
File file = new File(name);
// world has already been deleted
if (!file.exists()) {
return;
}
Bukkit.unloadWorld(name, false);
try (Stream<Path> files = Files.walk(file.toPath())) {
files.sorted(Comparator.reverseOrder())
.map(Path::toFile)
.forEach(File::delete);
} catch (Exception ex) {
LoParkour.logging().stack("Error while trying to delete the parkour world", ex);
}
}
/**
* @return the name of the parkour world.
*/
public static String getName() {
return name;
}
/**
* @return the Bukkit world wherein LoParkour is currently active.
*/
public static org.bukkit.World getWorld() {
return world;
}
}