Parkour implementation safety save 2

This is just a safety save in case the code gets too broken to fix.
This commit is contained in:
2023-04-13 20:13:29 +02:00
parent 9a3f9841ab
commit 1acaebb3bc
77 changed files with 1168 additions and 847 deletions

View File

@@ -0,0 +1,256 @@
package net.knarcraft.minigames;
import net.knarcraft.minigames.arena.dropper.DropperArenaData;
import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaHandler;
import net.knarcraft.minigames.arena.dropper.DropperArenaPlayerRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArenaRecordsRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArenaSession;
import net.knarcraft.minigames.arena.parkour.ParkourArenaHandler;
import net.knarcraft.minigames.arena.parkour.ParkourArenaPlayerRegistry;
import net.knarcraft.minigames.arena.record.IntegerRecord;
import net.knarcraft.minigames.arena.record.LongRecord;
import net.knarcraft.minigames.command.CreateArenaCommand;
import net.knarcraft.minigames.command.EditArenaCommand;
import net.knarcraft.minigames.command.EditArenaTabCompleter;
import net.knarcraft.minigames.command.GroupListCommand;
import net.knarcraft.minigames.command.GroupSetCommand;
import net.knarcraft.minigames.command.GroupSwapCommand;
import net.knarcraft.minigames.command.JoinArenaCommand;
import net.knarcraft.minigames.command.JoinArenaTabCompleter;
import net.knarcraft.minigames.command.LeaveArenaCommand;
import net.knarcraft.minigames.command.ListArenaCommand;
import net.knarcraft.minigames.command.ReloadCommand;
import net.knarcraft.minigames.command.RemoveArenaCommand;
import net.knarcraft.minigames.command.RemoveArenaTabCompleter;
import net.knarcraft.minigames.config.DropperConfiguration;
import net.knarcraft.minigames.config.ParkourConfiguration;
import net.knarcraft.minigames.config.SharedConfiguration;
import net.knarcraft.minigames.container.SerializableMaterial;
import net.knarcraft.minigames.container.SerializableUUID;
import net.knarcraft.minigames.listener.CommandListener;
import net.knarcraft.minigames.listener.DamageListener;
import net.knarcraft.minigames.listener.MoveListener;
import net.knarcraft.minigames.listener.PlayerLeaveListener;
import net.knarcraft.minigames.placeholder.DropperRecordExpansion;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.logging.Level;
/**
* The dropper plugin's main class
*/
@SuppressWarnings("unused")
public final class MiniGames extends JavaPlugin {
private static MiniGames instance;
private SharedConfiguration sharedConfiguration;
private DropperConfiguration dropperConfiguration;
private ParkourConfiguration parkourConfiguration;
private DropperArenaHandler dropperArenaHandler;
private DropperArenaPlayerRegistry dropperArenaPlayerRegistry;
private DropperRecordExpansion dropperRecordExpansion;
private ParkourArenaHandler parkourArenaHandler;
private ParkourArenaPlayerRegistry parkourArenaPlayerRegistry;
/**
* Gets an instance of this plugin
*
* @return <p>An instance of this plugin, or null if not initialized yet.</p>
*/
public static MiniGames getInstance() {
return instance;
}
/**
* Gets the dropper arena handler for this instance
*
* @return <p>A dropper arena handler</p>
*/
public DropperArenaHandler getDropperArenaHandler() {
return this.dropperArenaHandler;
}
/**
* Gets the parkour arena handler for this instance
*
* @return <p>A parkour arena handler</p>
*/
public ParkourArenaHandler getParkourArenaHandler() {
return this.parkourArenaHandler;
}
/**
* Gets the dropper arena player registry for this instance
*
* @return <p>A dropper arena player registry</p>
*/
public DropperArenaPlayerRegistry getDropperArenaPlayerRegistry() {
return this.dropperArenaPlayerRegistry;
}
/**
* Gets the parkour arena player registry for this instance
*
* @return <p>A parkour arena player registry</p>
*/
public ParkourArenaPlayerRegistry getParkourArenaPlayerRegistry() {
return this.parkourArenaPlayerRegistry;
}
/**
* Gets the shared configuration
*
* <p>The configuration for options which don't affect specific types of mini-games.</p>
*
* @return <p>The shared configuration</p>
*/
public SharedConfiguration getSharedConfiguration() {
return this.sharedConfiguration;
}
/**
* Gets the dropper configuration
*
* @return <p>The dropper configuration</p>
*/
public DropperConfiguration getDropperConfiguration() {
return this.dropperConfiguration;
}
/**
* Gets the parkour configuration
*
* @return <p>The parkour configuration</p>
*/
public ParkourConfiguration getParkourConfiguration() {
return this.parkourConfiguration;
}
/**
* Logs a message
*
* @param level <p>The message level to log at</p>
* @param message <p>The message to log</p>
*/
public static void log(Level level, String message) {
MiniGames.getInstance().getLogger().log(level, message);
}
/**
* Reloads all configurations and data from disk
*/
public void reload() {
// Load all arenas again
this.dropperArenaHandler.load();
this.parkourArenaHandler.load();
// Reload configuration
this.reloadConfig();
this.sharedConfiguration.load(this.getConfig());
this.dropperConfiguration.load(this.getConfig());
this.parkourConfiguration.load(this.getConfig());
// Clear record caches
this.dropperRecordExpansion.clearCaches();
}
@Override
public void onLoad() {
super.onLoad();
// Register serialization classes
ConfigurationSerialization.registerClass(SerializableMaterial.class);
ConfigurationSerialization.registerClass(DropperArenaRecordsRegistry.class);
ConfigurationSerialization.registerClass(SerializableUUID.class);
ConfigurationSerialization.registerClass(DropperArenaData.class);
ConfigurationSerialization.registerClass(DropperArenaGroup.class);
ConfigurationSerialization.registerClass(DropperArenaGameMode.class);
ConfigurationSerialization.registerClass(LongRecord.class);
ConfigurationSerialization.registerClass(IntegerRecord.class);
}
@Override
public void onEnable() {
// Plugin startup logic
instance = this;
this.saveDefaultConfig();
getConfig().options().copyDefaults(true);
saveConfig();
reloadConfig();
this.sharedConfiguration = new SharedConfiguration(this.getConfig());
this.dropperConfiguration = new DropperConfiguration(this.getConfig());
this.parkourConfiguration = new ParkourConfiguration(this.getConfig());
this.dropperArenaPlayerRegistry = new DropperArenaPlayerRegistry();
this.dropperArenaHandler = new DropperArenaHandler();
this.dropperArenaHandler.load();
this.parkourArenaHandler = new ParkourArenaHandler();
this.parkourArenaHandler.load();
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new DamageListener(), this);
pluginManager.registerEvents(new MoveListener(this.dropperConfiguration), this);
pluginManager.registerEvents(new PlayerLeaveListener(), this);
pluginManager.registerEvents(new CommandListener(), this);
registerCommand("dropperReload", new ReloadCommand(), null);
registerCommand("dropperCreate", new CreateArenaCommand(), null);
registerCommand("dropperList", new ListArenaCommand(), null);
registerCommand("dropperJoin", new JoinArenaCommand(), new JoinArenaTabCompleter());
registerCommand("dropperLeave", new LeaveArenaCommand(), null);
registerCommand("dropperEdit", new EditArenaCommand(this.dropperConfiguration), new EditArenaTabCompleter());
registerCommand("dropperRemove", new RemoveArenaCommand(), new RemoveArenaTabCompleter());
registerCommand("dropperGroupSet", new GroupSetCommand(), null);
registerCommand("dropperGroupSwap", new GroupSwapCommand(), null);
registerCommand("dropperGroupList", new GroupListCommand(), null);
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
this.dropperRecordExpansion = new DropperRecordExpansion(this);
if (!this.dropperRecordExpansion.register()) {
log(Level.WARNING, "Unable to register PlaceholderAPI expansion!");
}
}
}
@Override
public void onDisable() {
// Throw out currently playing players before exiting
for (Player player : getServer().getOnlinePlayers()) {
DropperArenaSession session = dropperArenaPlayerRegistry.getArenaSession(player.getUniqueId());
if (session != null) {
session.triggerQuit(true);
}
}
}
/**
* Registers a command
*
* @param commandName <p>The name of the command to register (defined in plugin.yml)</p>
* @param commandExecutor <p>The executor for the command</p>
* @param tabCompleter <p>The tab-completer to use, or null</p>
*/
private void registerCommand(@NotNull String commandName, @NotNull CommandExecutor commandExecutor,
@Nullable TabCompleter tabCompleter) {
PluginCommand command = this.getCommand(commandName);
if (command != null) {
command.setExecutor(commandExecutor);
if (tabCompleter != null) {
command.setTabCompleter(tabCompleter);
}
} else {
log(Level.SEVERE, "Unable to register the command " + commandName);
}
}
}

View File

@@ -0,0 +1,33 @@
package net.knarcraft.minigames.arena;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* An interface describing all arenas
*/
public interface Arena {
/**
* Gets the id of this arena
*
* @return <p>This arena's identifier</p>
*/
@NotNull UUID getArenaId();
/**
* Gets the name of this arena
*
* @return <p>The name of this arena</p>
*/
@NotNull String getArenaName();
/**
* Gets this arena's sanitized name
*
* @return <p>This arena's sanitized name</p>
*/
@NotNull String getArenaNameSanitized();
}

View File

@@ -0,0 +1,108 @@
package net.knarcraft.minigames.arena;
import net.knarcraft.minigames.container.SerializableContainer;
import net.knarcraft.minigames.container.SerializableUUID;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import static net.knarcraft.minigames.util.SerializableConverter.makeSerializable;
/**
* An interface describing generic arena data
*/
public abstract class ArenaData implements ConfigurationSerializable {
protected final @NotNull UUID arenaId;
private final @NotNull Map<ArenaGameMode, ArenaRecordsRegistry> recordRegistries;
private final @NotNull Map<ArenaGameMode, Set<UUID>> playersCompleted;
/**
* Instantiates arena data
*
* @param arenaId <p>The id of the arena this data belongs to</p>
* @param recordRegistries <p>The registry storing records for this arena</p>
* @param playersCompleted <p>The players that have completed this arena</p>
*/
public ArenaData(@NotNull UUID arenaId, @NotNull Map<ArenaGameMode, ArenaRecordsRegistry> recordRegistries,
@NotNull Map<ArenaGameMode, Set<UUID>> playersCompleted) {
this.arenaId = arenaId;
this.recordRegistries = recordRegistries;
this.playersCompleted = playersCompleted;
}
/**
* Gets the id of this arena
*
* @return <p>The id of this arena</p>
*/
public @NotNull UUID getArenaId() {
return this.arenaId;
}
/**
* Gets all record registries
*
* @return <p>All record registries</p>
*/
public @NotNull Map<ArenaGameMode, ArenaRecordsRegistry> getRecordRegistries() {
return new HashMap<>(this.recordRegistries);
}
/**
* Gets whether the given player has cleared this arena
*
* @param arenaGameMode <p>The game-mode to check for</p>
* @param player <p>The player to check</p>
* @return <p>True if the player has cleared the arena this data belongs to</p>
*/
public boolean hasNotCompleted(@NotNull ArenaGameMode arenaGameMode, @NotNull Player player) {
return !this.playersCompleted.getOrDefault(arenaGameMode, new HashSet<>()).contains(player.getUniqueId());
}
/**
* Registers the given player as having completed this arena
*
* @param arenaGameMode <p>The game-mode the player completed</p>
* @param player <p>The player that completed this data's arena</p>
*/
public boolean setCompleted(@NotNull ArenaGameMode arenaGameMode, @NotNull Player player) {
// Make sure to add an empty set to prevent a NullPointerException
if (!this.playersCompleted.containsKey(arenaGameMode)) {
this.playersCompleted.put(arenaGameMode, new HashSet<>());
}
boolean added = this.playersCompleted.get(arenaGameMode).add(player.getUniqueId());
// Persistently save the completion
if (added) {
saveData();
}
return added;
}
/**
* Saves this data to disk
*/
public abstract void saveData();
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("arenaId", new SerializableUUID(this.arenaId));
data.put("recordsRegistry", this.recordRegistries);
// Convert normal UUIDs to serializable UUIDs
Map<ArenaGameMode, Set<SerializableContainer<UUID>>> serializablePlayersCompleted = new HashMap<>();
makeSerializable(this.playersCompleted, serializablePlayersCompleted, new SerializableUUID(null));
data.put("playersCompleted", serializablePlayersCompleted);
return data;
}
}

View File

@@ -0,0 +1,4 @@
package net.knarcraft.minigames.arena;
public interface ArenaGameMode {
}

View File

@@ -0,0 +1,165 @@
package net.knarcraft.minigames.arena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.container.SerializableUUID;
import net.knarcraft.minigames.util.StringSanitizer;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public abstract class ArenaGroup implements ConfigurationSerializable {
/**
* The unique id for this group of arenas
*/
private final UUID groupId;
/**
* The unique name for this group of arenas
*/
private final String groupName;
/**
* The arenas in this group, ordered from stage 1 to stage n
*/
protected final List<UUID> arenas;
/**
* Instantiates a new dropper arena group
*
* @param groupName <p>The name of this group</p>
*/
protected ArenaGroup(@NotNull String groupName) {
this.groupId = UUID.randomUUID();
this.groupName = groupName;
this.arenas = new ArrayList<>();
}
/**
* Instantiates a new arena group
*
* @param groupId <p>The unique id of this group</p>
* @param groupName <p>The name of this group</p>
* @param arenas <p>The arenas in this group</p>
*/
protected ArenaGroup(@NotNull UUID groupId, @NotNull String groupName, @NotNull List<UUID> arenas) {
this.groupId = groupId;
this.groupName = groupName;
this.arenas = new ArrayList<>(arenas);
}
/**
* Gets the id of this arena group
*
* @return <p>The id of this group</p>
*/
public @NotNull UUID getGroupId() {
return this.groupId;
}
/**
* Gets the name of this arena group
*
* @return <p>The name of this group</p>
*/
public @NotNull String getGroupName() {
return this.groupName;
}
/**
* Gets the arenas contained in this group in the correct order
*
* @return <p>The ids of the arenas in this group</p>
*/
public @NotNull List<UUID> getArenas() {
return new ArrayList<>(arenas);
}
/**
* Removes the given arena from this group
*
* @param arenaId <p>The id of the arena to remove</p>
*/
public void removeArena(UUID arenaId) {
this.arenas.remove(arenaId);
}
/**
* Adds an arena to the end of this group
*
* @param arenaId <p>The arena to add to this group</p>
*/
public void addArena(UUID arenaId) {
addArena(arenaId, this.arenas.size());
}
/**
* Adds an arena to the end of this group
*
* @param arenaId <p>The arena to add to this group</p>
* @param index <p>The index to put the arena in</p>
*/
public void addArena(UUID arenaId, int index) {
// Make sure we don't have duplicates
if (!this.arenas.contains(arenaId)) {
this.arenas.add(index, arenaId);
}
}
/**
* Gets this group's name, but sanitized
*
* @return <p>The sanitized group name</p>
*/
public @NotNull String getGroupNameSanitized() {
return StringSanitizer.sanitizeArenaName(this.getGroupName());
}
/**
* Swaps the arenas at the given indices
*
* @param index1 <p>The index of the first arena to swap</p>
* @param index2 <p>The index of the second arena to swap</p>
*/
public void swapArenas(int index1, int index2) {
// Change nothing if not a valid request
if (index1 == index2 || index1 < 0 || index2 < 0 || index1 >= this.arenas.size() ||
index2 >= this.arenas.size()) {
return;
}
// Swap the two arena ids
UUID temporaryValue = this.arenas.get(index2);
this.arenas.set(index2, this.arenas.get(index1));
this.arenas.set(index1, temporaryValue);
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("groupId", new SerializableUUID(this.groupId));
data.put("groupName", this.groupName);
List<SerializableUUID> serializableArenas = new ArrayList<>();
for (UUID arenaId : arenas) {
serializableArenas.add(new SerializableUUID(arenaId));
}
data.put("arenas", serializableArenas);
return data;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof DropperArenaGroup otherGroup)) {
return false;
}
return this.getGroupNameSanitized().equals(otherGroup.getGroupNameSanitized());
}
}

View File

@@ -0,0 +1,173 @@
package net.knarcraft.minigames.arena;
import net.knarcraft.minigames.arena.record.ArenaRecord;
import net.knarcraft.minigames.arena.record.IntegerRecord;
import net.knarcraft.minigames.arena.record.LongRecord;
import net.knarcraft.minigames.arena.record.SummableArenaRecord;
import net.knarcraft.minigames.container.SerializableUUID;
import net.knarcraft.minigames.property.RecordResult;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
/**
* A records registry storing records for an arena
*/
public abstract class ArenaRecordsRegistry implements ConfigurationSerializable {
protected final UUID arenaId;
private final @NotNull Set<IntegerRecord> leastDeaths;
private final @NotNull Set<LongRecord> shortestTimeMilliSeconds;
/**
* Instantiates a new empty records registry
*/
public ArenaRecordsRegistry(@NotNull UUID arenaId) {
this.arenaId = arenaId;
this.leastDeaths = new HashSet<>();
this.shortestTimeMilliSeconds = new HashSet<>();
}
/**
* Instantiates a new records registry
*
* @param leastDeaths <p>The existing least death records to use</p>
* @param shortestTimeMilliSeconds <p>The existing leash time records to use</p>
*/
protected ArenaRecordsRegistry(@NotNull UUID arenaId, @NotNull Set<IntegerRecord> leastDeaths,
@NotNull Set<LongRecord> shortestTimeMilliSeconds) {
this.arenaId = arenaId;
this.leastDeaths = new HashSet<>(leastDeaths);
this.shortestTimeMilliSeconds = new HashSet<>(shortestTimeMilliSeconds);
}
/**
* Gets all existing death records
*
* @return <p>Existing death records</p>
*/
public Set<SummableArenaRecord<Integer>> getLeastDeathsRecords() {
return new HashSet<>(this.leastDeaths);
}
/**
* Gets all existing time records
*
* @return <p>Existing time records</p>
*/
public Set<SummableArenaRecord<Long>> getShortestTimeMilliSecondsRecords() {
return new HashSet<>(this.shortestTimeMilliSeconds);
}
/**
* Registers a new deaths-record
*
* @param playerId <p>The id of the player that performed the records</p>
* @param deaths <p>The number of deaths suffered before the player finished the arena</p>
* @return <p>The result explaining what type of record was achieved</p>
*/
public @NotNull RecordResult registerDeathRecord(@NotNull UUID playerId, int deaths) {
Consumer<Integer> consumer = (value) -> {
leastDeaths.removeIf((item) -> item.getUserId().equals(playerId));
leastDeaths.add(new IntegerRecord(playerId, value));
};
return registerRecord(new HashSet<>(leastDeaths), consumer, playerId, deaths);
}
/**
* Registers a new time-record
*
* @param playerId <p>The id of the player that performed the records</p>
* @param milliseconds <p>The number of milliseconds it took the player to finish the dropper arena</p>
* @return <p>The result explaining what type of record was achieved</p>
*/
public @NotNull RecordResult registerTimeRecord(@NotNull UUID playerId, long milliseconds) {
Consumer<Long> consumer = (value) -> {
shortestTimeMilliSeconds.removeIf((item) -> item.getUserId().equals(playerId));
shortestTimeMilliSeconds.add(new LongRecord(playerId, value));
};
return registerRecord(new HashSet<>(shortestTimeMilliSeconds), consumer, playerId, milliseconds);
}
/**
* Saves changed records
*/
protected abstract void save();
/**
* Registers a new record if applicable
*
* @param existingRecords <p>The map of existing records to use</p>
* @param recordSetter <p>The consumer used to set a new record</p>
* @param playerId <p>The id of the player that potentially achieved a record</p>
* @param amount <p>The amount of whatever the player achieved</p>
* @return <p>The result of the player's record attempt</p>
*/
private <T extends Comparable<T>> @NotNull RecordResult registerRecord(@NotNull Set<ArenaRecord<T>> existingRecords,
@NotNull Consumer<T> recordSetter,
@NotNull UUID playerId, T amount) {
RecordResult result;
if (existingRecords.stream().allMatch((entry) -> amount.compareTo(entry.getRecord()) < 0)) {
// If the given value is less than all other values, that's a world record!
result = RecordResult.WORLD_RECORD;
recordSetter.accept(amount);
save();
return result;
}
ArenaRecord<T> playerRecord = getRecord(existingRecords, playerId);
if (playerRecord != null && amount.compareTo(playerRecord.getRecord()) < 0) {
// If the given value is less than the player's previous value, that's a personal best!
result = RecordResult.PERSONAL_BEST;
recordSetter.accept(amount);
save();
} else {
// Make sure to save the record if this is the user's first attempt
if (playerRecord == null) {
recordSetter.accept(amount);
save();
}
result = RecordResult.NONE;
}
return result;
}
/**
* Gets the record stored for the given player
*
* @param existingRecords <p>The existing records to look through</p>
* @param playerId <p>The id of the player to look for</p>
* @param <T> <p>The type of the stored record</p>
* @return <p>The record, or null if not found</p>
*/
private <T extends Comparable<T>> @Nullable ArenaRecord<T> getRecord(@NotNull Set<ArenaRecord<T>> existingRecords,
@NotNull UUID playerId) {
AtomicReference<ArenaRecord<T>> record = new AtomicReference<>();
existingRecords.forEach((item) -> {
if (item.getUserId().equals(playerId)) {
record.set(item);
}
});
return record.get();
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("arenaId", new SerializableUUID(this.arenaId));
data.put("leastDeaths", this.leastDeaths);
data.put("shortestTime", this.shortestTimeMilliSeconds);
return data;
}
}

View File

@@ -0,0 +1,315 @@
package net.knarcraft.minigames.arena.dropper;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.Arena;
import net.knarcraft.minigames.arena.ArenaGameMode;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.config.DropperConfiguration;
import net.knarcraft.minigames.util.StringSanitizer;
import org.bukkit.Location;
import org.bukkit.Material;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import static net.knarcraft.minigames.util.InputValidationHelper.isInvalid;
/**
* A representation of one dropper arena
*/
public class DropperArena implements Arena {
/**
* An unique and persistent identifier for this arena
*/
private final UUID arenaId;
/**
* A name used when listing and storing this arena.
*/
private @NotNull String arenaName;
/**
* The location players are teleported to when joining this arena.
*/
private @NotNull Location spawnLocation;
/**
* The location players will be sent to when they win or lose the arena. If not set, their entry location should be
* used instead.
*/
private @Nullable Location exitLocation;
/**
* The velocity in the y-direction to apply to all players in this arena.
*/
private double playerVerticalVelocity;
/**
* The velocity in the x-direction to apply to all players in this arena
*
* <p>This is technically the fly speed</p>
*/
private float playerHorizontalVelocity;
/**
* The material of the block players have to hit to win this dropper arena
*/
private @NotNull Material winBlockType;
/**
* The arena data for this arena
*/
private final DropperArenaData dropperArenaData;
private final DropperArenaHandler dropperArenaHandler;
/**
* Instantiates a new dropper arena
*
* @param arenaId <p>The id of the arena</p>
* @param arenaName <p>The name of the arena</p>
* @param spawnLocation <p>The location players spawn in when entering the arena</p>
* @param exitLocation <p>The location the players are teleported to when exiting the arena, or null</p>
* @param playerVerticalVelocity <p>The velocity to use for players' vertical velocity</p>
* @param playerHorizontalVelocity <p>The velocity to use for players' horizontal velocity (-1 to 1)</p>
* @param winBlockType <p>The material of the block players have to hit to win this dropper arena</p>
* @param dropperArenaData <p>The arena data keeping track of which players have done what in this arena</p>
* @param arenaHandler <p>The arena handler used for saving any changes</p>
*/
public DropperArena(@NotNull UUID arenaId, @NotNull String arenaName, @NotNull Location spawnLocation,
@Nullable Location exitLocation, double playerVerticalVelocity, float playerHorizontalVelocity,
@NotNull Material winBlockType, @NotNull DropperArenaData dropperArenaData,
@NotNull DropperArenaHandler arenaHandler) {
this.arenaId = arenaId;
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
this.exitLocation = exitLocation;
this.playerVerticalVelocity = playerVerticalVelocity;
this.playerHorizontalVelocity = playerHorizontalVelocity;
this.winBlockType = winBlockType;
this.dropperArenaData = dropperArenaData;
this.dropperArenaHandler = arenaHandler;
}
/**
* Instantiates a new dropper arena
*
* <p>Note that this minimal constructor can be used to quickly create a new dropper arena at the player's given
* location, simply by them giving an arena name.</p>
*
* @param arenaName <p>The name of the arena</p>
* @param spawnLocation <p>The location players spawn in when entering the arena</p>
* @param arenaHandler <p>The arena handler used for saving any changes</p>
*/
public DropperArena(@NotNull String arenaName, @NotNull Location spawnLocation,
@NotNull DropperArenaHandler arenaHandler) {
DropperConfiguration configuration = MiniGames.getInstance().getDropperConfiguration();
this.arenaId = UUID.randomUUID();
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
this.exitLocation = null;
this.playerVerticalVelocity = configuration.getVerticalVelocity();
this.playerHorizontalVelocity = configuration.getHorizontalVelocity();
Map<ArenaGameMode, ArenaRecordsRegistry> recordRegistries = new HashMap<>();
for (ArenaGameMode arenaGameMode : DropperArenaGameMode.values()) {
recordRegistries.put(arenaGameMode, new DropperArenaRecordsRegistry(this.arenaId));
}
this.dropperArenaData = new DropperArenaData(this.arenaId, recordRegistries, new HashMap<>());
this.winBlockType = Material.WATER;
this.dropperArenaHandler = arenaHandler;
}
/**
* Gets this arena's data
*
* @return <p>This arena's data</p>
*/
public @NotNull DropperArenaData getData() {
return this.dropperArenaData;
}
@Override
public @NotNull UUID getArenaId() {
return this.arenaId;
}
@Override
public @NotNull String getArenaName() {
return this.arenaName;
}
/**
* Gets this arena's spawn location
*
* <p>The spawn location is the location every player starts from when entering the dropper.</p>
*
* @return <p>This arena's spawn location.</p>
*/
public @NotNull Location getSpawnLocation() {
return this.spawnLocation.clone();
}
/**
* Gets this arena's exit location
*
* @return <p>This arena's exit location, or null if no such location is set.</p>
*/
public @Nullable Location getExitLocation() {
return this.exitLocation != null ? this.exitLocation.clone() : null;
}
/**
* Gets the vertical velocity for players in this arena
*
* <p>This velocity will be set on the negative y-axis, for all players in this arena.</p>
*
* @return <p>Players' velocity in this arena</p>
*/
public double getPlayerVerticalVelocity() {
return this.playerVerticalVelocity;
}
/**
* Gets the horizontal for players in this arena
*
* <p>This will be used for players' fly-speed in this arena</p>
*
* @return <p>Players' velocity in this arena</p>
*/
public float getPlayerHorizontalVelocity() {
return this.playerHorizontalVelocity;
}
/**
* Gets the type of block a player has to hit to win this arena
*
* @return <p>The kind of block players must hit</p>
*/
public @NotNull Material getWinBlockType() {
return this.winBlockType;
}
@Override
public @NotNull String getArenaNameSanitized() {
return StringSanitizer.sanitizeArenaName(this.getArenaName());
}
/**
* Sets the spawn location for this arena
*
* @param newLocation <p>The new spawn location</p>
* @return <p>True if successfully updated</p>
*/
public boolean setSpawnLocation(@NotNull Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.spawnLocation = newLocation;
dropperArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the exit location for this arena
*
* @param newLocation <p>The new exit location</p>
* @return <p>True if successfully updated</p>
*/
public boolean setExitLocation(@NotNull Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.exitLocation = newLocation;
dropperArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the name of this arena
*
* @param arenaName <p>The new name</p>
* @return <p>True if successfully updated</p>
*/
public boolean setName(@NotNull String arenaName) {
if (!arenaName.isBlank()) {
String oldName = this.getArenaNameSanitized();
this.arenaName = arenaName;
// Update the arena lookup map to make sure the new name can be used immediately
dropperArenaHandler.updateLookupName(oldName, this.getArenaNameSanitized());
dropperArenaHandler.saveArenas();
return true;
} else {
return false;
}
}
/**
* Sets the material of the win block type
*
* <p>The win block type is the type of block a player must hit to win in this arena</p>
*
* @param material <p>The material to set for the win block type</p>
* @return <p>True if successfully updated</p>
*/
public boolean setWinBlockType(@NotNull Material material) {
if (material.isAir() || !material.isBlock()) {
return false;
} else {
this.winBlockType = material;
dropperArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the horizontal velocity of this arena's players
*
* <p>Note: It's assumed the given value is already bound-checked! (-1 to 1)</p>
*
* @param horizontalVelocity <p>The horizontal velocity to use</p>
* @return <p>True if successfully updated</p>
*/
public boolean setHorizontalVelocity(float horizontalVelocity) {
if (horizontalVelocity < -1 || horizontalVelocity > 1) {
return false;
} else {
this.playerHorizontalVelocity = horizontalVelocity;
dropperArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the vertical velocity of this arena's players
*
* @param verticalVelocity <p>The vertical velocity to use</p>
* @return <p>True if successfully updated</p>
*/
public boolean setVerticalVelocity(double verticalVelocity) {
if (verticalVelocity <= 0 || verticalVelocity > 100) {
return false;
} else {
this.playerVerticalVelocity = verticalVelocity;
dropperArenaHandler.saveArenas();
return true;
}
}
@Override
public boolean equals(Object other) {
if (!(other instanceof DropperArena otherArena)) {
return false;
}
return this.getArenaNameSanitized().equals(otherArena.getArenaNameSanitized());
}
}

View File

@@ -0,0 +1,72 @@
package net.knarcraft.minigames.arena.dropper;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaData;
import net.knarcraft.minigames.arena.ArenaGameMode;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.container.SerializableContainer;
import net.knarcraft.minigames.container.SerializableUUID;
import net.knarcraft.minigames.util.SerializableConverter;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* Data stored for an arena
*/
public class DropperArenaData extends ArenaData {
/**
* Instantiates a new dropper arena data object
*
* @param arenaId <p>The id of the arena this data belongs to</p>
* @param recordRegistries <p>The registries of this arena's records</p>
* @param playersCompleted <p>The set of the players that have cleared this arena for each game-mode</p>
*/
public DropperArenaData(@NotNull UUID arenaId,
@NotNull Map<ArenaGameMode, ArenaRecordsRegistry> recordRegistries,
@NotNull Map<ArenaGameMode, Set<UUID>> playersCompleted) {
super(arenaId, recordRegistries, playersCompleted);
}
@Override
public void saveData() {
MiniGames.getInstance().getDropperArenaHandler().saveData(this.arenaId);
}
/**
* Deserializes a dropper arena data from the given data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized dropper arena data</p>
*/
@SuppressWarnings({"unused", "unchecked"})
public static @NotNull DropperArenaData deserialize(@NotNull Map<String, Object> data) {
SerializableUUID serializableUUID = (SerializableUUID) data.get("arenaId");
Map<ArenaGameMode, ArenaRecordsRegistry> recordsRegistry =
(Map<ArenaGameMode, ArenaRecordsRegistry>) data.get("recordsRegistry");
Map<ArenaGameMode, Set<SerializableContainer<UUID>>> playersCompletedData =
(Map<ArenaGameMode, Set<SerializableContainer<UUID>>>) data.get("playersCompleted");
if (recordsRegistry == null) {
recordsRegistry = new HashMap<>();
} else if (playersCompletedData == null) {
playersCompletedData = new HashMap<>();
}
// Convert the serializable UUIDs to normal UUIDs
Map<ArenaGameMode, Set<UUID>> allPlayersCompleted = new HashMap<>();
SerializableConverter.getRawValue(playersCompletedData, allPlayersCompleted);
for (ArenaGameMode arenaGameMode : playersCompletedData.keySet()) {
if (!recordsRegistry.containsKey(arenaGameMode) || recordsRegistry.get(arenaGameMode) == null) {
recordsRegistry.put(arenaGameMode, new DropperArenaRecordsRegistry(serializableUUID.getRawValue()));
}
}
return new DropperArenaData(serializableUUID.getRawValue(), recordsRegistry, allPlayersCompleted);
}
}

View File

@@ -0,0 +1,91 @@
package net.knarcraft.minigames.arena.dropper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
/**
* All editable properties of a dropper arena
*/
public enum DropperArenaEditableProperty {
/**
* The name of the arena
*/
NAME("name", DropperArena::getArenaName),
/**
* The arena's spawn location
*/
SPAWN_LOCATION("spawnLocation", (arena) -> String.valueOf(arena.getSpawnLocation())),
/**
* The arena's exit location
*/
EXIT_LOCATION("exitLocation", (arena) -> String.valueOf(arena.getExitLocation())),
/**
* The arena's vertical velocity
*/
VERTICAL_VELOCITY("verticalVelocity", (arena) -> String.valueOf(arena.getPlayerVerticalVelocity())),
/**
* The arena's horizontal velocity
*/
HORIZONTAL_VELOCITY("horizontalVelocity", (arena) -> String.valueOf(arena.getPlayerHorizontalVelocity())),
/**
* The arena's win block type
*/
WIN_BLOCK_TYPE("winBlockType", (arena) -> arena.getWinBlockType().toString()),
;
private final @NotNull String argumentString;
private final Function<DropperArena, String> currentValueProvider;
/**
* Instantiates a new arena editable property
*
* @param argumentString <p>The argument string used to specify this property</p>
*/
DropperArenaEditableProperty(@NotNull String argumentString, Function<DropperArena, String> currentValueProvider) {
this.argumentString = argumentString;
this.currentValueProvider = currentValueProvider;
}
/**
* Gets the string representation of this property's current value
*
* @param arena <p>The arena to check the value for</p>
* @return <p>The current value as a string</p>
*/
public String getCurrentValueAsString(DropperArena arena) {
return this.currentValueProvider.apply(arena);
}
/**
* Gets the argument string used to specify this property
*
* @return <p>The argument string</p>
*/
public @NotNull String getArgumentString() {
return this.argumentString;
}
/**
* Gets the editable property corresponding to the given argument string
*
* @param argumentString <p>The argument string used to specify an editable property</p>
* @return <p>The corresponding editable property, or null if not found</p>
*/
public static @Nullable DropperArenaEditableProperty getFromArgumentString(String argumentString) {
for (DropperArenaEditableProperty property : DropperArenaEditableProperty.values()) {
if (property.argumentString.equalsIgnoreCase(argumentString)) {
return property;
}
}
return null;
}
}

View File

@@ -0,0 +1,67 @@
package net.knarcraft.minigames.arena.dropper;
import net.knarcraft.minigames.arena.ArenaGameMode;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
* A representation of possible arena game-modes
*/
public enum DropperArenaGameMode implements ConfigurationSerializable, ArenaGameMode {
/**
* The default game-mode. Failing once throws the player out.
*/
DEFAULT,
/**
* A game-mode where the player's directional buttons are inverted
*/
INVERTED,
/**
* A game-mode which swaps between normal and inverted controls on a set schedule of a few seconds
*/
RANDOM_INVERTED,
;
/**
* Tries to match the correct game-mode according to the given string
*
* @param gameMode <p>The game-mode string to match</p>
* @return <p>The specified arena game-mode</p>
*/
public static @NotNull DropperArenaGameMode matchGamemode(@NotNull String gameMode) {
String sanitized = gameMode.trim().toLowerCase();
if (sanitized.matches("(invert(ed)?|inverse)")) {
return DropperArenaGameMode.INVERTED;
} else if (sanitized.matches("rand(om)?")) {
return DropperArenaGameMode.RANDOM_INVERTED;
} else {
return DropperArenaGameMode.DEFAULT;
}
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("name", this.name());
return data;
}
/**
* Deserializes the arena game-mode specified by the given data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized arena game-mode</p>
*/
@SuppressWarnings("unused")
public static DropperArenaGameMode deserialize(Map<String, Object> data) {
return DropperArenaGameMode.valueOf((String) data.get("name"));
}
}

View File

@@ -0,0 +1,122 @@
package net.knarcraft.minigames.arena.dropper;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaGroup;
import net.knarcraft.minigames.container.SerializableUUID;
import net.knarcraft.minigames.util.SerializableConverter;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
/**
* A sorted group of arenas that must be completed in sequence
*/
public class DropperArenaGroup extends ArenaGroup {
/**
* Instantiates a new dropper arena group
*
* @param groupName <p>The name of this group</p>
*/
public DropperArenaGroup(@NotNull String groupName) {
super(groupName);
}
/**
* Instantiates a new dropper arena group
*
* @param groupId <p>The unique id of this group</p>
* @param groupName <p>The name of this group</p>
* @param arenas <p>The arenas in this group</p>
*/
private DropperArenaGroup(@NotNull UUID groupId, @NotNull String groupName, @NotNull List<UUID> arenas) {
super(groupId, groupName, arenas);
}
/**
* Checks whether the given player has beaten all arenas in this group on the given game-mode
*
* @param gameMode <p>The game-mode to check</p>
* @param player <p>The player to check</p>
* @return <p>True if the player has beaten all arenas, false otherwise</p>
*/
public boolean hasBeatenAll(DropperArenaGameMode gameMode, Player player) {
DropperArenaHandler arenaHandler = MiniGames.getInstance().getDropperArenaHandler();
for (UUID anArenaId : this.getArenas()) {
DropperArena dropperArena = arenaHandler.getArena(anArenaId);
if (dropperArena == null) {
// The arena would only be null if the arena has been deleted, but not removed from this group
MiniGames.log(Level.WARNING, "The dropper group " + this.getGroupName() +
" contains the arena id " + anArenaId + " which is not a valid arena id!");
continue;
}
if (dropperArena.getData().hasNotCompleted(gameMode, player)) {
return false;
}
}
return true;
}
/**
* Gets whether the given player can play the given arena part of this group, on the given game-mode
*
* @param gameMode <p>The game-mode the player is trying to play</p>
* @param player <p>The player to check</p>
* @param arenaId <p>The id of the arena in this group to check</p>
* @return <p>True if the player is allowed to play the arena</p>
* @throws IllegalArgumentException <p>If checking an arena not in this group</p>
*/
public boolean canPlay(DropperArenaGameMode gameMode, Player player, UUID arenaId) throws IllegalArgumentException {
if (!this.arenas.contains(arenaId)) {
throw new IllegalArgumentException("Cannot check for playability for arena not in this group!");
}
DropperArenaHandler arenaHandler = MiniGames.getInstance().getDropperArenaHandler();
for (UUID anArenaId : this.getArenas()) {
// If the target arena is reached, allow, as all previous arenas must have been cleared
if (arenaId.equals(anArenaId)) {
return true;
}
DropperArena dropperArena = arenaHandler.getArena(anArenaId);
if (dropperArena == null) {
// The arena would only be null if the arena has been deleted, but not removed from this group
MiniGames.log(Level.WARNING, String.format("The dropper group %s contains the" +
" arena id %s which is not a valid arena id!", this.getGroupName(), anArenaId));
continue;
}
// This is a lower-numbered arena the player has yet to complete
if (dropperArena.getData().hasNotCompleted(gameMode, player)) {
return false;
}
}
return false;
}
/**
* Deserializes the given data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized arena group</p>
*/
@SuppressWarnings({"unused", "unchecked"})
public static @NotNull DropperArenaGroup deserialize(@NotNull Map<String, Object> data) {
UUID id = ((SerializableUUID) data.get("groupId")).getRawValue();
String name = (String) data.get("groupName");
List<SerializableUUID> serializableArenas = (List<SerializableUUID>) data.get("arenas");
List<UUID> arenas = new ArrayList<>();
SerializableConverter.getRawValue(new ArrayList<>(serializableArenas), arenas);
return new DropperArenaGroup(id, name, arenas);
}
}

View File

@@ -0,0 +1,249 @@
package net.knarcraft.minigames.arena.dropper;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.util.ArenaStorageHelper;
import net.knarcraft.minigames.util.StringSanitizer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
/**
* A handler that keeps track of all dropper arenas
*/
public class DropperArenaHandler {
private Map<UUID, DropperArena> arenas = new HashMap<>();
private Map<UUID, DropperArenaGroup> arenaGroups = new HashMap<>();
private Map<String, UUID> arenaNameLookup = new HashMap<>();
/**
* Gets all arenas that are within a group
*
* @return <p>All arenas in a group</p>
*/
public @NotNull Set<DropperArena> getArenasInAGroup() {
Set<DropperArena> arenas = new HashSet<>();
for (UUID arenaId : arenaGroups.keySet()) {
arenas.add(this.arenas.get(arenaId));
}
return arenas;
}
/**
* Gets a copy of all dropper groups
*
* @return <p>All dropper groups</p>
*/
public Set<DropperArenaGroup> getAllGroups() {
return new HashSet<>(arenaGroups.values());
}
/**
* Gets the group the given arena belongs to
*
* @param arenaId <p>The id of the arena to get the group of</p>
* @return <p>The group the arena belongs to, or null if not in a group</p>
*/
public @Nullable DropperArenaGroup getGroup(@NotNull UUID arenaId) {
return this.arenaGroups.get(arenaId);
}
/**
* Sets the group for the given arena
*
* @param arenaId <p>The id of the arena to change</p>
* @param arenaGroup <p>The group to add the arena to, or null to remove the current group</p>
*/
public void setGroup(@NotNull UUID arenaId, @Nullable DropperArenaGroup arenaGroup) {
if (arenaGroup == null) {
// No need to remove something non-existing
if (!this.arenaGroups.containsKey(arenaId)) {
return;
}
// Remove the existing group
DropperArenaGroup oldGroup = this.arenaGroups.remove(arenaId);
oldGroup.removeArena(arenaId);
} else {
// Make sure to remove the arena from the old group's internal tracking
if (this.arenaGroups.containsKey(arenaId)) {
this.arenaGroups.remove(arenaId).removeArena(arenaId);
}
this.arenaGroups.put(arenaId, arenaGroup);
arenaGroup.addArena(arenaId);
}
saveGroups();
}
/**
* Gets the dropper arena group with the given name
*
* @param groupName <p>The name of the group to get</p>
* @return <p>The group, or null if not found</p>
*/
public @Nullable DropperArenaGroup getGroup(String groupName) {
String sanitized = StringSanitizer.sanitizeArenaName(groupName);
for (DropperArenaGroup arenaGroup : this.arenaGroups.values()) {
if (arenaGroup.getGroupNameSanitized().equals(sanitized)) {
return arenaGroup;
}
}
return null;
}
/**
* Replaces an arena's lookup name
*
* @param oldName <p>The arena's old sanitized lookup name</p>
* @param newName <p>The arena's new sanitized lookup name</p>
*/
public void updateLookupName(@NotNull String oldName, @NotNull String newName) {
UUID arenaId = this.arenaNameLookup.remove(oldName);
if (arenaId != null) {
this.arenaNameLookup.put(newName, arenaId);
}
}
/**
* Adds a new arena
*
* @param arena <p>The arena to add</p>
*/
public void addArena(@NotNull DropperArena arena) {
this.arenas.put(arena.getArenaId(), arena);
this.arenaNameLookup.put(arena.getArenaNameSanitized(), arena.getArenaId());
this.saveArenas();
}
/**
* Gets the arena with the given id
*
* @param arenaId <p>The id of the arena to get</p>
* @return <p>The arena, or null if no arena could be found</p>
*/
public @Nullable DropperArena getArena(@NotNull UUID arenaId) {
return this.arenas.get(arenaId);
}
/**
* Gets the arena with the given name
*
* @param arenaName <p>The arena to get</p>
* @return <p>The arena with the given name, or null if not found</p>
*/
public @Nullable DropperArena getArena(@NotNull String arenaName) {
return this.arenas.get(this.arenaNameLookup.get(StringSanitizer.sanitizeArenaName(arenaName)));
}
/**
* Gets all known arenas
*
* @return <p>All known arenas</p>
*/
public @NotNull Map<UUID, DropperArena> getArenas() {
return new HashMap<>(this.arenas);
}
/**
* Removes the given arena
*
* @param arena <p>The arena to remove</p>
*/
public void removeArena(@NotNull DropperArena arena) {
UUID arenaId = arena.getArenaId();
MiniGames.getInstance().getDropperArenaPlayerRegistry().removeForArena(arena);
this.arenas.remove(arenaId);
this.arenaNameLookup.remove(arena.getArenaNameSanitized());
this.arenaGroups.remove(arenaId);
if (!ArenaStorageHelper.removeDropperArenaData(arenaId)) {
MiniGames.log(Level.WARNING, "Unable to remove dropper arena data file " + arenaId + ".yml. " +
"You must remove it manually!");
}
this.saveArenas();
}
/**
* Stores the data for the given arena
*
* @param arenaId <p>The id of the arena whose data should be saved</p>
*/
public void saveData(UUID arenaId) {
try {
ArenaStorageHelper.saveDropperArenaData(this.arenas.get(arenaId).getData());
} catch (IOException e) {
MiniGames.log(Level.SEVERE, "Unable to save arena data! Data loss can occur!");
MiniGames.log(Level.SEVERE, e.getMessage());
}
}
/**
* Saves all current dropper groups to disk
*/
public void saveGroups() {
try {
ArenaStorageHelper.saveDropperArenaGroups(new HashSet<>(this.arenaGroups.values()));
} catch (IOException e) {
MiniGames.log(Level.SEVERE, "Unable to save current arena groups! " +
"Data loss can occur!");
MiniGames.log(Level.SEVERE, e.getMessage());
}
}
/**
* Loads all arenas and groups from disk
*/
public void load() {
loadArenas();
loadGroups();
}
/**
* Loads all dropper groups from disk
*/
private void loadGroups() {
Set<DropperArenaGroup> arenaGroups = ArenaStorageHelper.loadDropperArenaGroups();
Map<UUID, DropperArenaGroup> arenaGroupMap = new HashMap<>();
for (DropperArenaGroup arenaGroup : arenaGroups) {
for (UUID arenaId : arenaGroup.getArenas()) {
arenaGroupMap.put(arenaId, arenaGroup);
}
}
this.arenaGroups = arenaGroupMap;
}
/**
* Saves all current arenas to disk
*/
public void saveArenas() {
try {
ArenaStorageHelper.saveDropperArenas(this.arenas);
} catch (IOException e) {
MiniGames.log(Level.SEVERE, "Unable to save current arenas! " +
"Data loss can occur!");
MiniGames.log(Level.SEVERE, e.getMessage());
}
}
/**
* Loads all arenas from disk
*/
private void loadArenas() {
this.arenas = ArenaStorageHelper.loadDropperArenas();
// Save a map from arena name to arena id for improved performance
this.arenaNameLookup = new HashMap<>();
for (Map.Entry<UUID, DropperArena> arena : this.arenas.entrySet()) {
String sanitizedName = arena.getValue().getArenaNameSanitized();
this.arenaNameLookup.put(sanitizedName, arena.getKey());
}
}
}

View File

@@ -0,0 +1,61 @@
package net.knarcraft.minigames.arena.dropper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* A registry to keep track of which players are playing in which arenas
*/
public class DropperArenaPlayerRegistry {
private final Map<UUID, DropperArenaSession> arenaPlayers = new HashMap<>();
/**
* Registers that the given player has started playing the given dropper arena session
*
* @param playerId <p>The id of the player that started playing</p>
* @param arena <p>The arena session to register</p>
*/
public void registerPlayer(@NotNull UUID playerId, @NotNull DropperArenaSession arena) {
this.arenaPlayers.put(playerId, arena);
}
/**
* Removes this player from players currently playing
*
* @param playerId <p>The id of the player to remove</p>
*/
public boolean removePlayer(@NotNull UUID playerId) {
return this.arenaPlayers.remove(playerId) != null;
}
/**
* Gets the player's active dropper arena session
*
* @param playerId <p>The id of the player to get arena for</p>
* @return <p>The player's active arena session, or null if not currently playing</p>
*/
public @Nullable DropperArenaSession getArenaSession(@NotNull UUID playerId) {
return this.arenaPlayers.getOrDefault(playerId, null);
}
/**
* Removes all active sessions for the given arena
*
* @param arena <p>The arena to remove sessions for</p>
*/
public void removeForArena(DropperArena arena) {
for (Map.Entry<UUID, DropperArenaSession> entry : this.arenaPlayers.entrySet()) {
if (entry.getValue().getArena() == arena) {
// Kick the player gracefully
entry.getValue().triggerQuit(false);
this.arenaPlayers.remove(entry.getKey());
}
}
}
}

View File

@@ -0,0 +1,65 @@
package net.knarcraft.minigames.arena.dropper;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.arena.record.IntegerRecord;
import net.knarcraft.minigames.arena.record.LongRecord;
import net.knarcraft.minigames.container.SerializableUUID;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
/**
* A registry keeping track of all records
*/
public class DropperArenaRecordsRegistry extends ArenaRecordsRegistry {
/**
* Instantiates a new empty records registry
*/
public DropperArenaRecordsRegistry(@NotNull UUID arenaId) {
super(arenaId);
}
/**
* Instantiates a new records registry
*
* @param leastDeaths <p>The existing least death records to use</p>
* @param shortestTimeMilliSeconds <p>The existing leash time records to use</p>
*/
private DropperArenaRecordsRegistry(@NotNull UUID arenaId, @NotNull Set<IntegerRecord> leastDeaths,
@NotNull Set<LongRecord> shortestTimeMilliSeconds) {
super(arenaId, leastDeaths, shortestTimeMilliSeconds);
}
/**
* Saves changed records
*/
protected void save() {
MiniGames.getInstance().getDropperArenaHandler().saveData(this.arenaId);
}
/**
* Deserializes the given data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized records registry</p>
*/
@SuppressWarnings({"unused", "unchecked"})
public static DropperArenaRecordsRegistry deserialize(Map<String, Object> data) {
UUID arenaId = ((SerializableUUID) data.get("arenaId")).getRawValue();
Set<IntegerRecord> leastDeaths =
(Set<IntegerRecord>) data.getOrDefault("leastDeaths", new HashMap<>());
Set<LongRecord> shortestTimeMilliseconds =
(Set<LongRecord>) data.getOrDefault("shortestTime", new HashMap<>());
leastDeaths.removeIf(Objects::isNull);
shortestTimeMilliseconds.removeIf(Objects::isNull);
return new DropperArenaRecordsRegistry(arenaId, leastDeaths, shortestTimeMilliseconds);
}
}

View File

@@ -0,0 +1,206 @@
package net.knarcraft.minigames.arena.dropper;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.config.DropperConfiguration;
import net.knarcraft.minigames.property.RecordResult;
import net.knarcraft.minigames.util.PlayerTeleporter;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.logging.Level;
/**
* A representation of a player's current session in a dropper arena
*/
public class DropperArenaSession {
private final @NotNull DropperArena arena;
private final @NotNull Player player;
private final @NotNull DropperArenaGameMode gameMode;
private int deaths;
private final long startTime;
private final DropperPlayerEntryState entryState;
/**
* Instantiates a new dropper arena session
*
* @param dropperArena <p>The arena that's being played in</p>
* @param player <p>The player playing the arena</p>
* @param gameMode <p>The game-mode</p>
*/
public DropperArenaSession(@NotNull DropperArena dropperArena, @NotNull Player player,
@NotNull DropperArenaGameMode gameMode) {
this.arena = dropperArena;
this.player = player;
this.gameMode = gameMode;
this.deaths = 0;
this.startTime = System.currentTimeMillis();
DropperConfiguration configuration = MiniGames.getInstance().getDropperConfiguration();
boolean makeInvisible = configuration.makePlayersInvisible();
boolean disableCollision = configuration.disableHitCollision();
this.entryState = new DropperPlayerEntryState(player, gameMode, makeInvisible, disableCollision);
// Make the player fly to improve mobility in the air
this.entryState.setArenaState(this.arena.getPlayerHorizontalVelocity());
}
/**
* Gets the game-mode the player is playing in this session
*
* @return <p>The game-mode for this session</p>
*/
public @NotNull DropperArenaGameMode getGameMode() {
return this.gameMode;
}
/**
* Gets the state of the player when they joined the session
*
* @return <p>The player's entry state</p>
*/
public @NotNull DropperPlayerEntryState getEntryState() {
return this.entryState;
}
/**
* Triggers a win for the player playing in this session
*/
public void triggerWin() {
// Stop this session
stopSession();
// Check for, and display, records
MiniGames miniGames = MiniGames.getInstance();
boolean ignore = miniGames.getDropperConfiguration().ignoreRecordsUntilGroupBeatenOnce();
DropperArenaGroup group = miniGames.getDropperArenaHandler().getGroup(this.arena.getArenaId());
if (!ignore || group == null || group.hasBeatenAll(this.gameMode, this.player)) {
registerRecord();
}
// Mark the arena as cleared
if (this.arena.getData().setCompleted(this.gameMode, this.player)) {
this.player.sendMessage("You cleared the arena!");
}
this.player.sendMessage("You won!");
// Teleport the player out of the arena
teleportToExit(false);
}
/**
* Teleports the playing player out of the arena
*/
private void teleportToExit(boolean immediately) {
// Teleport the player out of the arena
Location exitLocation;
if (this.arena.getExitLocation() != null) {
exitLocation = this.arena.getExitLocation();
} else {
exitLocation = this.entryState.getEntryLocation();
}
PlayerTeleporter.teleportPlayer(this.player, exitLocation, true, immediately);
}
/**
* Removes this session from current sessions
*/
private void removeSession() {
// Remove this session for game sessions to stop listeners from fiddling more with the player
boolean removedSession = MiniGames.getInstance().getDropperArenaPlayerRegistry().removePlayer(player.getUniqueId());
if (!removedSession) {
MiniGames.log(Level.SEVERE, "Unable to remove dropper arena session for " + player.getName() + ". " +
"This will have unintended consequences.");
}
}
/**
* Registers the player's record if necessary, and prints record information to the player
*/
private void registerRecord() {
ArenaRecordsRegistry recordsRegistry = this.arena.getData().getRecordRegistries().get(this.gameMode);
long timeElapsed = System.currentTimeMillis() - this.startTime;
announceRecord(recordsRegistry.registerTimeRecord(this.player.getUniqueId(), timeElapsed), "time");
announceRecord(recordsRegistry.registerDeathRecord(this.player.getUniqueId(), this.deaths), "least deaths");
}
/**
* Announces a record set by this player
*
* @param recordResult <p>The result of the record</p>
* @param type <p>The type of record set (time or deaths)</p>
*/
private void announceRecord(@NotNull RecordResult recordResult, @NotNull String type) {
if (recordResult == RecordResult.NONE) {
return;
}
// Gets a string representation of the played game-mode
String gameModeString = switch (this.gameMode) {
case DEFAULT -> "default";
case INVERTED -> "inverted";
case RANDOM_INVERTED -> "random";
};
String recordString = "You just set a %s on the %s game-mode!";
recordString = switch (recordResult) {
case WORLD_RECORD -> String.format(recordString, "new %s record", gameModeString);
case PERSONAL_BEST -> String.format(recordString, "personal %s record", gameModeString);
default -> throw new IllegalStateException("Unexpected value: " + recordResult);
};
player.sendMessage(String.format(recordString, type));
}
/**
* Triggers a loss for the player playing in this session
*/
public void triggerLoss() {
this.deaths++;
//Teleport the player back to the top
PlayerTeleporter.teleportPlayer(this.player, this.arena.getSpawnLocation(), true, false);
this.entryState.setArenaState(this.arena.getPlayerHorizontalVelocity());
}
/**
* Triggers a quit for the player playing in this session
*/
public void triggerQuit(boolean immediately) {
// Stop this session
stopSession();
// Teleport the player out of the arena
teleportToExit(immediately);
player.sendMessage("You quit the arena!");
}
/**
* Stops this session, and disables flight mode
*/
private void stopSession() {
// Remove this session from game sessions to stop listeners from fiddling more with the player
removeSession();
// Remove flight mode
entryState.restore();
}
/**
* Gets the arena this session is being played in
*
* @return <p>The session's arena</p>
*/
public @NotNull DropperArena getArena() {
return this.arena;
}
/**
* Gets the player playing in this session
*
* @return <p>This session's player</p>
*/
public @NotNull Player getPlayer() {
return this.player;
}
}

View File

@@ -0,0 +1,71 @@
package net.knarcraft.minigames.arena.dropper;
import org.jetbrains.annotations.NotNull;
/**
* A representation of each key used for storing arena data
*/
public enum DropperArenaStorageKey {
/**
* The key for an arena's id
*/
ID("arenaId"),
/**
* The key for an arena's name
*/
NAME("arenaName"),
/**
* The key for an arena's spawn location
*/
SPAWN_LOCATION("arenaSpawnLocation"),
/**
* The key for an arena's exit location
*/
EXIT_LOCATION("arenaExitLocation"),
/**
* The key for a player in this arena's vertical velocity
*/
PLAYER_VERTICAL_VELOCITY("arenaPlayerVerticalVelocity"),
/**
* The key for a player in this arena's horizontal velocity
*/
PLAYER_HORIZONTAL_VELOCITY("arenaPlayerHorizontalVelocity"),
/**
* The key for the type of this arena's win block
*/
WIN_BLOCK_TYPE("winBlockType"),
/**
* The hey for this arena's data
*/
DATA("arenaData"),
;
private final @NotNull String key;
/**
* Instantiates a new arena storage key
*
* @param key <p>The string path of the configuration key this value represents.</p>
*/
DropperArenaStorageKey(@NotNull String key) {
this.key = key;
}
/**
* Gets the configuration key this enum represents
*
* @return <p>The string key representation.</p>
*/
public @NotNull String getKey() {
return this.key;
}
}

View File

@@ -0,0 +1,102 @@
package net.knarcraft.minigames.arena.dropper;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
/**
* The state of a player before entering a dropper arena
*/
public class DropperPlayerEntryState {
private final Player player;
private final Location entryLocation;
private final boolean originalIsFlying;
private final float originalFlySpeed;
private final GameMode originalGameMode;
private final boolean originalAllowFlight;
private final boolean originalInvulnerable;
private final boolean originalIsSwimming;
private final boolean originalCollideAble;
private final boolean makePlayerInvisible;
private final boolean disableHitCollision;
private final DropperArenaGameMode arenaGameMode;
/**
* Instantiates a new player state
*
* @param player <p>The player whose state should be stored</p>
*/
public DropperPlayerEntryState(@NotNull Player player, @NotNull DropperArenaGameMode arenaGameMode, boolean makePlayerInvisible,
boolean disableHitCollision) {
this.player = player;
this.entryLocation = player.getLocation().clone();
this.originalFlySpeed = player.getFlySpeed();
this.originalIsFlying = player.isFlying();
this.originalGameMode = player.getGameMode();
this.originalAllowFlight = player.getAllowFlight();
this.originalInvulnerable = player.isInvulnerable();
this.originalIsSwimming = player.isSwimming();
this.arenaGameMode = arenaGameMode;
this.originalCollideAble = player.isCollidable();
this.makePlayerInvisible = makePlayerInvisible;
this.disableHitCollision = disableHitCollision;
}
/**
* Sets the state of the stored player to the state used by arenas
*
* @param horizontalVelocity <p>The horizontal velocity to apply to the player</p>
*/
public void setArenaState(float horizontalVelocity) {
this.player.setAllowFlight(true);
this.player.setFlying(true);
this.player.setGameMode(GameMode.ADVENTURE);
this.player.setSwimming(false);
if (this.disableHitCollision) {
this.player.setCollidable(false);
}
if (this.makePlayerInvisible) {
this.player.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY,
PotionEffect.INFINITE_DURATION, 3));
}
// If playing on the inverted game-mode, negate the horizontal velocity to swap the controls
if (arenaGameMode == DropperArenaGameMode.INVERTED) {
this.player.setFlySpeed(-horizontalVelocity);
} else {
this.player.setFlySpeed(horizontalVelocity);
}
}
/**
* Restores the stored state for the stored player
*/
public void restore() {
this.player.setFlying(this.originalIsFlying);
this.player.setGameMode(this.originalGameMode);
this.player.setAllowFlight(this.originalAllowFlight);
this.player.setFlySpeed(this.originalFlySpeed);
this.player.setInvulnerable(this.originalInvulnerable);
this.player.setSwimming(this.originalIsSwimming);
if (this.disableHitCollision) {
this.player.setCollidable(this.originalCollideAble);
}
if (this.makePlayerInvisible) {
this.player.removePotionEffect(PotionEffectType.INVISIBILITY);
}
}
/**
* Gets the location the player entered from
*
* @return <p>The location the player entered from</p>
*/
public Location getEntryLocation() {
return this.entryLocation;
}
}

View File

@@ -0,0 +1,363 @@
package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaGameMode;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.util.StringSanitizer;
import org.bukkit.Location;
import org.bukkit.Material;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import static net.knarcraft.minigames.util.InputValidationHelper.isInvalid;
/**
* A representation of one parkour arena
*/
public class ParkourArena {
/**
* An unique and persistent identifier for this arena
*/
private final UUID arenaId;
/**
* A name used when listing and storing this arena.
*/
private @NotNull String arenaName;
/**
* The location players are teleported to when joining this arena.
*/
private @NotNull Location spawnLocation;
/**
* The location players will be sent to when they win or lose the arena. If not set, their entry location should be
* used instead.
*/
private @Nullable Location exitLocation;
/**
* The material of the block players have to hit to win this parkour arena
*/
private @NotNull Material winBlockType;
/**
* The location the player has to reach to win. If not set, winBlockType is used instead
*/
private @Nullable Location winLocation;
/**
* The block types constituting this arena's kill plane
*/
private @Nullable Set<Material> killPlaneBlocks;
/**
* The checkpoints for this arena. Entering a checkpoint overrides the player's spawn location.
*/
private @NotNull List<Location> checkpoints;
/**
* The arena data for this arena
*/
private final ParkourArenaData parkourArenaData;
private final ParkourArenaHandler parkourArenaHandler;
/**
* Instantiates a new parkour arena
*
* @param arenaId <p>The id of the arena</p>
* @param arenaName <p>The name of the arena</p>
* @param spawnLocation <p>The location players spawn in when entering the arena</p>
* @param exitLocation <p>The location the players are teleported to when exiting the arena, or null</p>
* @param winBlockType <p>The material of the block players have to hit to win this parkour arena</p>
* @param parkourArenaData <p>The arena data keeping track of which players have done what in this arena</p>
* @param arenaHandler <p>The arena handler used for saving any changes</p>
*/
public ParkourArena(@NotNull UUID arenaId, @NotNull String arenaName, @NotNull Location spawnLocation,
@Nullable Location exitLocation, @NotNull Material winBlockType,
@Nullable Set<Material> killPlaneBlocks, @NotNull List<Location> checkpoints,
@NotNull ParkourArenaData parkourArenaData, @NotNull ParkourArenaHandler arenaHandler) {
this.arenaId = arenaId;
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
this.exitLocation = exitLocation;
this.winBlockType = winBlockType;
this.killPlaneBlocks = killPlaneBlocks;
this.checkpoints = checkpoints;
this.parkourArenaData = parkourArenaData;
this.parkourArenaHandler = arenaHandler;
}
/**
* Instantiates a new parkour arena
*
* <p>Note that this minimal constructor can be used to quickly create a new parkour arena at the player's given
* location, simply by them giving an arena name.</p>
*
* @param arenaName <p>The name of the arena</p>
* @param spawnLocation <p>The location players spawn in when entering the arena</p>
* @param arenaHandler <p>The arena handler used for saving any changes</p>
*/
public ParkourArena(@NotNull String arenaName, @NotNull Location spawnLocation,
@NotNull ParkourArenaHandler arenaHandler) {
this.arenaId = UUID.randomUUID();
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
this.exitLocation = null;
this.winLocation = null;
Map<ArenaGameMode, ArenaRecordsRegistry> recordRegistries = new HashMap<>();
for (ParkourArenaGameMode arenaGameMode : ParkourArenaGameMode.values()) {
recordRegistries.put(arenaGameMode, new ParkourArenaRecordsRegistry(this.arenaId));
}
this.parkourArenaData = new ParkourArenaData(this.arenaId, recordRegistries, new HashMap<>());
this.winBlockType = Material.EMERALD_BLOCK;
this.killPlaneBlocks = null;
this.checkpoints = new ArrayList<>();
this.parkourArenaHandler = arenaHandler;
}
/**
* Gets this arena's data
*
* @return <p>This arena's data</p>
*/
public @NotNull ParkourArenaData getData() {
return this.parkourArenaData;
}
/**
* Gets the id of this arena
*
* @return <p>This arena's identifier</p>
*/
public @NotNull UUID getArenaId() {
return this.arenaId;
}
/**
* Gets the name of this arena
*
* @return <p>The name of this arena</p>
*/
public @NotNull String getArenaName() {
return this.arenaName;
}
/**
* Gets this arena's spawn location
*
* <p>The spawn location is the location every player starts from when entering the parkour arena.</p>
*
* @return <p>This arena's spawn location.</p>
*/
public @NotNull Location getSpawnLocation() {
return this.spawnLocation;
}
/**
* Gets this arena's exit location
*
* @return <p>This arena's exit location, or null if no such location is set.</p>
*/
public @Nullable Location getExitLocation() {
return this.exitLocation;
}
/**
* Gets the type of block a player has to hit to win this arena
*
* @return <p>The kind of block players must hit</p>
*/
public @NotNull Material getWinBlockType() {
return this.winBlockType;
}
/**
* The location a player has to reach to win this arena
*
* <p></p>
*
* @return <p>The win trigger's location</p>
*/
public @Nullable Location getWinLocation() {
return this.winLocation != null ? this.winLocation.clone() : null;
}
/**
* Gets the block types used for this parkour arena's kill plane
*
* @return <p>The types of blocks that cause a loss</p>
*/
public Set<Material> getKillPlaneBlocks() {
if (this.killPlaneBlocks != null) {
return new HashSet<>(this.killPlaneBlocks);
} else {
return MiniGames.getInstance().getParkourConfiguration().getKillPlaneBlocks();
}
}
/**
* Gets all checkpoint locations for this arena
*
* @return <p>All checkpoint locations for this arena</p>
*/
public List<Location> getCheckpoints() {
List<Location> copy = new ArrayList<>(this.checkpoints.size());
for (Location location : this.checkpoints) {
copy.add(location.clone());
}
return copy;
}
/**
* Gets this arena's sanitized name
*
* @return <p>This arena's sanitized name</p>
*/
public @NotNull String getArenaNameSanitized() {
return StringSanitizer.sanitizeArenaName(this.getArenaName());
}
/**
* Sets the spawn location for this arena
*
* @param newLocation <p>The new spawn location</p>
* @return <p>True if successfully updated</p>
*/
public boolean setSpawnLocation(@NotNull Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.spawnLocation = newLocation;
parkourArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the exit location for this arena
*
* @param newLocation <p>The new exit location</p>
* @return <p>True if successfully updated</p>
*/
public boolean setExitLocation(@NotNull Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.exitLocation = newLocation;
parkourArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the name of this arena
*
* @param arenaName <p>The new name</p>
* @return <p>True if successfully updated</p>
*/
public boolean setName(@NotNull String arenaName) {
if (!arenaName.isBlank()) {
String oldName = this.getArenaNameSanitized();
this.arenaName = arenaName;
// Update the arena lookup map to make sure the new name can be used immediately
parkourArenaHandler.updateLookupName(oldName, this.getArenaNameSanitized());
parkourArenaHandler.saveArenas();
return true;
} else {
return false;
}
}
/**
* Sets the material of the win block type
*
* <p>The win block type is the type of block a player must hit to win in this arena</p>
*
* @param material <p>The material to set for the win block type</p>
* @return <p>True if successfully updated</p>
*/
public boolean setWinBlockType(@NotNull Material material) {
if (material.isAir() || !material.isBlock()) {
return false;
} else {
this.winBlockType = material;
parkourArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the location players need to reach to win this arena
*
* @param newLocation <p>The location players have to reach</p>
* @return <p>True if successfully changed</p>
*/
public boolean setWinLocation(@NotNull Location newLocation) {
if (isInvalid(newLocation)) {
return false;
} else {
this.exitLocation = newLocation.clone();
parkourArenaHandler.saveArenas();
return true;
}
}
/**
* Sets the type of blocks constituting this arena's kill plane
*
* @param killPlaneBlocks <p>The blocks that will cause players to lose</p>
* @return <p>True if successfully changed</p>
*/
public boolean setKillPlaneBlocks(@NotNull Set<Material> killPlaneBlocks) {
for (Material material : killPlaneBlocks) {
// Make sure no nulls have entered the set
if (material == null) {
return false;
}
}
this.killPlaneBlocks = new HashSet<>(killPlaneBlocks);
return true;
}
/**
* Sets the checkpoints of this arena
*
* @param checkpoints <p>The checkpoints to use</p>
* @return <p>True if successfully changed</p>
*/
public boolean setCheckpoints(@NotNull List<Location> checkpoints) {
List<Location> copy = new ArrayList<>(checkpoints.size());
for (Location location : checkpoints) {
if (isInvalid(location)) {
return false;
}
copy.add(location.clone());
}
this.checkpoints = copy;
return true;
}
@Override
public boolean equals(Object other) {
if (!(other instanceof ParkourArena otherArena)) {
return false;
}
return this.getArenaNameSanitized().equals(otherArena.getArenaNameSanitized());
}
}

View File

@@ -0,0 +1,73 @@
package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaData;
import net.knarcraft.minigames.arena.ArenaGameMode;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArenaRecordsRegistry;
import net.knarcraft.minigames.container.SerializableContainer;
import net.knarcraft.minigames.container.SerializableUUID;
import net.knarcraft.minigames.util.SerializableConverter;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* Data stored for an arena
*/
public class ParkourArenaData extends ArenaData {
/**
* Instantiates a new parkour arena data object
*
* @param arenaId <p>The id of the arena this data belongs to</p>
* @param recordRegistries <p>The registry of this arena's records</p>
* @param playersCompleted <p>The set of the players that have cleared this arena</p>
*/
public ParkourArenaData(@NotNull UUID arenaId,
@NotNull Map<ArenaGameMode, ArenaRecordsRegistry> recordRegistries,
@NotNull Map<ArenaGameMode, Set<UUID>> playersCompleted) {
super(arenaId, recordRegistries, playersCompleted);
}
@Override
public void saveData() {
MiniGames.getInstance().getParkourArenaHandler().saveData(this.arenaId);
}
/**
* Deserializes a dropper arena data from the given data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized dropper arena data</p>
*/
@SuppressWarnings({"unused", "unchecked"})
public static @NotNull ParkourArenaData deserialize(@NotNull Map<String, Object> data) {
SerializableUUID serializableUUID = (SerializableUUID) data.get("arenaId");
Map<ArenaGameMode, ArenaRecordsRegistry> recordsRegistry =
(Map<ArenaGameMode, ArenaRecordsRegistry>) data.get("recordsRegistry");
Map<ArenaGameMode, Set<SerializableContainer<UUID>>> playersCompletedData =
(Map<ArenaGameMode, Set<SerializableContainer<UUID>>>) data.get("playersCompleted");
if (recordsRegistry == null) {
recordsRegistry = new HashMap<>();
} else if (playersCompletedData == null) {
playersCompletedData = new HashMap<>();
}
// Convert the serializable UUIDs to normal UUIDs
Map<ArenaGameMode, Set<UUID>> allPlayersCompleted = new HashMap<>();
SerializableConverter.getRawValue(playersCompletedData, allPlayersCompleted);
for (ArenaGameMode arenaGameMode : playersCompletedData.keySet()) {
if (!recordsRegistry.containsKey(arenaGameMode) || recordsRegistry.get(arenaGameMode) == null) {
recordsRegistry.put(arenaGameMode, new DropperArenaRecordsRegistry(serializableUUID.getRawValue()));
}
}
return new ParkourArenaData(serializableUUID.getRawValue(), recordsRegistry, allPlayersCompleted);
}
}

View File

@@ -0,0 +1,50 @@
package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.minigames.arena.ArenaGameMode;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
* A representation of possible arena game-modes
*/
public enum ParkourArenaGameMode implements ConfigurationSerializable, ArenaGameMode {
/**
* The default game-mode. Failing once throws the player out.
*/
DEFAULT,
;
/**
* Tries to match the correct game-mode according to the given string
*
* @param gameMode <p>The game-mode string to match</p>
* @return <p>The specified arena game-mode</p>
*/
public static @NotNull ParkourArenaGameMode matchGamemode(@NotNull String gameMode) {
return ParkourArenaGameMode.DEFAULT;
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("name", this.name());
return data;
}
/**
* Deserializes the arena game-mode specified by the given data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized arena game-mode</p>
*/
@SuppressWarnings("unused")
public static ParkourArenaGameMode deserialize(Map<String, Object> data) {
return ParkourArenaGameMode.valueOf((String) data.get("name"));
}
}

View File

@@ -0,0 +1,122 @@
package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaGroup;
import net.knarcraft.minigames.container.SerializableUUID;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
/**
* A sorted group of arenas that must be completed in sequence
*/
public class ParkourArenaGroup extends ArenaGroup {
/**
* Instantiates a new parkour arena group
*
* @param groupName <p>The name of this group</p>
*/
public ParkourArenaGroup(@NotNull String groupName) {
super(groupName);
}
/**
* Instantiates a new parkour arena group
*
* @param groupId <p>The unique id of this group</p>
* @param groupName <p>The name of this group</p>
* @param arenas <p>The arenas in this group</p>
*/
private ParkourArenaGroup(@NotNull UUID groupId, @NotNull String groupName, @NotNull List<UUID> arenas) {
super(groupId, groupName, arenas);
}
/**
* Checks whether the given player has beaten all arenas in this group on the given game-mode
*
* @param gameMode <p>The game-mode to check</p>
* @param player <p>The player to check</p>
* @return <p>True if the player has beaten all arenas, false otherwise</p>
*/
public boolean hasBeatenAll(ParkourArenaGameMode gameMode, Player player) {
ParkourArenaHandler arenaHandler = MiniGames.getInstance().getParkourArenaHandler();
for (UUID anArenaId : this.getArenas()) {
ParkourArena parkourArena = arenaHandler.getArena(anArenaId);
if (parkourArena == null) {
// The arena would only be null if the arena has been deleted, but not removed from this group
MiniGames.log(Level.WARNING, "The parkour group " + this.getGroupName() +
" contains the arena id " + anArenaId + " which is not a valid arena id!");
continue;
}
if (parkourArena.getData().hasNotCompleted(gameMode, player)) {
return false;
}
}
return true;
}
/**
* Gets whether the given player can play the given arena part of this group, on the given game-mode
*
* @param gameMode <p>The game-mode the player is trying to play</p>
* @param player <p>The player to check</p>
* @param arenaId <p>The id of the arena in this group to check</p>
* @return <p>True if the player is allowed to play the arena</p>
* @throws IllegalArgumentException <p>If checking an arena not in this group</p>
*/
public boolean canPlay(ParkourArenaGameMode gameMode, Player player, UUID arenaId) throws IllegalArgumentException {
if (!this.arenas.contains(arenaId)) {
throw new IllegalArgumentException("Cannot check for playability for arena not in this group!");
}
ParkourArenaHandler arenaHandler = MiniGames.getInstance().getParkourArenaHandler();
for (UUID anArenaId : this.getArenas()) {
// If the target arena is reached, allow, as all previous arenas must have been cleared
if (arenaId.equals(anArenaId)) {
return true;
}
ParkourArena parkourArena = arenaHandler.getArena(anArenaId);
if (parkourArena == null) {
// The arena would only be null if the arena has been deleted, but not removed from this group
MiniGames.log(Level.WARNING, String.format("The parkour group %s contains the" +
" arena id %s which is not a valid arena id!", this.getGroupName(), anArenaId));
continue;
}
// This is a lower-numbered arena the player has yet to complete
if (parkourArena.getData().hasNotCompleted(gameMode, player)) {
return false;
}
}
return false;
}
/**
* Deserializes the given data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized arena group</p>
*/
@SuppressWarnings({"unused", "unchecked"})
public static @NotNull ParkourArenaGroup deserialize(@NotNull Map<String, Object> data) {
UUID id = ((SerializableUUID) data.get("groupId")).getRawValue();
String name = (String) data.get("groupName");
List<SerializableUUID> serializableArenas = (List<SerializableUUID>) data.get("arenas");
List<UUID> arenas = new ArrayList<>();
for (SerializableUUID arenaId : serializableArenas) {
arenas.add(arenaId.getRawValue());
}
return new ParkourArenaGroup(id, name, arenas);
}
}

View File

@@ -0,0 +1,249 @@
package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.util.ArenaStorageHelper;
import net.knarcraft.minigames.util.StringSanitizer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
/**
* A handler that keeps track of all parkour arenas
*/
public class ParkourArenaHandler {
private Map<UUID, ParkourArena> arenas = new HashMap<>();
private Map<UUID, ParkourArenaGroup> arenaGroups = new HashMap<>();
private Map<String, UUID> arenaNameLookup = new HashMap<>();
/**
* Gets all arenas that are within a group
*
* @return <p>All arenas in a group</p>
*/
public @NotNull Set<ParkourArena> getArenasInAGroup() {
Set<ParkourArena> arenas = new HashSet<>();
for (UUID arenaId : arenaGroups.keySet()) {
arenas.add(this.arenas.get(arenaId));
}
return arenas;
}
/**
* Gets a copy of all parkour groups
*
* @return <p>All parkour groups</p>
*/
public Set<ParkourArenaGroup> getAllGroups() {
return new HashSet<>(arenaGroups.values());
}
/**
* Gets the group the given arena belongs to
*
* @param arenaId <p>The id of the arena to get the group of</p>
* @return <p>The group the arena belongs to, or null if not in a group</p>
*/
public @Nullable ParkourArenaGroup getGroup(@NotNull UUID arenaId) {
return this.arenaGroups.get(arenaId);
}
/**
* Sets the group for the given arena
*
* @param arenaId <p>The id of the arena to change</p>
* @param arenaGroup <p>The group to add the arena to, or null to remove the current group</p>
*/
public void setGroup(@NotNull UUID arenaId, @Nullable ParkourArenaGroup arenaGroup) {
if (arenaGroup == null) {
// No need to remove something non-existing
if (!this.arenaGroups.containsKey(arenaId)) {
return;
}
// Remove the existing group
ParkourArenaGroup oldGroup = this.arenaGroups.remove(arenaId);
oldGroup.removeArena(arenaId);
} else {
// Make sure to remove the arena from the old group's internal tracking
if (this.arenaGroups.containsKey(arenaId)) {
this.arenaGroups.remove(arenaId).removeArena(arenaId);
}
this.arenaGroups.put(arenaId, arenaGroup);
arenaGroup.addArena(arenaId);
}
saveGroups();
}
/**
* Gets the parkour arena group with the given name
*
* @param groupName <p>The name of the group to get</p>
* @return <p>The group, or null if not found</p>
*/
public @Nullable ParkourArenaGroup getGroup(String groupName) {
String sanitized = StringSanitizer.sanitizeArenaName(groupName);
for (ParkourArenaGroup arenaGroup : this.arenaGroups.values()) {
if (arenaGroup.getGroupNameSanitized().equals(sanitized)) {
return arenaGroup;
}
}
return null;
}
/**
* Replaces an arena's lookup name
*
* @param oldName <p>The arena's old sanitized lookup name</p>
* @param newName <p>The arena's new sanitized lookup name</p>
*/
public void updateLookupName(@NotNull String oldName, @NotNull String newName) {
UUID arenaId = this.arenaNameLookup.remove(oldName);
if (arenaId != null) {
this.arenaNameLookup.put(newName, arenaId);
}
}
/**
* Adds a new arena
*
* @param arena <p>The arena to add</p>
*/
public void addArena(@NotNull ParkourArena arena) {
this.arenas.put(arena.getArenaId(), arena);
this.arenaNameLookup.put(arena.getArenaNameSanitized(), arena.getArenaId());
this.saveArenas();
}
/**
* Gets the arena with the given id
*
* @param arenaId <p>The id of the arena to get</p>
* @return <p>The arena, or null if no arena could be found</p>
*/
public @Nullable ParkourArena getArena(@NotNull UUID arenaId) {
return this.arenas.get(arenaId);
}
/**
* Gets the arena with the given name
*
* @param arenaName <p>The arena to get</p>
* @return <p>The arena with the given name, or null if not found</p>
*/
public @Nullable ParkourArena getArena(@NotNull String arenaName) {
return this.arenas.get(this.arenaNameLookup.get(StringSanitizer.sanitizeArenaName(arenaName)));
}
/**
* Gets all known arenas
*
* @return <p>All known arenas</p>
*/
public @NotNull Map<UUID, ParkourArena> getArenas() {
return new HashMap<>(this.arenas);
}
/**
* Removes the given arena
*
* @param arena <p>The arena to remove</p>
*/
public void removeArena(@NotNull ParkourArena arena) {
UUID arenaId = arena.getArenaId();
MiniGames.getInstance().getParkourArenaPlayerRegistry().removeForArena(arena);
this.arenas.remove(arenaId);
this.arenaNameLookup.remove(arena.getArenaNameSanitized());
this.arenaGroups.remove(arenaId);
if (!ArenaStorageHelper.removeParkourArenaData(arenaId)) {
MiniGames.log(Level.WARNING, "Unable to remove parkour arena data file " + arenaId + ".yml. " +
"You must remove it manually!");
}
this.saveArenas();
}
/**
* Stores the data for the given arena
*
* @param arenaId <p>The id of the arena whose data should be saved</p>
*/
public void saveData(UUID arenaId) {
try {
ArenaStorageHelper.saveParkourArenaData(this.arenas.get(arenaId).getData());
} catch (IOException e) {
MiniGames.log(Level.SEVERE, "Unable to save arena data! Data loss can occur!");
MiniGames.log(Level.SEVERE, e.getMessage());
}
}
/**
* Saves all current parkour groups to disk
*/
public void saveGroups() {
try {
ArenaStorageHelper.saveParkourArenaGroups(new HashSet<>(this.arenaGroups.values()));
} catch (IOException e) {
MiniGames.log(Level.SEVERE, "Unable to save current arena groups! " +
"Data loss can occur!");
MiniGames.log(Level.SEVERE, e.getMessage());
}
}
/**
* Loads all arenas and groups from disk
*/
public void load() {
loadArenas();
loadGroups();
}
/**
* Loads all parkour groups from disk
*/
private void loadGroups() {
Set<ParkourArenaGroup> arenaGroups = ArenaStorageHelper.loadParkourArenaGroups();
Map<UUID, ParkourArenaGroup> arenaGroupMap = new HashMap<>();
for (ParkourArenaGroup arenaGroup : arenaGroups) {
for (UUID arenaId : arenaGroup.getArenas()) {
arenaGroupMap.put(arenaId, arenaGroup);
}
}
this.arenaGroups = arenaGroupMap;
}
/**
* Saves all current arenas to disk
*/
public void saveArenas() {
try {
ArenaStorageHelper.saveParkourArenas(this.arenas);
} catch (IOException e) {
MiniGames.log(Level.SEVERE, "Unable to save current arenas! " +
"Data loss can occur!");
MiniGames.log(Level.SEVERE, e.getMessage());
}
}
/**
* Loads all arenas from disk
*/
private void loadArenas() {
this.arenas = ArenaStorageHelper.loadParkourArenas();
// Save a map from arena name to arena id for improved performance
this.arenaNameLookup = new HashMap<>();
for (Map.Entry<UUID, ParkourArena> arena : this.arenas.entrySet()) {
String sanitizedName = arena.getValue().getArenaNameSanitized();
this.arenaNameLookup.put(sanitizedName, arena.getKey());
}
}
}

View File

@@ -0,0 +1,61 @@
package net.knarcraft.minigames.arena.parkour;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* A registry to keep track of which players are playing in which arenas
*/
public class ParkourArenaPlayerRegistry {
private final Map<UUID, ParkourArenaSession> arenaPlayers = new HashMap<>();
/**
* Registers that the given player has started playing the given parkour arena session
*
* @param playerId <p>The id of the player that started playing</p>
* @param arena <p>The arena session to register</p>
*/
public void registerPlayer(@NotNull UUID playerId, @NotNull ParkourArenaSession arena) {
this.arenaPlayers.put(playerId, arena);
}
/**
* Removes this player from players currently playing
*
* @param playerId <p>The id of the player to remove</p>
*/
public boolean removePlayer(@NotNull UUID playerId) {
return this.arenaPlayers.remove(playerId) != null;
}
/**
* Gets the player's active parkour arena session
*
* @param playerId <p>The id of the player to get arena for</p>
* @return <p>The player's active arena session, or null if not currently playing</p>
*/
public @Nullable ParkourArenaSession getArenaSession(@NotNull UUID playerId) {
return this.arenaPlayers.getOrDefault(playerId, null);
}
/**
* Removes all active sessions for the given arena
*
* @param arena <p>The arena to remove sessions for</p>
*/
public void removeForArena(ParkourArena arena) {
for (Map.Entry<UUID, ParkourArenaSession> entry : this.arenaPlayers.entrySet()) {
if (entry.getValue().getArena() == arena) {
// Kick the player gracefully
entry.getValue().triggerQuit(false);
this.arenaPlayers.remove(entry.getKey());
}
}
}
}

View File

@@ -0,0 +1,66 @@
package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.arena.record.IntegerRecord;
import net.knarcraft.minigames.arena.record.LongRecord;
import net.knarcraft.minigames.container.SerializableUUID;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
/**
* A registry keeping track of all records
*/
public class ParkourArenaRecordsRegistry extends ArenaRecordsRegistry {
/**
* Instantiates a new empty records registry
*/
public ParkourArenaRecordsRegistry(@NotNull UUID arenaId) {
super(arenaId);
}
/**
* Instantiates a new records registry
*
* @param leastDeaths <p>The existing least death records to use</p>
* @param shortestTimeMilliSeconds <p>The existing leash time records to use</p>
*/
private ParkourArenaRecordsRegistry(@NotNull UUID arenaId, @NotNull Set<IntegerRecord> leastDeaths,
@NotNull Set<LongRecord> shortestTimeMilliSeconds) {
super(arenaId, leastDeaths, shortestTimeMilliSeconds);
}
/**
* Saves changed records
*/
@Override
protected void save() {
MiniGames.getInstance().getParkourArenaHandler().saveData(this.arenaId);
}
/**
* Deserializes the given data
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized records registry</p>
*/
@SuppressWarnings({"unused", "unchecked"})
public static ParkourArenaRecordsRegistry deserialize(Map<String, Object> data) {
UUID arenaId = ((SerializableUUID) data.get("arenaId")).getRawValue();
Set<IntegerRecord> leastDeaths =
(Set<IntegerRecord>) data.getOrDefault("leastDeaths", new HashMap<>());
Set<LongRecord> shortestTimeMilliseconds =
(Set<LongRecord>) data.getOrDefault("shortestTime", new HashMap<>());
leastDeaths.removeIf(Objects::isNull);
shortestTimeMilliseconds.removeIf(Objects::isNull);
return new ParkourArenaRecordsRegistry(arenaId, leastDeaths, shortestTimeMilliseconds);
}
}

View File

@@ -0,0 +1,194 @@
package net.knarcraft.minigames.arena.parkour;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.config.ParkourConfiguration;
import net.knarcraft.minigames.property.RecordResult;
import net.knarcraft.minigames.util.PlayerTeleporter;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.logging.Level;
/**
* A representation of a player's current session in a parkour arena
*/
public class ParkourArenaSession {
private final @NotNull ParkourArena arena;
private final @NotNull Player player;
private final @NotNull ParkourArenaGameMode gameMode;
private int deaths;
private final long startTime;
private final ParkourPlayerEntryState entryState;
/**
* Instantiates a new parkour arena session
*
* @param parkourArena <p>The arena that's being played in</p>
* @param player <p>The player playing the arena</p>
* @param gameMode <p>The game-mode</p>
*/
public ParkourArenaSession(@NotNull ParkourArena parkourArena, @NotNull Player player,
@NotNull ParkourArenaGameMode gameMode) {
this.arena = parkourArena;
this.player = player;
this.gameMode = gameMode;
this.deaths = 0;
this.startTime = System.currentTimeMillis();
ParkourConfiguration configuration = MiniGames.getInstance().getParkourConfiguration();
boolean makeInvisible = configuration.makePlayersInvisible();
this.entryState = new ParkourPlayerEntryState(player, makeInvisible);
// Make the player fly to improve mobility in the air
this.entryState.setArenaState();
}
/**
* Gets the state of the player when they joined the session
*
* @return <p>The player's entry state</p>
*/
public @NotNull ParkourPlayerEntryState getEntryState() {
return this.entryState;
}
/**
* Triggers a win for the player playing in this session
*/
public void triggerWin() {
// Stop this session
stopSession();
// Check for, and display, records
MiniGames miniGames = MiniGames.getInstance();
boolean ignore = miniGames.getDropperConfiguration().ignoreRecordsUntilGroupBeatenOnce();
ParkourArenaGroup group = miniGames.getParkourArenaHandler().getGroup(this.arena.getArenaId());
if (!ignore || group == null || group.hasBeatenAll(this.gameMode, this.player)) {
registerRecord();
}
// Mark the arena as cleared
if (this.arena.getData().setCompleted(this.gameMode, this.player)) {
this.player.sendMessage("You cleared the arena!");
}
this.player.sendMessage("You won!");
// Teleport the player out of the arena
teleportToExit(false);
}
/**
* Teleports the playing player out of the arena
*/
private void teleportToExit(boolean immediately) {
// Teleport the player out of the arena
Location exitLocation;
if (this.arena.getExitLocation() != null) {
exitLocation = this.arena.getExitLocation();
} else {
exitLocation = this.entryState.getEntryLocation();
}
PlayerTeleporter.teleportPlayer(this.player, exitLocation, true, immediately);
}
/**
* Removes this session from current sessions
*/
private void removeSession() {
// Remove this session for game sessions to stop listeners from fiddling more with the player
boolean removedSession = MiniGames.getInstance().getDropperArenaPlayerRegistry().removePlayer(player.getUniqueId());
if (!removedSession) {
MiniGames.log(Level.SEVERE, "Unable to remove dropper arena session for " + player.getName() + ". " +
"This will have unintended consequences.");
}
}
/**
* Registers the player's record if necessary, and prints record information to the player
*/
private void registerRecord() {
ArenaRecordsRegistry recordsRegistry = this.arena.getData().getRecordRegistries().get(this.gameMode);
long timeElapsed = System.currentTimeMillis() - this.startTime;
announceRecord(recordsRegistry.registerTimeRecord(this.player.getUniqueId(), timeElapsed), "time");
announceRecord(recordsRegistry.registerDeathRecord(this.player.getUniqueId(), this.deaths), "least deaths");
}
/**
* Announces a record set by this player
*
* @param recordResult <p>The result of the record</p>
* @param type <p>The type of record set (time or deaths)</p>
*/
private void announceRecord(@NotNull RecordResult recordResult, @NotNull String type) {
if (recordResult == RecordResult.NONE) {
return;
}
// Gets a string representation of the played game-mode
String gameModeString = switch (this.gameMode) {
case DEFAULT -> "default";
};
String recordString = "You just set a %s on the %s game-mode!";
recordString = switch (recordResult) {
case WORLD_RECORD -> String.format(recordString, "new %s record", gameModeString);
case PERSONAL_BEST -> String.format(recordString, "personal %s record", gameModeString);
default -> throw new IllegalStateException("Unexpected value: " + recordResult);
};
player.sendMessage(String.format(recordString, type));
}
/**
* Triggers a loss for the player playing in this session
*/
public void triggerLoss() {
this.deaths++;
//Teleport the player back to the top
PlayerTeleporter.teleportPlayer(this.player, this.arena.getSpawnLocation(), true, false);
this.entryState.setArenaState();
}
/**
* Triggers a quit for the player playing in this session
*/
public void triggerQuit(boolean immediately) {
// Stop this session
stopSession();
// Teleport the player out of the arena
teleportToExit(immediately);
player.sendMessage("You quit the arena!");
}
/**
* Stops this session, and disables flight mode
*/
private void stopSession() {
// Remove this session from game sessions to stop listeners from fiddling more with the player
removeSession();
// Remove flight mode
entryState.restore();
}
/**
* Gets the arena this session is being played in
*
* @return <p>The session's arena</p>
*/
public @NotNull ParkourArena getArena() {
return this.arena;
}
/**
* Gets the player playing in this session
*
* @return <p>This session's player</p>
*/
public @NotNull Player getPlayer() {
return this.player;
}
}

View File

@@ -0,0 +1,76 @@
package net.knarcraft.minigames.arena.parkour;
import org.jetbrains.annotations.NotNull;
/**
* A representation of each key used for storing arena data
*/
public enum ParkourArenaStorageKey {
/**
* The key for an arena's id
*/
ID("arenaId"),
/**
* The key for an arena's name
*/
NAME("arenaName"),
/**
* The key for an arena's spawn location
*/
SPAWN_LOCATION("arenaSpawnLocation"),
/**
* The key for an arena's exit location
*/
EXIT_LOCATION("arenaExitLocation"),
/**
* The key for the type of this arena's win block
*/
WIN_BLOCK_TYPE("winBlockType"),
/**
* The key for this arena's win location (overrides win block type)
*/
WIN_LOCATION("winLocation"),
/**
* The key for this arena's kill plane blocks (overrides the config)
*/
KILL_PLANE_BLOCKS("killPlaneBlocks"),
/**
* The key for this arena's checkpoint locations
*/
CHECKPOINTS("checkpoints"),
/**
* The hey for this arena's data
*/
DATA("arenaData"),
;
private final @NotNull String key;
/**
* Instantiates a new arena storage key
*
* @param key <p>The string path of the configuration key this value represents.</p>
*/
ParkourArenaStorageKey(@NotNull String key) {
this.key = key;
}
/**
* Gets the configuration key this enum represents
*
* @return <p>The string key representation.</p>
*/
public @NotNull String getKey() {
return this.key;
}
}

View File

@@ -0,0 +1,81 @@
package net.knarcraft.minigames.arena.parkour;
import org.bukkit.GameMode;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
/**
* The state of a player before entering a dropper arena
*/
public class ParkourPlayerEntryState {
private final Player player;
private final Location entryLocation;
private final boolean originalIsFlying;
private final GameMode originalGameMode;
private final boolean originalAllowFlight;
private final boolean originalInvulnerable;
private final boolean originalIsSwimming;
private final boolean originalCollideAble;
private final boolean makePlayerInvisible;
/**
* Instantiates a new player state
*
* @param player <p>The player whose state should be stored</p>
*/
public ParkourPlayerEntryState(@NotNull Player player, boolean makePlayerInvisible) {
this.player = player;
this.entryLocation = player.getLocation().clone();
this.originalIsFlying = player.isFlying();
this.originalGameMode = player.getGameMode();
this.originalAllowFlight = player.getAllowFlight();
this.originalInvulnerable = player.isInvulnerable();
this.originalIsSwimming = player.isSwimming();
this.originalCollideAble = player.isCollidable();
this.makePlayerInvisible = makePlayerInvisible;
}
/**
* Sets the state of the stored player to the state used by arenas
*/
public void setArenaState() {
this.player.setAllowFlight(false);
this.player.setFlying(false);
this.player.setGameMode(GameMode.ADVENTURE);
this.player.setSwimming(false);
this.player.setCollidable(false);
if (this.makePlayerInvisible) {
this.player.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY,
PotionEffect.INFINITE_DURATION, 3));
}
}
/**
* Restores the stored state for the stored player
*/
public void restore() {
this.player.setFlying(this.originalIsFlying);
this.player.setGameMode(this.originalGameMode);
this.player.setAllowFlight(this.originalAllowFlight);
this.player.setInvulnerable(this.originalInvulnerable);
this.player.setSwimming(this.originalIsSwimming);
this.player.setCollidable(this.originalCollideAble);
if (this.makePlayerInvisible) {
this.player.removePotionEffect(PotionEffectType.INVISIBILITY);
}
}
/**
* Gets the location the player entered from
*
* @return <p>The location the player entered from</p>
*/
public Location getEntryLocation() {
return this.entryLocation;
}
}

View File

@@ -0,0 +1,76 @@
package net.knarcraft.minigames.arena.record;
import net.knarcraft.minigames.container.SerializableUUID;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
/**
* A record stored for an arena
*/
public abstract class ArenaRecord<K extends Comparable<K>> implements Comparable<ArenaRecord<K>>, ConfigurationSerializable {
private final UUID userId;
private final K record;
/**
* @param userId <p>The id of the player that achieved the record</p>
* @param record <p>The record achieved</p>
*/
public ArenaRecord(UUID userId, K record) {
this.userId = userId;
this.record = record;
}
/**
* Gets the id of the user this record belongs to
*
* @return <p>The record's achiever</p>
*/
public UUID getUserId() {
return userId;
}
/**
* Gets the value of the stored record
*
* @return <p>The record value</p>
*/
public K getRecord() {
return record;
}
@Override
public boolean equals(Object other) {
return other instanceof ArenaRecord<?> && userId.equals(((ArenaRecord<?>) other).userId);
}
@Override
public int compareTo(@NotNull ArenaRecord<K> other) {
return record.compareTo(other.record);
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("userId", new SerializableUUID(getUserId()));
data.put("record", record);
return data;
}
@Override
public int hashCode() {
return Objects.hash(userId, record);
}
@Override
public String toString() {
return userId + ": " + record;
}
}

View File

@@ -0,0 +1,43 @@
package net.knarcraft.minigames.arena.record;
import net.knarcraft.minigames.container.SerializableUUID;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.UUID;
/**
* A record storing an integer
*/
public class IntegerRecord extends SummableArenaRecord<Integer> {
/**
* @param userId <p>The id of the player that achieved the record</p>
* @param record <p>The record achieved</p>
*/
public IntegerRecord(UUID userId, Integer record) {
super(userId, record);
}
@Override
public SummableArenaRecord<Integer> sum(Integer value) {
return new IntegerRecord(this.getUserId(), this.getRecord() + value);
}
@Override
public boolean equals(Object other) {
return other instanceof IntegerRecord && this.getUserId().equals(((IntegerRecord) other).getUserId());
}
/**
* Deserializes the saved arena record
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized data</p>
*/
@SuppressWarnings("unused")
public static IntegerRecord deserialize(@NotNull Map<String, Object> data) {
return new IntegerRecord(((SerializableUUID) data.get("userId")).getRawValue(), (Integer) data.get("record"));
}
}

View File

@@ -0,0 +1,43 @@
package net.knarcraft.minigames.arena.record;
import net.knarcraft.minigames.container.SerializableUUID;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.UUID;
/**
* A record storing a Long
*/
public class LongRecord extends SummableArenaRecord<Long> {
/**
* @param userId <p>The id of the player that achieved the record</p>
* @param record <p>The record achieved</p>
*/
public LongRecord(UUID userId, Long record) {
super(userId, record);
}
@Override
public boolean equals(Object other) {
return other instanceof LongRecord && this.getUserId().equals(((LongRecord) other).getUserId());
}
@Override
public SummableArenaRecord<Long> sum(Long value) {
return new LongRecord(this.getUserId(), this.getRecord() + value);
}
/**
* Deserializes the saved arena record
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized data</p>
*/
@SuppressWarnings("unused")
public static LongRecord deserialize(@NotNull Map<String, Object> data) {
return new LongRecord(((SerializableUUID) data.get("userId")).getRawValue(), ((Number) data.get("record")).longValue());
}
}

View File

@@ -0,0 +1,28 @@
package net.knarcraft.minigames.arena.record;
import java.util.UUID;
/**
* A type of arena record which can be summed together
*
* @param <K> <p>The type of the stored value</p>
*/
public abstract class SummableArenaRecord<K extends Comparable<K>> extends ArenaRecord<K> {
/**
* @param userId <p>The id of the player that achieved the record</p>
* @param record <p>The record achieved</p>
*/
public SummableArenaRecord(UUID userId, K record) {
super(userId, record);
}
/**
* Returns a summable record with the resulting sum
*
* @param value <p>The value to add to the existing value</p>
* @return <p>A record with the sum of this record and the given value</p>
*/
public abstract SummableArenaRecord<K> sum(K value);
}

View File

@@ -0,0 +1,53 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaHandler;
import net.knarcraft.minigames.util.StringSanitizer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* The command for creating a new dropper arena
*/
public class CreateArenaCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be used by a player");
return false;
}
// Abort if no name was specified
if (arguments.length < 1) {
return false;
}
// Remove known characters that are likely to cause trouble if used in an arena name
String arenaName = StringSanitizer.removeUnwantedCharacters(arguments[0]);
// An arena name is required
if (arenaName.isBlank()) {
return false;
}
DropperArenaHandler arenaHandler = MiniGames.getInstance().getDropperArenaHandler();
DropperArena existingArena = arenaHandler.getArena(arenaName);
if (existingArena != null) {
commandSender.sendMessage("There already exists a dropper arena with that name!");
return false;
}
DropperArena arena = new DropperArena(arenaName, player.getLocation(), arenaHandler);
arenaHandler.addArena(arena);
commandSender.sendMessage("The arena was successfully created!");
return true;
}
}

View File

@@ -0,0 +1,167 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaEditableProperty;
import net.knarcraft.minigames.config.DropperConfiguration;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* The command for editing an existing dropper arena
*/
public class EditArenaCommand implements CommandExecutor {
private final DropperConfiguration configuration;
/**
* Instantiates a new edit arena command
*
* @param configuration <p>The configuration to use</p>
*/
public EditArenaCommand(DropperConfiguration configuration) {
this.configuration = configuration;
}
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be used by a player");
return false;
}
if (arguments.length < 2) {
return false;
}
DropperArena specifiedArena = MiniGames.getInstance().getDropperArenaHandler().getArena(arguments[0]);
if (specifiedArena == null) {
commandSender.sendMessage("Unable to find the specified dropper arena.");
return false;
}
DropperArenaEditableProperty editableProperty = DropperArenaEditableProperty.getFromArgumentString(arguments[1]);
if (editableProperty == null) {
commandSender.sendMessage("Unknown property specified.");
return false;
}
String currentValueFormat = "Current value of %s is: %s";
if (arguments.length < 3) {
// Print the current value of the property
String value = editableProperty.getCurrentValueAsString(specifiedArena);
commandSender.sendMessage(String.format(currentValueFormat, editableProperty.getArgumentString(), value));
return true;
} else {
boolean successful = changeValue(specifiedArena, editableProperty, arguments[2], player);
if (successful) {
player.sendMessage(String.format("Property %s changed to: %s", editableProperty, arguments[2]));
} else {
player.sendMessage("Unable to change the property. Make sure your input is valid!");
}
return successful;
}
}
/**
* Changes the given property to the given value
*
* @param arena <p>The arena to change the property for</p>
* @param property <p>The property to change</p>
* @param value <p>The new value of the property</p>
* @param player <p>The player trying to change the value</p>
* @return <p>True if the value was successfully changed</p>
*/
private boolean changeValue(@NotNull DropperArena arena, @NotNull DropperArenaEditableProperty property,
@NotNull String value, @NotNull Player player) {
return switch (property) {
case WIN_BLOCK_TYPE -> arena.setWinBlockType(parseMaterial(value));
case HORIZONTAL_VELOCITY -> arena.setHorizontalVelocity(sanitizeHorizontalVelocity(value));
case VERTICAL_VELOCITY -> arena.setVerticalVelocity(sanitizeVerticalVelocity(value));
case SPAWN_LOCATION -> arena.setSpawnLocation(parseLocation(player, value));
case NAME -> arena.setName(value);
case EXIT_LOCATION -> arena.setExitLocation(parseLocation(player, value));
};
}
/**
* Sanitizes the player's specified vertical velocity
*
* @param velocityString <p>The string to parse into a velocity</p>
* @return <p>The parsed velocity, defaulting to 0.5 if not parse-able</p>
*/
private double sanitizeVerticalVelocity(@NotNull String velocityString) {
// Vertical velocity should not be negative, as it would make the player go upwards. There is technically not a
// max speed limit, but setting it too high makes the arena unplayable
double velocity;
try {
velocity = Double.parseDouble(velocityString);
} catch (NumberFormatException exception) {
velocity = configuration.getVerticalVelocity();
}
// Require at least speed of 0.001, and at most 75 blocks/s
return Math.min(Math.max(velocity, 0.001), 75);
}
/**
* Sanitizes the user's specified horizontal velocity
*
* @param velocityString <p>The string to parse into a velocity</p>
* @return <p>The parsed velocity, defaulting to 1 if not parse-able</p>
*/
private float sanitizeHorizontalVelocity(@NotNull String velocityString) {
// Horizontal velocity is valid between -1 and 1, where negative values swaps directions
float velocity;
try {
velocity = Float.parseFloat(velocityString);
} catch (NumberFormatException exception) {
velocity = configuration.getHorizontalVelocity();
}
// If outside bonds, choose the most extreme value
return Math.min(Math.max(0.1f, velocity), 1);
}
/**
* Parses the given location string
*
* @param player <p>The player changing a location</p>
* @param locationString <p>The location string to parse</p>
* @return <p>The parsed location, or the player's location if not parse-able</p>
*/
private @NotNull Location parseLocation(Player player, String locationString) {
if ((locationString.trim() + ",").matches("([0-9]+.?[0-9]*,){3}")) {
String[] parts = locationString.split(",");
Location newLocation = player.getLocation().clone();
newLocation.setX(Double.parseDouble(parts[0].trim()));
newLocation.setY(Double.parseDouble(parts[1].trim()));
newLocation.setZ(Double.parseDouble(parts[2].trim()));
return newLocation;
} else {
return player.getLocation().clone();
}
}
/**
* Parses the given material name
*
* @param materialName <p>The material name to parse</p>
* @return <p>The parsed material, or AIR if not valid</p>
*/
private @NotNull Material parseMaterial(String materialName) {
Material material = Material.matchMaterial(materialName);
if (material == null) {
material = Material.AIR;
}
return material;
}
}

View File

@@ -0,0 +1,33 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.util.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The tab-completer for the edit arena command
*/
public class EditArenaTabCompleter implements TabCompleter {
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
if (args.length == 1) {
return TabCompleteHelper.getArenas();
} else if (args.length == 2) {
return TabCompleteHelper.getArenaProperties();
} else if (args.length == 3) {
//TODO: Tab-complete possible values for the given property
return null;
} else {
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,93 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaHandler;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* The command for listing groups and the stages within
*/
public class GroupListCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
DropperArenaHandler arenaHandler = MiniGames.getInstance().getDropperArenaHandler();
if (arguments.length == 0) {
displayExistingGroups(arenaHandler, commandSender);
return true;
} else if (arguments.length == 1) {
return displayOrderedArenaNames(arenaHandler, commandSender, arguments[0]);
} else {
return false;
}
}
/**
* Displays all currently existing dropper arena groups
*
* @param arenaHandler <p>The arena handler to get groups from</p>
* @param sender <p>The command sender to display the groups to</p>
*/
private void displayExistingGroups(@NotNull DropperArenaHandler arenaHandler, @NotNull CommandSender sender) {
StringBuilder builder = new StringBuilder("Dropper arena groups:").append("\n");
arenaHandler.getAllGroups().stream().sorted().forEachOrdered((group) ->
builder.append(group.getGroupName()).append("\n"));
sender.sendMessage(builder.toString());
}
/**
* Displays the ordered stages in a specified group to the specified command sender
*
* @param arenaHandler <p>The arena handler to get groups from</p>
* @param sender <p>The command sender to display the stages to</p>
* @param groupName <p>The name of the group to display stages for</p>
* @return <p>True if the stages were successfully displayed</p>
*/
private boolean displayOrderedArenaNames(@NotNull DropperArenaHandler arenaHandler, @NotNull CommandSender sender,
@NotNull String groupName) {
DropperArenaGroup arenaGroup = arenaHandler.getGroup(groupName);
if (arenaGroup == null) {
sender.sendMessage("Unable to find the specified group!");
return false;
}
// Send a list of all stages (arenas in the group)
StringBuilder builder = new StringBuilder(groupName).append("'s stages:").append("\n");
int counter = 1;
for (UUID arenaId : arenaGroup.getArenas()) {
DropperArena arena = arenaHandler.getArena(arenaId);
if (arena != null) {
builder.append(counter++).append(". ").append(arena.getArenaName()).append("\n");
}
}
sender.sendMessage(builder.toString());
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
List<String> groupNames = new ArrayList<>();
for (DropperArenaGroup group : MiniGames.getInstance().getDropperArenaHandler().getAllGroups()) {
groupNames.add(group.getGroupName());
}
return groupNames;
} else {
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,79 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaHandler;
import net.knarcraft.minigames.util.StringSanitizer;
import net.knarcraft.minigames.util.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The command for setting the group of an arena
*/
public class GroupSetCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length < 2) {
return false;
}
DropperArenaHandler arenaHandler = MiniGames.getInstance().getDropperArenaHandler();
DropperArena specifiedArena = arenaHandler.getArena(arguments[0]);
if (specifiedArena == null) {
commandSender.sendMessage("Unable to find the specified dropper arena.");
return false;
}
String groupName = StringSanitizer.removeUnwantedCharacters(arguments[1]);
if (groupName.isBlank()) {
return false;
}
DropperArenaGroup arenaGroup;
if (groupName.equalsIgnoreCase("null") || groupName.equalsIgnoreCase("none")) {
arenaGroup = null;
} else {
arenaGroup = arenaHandler.getGroup(groupName);
if (arenaGroup == null) {
arenaGroup = new DropperArenaGroup(groupName);
}
}
arenaHandler.setGroup(specifiedArena.getArenaId(), arenaGroup);
commandSender.sendMessage("The arena's group has been updated");
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
return TabCompleteHelper.getArenas();
} else if (arguments.length == 2) {
List<String> possibleValues = new ArrayList<>();
possibleValues.add("none");
possibleValues.add("GroupName");
for (DropperArenaGroup group : MiniGames.getInstance().getDropperArenaHandler().getAllGroups()) {
possibleValues.add(group.getGroupName());
}
return possibleValues;
} else {
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,102 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaHandler;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* The command for swapping the order of two arenas in a group
*/
public class GroupSwapCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length < 2) {
return false;
}
DropperArenaHandler arenaHandler = MiniGames.getInstance().getDropperArenaHandler();
DropperArena arena1 = arenaHandler.getArena(arguments[0]);
if (arena1 == null) {
commandSender.sendMessage("Unable to find the first specified dropper arena.");
return false;
}
DropperArena arena2 = arenaHandler.getArena(arguments[1]);
if (arena2 == null) {
commandSender.sendMessage("Unable to find the second specified dropper arena.");
return false;
}
DropperArenaGroup arena1Group = arenaHandler.getGroup(arena1.getArenaId());
DropperArenaGroup arena2Group = arenaHandler.getGroup(arena2.getArenaId());
if (arena1Group == null || !arena1Group.equals(arena2Group)) {
commandSender.sendMessage("You cannot swap arenas in different groups!");
return false;
}
arena1Group.swapArenas(arena1Group.getArenas().indexOf(arena1.getArenaId()),
arena1Group.getArenas().indexOf(arena2.getArenaId()));
commandSender.sendMessage("The arenas have been swapped!");
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
DropperArenaHandler arenaHandler = MiniGames.getInstance().getDropperArenaHandler();
if (arguments.length == 1) {
List<String> arenaNames = new ArrayList<>();
for (DropperArena dropperArena : arenaHandler.getArenasInAGroup()) {
arenaNames.add(dropperArena.getArenaName());
}
return arenaNames;
} else if (arguments.length == 2) {
return getArenaNamesInSameGroup(arguments[0]);
} else {
return new ArrayList<>();
}
}
/**
* Gets the names of all arenas in the same group as the specified arena
*
* @param arenaName <p>The name of the specified arena</p>
* @return <p>The names of the arenas in the same group</p>
*/
private List<String> getArenaNamesInSameGroup(String arenaName) {
DropperArenaHandler arenaHandler = MiniGames.getInstance().getDropperArenaHandler();
DropperArena arena1 = arenaHandler.getArena(arenaName);
if (arena1 == null) {
return new ArrayList<>();
}
// Only display other arenas in the selected group
List<String> arenaNames = new ArrayList<>();
DropperArenaGroup group = arenaHandler.getGroup(arena1.getArenaId());
if (group == null) {
return new ArrayList<>();
}
for (UUID arenaId : group.getArenas()) {
DropperArena arena = arenaHandler.getArena(arenaId);
if (arena != null && arena.getArenaId() != arena1.getArenaId()) {
arenaNames.add(arena.getArenaName());
}
}
return arenaNames;
}
}

View File

@@ -0,0 +1,136 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaPlayerRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArenaSession;
import net.knarcraft.minigames.config.DropperConfiguration;
import net.knarcraft.minigames.util.PlayerTeleporter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
/**
* The command used to join a dropper arena
*/
public class JoinArenaCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be used by a player");
return false;
}
if (arguments.length < 1) {
return false;
}
// Disallow joining if the player is already in a dropper arena
DropperArenaSession existingSession = MiniGames.getInstance().getDropperArenaPlayerRegistry().getArenaSession(player.getUniqueId());
if (existingSession != null) {
commandSender.sendMessage("You are already in a dropper arena!");
return false;
}
// Make sure the arena exists
DropperArena specifiedArena = MiniGames.getInstance().getDropperArenaHandler().getArena(arguments[0]);
if (specifiedArena == null) {
commandSender.sendMessage("Unable to find the specified dropper arena.");
return false;
}
// Deny vehicles as allowing this is tricky, and will cause problems in some cases
if (player.isInsideVehicle() || !player.getPassengers().isEmpty()) {
commandSender.sendMessage("You cannot join an arena while inside a vehicle or carrying a passenger.");
return false;
}
return joinArena(specifiedArena, player, arguments);
}
/**
* Performs the actual arena joining
*
* @param specifiedArena <p>The arena the player wants to join</p>
* @param player <p>The player joining the arena</p>
* @param arguments <p>The arguments given</p>
* @return <p>Whether the arena was joined successfully</p>
*/
private boolean joinArena(DropperArena specifiedArena, Player player, String[] arguments) {
// Find the specified game-mode
DropperArenaGameMode gameMode;
if (arguments.length > 1) {
gameMode = DropperArenaGameMode.matchGamemode(arguments[1]);
} else {
gameMode = DropperArenaGameMode.DEFAULT;
}
// Make sure the player has beaten the necessary levels
DropperArenaGroup arenaGroup = MiniGames.getInstance().getDropperArenaHandler().getGroup(specifiedArena.getArenaId());
if (arenaGroup != null && !doGroupChecks(specifiedArena, arenaGroup, gameMode, player)) {
return false;
}
// Make sure the player has beaten the arena once in normal mode before playing another mode
if (MiniGames.getInstance().getDropperConfiguration().mustDoNormalModeFirst() &&
gameMode != DropperArenaGameMode.DEFAULT &&
specifiedArena.getData().hasNotCompleted(DropperArenaGameMode.DEFAULT, player)) {
player.sendMessage("You must complete this arena in normal mode first!");
return false;
}
// Register the player's session
DropperArenaSession newSession = new DropperArenaSession(specifiedArena, player, gameMode);
DropperArenaPlayerRegistry playerRegistry = MiniGames.getInstance().getDropperArenaPlayerRegistry();
playerRegistry.registerPlayer(player.getUniqueId(), newSession);
// Try to teleport the player to the arena
boolean teleported = PlayerTeleporter.teleportPlayer(player, specifiedArena.getSpawnLocation(), false, false);
if (!teleported) {
player.sendMessage("Unable to teleport you to the dropper arena. Make sure you're not in a vehicle," +
"and not carrying a passenger!");
newSession.triggerQuit(false);
return false;
} else {
// Make sure to update the state again in the air to remove a potential swimming state
newSession.getEntryState().setArenaState(specifiedArena.getPlayerHorizontalVelocity());
return true;
}
}
/**
* Performs necessary check for the given arena's group
*
* @param dropperArena <p>The arena the player is trying to join</p>
* @param arenaGroup <p>The arena group the arena belongs to</p>
* @param arenaGameMode <p>The game-mode the player selected</p>
* @param player <p>The the player trying to join the arena</p>
* @return <p>False if any checks failed</p>
*/
private boolean doGroupChecks(@NotNull DropperArena dropperArena, @NotNull DropperArenaGroup arenaGroup,
@NotNull DropperArenaGameMode arenaGameMode, @NotNull Player player) {
DropperConfiguration configuration = MiniGames.getInstance().getDropperConfiguration();
// Require that players beat all arenas in the group in the normal game-mode before trying challenge modes
if (configuration.mustDoNormalModeFirst() && arenaGameMode != DropperArenaGameMode.DEFAULT &&
!arenaGroup.hasBeatenAll(DropperArenaGameMode.DEFAULT, player)) {
player.sendMessage("You have not yet beaten all arenas in this group!");
return false;
}
// Require that the player has beaten the previous arena on the same game-mode before trying this one
if (configuration.mustDoGroupedInSequence() &&
!arenaGroup.canPlay(arenaGameMode, player, dropperArena.getArenaId())) {
player.sendMessage("You have not yet beaten the previous arena!");
return false;
}
return true;
}
}

View File

@@ -0,0 +1,34 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.util.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The tab-completer for the join command
*/
public class JoinArenaTabCompleter implements TabCompleter {
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] arguments) {
if (arguments.length == 1) {
return TabCompleteHelper.getArenas();
} else if (arguments.length == 2) {
List<String> gameModes = new ArrayList<>();
gameModes.add("default");
gameModes.add("inverted");
gameModes.add("random");
return gameModes;
} else {
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,46 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArenaSession;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The command used to leave the current dropper arena
*/
public class LeaveArenaCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] strings) {
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be used by a player");
return false;
}
DropperArenaSession existingSession = MiniGames.getInstance().getDropperArenaPlayerRegistry().getArenaSession(
player.getUniqueId());
if (existingSession == null) {
commandSender.sendMessage("You are not in a dropper arena!");
return false;
}
existingSession.triggerQuit(false);
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
return new ArrayList<>();
}
}

View File

@@ -0,0 +1,35 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.util.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* A command for listing existing dropper arenas
*/
public class ListArenaCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
sender.sendMessage("Dropper arenas:");
for (String arenaName : TabCompleteHelper.getArenas()) {
sender.sendMessage(arenaName);
}
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
return new ArrayList<>();
}
}

View File

@@ -0,0 +1,33 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.MiniGames;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The command for reloading the plugin
*/
public class ReloadCommand implements TabExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
MiniGames.getInstance().reload();
commandSender.sendMessage("Plugin reloaded!");
return true;
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
return new ArrayList<>();
}
}

View File

@@ -0,0 +1,36 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
/**
* The method used for removing an existing arena
*/
public class RemoveArenaCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
// Abort if no name was specified
if (arguments.length < 1) {
return false;
}
// Get the specified arena
DropperArena targetArena = MiniGames.getInstance().getDropperArenaHandler().getArena(arguments[0]);
if (targetArena == null) {
commandSender.sendMessage("Unable to find the specified arena");
return false;
}
// Remove the arena
MiniGames.getInstance().getDropperArenaHandler().removeArena(targetArena);
commandSender.sendMessage("The specified arena has been successfully removed");
return true;
}
}

View File

@@ -0,0 +1,29 @@
package net.knarcraft.minigames.command;
import net.knarcraft.minigames.util.TabCompleteHelper;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* The tab-completer for the remove arena command
*/
public class RemoveArenaTabCompleter implements TabCompleter {
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (arguments.length == 1) {
return TabCompleteHelper.getArenas();
} else {
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,188 @@
package net.knarcraft.minigames.config;
import org.bukkit.Material;
import org.bukkit.configuration.file.FileConfiguration;
import java.util.HashSet;
import java.util.Set;
/**
* The configuration keeping track of dropper settings
*/
public class DropperConfiguration extends MiniGameConfiguration {
private final static String rootNode = "dropper.";
private double verticalVelocity;
private float horizontalVelocity;
private int randomlyInvertedTimer;
private boolean mustDoGroupedInSequence;
private boolean ignoreRecordsUntilGroupBeatenOnce;
private boolean mustDoNormalModeFirst;
private boolean makePlayersInvisible;
private boolean disableHitCollision;
private boolean blockSneaking;
private boolean blockSprinting;
private Set<Material> blockWhitelist;
/**
* Instantiates a new dropper configuration
*
* @param configuration <p>The YAML configuration to use internally</p>
*/
public DropperConfiguration(FileConfiguration configuration) {
super(configuration);
}
/**
* Gets the default vertical velocity
*
* @return <p>The default vertical velocity</p>
*/
public double getVerticalVelocity() {
return this.verticalVelocity;
}
/**
* Gets the default horizontal velocity
*
* @return <p>The default horizontal velocity</p>
*/
public float getHorizontalVelocity() {
return this.horizontalVelocity;
}
/**
* Gets the number of seconds before the randomly inverted game-mode toggles
*
* @return <p>Number of seconds before the inversion toggles</p>
*/
public int getRandomlyInvertedTimer() {
return this.randomlyInvertedTimer;
}
/**
* Gets whether grouped arenas must be done in the set sequence
*
* @return <p>Whether grouped arenas must be done in sequence</p>
*/
public boolean mustDoGroupedInSequence() {
return this.mustDoGroupedInSequence;
}
/**
* Gets whether the normal/default mode must be beaten before playing another game-mode
*
* @return <p>Whether the normal game-mode must be beaten first</p>
*/
public boolean mustDoNormalModeFirst() {
return this.mustDoNormalModeFirst;
}
/**
* Gets the types of block which should not trigger a loss
*
* @return <p>The materials that should not trigger a loss</p>
*/
public Set<Material> getBlockWhitelist() {
return new HashSet<>(this.blockWhitelist);
}
/**
* Gets whether records should be discarded, unless the player has already beaten all arenas in the group
*
* @return <p>Whether to ignore records on the first play-through</p>
*/
public boolean ignoreRecordsUntilGroupBeatenOnce() {
return this.ignoreRecordsUntilGroupBeatenOnce;
}
/**
* Gets whether players should be made invisible while in an arena
*
* @return <p>Whether players should be made invisible</p>
*/
public boolean makePlayersInvisible() {
return this.makePlayersInvisible;
}
/**
* Gets whether entity hit-collision of players in an arena should be disabled
*
* @return <p>Whether to disable hit collision</p>
*/
public boolean disableHitCollision() {
return this.disableHitCollision;
}
/**
* Gets whether players trying to sneak while in a dropper arena to increase their downwards speed should be blocked
*
* @return <p>Whether to block sneak to speed up</p>
*/
public boolean blockSneaking() {
return blockSneaking;
}
/**
* Gets whether players trying to sprint to improve their horizontal speed while in a dropper arena should be blocked
*
* @return <p>Whether to block sprint to speed up</p>
*/
public boolean blockSprinting() {
return this.blockSprinting;
}
@Override
protected void load() {
this.verticalVelocity = configuration.getDouble(rootNode + "verticalVelocity", 1.0);
this.horizontalVelocity = (float) configuration.getDouble(rootNode + "horizontalVelocity", 1.0);
this.randomlyInvertedTimer = configuration.getInt(rootNode + "randomlyInvertedTimer", 7);
this.mustDoGroupedInSequence = configuration.getBoolean(rootNode + "mustDoGroupedInSequence", true);
this.ignoreRecordsUntilGroupBeatenOnce = configuration.getBoolean(rootNode + "ignoreRecordsUntilGroupBeatenOnce", false);
this.mustDoNormalModeFirst = configuration.getBoolean(rootNode + "mustDoNormalModeFirst", true);
this.makePlayersInvisible = configuration.getBoolean(rootNode + "makePlayersInvisible", false);
this.disableHitCollision = configuration.getBoolean(rootNode + "disableHitCollision", true);
this.blockSprinting = configuration.getBoolean(rootNode + "blockSprinting", true);
this.blockSneaking = configuration.getBoolean(rootNode + "blockSneaking", true);
this.blockWhitelist = loadMaterialList(rootNode + "blockWhitelist");
sanitizeValues();
}
/**
* Sanitizes configuration values to ensure they are within expected bounds
*/
private void sanitizeValues() {
if (this.horizontalVelocity > 1 || this.horizontalVelocity <= 0) {
this.horizontalVelocity = 1;
}
if (this.verticalVelocity <= 0 || this.verticalVelocity > 75) {
this.verticalVelocity = 1;
}
if (this.randomlyInvertedTimer <= 0 || this.randomlyInvertedTimer > 3600) {
this.randomlyInvertedTimer = 7;
}
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder(
"Current configuration:" +
"\n" + "Vertical velocity: " + verticalVelocity +
"\n" + "Horizontal velocity: " + horizontalVelocity +
"\n" + "Randomly inverted timer: " + randomlyInvertedTimer +
"\n" + "Must do groups in sequence: " + mustDoGroupedInSequence +
"\n" + "Ignore records until group beaten once: " + ignoreRecordsUntilGroupBeatenOnce +
"\n" + "Must do normal mode first: " + mustDoNormalModeFirst +
"\n" + "Make players invisible: " + makePlayersInvisible +
"\n" + "Disable hit collision: " + disableHitCollision +
"\n" + "Block whitelist: ");
for (Material material : blockWhitelist) {
builder.append("\n - ").append(material.name());
}
return builder.toString();
}
}

View File

@@ -0,0 +1,102 @@
package net.knarcraft.minigames.config;
import net.knarcraft.minigames.MiniGames;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Tag;
import org.bukkit.configuration.file.FileConfiguration;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A configuration for a mini-game
*/
public abstract class MiniGameConfiguration {
protected FileConfiguration configuration;
/**
* Instantiates a new mini-game configuration
*
* @param configuration <p>The YAML configuration to use internally</p>
*/
public MiniGameConfiguration(@NotNull FileConfiguration configuration) {
this.configuration = configuration;
this.load();
}
/**
* Loads all configuration values from disk
*
* @param configuration <p>The configuration to load</p>
*/
public void load(FileConfiguration configuration) {
this.configuration = configuration;
this.load();
}
/**
* Loads all configuration values from disk, using the current file configuration
*/
protected abstract void load();
/**
* Loads the materials specified in the block whitelist
*/
protected @NotNull Set<Material> loadMaterialList(@NotNull String path) {
Set<Material> parsedMaterials = new HashSet<>();
List<?> blockWhitelist = configuration.getList(path, new ArrayList<>());
for (Object value : blockWhitelist) {
if (!(value instanceof String string)) {
continue;
}
// Try to parse a material tag first
if (parseMaterialTag(parsedMaterials, string)) {
continue;
}
// Try to parse a material name
Material matched = Material.matchMaterial(string);
if (matched != null) {
parsedMaterials.add(matched);
} else {
MiniGames.log(Level.WARNING, "Unable to parse: " + string);
}
}
return parsedMaterials;
}
/**
* Tries to parse the material tag in the specified material name
*
* @param targetSet <p>The set all parsed materials should be added to</p>
* @param materialName <p>The material name that might be a material tag</p>
* @return <p>True if a tag was found</p>
*/
protected boolean parseMaterialTag(@NotNull Set<Material> targetSet, @NotNull String materialName) {
Pattern pattern = Pattern.compile("^\\+([a-zA-Z_]+)");
Matcher matcher = pattern.matcher(materialName);
if (matcher.find()) {
// The material is a material tag
Tag<Material> tag = Bukkit.getTag(Tag.REGISTRY_BLOCKS, NamespacedKey.minecraft(
matcher.group(1).toLowerCase()), Material.class);
if (tag != null) {
targetSet.addAll(tag.getValues());
} else {
MiniGames.log(Level.WARNING, "Unable to parse: " + materialName);
}
return true;
}
return false;
}
}

View File

@@ -0,0 +1,89 @@
package net.knarcraft.minigames.config;
import org.bukkit.Material;
import org.bukkit.configuration.file.FileConfiguration;
import java.util.HashSet;
import java.util.Set;
/**
* The configuration keeping track of parkour settings
*/
public class ParkourConfiguration extends MiniGameConfiguration {
private final static String rootNode = "parkour.";
private boolean mustDoGroupedInSequence;
private boolean ignoreRecordsUntilGroupBeatenOnce;
private boolean makePlayersInvisible;
private Set<Material> killPlaneBlocks;
/**
* Instantiates a new dropper configuration
*
* @param configuration <p>The YAML configuration to use internally</p>
*/
public ParkourConfiguration(FileConfiguration configuration) {
super(configuration);
}
/**
* Gets whether grouped arenas must be done in the set sequence
*
* @return <p>Whether grouped arenas must be done in sequence</p>
*/
public boolean mustDoGroupedInSequence() {
return this.mustDoGroupedInSequence;
}
/**
* Gets whether records should be discarded, unless the player has already beaten all arenas in the group
*
* @return <p>Whether to ignore records on the first play-through</p>
*/
public boolean ignoreRecordsUntilGroupBeatenOnce() {
return this.ignoreRecordsUntilGroupBeatenOnce;
}
/**
* Gets whether players should be made invisible while in an arena
*
* @return <p>Whether players should be made invisible</p>
*/
public boolean makePlayersInvisible() {
return this.makePlayersInvisible;
}
/**
* Gets all types of blocks constituting parkour arenas' kill planes
*
* @return <p>The types of blocks causing a player to fail a parkour map</p>
*/
public Set<Material> getKillPlaneBlocks() {
return new HashSet<>(this.killPlaneBlocks);
}
@Override
protected void load() {
this.mustDoGroupedInSequence = configuration.getBoolean(rootNode + "mustDoGroupedInSequence", true);
this.ignoreRecordsUntilGroupBeatenOnce = configuration.getBoolean(rootNode + "ignoreRecordsUntilGroupBeatenOnce", false);
this.makePlayersInvisible = configuration.getBoolean(rootNode + "makePlayersInvisible", false);
this.killPlaneBlocks = loadMaterialList(rootNode + "killPlaneBlocks");
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder(
"Current configuration:" +
"Current configuration:" +
"\n" + "Must do groups in sequence: " + mustDoGroupedInSequence +
"\n" + "Ignore records until group beaten once: " + ignoreRecordsUntilGroupBeatenOnce +
"\n" + "Make players invisible: " + makePlayersInvisible +
"\n" + "Kill plane blocks: ");
for (Material material : killPlaneBlocks) {
builder.append("\n - ").append(material.name());
}
return builder.toString();
}
}

View File

@@ -0,0 +1,69 @@
package net.knarcraft.minigames.config;
import org.bukkit.configuration.file.FileConfiguration;
import org.jetbrains.annotations.NotNull;
/**
* The configuration keeping track of shared settings
*/
public class SharedConfiguration extends MiniGameConfiguration {
private final static String rootNode = "shared.";
private double liquidHitBoxDepth;
private double solidHitBoxDistance;
/**
* Instantiates a new shared configuration
*
* @param configuration <p>The YAML configuration to use internally</p>
*/
public SharedConfiguration(@NotNull FileConfiguration configuration) {
super(configuration);
}
/**
* Gets the negative depth a player must reach in a liquid block for fail/win detection to trigger
*
* <p>This decides how far inside a non-solid block the player must go before detection triggers. The closer to -1
* it is, the more accurate it will seem to the player, but the likelihood of not detecting the hit increases.</p>
*
* @return <p>The liquid hit box depth to use</p>
*/
public double getLiquidHitBoxDepth() {
return this.liquidHitBoxDepth;
}
/**
* Gets the positive distance a player must at most be from a block for fail/win detection to trigger
*
* <p>This decides the distance the player must be from a block below them before a hit triggers. If too low, the
* likelihood of detecting the hit decreases, but it won't look like the player hit the block without being near.</p>
*
* @return <p>The solid hit box distance to use</p>
*/
public double getSolidHitBoxDistance() {
return this.solidHitBoxDistance;
}
@Override
protected void load() {
this.liquidHitBoxDepth = configuration.getDouble(rootNode + "liquidHitBoxDepth", -0.8);
this.solidHitBoxDistance = configuration.getDouble(rootNode + "solidHitBoxDistance", 0.2);
if (this.liquidHitBoxDepth <= -1 || this.liquidHitBoxDepth > 0) {
this.liquidHitBoxDepth = -0.8;
}
if (this.solidHitBoxDistance <= 0 || this.solidHitBoxDistance > 1) {
this.solidHitBoxDistance = 0.2;
}
}
@Override
public String toString() {
return "Current configuration:" +
"\n" + "Liquid hit box depth: " + liquidHitBoxDepth +
"\n" + "Solid hit box distance: " + solidHitBoxDistance;
}
}

View File

@@ -0,0 +1,49 @@
package net.knarcraft.minigames.container;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
/**
* A container that is serializable
*
* @param <K> <p>The type of the contained object</p>
*/
public abstract class SerializableContainer<K> implements ConfigurationSerializable {
private final K value;
/**
* Instantiates a new serializable container
*
* @param value <p>The value to contain</p>
*/
public SerializableContainer(K value) {
this.value = value;
}
/**
* Gets the raw, non-serializable object
*
* @return <p>The raw stored value</p>
*/
public K getRawValue() {
return value;
}
/**
* Gets a serializable container containing the given value
*
* @param value <p>The value to make serializable</p>
* @return <p>The serializable value</p>
*/
public abstract SerializableContainer<K> getSerializable(K value);
@Override
public boolean equals(Object object) {
if (object instanceof SerializableContainer<?>) {
return this.getRawValue().equals(((SerializableContainer<?>) object).getRawValue());
} else {
return false;
}
}
}

View File

@@ -0,0 +1,52 @@
package net.knarcraft.minigames.container;
import org.bukkit.Material;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
* A material container able to be serialized
*/
public class SerializableMaterial extends SerializableContainer<Material> {
/**
* Instantiates a new serializable material
*
* @param material <p>The material to contain</p>
*/
public SerializableMaterial(Material material) {
super(material);
}
@Override
public SerializableContainer<Material> getSerializable(Material value) {
return new SerializableMaterial(value);
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("name", getRawValue().name());
return data;
}
/**
* Deserializes a serialized material
*
* @param data <p>The serialized data</p>
* @return <p>The deserialized material</p>
*/
@SuppressWarnings("unused")
public static SerializableMaterial deserialize(Map<String, Object> data) {
Material material = Material.matchMaterial((String) data.get("name"));
if (material == null) {
return null;
} else {
return new SerializableMaterial(material);
}
}
}

View File

@@ -0,0 +1,52 @@
package net.knarcraft.minigames.container;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* A UUID container able to be serialized
*/
public class SerializableUUID extends SerializableContainer<UUID> {
/**
* Instantiates a new serializable uuid
*
* @param value <p>The uuid to contain</p>
*/
public SerializableUUID(UUID value) {
super(value);
}
@Override
public SerializableContainer<UUID> getSerializable(UUID value) {
return new SerializableUUID(value);
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("id", getRawValue().toString());
return data;
}
/**
* Deserializes a serialized UUID
*
* @param data <p>The serialized data</p>
* @return <p>The deserialized UUID</p>
*/
@SuppressWarnings("unused")
public static SerializableUUID deserialize(Map<String, Object> data) {
String id = (String) data.getOrDefault("id", null);
if (id != null) {
return new SerializableUUID(UUID.fromString(id));
} else {
return null;
}
}
}

View File

@@ -0,0 +1,46 @@
package net.knarcraft.minigames.listener;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArenaSession;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import java.util.ArrayList;
import java.util.List;
/**
* A listener for players trying to use commands while inside a dropper arena
*/
public class CommandListener implements Listener {
@EventHandler
public void onCommand(PlayerCommandPreprocessEvent event) {
Player player = event.getPlayer();
DropperArenaSession existingSession = MiniGames.getInstance().getDropperArenaPlayerRegistry().getArenaSession(
player.getUniqueId());
if (existingSession == null) {
return;
}
List<String> allowedCommands = new ArrayList<>();
allowedCommands.add("/dropperleave");
allowedCommands.add("/dleave");
String message = event.getMessage();
if (!message.startsWith("/")) {
return;
}
for (String command : allowedCommands) {
if (message.equals(command)) {
return;
}
}
player.sendMessage("You cannot use that command while in an arena!");
event.setCancelled(true);
}
}

View File

@@ -0,0 +1,55 @@
package net.knarcraft.minigames.listener;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArenaPlayerRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArenaSession;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityCombustEvent;
import org.bukkit.event.entity.EntityDamageEvent;
/**
* A listener for checking if a player takes damage within a dropper arena
*/
public class DamageListener implements Listener {
@EventHandler
public void onPlayerDamage(EntityDamageEvent event) {
// Only player damage matters
if (event.getEntityType() != EntityType.PLAYER) {
return;
}
Player player = (Player) event.getEntity();
// We don't care about damage outside arenas
DropperArenaSession arenaSession = MiniGames.getInstance().getDropperArenaPlayerRegistry().getArenaSession(player.getUniqueId());
if (arenaSession == null) {
return;
}
event.setCancelled(true);
// Only trigger a loss when a player suffers fall damage
if (event.getCause() == EntityDamageEvent.DamageCause.FALL) {
arenaSession.triggerLoss();
}
}
@EventHandler(ignoreCancelled = true)
public void onPlayerCombustion(EntityCombustEvent event) {
if (event.getEntityType() != EntityType.PLAYER) {
return;
}
DropperArenaPlayerRegistry registry = MiniGames.getInstance().getDropperArenaPlayerRegistry();
DropperArenaSession arenaSession = registry.getArenaSession(event.getEntity().getUniqueId());
if (arenaSession != null) {
// Cancel combustion for any player in an arena
event.setCancelled(true);
}
}
}

View File

@@ -0,0 +1,168 @@
package net.knarcraft.minigames.listener;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import net.knarcraft.minigames.arena.dropper.DropperArenaPlayerRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArenaSession;
import net.knarcraft.minigames.config.DropperConfiguration;
import net.knarcraft.minigames.config.SharedConfiguration;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.NotNull;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Set;
/**
* A listener for players moving inside a dropper arena
*/
public class MoveListener implements Listener {
private final DropperConfiguration configuration;
/**
* Instantiates a new move listener
*
* @param configuration <p>The configuration to use</p>
*/
public MoveListener(DropperConfiguration configuration) {
this.configuration = configuration;
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
// Ignore if no actual movement is happening
if (event.getFrom().equals(event.getTo()) || event.getTo() == null) {
return;
}
Player player = event.getPlayer();
DropperArenaPlayerRegistry playerRegistry = MiniGames.getInstance().getDropperArenaPlayerRegistry();
DropperArenaSession arenaSession = playerRegistry.getArenaSession(player.getUniqueId());
if (arenaSession == null) {
return;
}
// Prevent the player from flying upwards while in flight mode
if (event.getFrom().getY() < event.getTo().getY() ||
(configuration.blockSneaking() && event.getPlayer().isSneaking()) ||
(configuration.blockSprinting() && event.getPlayer().isSprinting())) {
event.setCancelled(true);
return;
}
// Only do block type checking if the block beneath the player changes
if (event.getFrom().getBlock() != event.getTo().getBlock() &&
checkForSpecialBlock(arenaSession, event.getTo())) {
return;
}
//Updates the player's velocity to the one set by the arena
updatePlayerVelocity(arenaSession);
}
/**
* Check if the player in the session is triggering a block with a special significance
*
* <p>This basically looks for the win block, or whether the player is hitting a solid block.</p>
*
* @param arenaSession <p>The arena session to check for</p>
* @param toLocation <p>The location the player's session is about to hit</p>
* @return <p>True if a special block has been hit</p>
*/
private boolean checkForSpecialBlock(DropperArenaSession arenaSession, Location toLocation) {
SharedConfiguration sharedConfiguration = MiniGames.getInstance().getSharedConfiguration();
double liquidDepth = sharedConfiguration.getLiquidHitBoxDepth();
double solidDepth = sharedConfiguration.getSolidHitBoxDistance();
// Check if the player enters water
Material winBlockType = arenaSession.getArena().getWinBlockType();
// For water, only trigger when the player enters the water, but trigger earlier for everything else
double depth = !winBlockType.isSolid() ? liquidDepth : solidDepth;
for (Block block : getBlocksBeneathLocation(toLocation, depth)) {
if (block.getType() == winBlockType) {
arenaSession.triggerWin();
return true;
}
}
// Check if the player is about to hit a non-air and non-liquid block
Set<Material> whitelisted = configuration.getBlockWhitelist();
for (Block block : getBlocksBeneathLocation(toLocation, solidDepth)) {
Material blockType = block.getType();
if (!blockType.isAir() && !whitelisted.contains(blockType)) {
arenaSession.triggerLoss();
return true;
}
}
return false;
}
/**
* Gets the blocks at the given location that will be affected by the player's hit-box
*
* @param location <p>The location to check</p>
* @return <p>The blocks beneath the player</p>
*/
private Set<Block> getBlocksBeneathLocation(Location location, double depth) {
Set<Block> blocksBeneath = new HashSet<>();
double halfPlayerWidth = 0.3;
blocksBeneath.add(location.clone().subtract(halfPlayerWidth, depth, halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(-halfPlayerWidth, depth, halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(halfPlayerWidth, depth, -halfPlayerWidth).getBlock());
blocksBeneath.add(location.clone().subtract(-halfPlayerWidth, depth, -halfPlayerWidth).getBlock());
return blocksBeneath;
}
/**
* Updates the velocity of the player in the given session
*
* @param session <p>The session to update the velocity for</p>
*/
private void updatePlayerVelocity(@NotNull DropperArenaSession session) {
// Override the vertical velocity
Player player = session.getPlayer();
Vector playerVelocity = player.getVelocity();
double arenaVelocity = session.getArena().getPlayerVerticalVelocity();
Vector newVelocity = new Vector(playerVelocity.getX() * 5, -arenaVelocity, playerVelocity.getZ() * 5);
player.setVelocity(newVelocity);
// Toggle the direction of the player's flying, as necessary
toggleFlyInversion(session);
}
/**
* Toggles the player's flying direction inversion if playing on the random game-mode
*
* @param session <p>The session to possibly invert flying for</p>
*/
private void toggleFlyInversion(@NotNull DropperArenaSession session) {
if (session.getGameMode() != DropperArenaGameMode.RANDOM_INVERTED) {
return;
}
Player player = session.getPlayer();
float horizontalVelocity = session.getArena().getPlayerHorizontalVelocity();
float secondsBetweenToggle = configuration.getRandomlyInvertedTimer();
int seconds = Calendar.getInstance().get(Calendar.SECOND);
/*
* A trick to make the inversion change after a customizable amount of seconds
* If the quotient of dividing the current number of seconds with the set amount of seconds is even, invert.
* So, if the number of seconds between toggles is 5, that would mean for the first 5 seconds, the flying would
* be inverted. Once 5 seconds have passed, the quotient becomes 1, which is odd, so the flying is no longer
* inverted. After 10 seconds, the quotient is 2, which is even, and inverts the flying.
*/
boolean invert = Math.floor(seconds / secondsBetweenToggle) % 2 == 0;
player.setFlySpeed(invert ? -horizontalVelocity : horizontalVelocity);
}
}

View File

@@ -0,0 +1,82 @@
package net.knarcraft.minigames.listener;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArenaSession;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Level;
/**
* A listener for players leaving the server or the arena
*/
public class PlayerLeaveListener implements Listener {
private final Map<UUID, DropperArenaSession> leftSessions = new HashMap<>();
@EventHandler
public void onPlayerLeave(PlayerQuitEvent event) {
Player player = event.getPlayer();
DropperArenaSession arenaSession = getSession(player);
if (arenaSession == null) {
return;
}
MiniGames.log(Level.WARNING, "Found player " + player.getUniqueId() +
" leaving in the middle of a session!");
leftSessions.put(player.getUniqueId(), arenaSession);
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
UUID playerId = event.getPlayer().getUniqueId();
// Force the player to quit from the session once they re-join
if (leftSessions.containsKey(playerId)) {
MiniGames.log(Level.WARNING, "Found un-exited dropper session!");
Bukkit.getScheduler().runTaskLater(MiniGames.getInstance(), () -> {
leftSessions.get(playerId).triggerQuit(false);
MiniGames.log(Level.WARNING, "Triggered a quit!");
leftSessions.remove(playerId);
}, 80);
}
}
@EventHandler
public void onPlayerTeleport(PlayerTeleportEvent event) {
if (event.getTo() == null || event.isCancelled()) {
return;
}
DropperArenaSession arenaSession = getSession(event.getPlayer());
if (arenaSession == null) {
return;
}
if (event.getTo().equals(arenaSession.getArena().getSpawnLocation())) {
return;
}
arenaSession.triggerQuit(false);
}
/**
* Gets the arena session for the given player
*
* @param player <p>The player to get the arena session for</p>
* @return <p>The player's session, or null if not in a session</p>
*/
private @Nullable DropperArenaSession getSession(@NotNull Player player) {
return MiniGames.getInstance().getDropperArenaPlayerRegistry().getArenaSession(player.getUniqueId());
}
}

View File

@@ -0,0 +1,335 @@
package net.knarcraft.minigames.placeholder;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaGameMode;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaHandler;
import net.knarcraft.minigames.arena.dropper.DropperArenaRecordsRegistry;
import net.knarcraft.minigames.arena.record.ArenaRecord;
import net.knarcraft.minigames.placeholder.parsing.InfoType;
import net.knarcraft.minigames.placeholder.parsing.SelectionType;
import net.knarcraft.minigames.property.RecordType;
import net.knarcraft.minigames.util.DropperGroupRecordHelper;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
/**
* A placeholder expansion for dropper record placeholders
*/
public class DropperRecordExpansion extends PlaceholderExpansion {
private final MiniGames plugin;
private final Map<UUID, Set<GroupRecordCache<Integer>>> groupRecordDeathsCache;
private final Map<UUID, Set<GroupRecordCache<Long>>> groupRecordTimeCache;
/**
* Initializes a new record expansion
*
* @param plugin <p>A reference to the dropper plugin</p>
*/
public DropperRecordExpansion(MiniGames plugin) {
this.plugin = plugin;
this.groupRecordDeathsCache = new HashMap<>();
this.groupRecordTimeCache = new HashMap<>();
}
@Override
public String getIdentifier() {
return "dropper";
}
@Override
public String getAuthor() {
return "EpicKnarvik97";
}
@Override
public String getVersion() {
return "1.0.0";
}
@Override
public boolean persist() {
return true;
}
@Override
public String onRequest(OfflinePlayer player, String parameters) {
String[] parts = parameters.split("_");
// Record is used as the prefix for all record placeholders in case more placeholder types are added
if (parts.length < 7 || !parts[0].equals("record")) {
return parameters;
}
RecordType recordType = RecordType.getFromString(parts[1]);
DropperArenaGameMode gameMode = DropperArenaGameMode.matchGamemode(parts[2]);
SelectionType selectionType = SelectionType.getFromString(parts[3]);
String identifier = parts[4];
int recordNumber = Integer.parseInt(parts[5]) - 1;
InfoType infoType = InfoType.getFromString(parts[6]);
if (recordType == null || infoType == null) {
return parameters;
}
String info = null;
DropperArenaHandler arenaHandler = plugin.getDropperArenaHandler();
if (selectionType == SelectionType.GROUP) {
info = getGroupRecord(arenaHandler, identifier, gameMode, recordType, recordNumber, infoType);
} else if (selectionType == SelectionType.ARENA) {
info = getArenaRecord(arenaHandler, identifier, gameMode, recordType, recordNumber, infoType);
}
return Objects.requireNonNullElse(info, parameters);
}
/**
* Clears all record caches
*/
public void clearCaches() {
this.groupRecordDeathsCache.clear();
this.groupRecordTimeCache.clear();
}
/**
* Gets a piece of record information from a dropper arena group
*
* @param arenaHandler <p>The arena handler to get the group from</p>
* @param identifier <p>The identifier (name/uuid) selecting the group</p>
* @param gameMode <p>The game-mode to get a record for</p>
* @param recordType <p>The type of record to get</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @param infoType <p>The type of info (player, value, combined) to get</p>
* @return <p>The selected information about the record, or null if not found</p>
*/
private @Nullable String getGroupRecord(@NotNull DropperArenaHandler arenaHandler, @NotNull String identifier,
@NotNull DropperArenaGameMode gameMode, @NotNull RecordType recordType,
int recordNumber, @NotNull InfoType infoType) {
// Allow specifying the group UUID or the arena name
DropperArenaGroup group;
try {
group = arenaHandler.getGroup(UUID.fromString(identifier));
} catch (IllegalArgumentException exception) {
group = arenaHandler.getGroup(identifier);
}
if (group == null) {
return null;
}
ArenaRecord<?> record;
if (recordType == RecordType.DEATHS) {
record = getGroupDeathRecord(group, gameMode, recordNumber);
} else {
record = getGroupTimeRecord(group, gameMode, recordNumber);
}
// If a record number is not found, leave it blank, so it looks neat
if (record == null) {
return "";
}
return getRecordData(infoType, record);
}
/**
* Gets a time record from a group, using the cache if possible
*
* @param group <p>The group to get the record from</p>
* @param gameMode <p>The game-mode to get the record from</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @return <p>The record, or null if not found</p>
*/
private @Nullable ArenaRecord<?> getGroupTimeRecord(@NotNull DropperArenaGroup group,
@NotNull DropperArenaGameMode gameMode, int recordNumber) {
return getCachedGroupRecord(group, gameMode, RecordType.TIME, recordNumber, groupRecordTimeCache,
() -> DropperGroupRecordHelper.getCombinedTime(group, gameMode));
}
/**
* Gets a death record from a group, using the cache if possible
*
* @param group <p>The group to get the record from</p>
* @param gameMode <p>The game-mode to get the record from</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @return <p>The record, or null if not found</p>
*/
private @Nullable ArenaRecord<?> getGroupDeathRecord(@NotNull DropperArenaGroup group,
@NotNull DropperArenaGameMode gameMode, int recordNumber) {
return getCachedGroupRecord(group, gameMode, RecordType.DEATHS, recordNumber, groupRecordDeathsCache,
() -> DropperGroupRecordHelper.getCombinedDeaths(group, gameMode));
}
/**
* Gets a group record, fetching from a cache if possible
*
* @param group <p>The group to get the record for</p>
* @param gameMode <p>The game-mode to get the record for</p>
* @param recordType <p>The type of record to get</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @param caches <p>The caches to use for looking for and saving the record</p>
* @param recordProvider <p>The provider of records if the cache cannot provide the record</p>
* @param <K> <p>The type of the provided records</p>
* @return <p>The specified record, or null if not found</p>
*/
private <K extends Comparable<K>> @Nullable ArenaRecord<?> getCachedGroupRecord(@NotNull DropperArenaGroup group,
@NotNull DropperArenaGameMode gameMode,
@NotNull RecordType recordType,
int recordNumber,
@NotNull Map<UUID, Set<GroupRecordCache<K>>> caches,
@NotNull Supplier<Set<ArenaRecord<K>>> recordProvider) {
UUID groupId = group.getGroupId();
if (!caches.containsKey(groupId)) {
caches.put(groupId, new HashSet<>());
}
Set<GroupRecordCache<K>> existingCaches = caches.get(groupId);
Set<GroupRecordCache<K>> expired = new HashSet<>();
Set<ArenaRecord<K>> cachedRecords = null;
for (GroupRecordCache<K> cache : existingCaches) {
// Expire caches after 30 seconds
if (System.currentTimeMillis() - cache.createdTime() > 30000) {
expired.add(cache);
}
// If of the correct type, and not expired, use the cache
if (cache.gameMode() == gameMode && cache.recordType() == recordType) {
cachedRecords = cache.records();
break;
}
}
existingCaches.removeAll(expired);
// If not found, generate and cache the specified record
if (cachedRecords == null) {
cachedRecords = recordProvider.get();
existingCaches.add(new GroupRecordCache<>(gameMode, recordType, cachedRecords, System.currentTimeMillis()));
}
return getRecord(cachedRecords, recordNumber);
}
/**
* Gets a piece of record information from a dropper arena
*
* @param arenaHandler <p>The arena handler to get the arena from</p>
* @param identifier <p>The identifier (name/uuid) selecting the arena</p>
* @param gameMode <p>The game-mode to get a record for</p>
* @param recordType <p>The type of record to get</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @param infoType <p>The type of info (player, value, combined) to get</p>
* @return <p>The selected information about the record, or null if not found</p>
*/
private @Nullable String getArenaRecord(@NotNull DropperArenaHandler arenaHandler, @NotNull String identifier,
@NotNull DropperArenaGameMode gameMode, @NotNull RecordType recordType,
int recordNumber, @NotNull InfoType infoType) {
// Allow specifying the arena UUID or the arena name
DropperArena arena;
try {
arena = arenaHandler.getArena(UUID.fromString(identifier));
} catch (IllegalArgumentException exception) {
arena = arenaHandler.getArena(identifier);
}
if (arena == null) {
return null;
}
@NotNull Map<ArenaGameMode, ArenaRecordsRegistry> registries = arena.getData().getRecordRegistries();
ArenaRecordsRegistry recordsRegistry = registries.get(gameMode);
ArenaRecord<?> record = getRecord((DropperArenaRecordsRegistry) recordsRegistry, recordType, recordNumber);
// If a record number is not found, leave it blank, so it looks neat
if (record == null) {
return "";
}
return getRecordData(infoType, record);
}
/**
* Gets the specified record
*
* @param recordsRegistry <p>The records registry to get the record from</p>
* @param recordType <p>The type of record to get</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @return <p>The record, or null if not found</p>
*/
private @Nullable ArenaRecord<?> getRecord(@NotNull DropperArenaRecordsRegistry recordsRegistry,
@NotNull RecordType recordType, int recordNumber) {
return switch (recordType) {
case TIME -> getRecord(new HashSet<>(recordsRegistry.getShortestTimeMilliSecondsRecords()), recordNumber);
case DEATHS -> getRecord(new HashSet<>(recordsRegistry.getLeastDeathsRecords()), recordNumber);
};
}
/**
* Gets the record at the given index
*
* @param records <p>The records to search through</p>
* @param index <p>The index of the record to get</p>
* @param <K> <p>The type of record in the record list</p>
* @return <p>The record, or null if index is out of bounds</p>
*/
private <K extends Comparable<K>> @Nullable ArenaRecord<K> getRecord(Set<ArenaRecord<K>> records, int index) {
List<ArenaRecord<K>> sorted = getSortedRecords(records);
if (index < sorted.size() && index >= 0) {
return sorted.get(index);
} else {
return null;
}
}
/**
* Gets a piece of data from a record as a string
*
* @param infoType <p>The type of info to get data for</p>
* @param arenaRecord <p>The record to get the data from</p>
* @return <p>The requested data as a string, or null</p>
*/
private String getRecordData(@NotNull InfoType infoType, @NotNull ArenaRecord<?> arenaRecord) {
return switch (infoType) {
case PLAYER -> getPlayerName(arenaRecord.getUserId());
case VALUE -> arenaRecord.getRecord().toString();
case COMBINED -> getPlayerName(arenaRecord.getUserId()) + ": " + arenaRecord.getRecord().toString();
};
}
/**
* Gets the given set of records as a sorted list
*
* @param recordSet <p>The set of records to sort</p>
* @param <K> <p>The type of the records</p>
* @return <p>The sorted records</p>
*/
private <K extends Comparable<K>> @NotNull List<ArenaRecord<K>> getSortedRecords(
@NotNull Set<ArenaRecord<K>> recordSet) {
List<ArenaRecord<K>> records = new ArrayList<>(recordSet);
Collections.sort(records);
return records;
}
/**
* Gets the name of a player, given the player's UUID
*
* @param playerId <p>The id of the player to get the name for</p>
* @return <p>The name of the player, or a string representation of the UUID if not found</p>
*/
private String getPlayerName(@NotNull UUID playerId) {
return Bukkit.getOfflinePlayer(playerId).getName();
}
}

View File

@@ -0,0 +1,22 @@
package net.knarcraft.minigames.placeholder;
import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import net.knarcraft.minigames.arena.record.ArenaRecord;
import net.knarcraft.minigames.property.RecordType;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
/**
* A record for keeping track of records for a dropper group
*
* @param gameMode <p>The game-mode this cache is storing records for</p>
* @param recordType <p>The type of record stored</p>
* @param records <p>The stored records</p>
* @param createdTime <p>The time this cache was created</p>
*/
public record GroupRecordCache<K extends Comparable<K>>(@NotNull DropperArenaGameMode gameMode,
@NotNull RecordType recordType,
@NotNull Set<ArenaRecord<K>> records,
@NotNull Long createdTime) {
}

View File

@@ -0,0 +1,42 @@
package net.knarcraft.minigames.placeholder.parsing;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* The type of information returned by a placeholder
*/
public enum InfoType {
/**
* The player that achieved the record
*/
PLAYER,
/**
* The value of the record, whatever it is
*/
VALUE,
/**
* A combined PLAYER: VALUE
*/
COMBINED,
;
/**
* Gets the info type specified in the given string
*
* @param type <p>The string specifying the info type</p>
* @return <p>The info type, or null if not found</p>
*/
public static @Nullable InfoType getFromString(@NotNull String type) {
for (InfoType infoType : InfoType.values()) {
if (infoType.name().equalsIgnoreCase(type)) {
return infoType;
}
}
return null;
}
}

View File

@@ -0,0 +1,37 @@
package net.knarcraft.minigames.placeholder.parsing;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A type of selection performed by a placeholder
*/
public enum SelectionType {
/**
* The identifier is trying to select a group
*/
GROUP,
/**
* The identifier is trying to select an arena
*/
ARENA,
;
/**
* Gets the selection type specified in the given string
*
* @param type <p>The string specifying the selection type</p>
* @return <p>The selection type, or null if not found</p>
*/
public static @Nullable SelectionType getFromString(@NotNull String type) {
for (SelectionType selectionType : SelectionType.values()) {
if (selectionType.name().equalsIgnoreCase(type)) {
return selectionType;
}
}
return null;
}
}

View File

@@ -0,0 +1,23 @@
package net.knarcraft.minigames.property;
/**
* A representation of all possible record results
*/
public enum RecordResult {
/**
* No record was achieved
*/
NONE,
/**
* A personal bes was achieved
*/
PERSONAL_BEST,
/**
* A world record was achieved
*/
WORLD_RECORD
}

View File

@@ -0,0 +1,37 @@
package net.knarcraft.minigames.property;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A type of record a player can achieve
*/
public enum RecordType {
/**
* A least-deaths record
*/
DEATHS,
/**
*
*/
TIME,
;
/**
* Gets the record type specified in the given string
*
* @param type <p>The string specifying the record type</p>
* @return <p>The record type, or null if not found</p>
*/
public static @Nullable RecordType getFromString(@NotNull String type) {
for (RecordType recordType : RecordType.values()) {
if (recordType.name().equalsIgnoreCase(type)) {
return recordType;
}
}
return null;
}
}

View File

@@ -0,0 +1,334 @@
package net.knarcraft.minigames.util;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.ArenaGameMode;
import net.knarcraft.minigames.arena.ArenaRecordsRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaData;
import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaRecordsRegistry;
import net.knarcraft.minigames.arena.dropper.DropperArenaStorageKey;
import net.knarcraft.minigames.arena.parkour.ParkourArena;
import net.knarcraft.minigames.arena.parkour.ParkourArenaData;
import net.knarcraft.minigames.arena.parkour.ParkourArenaStorageKey;
import net.knarcraft.minigames.container.SerializableMaterial;
import net.knarcraft.minigames.container.SerializableUUID;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
/**
* A helper class for saving and loading arenas
*/
public final class ArenaStorageHelper {
private final static File dataFolder = MiniGames.getInstance().getDataFolder();
private final static String dropperArenasConfigurationSection = "dropperArenas";
private final static String dropperGroupsConfigurationSection = "dropperGroups";
private final static String parkourArenasConfigurationSection = "parkourArenas";
private final static String parkourGroupsConfigurationSection = "parkourGroups";
private static final File arenaFile = new File(dataFolder, "arenas.yml");
private static final File groupFile = new File(dataFolder, "groups.yml");
private static final File dropperArenaDataFolder = new File(dataFolder, "dropper_arena_data");
private static final File parkourArenaDataFolder = new File(dataFolder, "parkour_arena_data");
private ArenaStorageHelper() {
}
/**
* Saves the given dropper arena groups
*
* @param arenaGroups <p>The arena groups to save</p>
* @throws IOException <p>If unable to write to the file</p>
*/
public static void saveDropperArenaGroups(@NotNull Set<DropperArenaGroup> arenaGroups) throws IOException {
YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection groupSection = configuration.createSection(dropperGroupsConfigurationSection);
for (DropperArenaGroup arenaGroup : arenaGroups) {
groupSection.set(arenaGroup.getGroupId().toString(), arenaGroup);
}
configuration.save(groupFile);
}
/**
* Loads all existing dropper arena groups
*
* @return <p>The loaded arena groups</p>
*/
public static @NotNull Set<DropperArenaGroup> loadDropperArenaGroups() {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(groupFile);
ConfigurationSection groupSection = configuration.getConfigurationSection(dropperGroupsConfigurationSection);
//If no such section exists, it must be the case that there is no data to load
if (groupSection == null) {
return new HashSet<>();
}
Set<DropperArenaGroup> arenaGroups = new HashSet<>();
for (String sectionName : groupSection.getKeys(false)) {
arenaGroups.add((DropperArenaGroup) groupSection.get(sectionName));
}
return arenaGroups;
}
/**
* Saves the given arenas
*
* @param arenas <p>The arenas to save</p>
* @throws IOException <p>If unable to write to the file</p>
*/
public static void saveDropperArenas(@NotNull Map<UUID, DropperArena> arenas) throws IOException {
YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection arenaSection = configuration.createSection(dropperArenasConfigurationSection);
for (DropperArena arena : arenas.values()) {
//Note: While the arena name is used as the key, as the key has to be sanitized, the un-sanitized arena name
// must be stored as well
@NotNull ConfigurationSection configSection = arenaSection.createSection(arena.getArenaId().toString());
configSection.set(DropperArenaStorageKey.ID.getKey(), new SerializableUUID(arena.getArenaId()));
configSection.set(DropperArenaStorageKey.NAME.getKey(), arena.getArenaName());
configSection.set(DropperArenaStorageKey.SPAWN_LOCATION.getKey(), arena.getSpawnLocation());
configSection.set(DropperArenaStorageKey.EXIT_LOCATION.getKey(), arena.getExitLocation());
configSection.set(DropperArenaStorageKey.PLAYER_VERTICAL_VELOCITY.getKey(), arena.getPlayerVerticalVelocity());
configSection.set(DropperArenaStorageKey.PLAYER_HORIZONTAL_VELOCITY.getKey(), arena.getPlayerHorizontalVelocity());
configSection.set(DropperArenaStorageKey.WIN_BLOCK_TYPE.getKey(), new SerializableMaterial(arena.getWinBlockType()));
saveDropperArenaData(arena.getData());
}
configuration.save(arenaFile);
}
/**
* Saves the given arenas
*
* @param arenas <p>The arenas to save</p>
* @throws IOException <p>If unable to write to the file</p>
*/
public static void saveParkourArenas(@NotNull Map<UUID, ParkourArena> arenas) throws IOException {
YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection arenaSection = configuration.createSection(parkourArenasConfigurationSection);
for (ParkourArena arena : arenas.values()) {
//Note: While the arena name is used as the key, as the key has to be sanitized, the un-sanitized arena name
// must be stored as well
@NotNull ConfigurationSection configSection = arenaSection.createSection(arena.getArenaId().toString());
configSection.set(ParkourArenaStorageKey.ID.getKey(), new SerializableUUID(arena.getArenaId()));
configSection.set(ParkourArenaStorageKey.NAME.getKey(), arena.getArenaName());
configSection.set(ParkourArenaStorageKey.SPAWN_LOCATION.getKey(), arena.getSpawnLocation());
configSection.set(ParkourArenaStorageKey.EXIT_LOCATION.getKey(), arena.getExitLocation());
configSection.set(ParkourArenaStorageKey.WIN_BLOCK_TYPE.getKey(), new SerializableMaterial(arena.getWinBlockType()));
configSection.set(ParkourArenaStorageKey.WIN_LOCATION.getKey(), arena.getWinLocation());
configSection.set(ParkourArenaStorageKey.KILL_PLANE_BLOCKS.getKey(), arena.getKillPlaneBlocks());
configSection.set(ParkourArenaStorageKey.CHECKPOINTS.getKey(), arena.getCheckpoints());
saveParkourArenaData(arena.getData());
}
configuration.save(arenaFile);
}
/**
* Loads all arenas
*
* @return <p>The loaded arenas, or null if the arenas configuration section is missing.</p>
*/
public static @NotNull Map<UUID, DropperArena> loadDropperArenas() {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(arenaFile);
ConfigurationSection arenaSection = configuration.getConfigurationSection(dropperArenasConfigurationSection);
//If no such section exists, it must be the case that there is no data to load
if (arenaSection == null) {
return new HashMap<>();
}
Map<UUID, DropperArena> loadedArenas = new HashMap<>();
for (String sectionName : arenaSection.getKeys(false)) {
ConfigurationSection configurationSection = arenaSection.getConfigurationSection(sectionName);
//I'm not sure whether this could actually happen
if (configurationSection == null) {
continue;
}
DropperArena arena = loadDropperArena(configurationSection);
if (arena != null) {
loadedArenas.put(arena.getArenaId(), arena);
}
}
return loadedArenas;
}
/**
* Loads an arena from the given configuration section
*
* @param configurationSection <p>The configuration section containing arena data</p>
* @return <p>The loaded arena, or null if invalid</p>
*/
private static @Nullable DropperArena loadDropperArena(@NotNull ConfigurationSection configurationSection) {
UUID arenaId = ((SerializableUUID) configurationSection.get(DropperArenaStorageKey.ID.getKey(),
new SerializableUUID(UUID.randomUUID()))).getRawValue();
String arenaName = configurationSection.getString(DropperArenaStorageKey.NAME.getKey());
Location spawnLocation = (Location) configurationSection.get(DropperArenaStorageKey.SPAWN_LOCATION.getKey());
Location exitLocation = (Location) configurationSection.get(DropperArenaStorageKey.EXIT_LOCATION.getKey());
double verticalVelocity = configurationSection.getDouble(DropperArenaStorageKey.PLAYER_VERTICAL_VELOCITY.getKey());
float horizontalVelocity = sanitizeHorizontalVelocity((float) configurationSection.getDouble(
DropperArenaStorageKey.PLAYER_HORIZONTAL_VELOCITY.getKey()));
SerializableMaterial winBlockType = (SerializableMaterial) configurationSection.get(
DropperArenaStorageKey.WIN_BLOCK_TYPE.getKey());
if (arenaName == null || spawnLocation == null) {
MiniGames.log(Level.SEVERE, "Could not load the arena at configuration " +
"section " + configurationSection.getName() + ". Please check the arenas storage file for issues.");
return null;
}
if (winBlockType == null) {
winBlockType = new SerializableMaterial(Material.WATER);
}
DropperArenaData arenaData = loadDropperArenaData(arenaId);
if (arenaData == null) {
MiniGames.log(Level.SEVERE, "Unable to load arena data for " + arenaId);
Map<ArenaGameMode, ArenaRecordsRegistry> recordRegistries = new HashMap<>();
for (ArenaGameMode arenaGameMode : DropperArenaGameMode.values()) {
recordRegistries.put(arenaGameMode, new DropperArenaRecordsRegistry(arenaId));
}
arenaData = new DropperArenaData(arenaId, recordRegistries, new HashMap<>());
}
return new DropperArena(arenaId, arenaName, spawnLocation, exitLocation, verticalVelocity, horizontalVelocity,
winBlockType.getRawValue(), arenaData, MiniGames.getInstance().getDropperArenaHandler());
}
/**
* Stores the given arena data to a file
*
* @param arenaData <p>The arena data to store</p>
*/
public static void saveParkourArenaData(@NotNull ParkourArenaData arenaData) throws IOException {
YamlConfiguration configuration = new YamlConfiguration();
configuration.set(ParkourArenaStorageKey.DATA.getKey(), arenaData);
configuration.save(getParkourArenaDataFile(arenaData.getArenaId()));
}
/**
* Stores the given arena data to a file
*
* @param arenaData <p>The arena data to store</p>
*/
public static void saveDropperArenaData(@NotNull DropperArenaData arenaData) throws IOException {
YamlConfiguration configuration = new YamlConfiguration();
configuration.set(DropperArenaStorageKey.DATA.getKey(), arenaData);
configuration.save(getDropperArenaDataFile(arenaData.getArenaId()));
}
/**
* Loads arena data for the given arena id
*
* @param arenaId <p>The id of the arena to get data for</p>
* @return <p>The loaded arena data</p>
*/
private static @Nullable DropperArenaData loadDropperArenaData(@NotNull UUID arenaId) {
File arenaDataFile = getDropperArenaDataFile(arenaId);
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(arenaDataFile);
return (DropperArenaData) configuration.get(DropperArenaStorageKey.DATA.getKey());
}
/**
* Loads arena data for the given arena id
*
* @param arenaId <p>The id of the arena to get data for</p>
* @return <p>The loaded arena data</p>
*/
private static @Nullable ParkourArenaData loadParkourArenaData(@NotNull UUID arenaId) {
File arenaDataFile = getParkourArenaDataFile(arenaId);
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(arenaDataFile);
return (ParkourArenaData) configuration.get(ParkourArenaStorageKey.DATA.getKey());
}
/**
* Removes data for the arena with the given id
*
* @param arenaId <p>The id of the arena to remove data for</p>
* @return <p>True if the data was successfully removed</p>
*/
public static boolean removeDropperArenaData(@NotNull UUID arenaId) {
return getDropperArenaDataFile(arenaId).delete();
}
/**
* Removes data for the arena with the given id
*
* @param arenaId <p>The id of the arena to remove data for</p>
* @return <p>True if the data was successfully removed</p>
*/
public static boolean removeParkourArenaData(@NotNull UUID arenaId) {
return getParkourArenaDataFile(arenaId).delete();
}
/**
* Gets the file used to store the given arena id's data
*
* @param arenaId <p>The id of the arena to get a data file for</p>
* @return <p>The file the arena's data is/should be stored in</p>
*/
private static @NotNull File getDropperArenaDataFile(@NotNull UUID arenaId) {
return getArenaDataFile(dropperArenaDataFolder, arenaId);
}
/**
* Gets the file used to store the given arena id's data
*
* @param arenaId <p>The id of the arena to get a data file for</p>
* @return <p>The file the arena's data is/should be stored in</p>
*/
private static @NotNull File getParkourArenaDataFile(@NotNull UUID arenaId) {
return getArenaDataFile(parkourArenaDataFolder, arenaId);
}
/**
* Gets the file used to store the given arena id's data
*
* @param root <p>The root directory for the file</p>
* @param arenaId <p>The id of the arena to get a data file for</p>
* @return <p>The file the arena's data is/should be stored in</p>
*/
private static @NotNull File getArenaDataFile(File root, @NotNull UUID arenaId) {
File arenaDataFile = new File(root, arenaId + ".yml");
if (!root.exists() && !root.mkdirs()) {
MiniGames.log(Level.SEVERE, "Unable to create the arena data directories");
}
return arenaDataFile;
}
/**
* Sanitizes the given horizontal velocity to make sure it doesn't leave its bounds
*
* @param horizontalVelocity <p>The horizontal velocity to sanitize</p>
* @return <p>The sanitized horizontal velocity</p>
*/
private static float sanitizeHorizontalVelocity(float horizontalVelocity) {
if (horizontalVelocity < -1) {
return -1;
} else if (horizontalVelocity > 1) {
return 1;
} else {
return horizontalVelocity;
}
}
}

View File

@@ -0,0 +1,171 @@
package net.knarcraft.minigames.util;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaGameMode;
import net.knarcraft.minigames.arena.dropper.DropperArenaGroup;
import net.knarcraft.minigames.arena.dropper.DropperArenaHandler;
import net.knarcraft.minigames.arena.record.ArenaRecord;
import net.knarcraft.minigames.arena.record.SummableArenaRecord;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.BiFunction;
/**
* A helper class for getting combined record data for a dropper group
*/
public final class DropperGroupRecordHelper {
private DropperGroupRecordHelper() {
}
/**
* Gets the combined least-death records for the given group and game-mode
*
* @param group <p>The group to get records from</p>
* @param gameMode <p>The game-mode to get records for</p>
* @return <p>The combined death records</p>
*/
public static @NotNull Set<ArenaRecord<Integer>> getCombinedDeaths(@NotNull DropperArenaGroup group,
@NotNull DropperArenaGameMode gameMode) {
Map<UUID, SummableArenaRecord<Integer>> records = new HashMap<>();
@NotNull BiFunction<DropperArena, DropperArenaGameMode, Set<SummableArenaRecord<Integer>>> recordSupplier =
(arena, aGameMode) -> arena.getData().getRecordRegistries().get(gameMode).getLeastDeathsRecords();
return getCombined(group, gameMode, records, recordSupplier);
}
/**
* Gets the combined least-time records for the given group and game-mode
*
* @param group <p>The group to get records from</p>
* @param gameMode <p>The game-mode to get records for</p>
* @return <p>The combined least-time records</p>
*/
public static @NotNull Set<ArenaRecord<Long>> getCombinedTime(@NotNull DropperArenaGroup group,
@NotNull DropperArenaGameMode gameMode) {
Map<UUID, SummableArenaRecord<Long>> records = new HashMap<>();
@NotNull BiFunction<DropperArena, DropperArenaGameMode, Set<SummableArenaRecord<Long>>> recordSupplier =
(arena, aGameMode) -> arena.getData().getRecordRegistries().get(gameMode).getShortestTimeMilliSecondsRecords();
return getCombined(group, gameMode, records, recordSupplier);
}
/**
* Gets the combined records for a group and game-mode
*
* @param group <p>The group to get combined records for</p>
* @param gameMode <p>The game-mode to get records for</p>
* @param records <p>The map to store the combined records to</p>
* @param recordSupplier <p>The function that supplies records of this type</p>
* @param <K> <p>The type of the records to combine</p>
* @return <p>The combined records</p>
*/
private static <K extends Comparable<K>> @NotNull Set<ArenaRecord<K>> getCombined(@NotNull DropperArenaGroup group,
@NotNull DropperArenaGameMode gameMode,
@NotNull Map<UUID,
SummableArenaRecord<K>> records,
@NotNull BiFunction<DropperArena,
DropperArenaGameMode,
Set<SummableArenaRecord<K>>> recordSupplier) {
DropperArenaHandler arenaHandler = MiniGames.getInstance().getDropperArenaHandler();
// Get all arenas in the group
Set<DropperArena> arenas = getArenas(arenaHandler, group);
// Calculate the combined records
Map<UUID, Integer> recordsFound = new HashMap<>();
combineRecords(arenas, gameMode, records, recordsFound, recordSupplier);
// Filter out any players that haven't played through all arenas
filterRecords(records, recordsFound, arenas.size());
return new HashSet<>(records.values());
}
/**
* Filters away any records that belong to users who haven't set records for all arenas in the group
*
* @param records <p>The records to filter</p>
* @param recordsFound <p>The map of how many records have been registered for each user</p>
* @param arenas <p>The number of arenas in the group</p>
* @param <K> <p>The type of the given records</p>
*/
private static <K extends Comparable<K>> void filterRecords(@NotNull Map<UUID, SummableArenaRecord<K>> records,
@NotNull Map<UUID, Integer> recordsFound, int arenas) {
for (UUID userId : recordsFound.keySet()) {
if (recordsFound.get(userId) != arenas) {
records.remove(userId);
}
}
}
/**
* Gets all arenas in the given group
*
* @param arenaHandler <p>The arena handler to get arenas from</p>
* @param group <p>The group to get arenas for</p>
* @return <p>The arenas found in the group</p>
*/
private static @NotNull Set<DropperArena> getArenas(@NotNull DropperArenaHandler arenaHandler,
@NotNull DropperArenaGroup group) {
// Get all arenas in the group
Set<DropperArena> arenas = new HashSet<>();
for (UUID arenaId : group.getArenas()) {
DropperArena arena = arenaHandler.getArena(arenaId);
if (arena != null) {
arenas.add(arena);
}
}
return arenas;
}
/**
* Combines arena records
*
* @param arenas <p>The arenas whose records should be combined</p>
* @param gameMode <p>The game-mode to combine records for</p>
* @param combinedRecords <p>The map to store the combined records to</p>
* @param recordsFound <p>The map used to store the number of records registered for each player</p>
* @param recordSupplier <p>The function that supplies record data of this type</p>
* @param <K> <p>The type of record to combine</p>
*/
private static <K extends Comparable<K>> void combineRecords(@NotNull Set<DropperArena> arenas,
@NotNull DropperArenaGameMode gameMode,
@NotNull Map<UUID,
SummableArenaRecord<K>> combinedRecords,
@NotNull Map<UUID, Integer> recordsFound,
@NotNull BiFunction<DropperArena, DropperArenaGameMode,
Set<SummableArenaRecord<K>>> recordSupplier) {
for (DropperArena arena : arenas) {
Set<SummableArenaRecord<K>> existingRecords = recordSupplier.apply(arena, gameMode);
// For each arena's record registry, calculate the combined records
for (SummableArenaRecord<K> value : existingRecords) {
if (value == null) {
continue;
}
UUID userId = value.getUserId();
// Bump the number of records found for the user
if (!recordsFound.containsKey(userId)) {
recordsFound.put(userId, 0);
}
recordsFound.put(userId, recordsFound.get(userId) + 1);
// Put the value, or the sum with the existing value, into combined records
if (!combinedRecords.containsKey(userId)) {
combinedRecords.put(value.getUserId(), value);
} else {
combinedRecords.put(userId, combinedRecords.get(userId).sum(value.getRecord()));
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
package net.knarcraft.minigames.util;
import org.bukkit.Location;
import org.bukkit.World;
/**
* A helper class for validating whether given input is valid
*/
public final class InputValidationHelper {
private InputValidationHelper() {
}
/**
* Checks whether the given location is valid
*
* @param location <p>The location to validate</p>
* @return <p>False if the location is valid</p>
*/
public static boolean isInvalid(Location location) {
World world = location.getWorld();
return world == null || !world.getWorldBorder().isInside(location);
}
}

View File

@@ -0,0 +1,67 @@
package net.knarcraft.minigames.util;
import net.knarcraft.minigames.MiniGames;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.util.Vector;
/**
* A helper class for teleporting players
*/
public final class PlayerTeleporter {
private PlayerTeleporter() {
}
/**
* Teleports the given player to the given location
*
* <p>Forcing teleport should only be used inside an arena, to prevent the player from becoming stuck.</p>
*
* @param player <p>The player about to teleport</p>
* @param location <p>The location the player should be teleported to</p>
* @param force <p>Whether to force a player teleport, even in a vehicle or a passenger</p>
* @param immediately <p>Whether to to the teleportation immediately, not using any timers</p>
* @return <p>True if the player was successfully teleported</p>
*/
public static boolean teleportPlayer(Player player, Location location, boolean force, boolean immediately) {
if (!player.getPassengers().isEmpty()) {
if (force) {
for (Entity passenger : player.getPassengers()) {
passenger.eject();
passenger.teleport(location);
}
} else {
player.sendMessage("You cannot be teleported with a passenger!");
return false;
}
}
if (player.isInsideVehicle()) {
if (force && player.getVehicle() != null) {
Entity vehicle = player.getVehicle();
player.eject();
vehicle.teleport(location);
} else {
player.sendMessage("You cannot be teleported while in a vehicle");
return false;
}
}
//Stop the existing player velocity to prevent unevenness between players
player.setVelocity(new Vector(0, 0, 0));
player.setInvulnerable(true);
player.teleport(location);
player.setVelocity(new Vector(0, 0, 0));
//When teleporting a player out of the arena, sometimes the move listener is slow to react, giving the player
// lethal velocity, and causing damage. That's why the player is given 5 ticks of invulnerability
if (!immediately) {
Bukkit.getScheduler().runTaskLater(MiniGames.getInstance(), () -> player.setInvulnerable(false), 5);
} else {
player.setInvulnerable(false);
}
return true;
}
}

View File

@@ -0,0 +1,85 @@
package net.knarcraft.minigames.util;
import net.knarcraft.minigames.container.SerializableContainer;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* A converter for converting between normal and serializable classes
*/
public final class SerializableConverter {
private SerializableConverter() {
}
/**
* Converts the given collection into a collection of serializable objects
*
* @param values <p>The values to make serializable</p>
* @param targetCollection <p>The collection to store the converted objects to</p>
* @param target <p>An instance of the target serializable container</p>
* @param <T> <p>The type of the value to make serializable</p>
*/
public static <T> void makeSerializable(@NotNull Collection<T> values,
@NotNull Collection<SerializableContainer<T>> targetCollection,
@NotNull SerializableContainer<T> target) {
for (T item : values) {
targetCollection.add(target.getSerializable(item));
}
}
/**
* Converts the given collection of serializable containers into a collection of normal objects
*
* @param values <p>The values to convert to normal</p>
* @param targetCollection <p>The collection to store the converted objects to</p>
* @param <T> <p>The type of the value to convert to normal</p>
*/
public static <T> void getRawValue(@NotNull Collection<SerializableContainer<T>> values,
@NotNull Collection<T> targetCollection) {
for (SerializableContainer<T> item : values) {
targetCollection.add(item.getRawValue());
}
}
/**
* Converts the given collection into a collection of serializable objects
*
* @param values <p>The values to make serializable</p>
* @param targetMap <p>The map to store the converted objects to</p>
* @param target <p>An instance of the target serializable container</p>
* @param <T> <p>The type of the value to make serializable</p>
*/
public static <S, T> void makeSerializable(@NotNull Map<S, Set<T>> values,
@NotNull Map<S, Set<SerializableContainer<T>>> targetMap,
@NotNull SerializableContainer<T> target) {
for (Map.Entry<S, Set<T>> item : values.entrySet()) {
Set<SerializableContainer<T>> conversionCollection = new HashSet<>();
makeSerializable(item.getValue(), conversionCollection, target);
targetMap.put(item.getKey(), conversionCollection);
}
}
/**
* Converts the given collection of serializable containers into a collection of normal objects
*
* @param values <p>The values to convert to normal</p>
* @param targetMap <p>The map to store the converted objects to</p>
* @param <S> <p>The type of the map's key</p>
* @param <T> <p>The type of the value to convert to normal</p>
*/
public static <S, T> void getRawValue(@NotNull Map<S, Set<SerializableContainer<T>>> values,
@NotNull Map<S, Set<T>> targetMap) {
for (Map.Entry<S, Set<SerializableContainer<T>>> item : values.entrySet()) {
Set<T> conversionCollection = new HashSet<>();
getRawValue(item.getValue(), conversionCollection);
targetMap.put(item.getKey(), conversionCollection);
}
}
}

View File

@@ -0,0 +1,37 @@
package net.knarcraft.minigames.util;
import org.jetbrains.annotations.NotNull;
/**
* A helper-class for sanitizing strings
*/
public final class StringSanitizer {
private StringSanitizer() {
}
/**
* Removes unwanted characters from a string
*
* <p>This basically removes character that have a special meaning in YML, or ones that cannot be used in the
* chat.</p>
*
* @param input <p>The string to remove from</p>
* @return <p>The string with the unwanted characters removed</p>
*/
public static @NotNull String removeUnwantedCharacters(@NotNull String input) {
return input.replaceAll("[§ :=&]", "");
}
/**
* Sanitizes an arena name for usage as a YAML key
*
* @param arenaName <p>The arena name to sanitize</p>
* @return <p>The sanitized arena name</p>
*/
public static @NotNull String sanitizeArenaName(@NotNull String arenaName) {
return arenaName.toLowerCase().trim().replaceAll(" ", "_");
}
}

View File

@@ -0,0 +1,46 @@
package net.knarcraft.minigames.util;
import net.knarcraft.minigames.MiniGames;
import net.knarcraft.minigames.arena.dropper.DropperArena;
import net.knarcraft.minigames.arena.dropper.DropperArenaEditableProperty;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* A helper-class for common tab-completions
*/
public final class TabCompleteHelper {
private TabCompleteHelper() {
}
/**
* Gets the names of all current arenas
*
* @return <p>All arena names</p>
*/
public static @NotNull List<String> getArenas() {
List<String> arenaNames = new ArrayList<>();
for (DropperArena dropperArena : MiniGames.getInstance().getDropperArenaHandler().getArenas().values()) {
arenaNames.add(dropperArena.getArenaName());
}
return arenaNames;
}
/**
* Gets the argument strings of all arena properties
*
* @return <p>All arena properties</p>
*/
public static @NotNull List<String> getArenaProperties() {
List<String> arenaProperties = new ArrayList<>();
for (DropperArenaEditableProperty property : DropperArenaEditableProperty.values()) {
arenaProperties.add(property.getArgumentString());
}
return arenaProperties;
}
}