32 Commits

Author SHA1 Message Date
b95cc294ab Adjusts the README a bit 2023-04-07 15:54:32 +02:00
458dbc2beb Fixes displaying player names in placeholders 2023-04-07 15:37:50 +02:00
efaca03434 Revert "Removes group record for now"
This reverts commit fe016fd6
2023-04-07 15:23:47 +02:00
58b5b422f0 Merge branch 'master' of https://github.com/SunNetservers/Dropper
 Conflicts:
	src/main/java/net/knarcraft/dropper/placeholder/DropperRecordExpansion.java
2023-04-07 15:22:15 +02:00
483a0a16dc Merge branch 'group-placeholders' 2023-04-07 15:21:00 +02:00
e1c4a6a97c Fixes various issues
Makes ArenaRecord abstract to make serialization possible
Makes IntegerRecord and LongRecord serializable
Adds a null check when summing records
Fixes records not being saved, as a copy was edited
2023-04-07 15:15:41 +02:00
5be6f0d00e Fixes equals check for arena ids 2023-04-07 14:16:47 +02:00
f6a272b0c0 Implements group record placeholders 2023-04-07 13:42:45 +02:00
46e52812af Merge pull request #18 from SunNetservers/dev
Placeholders, and adjustments for multiple players
2023-04-06 22:23:39 +00:00
fe016fd620 Removes group record for now 2023-04-07 00:19:54 +02:00
8e9b274fc0 Adds unfinished code for group record placeholders 2023-04-07 00:17:57 +02:00
2b9cfeebb1 Implements placeholders for arenas 2023-04-06 17:07:36 +02:00
096f23d468 Generifies arena records, and prepares for PlaceholderAPI 2023-04-06 00:43:33 +02:00
3ebf5fa924 Disables player collisions, and makes players invisible in the arena 2023-04-05 23:15:19 +02:00
579c1ea0f9 Merge branch 'dev' 2023-04-01 01:20:43 +02:00
d41154281b Allows players to pass through wall signs 2023-03-31 23:46:17 +02:00
f852de7309 Fixes a bug where newly created dropper arenas had a null reference to the handler 2023-03-31 23:20:11 +02:00
fb6550afe7 Merge pull request #17 from SunNetservers/dev
Many changes and implementations
2023-03-31 16:48:39 +00:00
e5a3f9206d Implements the group swap command 2023-03-31 18:43:01 +02:00
dfdf04a0ee Implements the group list command 2023-03-31 17:35:25 +02:00
888b20bb93 Implements the group set command 2023-03-30 21:07:46 +02:00
c29fcdc166 Adds skeleton classes for the three group commands 2023-03-30 20:24:47 +02:00
f9008ca050 Implements #13 and #14
Removes deaths and time as separate game-modes, and instead always tracks records.
Adds two proper game-modes. One inverts the player's controls, and the other randomly inverts the player's controls every 7 seconds.
Saves cleared status and records for each game-mode separately
Only allows 0-1 for the horizontal velocity
Requires arenas in arena groups to be cleared in sequence on all game-modes, not just the default one.
2023-03-30 14:59:44 +02:00
572bb980c1 Changes the default vertical velocity to 3.92 2023-03-30 12:57:29 +02:00
c01e1c3509 Finishes the edit command #9 2023-03-29 18:15:31 +02:00
ca5b0fcbca Makes it possible to get the current value from /dedit
Finally gets rid of the redundancy when checking if a record has been beaten
Adds tab-completions for arenas and properties to the edit command
Adds an ArenaEditableProperty enum to keep track of editable arena properties
2023-03-29 11:37:26 +02:00
34989e6673 Removes some TODOs 2023-03-29 02:19:40 +02:00
45bdb3f2a6 Removes usage of SerializableUUID outside of serialization 2023-03-29 02:07:00 +02:00
21425f73a1 Performs a lot of changes. Mostly implements #3
Properly and persistently stores which players have beaten which arenas
Adds a hopefully fully functional DropperArenaGroup class
Stores arena groups in the arena handler
Adds a lookup map to improve performance of getting arena by name
Adds saving and loading of groups
Adds all necessary checks for whether players have beaten the required dropper arenas before joining
Removes stage from arenas
Adds a test to make sure changing the order of arenas in a group works as intended
2023-03-29 00:55:19 +02:00
51237cb11a Fixes #6 by re-applying the arena state
Additionally, as a feature of fixing #6, the hit-detection has been made more accurate.
Also, all non-solid blocks are now triggered when they are passed through, rather than when hit.
2023-03-28 16:59:50 +02:00
3626d997b8 Implements #2 and #4
Adds a unique identifier to arenas
Stores frequently updated arena data separately from arena settings
Stores arenas keyed by their id
Prepares for storage of which players have beaten an arena
2023-03-27 21:43:16 +02:00
9a7d3ca360 Implements #5 and stuff
Uses a player entry state to store and restore a player's state such as game-mode and fly-mode.
Removes the restriction of fly mode and game-mode, and sets all arena players to adventure mode.
Force-quits all sessions when exiting the plugin.
Prevents usage of any commands other than the leave command when playing in an arena.
2023-03-27 14:23:55 +02:00
42 changed files with 2875 additions and 338 deletions

123
README.md
View File

@ -8,46 +8,111 @@ To modify
## Permissions
| Node | Description |
|----------------|---------------------------------------------------|
| dropper.admin | Gives all permissions |
| dropper.join | Allows a player to participate in dropper arenas |
| dropper.create | Allows a player to create a new dropper arena |
| dropper.edit | Allows a player to edit an existing dropper arena |
| dropper.remove | Allows a player to remove a dropper arena |
| Node | Description |
|----------------|----------------------------------------------------|
| dropper.admin | Gives all permissions. |
| dropper.join | Allows a player to participate in dropper arenas. |
| dropper.create | Allows a player to create a new dropper arena. |
| dropper.edit | Allows a player to edit an existing dropper arena. |
| dropper.remove | Allows a player to remove a dropper arena. |
## Commands
| Command | Arguments | Description |
|------------------------------|-----------------------------|-------------------------------------------------|
| /dropperlist | | Lists available dropper arenas |
| [/dropperjoin](#dropperjoin) | \<arena> \[mode] | Joins the selected arena |
| /dropperleave | | Leaves the current dropper arena |
| /droppercreate | \<name> | Creates a new dropper arena with the given name |
| /dropperremove | \<arena> | Removes the specified dropper arena |
| [/dropperedit](#dropperedit) | \<arena> \<option> \[value] | Gets or sets a dropper arena option |
| Command | Alias | Arguments | Description |
|----------------------------------------|----------|-----------------------------|-------------------------------------------------------------------------------------|
| /dropperList | /dlist | | Lists available dropper arenas. |
| [/dropperJoin](#dropperJoin) | /djoin | \<arena> \[mode] | Joins the selected arena. |
| /dropperLeave | /dleave | | Leaves the current dropper arena. |
| /dropperCreate | /dcreate | \<name> | Creates a new dropper arena with the given name. The spawn is set to your location. |
| /dropperRemove | /dremove | \<arena> | Removes the specified dropper arena. |
| [/dropperEdit](#dropperEdit) | /dedit | \<arena> \<option> \[value] | Gets or sets a dropper arena option. |
| /dropperReload | /dreload | | Reloads all data from disk. |
| [/dropperGroupSet](#dropperGroupSet) | /dgset | \<arena> \<group> | Puts the given arena in the given group. Use "none" to remove an existing group. |
| /dropperGroupList | /dglist | \[group] | Lists groups, or the stages of a group if a group is specified. |
| [/dropperGroupSwap](#dropperGroupSwap) | /dgswap | \<arena1> \<arena2> | Swaps the two arenas in the group's ordered list. |
## Command explanation
### Command explanation
### /dropperjoin
#### /dropperJoin
This command is used for joining a dropper arena.
`/droppejoin <arena> [mode]`
`/dropperjoin <arena> [mode]`
| Argument | Usage |
|----------|------------------------------------------------------------------------------------------------------------------|
| arena | The name of the arena to join |
| mode | Additional challenge modes can be played after an arena has been cleared once. Available modes: deaths and time. |
| Argument | Usage |
|----------|----------------------------------------------------------------------------------------------------------------------|
| arena | The name of the arena to join. |
| mode | Additional challenge modes can be played after an arena has been cleared once. Available modes: inverted and random. |
### /dropperedit
#### /dropperEdit
This command allows editing the specified property for the specified dropper arena
This command allows editing the specified property for the specified dropper arena.
`/dropperedit <arena> <option> [value]`
| Argument | Usage |
|----------|--------------------------------------|
| arena | The name of the arena to edit |
| option | The option to display or change |
| value | The new value of the selected option |
| Argument | Usage |
|----------|---------------------------------------|
| arena | The name of the arena to edit. |
| option | The option to display or change. |
| value | The new value of the selected option. |
These are all the options that can be changed for an arena.
| Option | Details |
|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | The name of the arena. Used mainly to select the arena in commands. |
| spawnLocation | The spawn location of any player joining the arena. Use `56.546,64.0,44.45` to specify coordinates, or `here`, `this` or any other string to select your current location. |
| exitLocation | The location players will be sent to when exiting the arena. If not set, the player will be sent to where they joined from. Valid values are the same as for spawnLocation. |
| verticalVelocity | The vertical velocity set for players in the arena (basically their falling speed). It must be greater than 0, but max 75. `12.565` and other decimals are allowed. |
| horizontalVelocity | The horizontal velocity (technically fly speed) set for players in the arena. It must be between 0 and 1, and cannot be 0. Decimals are allowed. |
| winBlockType | The type of block players must hit to win the arena. It can be any material as long as it's a block, and not a type of air. |
#### /dropperGroupSet
This command is used to set the group of an arena
`/droppergroupset <arena> <group>`
Dropper groups are created and removed as necessary. If you specify a group named "potato", that group is created, and
will be used again if you specify the "potato" group for another arena. You use "none" or "null" to remove an arena from
its group. If the group has no arenas, it will be automatically removed. If the arena already is in a group, it will be
moved to the new group.
#### /dropperGroupSwap
This command is used for changing the order of arenas within a group.
`/droppergroupswap <arena1> <arena2>`
Groups define an order the arenas within that group has to be completed in. Use `/droppergrouplist group` to see the
actual order of the group. So, assuming your arenas in the group looked something like:
1. Forest
2. Sea
3. Nether
4. Savanna
You could use `/droppergroupswap Sea Savanna` to change the order to:
1. Forest
2. Savanna
3. Nether
4. Sea
## Record placeholders
Player records can be displayed on a leaderboard by using PlaceholderAPI. If you want to display a sign-based
leaderboard, you can use the [Placeholder Signs](https://git.knarcraft.net/EpicKnarvik97/PlaceholderSigns) plugin. The
format for the built-in placeholders is as follows:
`%dropper_record_recordType_gameModeType_identifierType_identifier_recordPlacing_infoType%`
| Variable | Values | Description |
|----------------|-----------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| dropper_record | | Denotes that it's a placeholder for a dropper record. Must be present as-is. |
| recordType | deaths / time | Selects the type of record to get (deaths or time). |
| gameModeType | default / inverted / random | Selects the game-mode to get the record for. |
| identifierType | arena / group | The type of thing the following identifier points to (an arena or an arena group). |
| identifier | ? | An identifier (the name or UUID) for an arena or a group (whichever was chosen as identifierType). |
| recordPlacing | 1 / 2 / 3 / ... | The position of the record to get (1 = first place, 2 = second place, etc.). |
| infoType | player / value / combined | The type of info to get. Player gets the player name, Value gets the value of the achieved record. Combined gets "Player: Record". |

16
pom.xml
View File

@ -58,6 +58,10 @@
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
<repository>
<id>placeholderapi</id>
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
</repository>
</repositories>
<dependencies>
@ -73,5 +77,17 @@
<version>24.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.10.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,11 +1,19 @@
package net.knarcraft.dropper;
import net.knarcraft.dropper.arena.DropperArenaData;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaHandler;
import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import net.knarcraft.dropper.arena.DropperArenaRecordsRegistry;
import net.knarcraft.dropper.arena.DropperArenaSession;
import net.knarcraft.dropper.arena.record.IntegerRecord;
import net.knarcraft.dropper.arena.record.LongRecord;
import net.knarcraft.dropper.command.CreateArenaCommand;
import net.knarcraft.dropper.command.EditArenaCommand;
import net.knarcraft.dropper.command.EditArenaTabCompleter;
import net.knarcraft.dropper.command.GroupListCommand;
import net.knarcraft.dropper.command.GroupSetCommand;
import net.knarcraft.dropper.command.GroupSwapCommand;
import net.knarcraft.dropper.command.JoinArenaCommand;
import net.knarcraft.dropper.command.JoinArenaTabCompleter;
import net.knarcraft.dropper.command.LeaveArenaCommand;
@ -15,13 +23,18 @@ import net.knarcraft.dropper.command.RemoveArenaCommand;
import net.knarcraft.dropper.command.RemoveArenaTabCompleter;
import net.knarcraft.dropper.container.SerializableMaterial;
import net.knarcraft.dropper.container.SerializableUUID;
import net.knarcraft.dropper.listener.CommandListener;
import net.knarcraft.dropper.listener.DamageListener;
import net.knarcraft.dropper.listener.MoveListener;
import net.knarcraft.dropper.listener.PlayerLeaveListener;
import net.knarcraft.dropper.placeholder.DropperRecordExpansion;
import net.knarcraft.dropper.property.ArenaGameMode;
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;
@ -72,6 +85,7 @@ public final class Dropper extends JavaPlugin {
public void reload() {
// Load all arenas again
this.arenaHandler.loadArenas();
this.arenaHandler.loadGroups();
}
@Override
@ -81,6 +95,11 @@ public final class Dropper extends JavaPlugin {
ConfigurationSerialization.registerClass(SerializableMaterial.class);
ConfigurationSerialization.registerClass(DropperArenaRecordsRegistry.class);
ConfigurationSerialization.registerClass(SerializableUUID.class);
ConfigurationSerialization.registerClass(DropperArenaData.class);
ConfigurationSerialization.registerClass(DropperArenaGroup.class);
ConfigurationSerialization.registerClass(ArenaGameMode.class);
ConfigurationSerialization.registerClass(LongRecord.class);
ConfigurationSerialization.registerClass(IntegerRecord.class);
}
@Override
@ -90,34 +109,41 @@ public final class Dropper extends JavaPlugin {
this.playerRegistry = new DropperArenaPlayerRegistry();
this.arenaHandler = new DropperArenaHandler();
this.arenaHandler.loadArenas();
//TODO: Add a command for joining a specific arena. Only teleport if the stage check succeeds (The server can
// use something like https://www.spigotmc.org/resources/commandblocks.62720/ for immersion)
//TODO: Store various information about players' performance, and hook into PlaceholderAPI
//TODO: Possibly implement an optional queue mode, which only allows one player inside one dropper arena at any
// time (to prevent players from pushing each-other)?
//TODO: Store which players have cleared which arenas to keep track of whether the trial game-modes should be
// available
this.arenaHandler.loadGroups();
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new DamageListener(), this);
pluginManager.registerEvents(new MoveListener(), 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(), new EditArenaTabCompleter());
registerCommand("dropperremove", new RemoveArenaCommand(), new RemoveArenaTabCompleter());
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(), 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) {
if (!new DropperRecordExpansion(this).register()) {
getLogger().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 = playerRegistry.getArenaSession(player.getUniqueId());
if (session != null) {
session.triggerQuit(true);
}
}
}
/**

View File

@ -1,84 +1,93 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.util.StringSanitizer;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* A representation of one dropper arena
*/
public class DropperArena {
/**
* An unique and persistent identifier for this arena
*/
private final UUID arenaId;
/**
* A name used when listing and storing this arena.
*/
private final @NotNull String arenaName;
private @NotNull String arenaName;
/**
* The location players are teleported to when joining this arena.
*/
private final @NotNull Location spawnLocation;
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 final @Nullable Location exitLocation;
private @Nullable Location exitLocation;
/**
* The velocity in the y-direction to apply to all players in this arena.
*/
private final double playerVerticalVelocity;
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 final double playerHorizontalVelocity;
/**
* The stage number of this arena. If not null, the previous stage number must be cleared before access.
*/
private final @Nullable Integer stage;
/**
* The registry used to save this arena's records
*/
private final @NotNull DropperArenaRecordsRegistry recordsRegistry;
private float playerHorizontalVelocity;
/**
* The material of the block players have to hit to win this dropper arena
*/
private final @NotNull Material winBlockType;
private @NotNull Material winBlockType;
//TODO: Store records for this arena (maps with player->deaths/time). It should be possible to get those in sorted
// order (smallest to largest)
/**
* 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</p>
* @param stage <p>The stage number of this stage, or null if not limited to stages</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 recordsRegistry <p>The registry keeping track of all of this arena's records</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 String arenaName, @NotNull Location spawnLocation, @Nullable Location exitLocation,
double playerVerticalVelocity, double playerHorizontalVelocity, @Nullable Integer stage, @NotNull Material winBlockType,
@NotNull DropperArenaRecordsRegistry recordsRegistry) {
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.stage = stage;
this.winBlockType = winBlockType;
this.recordsRegistry = recordsRegistry;
this.dropperArenaData = dropperArenaData;
this.dropperArenaHandler = arenaHandler;
}
/**
@ -89,31 +98,49 @@ public class DropperArena {
*
* @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) {
public DropperArena(@NotNull String arenaName, @NotNull Location spawnLocation,
@NotNull DropperArenaHandler arenaHandler) {
this.arenaId = UUID.randomUUID();
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
this.exitLocation = null;
this.playerVerticalVelocity = 1;
this.playerVerticalVelocity = 3.92;
this.playerHorizontalVelocity = 1;
this.stage = null;
this.recordsRegistry = new DropperArenaRecordsRegistry();
Map<ArenaGameMode, DropperArenaRecordsRegistry> recordRegistries = new HashMap<>();
for (ArenaGameMode arenaGameMode : ArenaGameMode.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 the registry keeping track of this arena's records
* Gets this arena's data
*
* @return <p>This arena's record registry</p>
* @return <p>This arena's data</p>
*/
public @NotNull DropperArenaRecordsRegistry getRecordsRegistry() {
return this.recordsRegistry;
public @NotNull DropperArenaData getData() {
return this.dropperArenaData;
}
/**
* 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>
* @return <p>The name of this arena</p>
*/
public @NotNull String getArenaName() {
return this.arenaName;
@ -158,22 +185,10 @@ public class DropperArena {
*
* @return <p>Players' velocity in this arena</p>
*/
public double getPlayerHorizontalVelocity() {
public float getPlayerHorizontalVelocity() {
return this.playerHorizontalVelocity;
}
/**
* Gets the stage this arena belongs to
*
* <p>It's assumed that arena stages go from 1,2,3,4,... and upwards. If the stage number is set, this arena can
* only be played if all previous stages have been beaten. If not set, however, this arena can be used freely.</p>
*
* @return <p>This arena's stage number</p>
*/
public @Nullable Integer getStage() {
return this.stage;
}
/**
* Gets the type of block a player has to hit to win this arena
*
@ -183,4 +198,135 @@ public class DropperArena {
return this.winBlockType;
}
/**
* 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;
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());
}
/**
* Checks whether the given location is valid
*
* @param location <p>The location to validate</p>
* @return <p>False if the location is valid</p>
*/
private boolean isInvalid(Location location) {
World world = location.getWorld();
return world == null || !world.getWorldBorder().isInside(location);
}
}

View File

@ -0,0 +1,128 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.container.SerializableUUID;
import net.knarcraft.dropper.property.ArenaGameMode;
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;
/**
* Data stored for an arena
*
* @param arenaId <p>The id of the arena this data belongs to</p>
* @param recordRegistries <p>The records belonging to the arena</p>
* @param playersCompleted <p>A list of all player that have completed this arena</p>
*/
public record DropperArenaData(@NotNull UUID arenaId,
@NotNull Map<ArenaGameMode, DropperArenaRecordsRegistry> recordRegistries,
@NotNull Map<ArenaGameMode, Set<UUID>> playersCompleted) implements ConfigurationSerializable {
/**
* 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, DropperArenaRecordsRegistry> recordRegistries,
@NotNull Map<ArenaGameMode, Set<UUID>> playersCompleted) {
this.arenaId = arenaId;
this.recordRegistries = recordRegistries;
this.playersCompleted = new HashMap<>(playersCompleted);
}
/**
* 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 player <p>The player that completed this data's arena</p>
*/
public boolean addCompleted(@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) {
Dropper.getInstance().getArenaHandler().saveData(this.arenaId);
}
return added;
}
@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<SerializableUUID>> serializablePlayersCompleted = new HashMap<>();
for (ArenaGameMode arenaGameMode : this.playersCompleted.keySet()) {
Set<SerializableUUID> playersCompleted = new HashSet<>();
for (UUID playerCompleted : this.playersCompleted.get(arenaGameMode)) {
playersCompleted.add(new SerializableUUID(playerCompleted));
}
serializablePlayersCompleted.put(arenaGameMode, playersCompleted);
}
data.put("playersCompleted", serializablePlayersCompleted);
return data;
}
/**
* 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, DropperArenaRecordsRegistry> recordsRegistry =
(Map<ArenaGameMode, DropperArenaRecordsRegistry>) data.get("recordsRegistry");
Map<ArenaGameMode, Set<SerializableUUID>> playersCompletedData =
(Map<ArenaGameMode, Set<SerializableUUID>>) 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<>();
for (ArenaGameMode arenaGameMode : playersCompletedData.keySet()) {
Set<UUID> playersCompleted = new HashSet<>();
for (SerializableUUID completedId : playersCompletedData.get(arenaGameMode)) {
playersCompleted.add(completedId.uuid());
}
allPlayersCompleted.put(arenaGameMode, playersCompleted);
if (!recordsRegistry.containsKey(arenaGameMode) || recordsRegistry.get(arenaGameMode) == null) {
recordsRegistry.put(arenaGameMode, new DropperArenaRecordsRegistry(serializableUUID.uuid()));
}
}
return new DropperArenaData(serializableUUID.uuid(), recordsRegistry, allPlayersCompleted);
}
}

View File

@ -0,0 +1,253 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.container.SerializableUUID;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.util.StringSanitizer;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.entity.Player;
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;
import java.util.logging.Level;
/**
* A sorted group of arenas that must be completed in sequence
*/
public class DropperArenaGroup 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
*/
private final List<UUID> arenas;
/**
* Instantiates a new dropper arena group
*
* @param groupName <p>The name of this group</p>
*/
public DropperArenaGroup(@NotNull String groupName) {
this.groupId = UUID.randomUUID();
this.groupName = groupName;
this.arenas = new ArrayList<>();
}
/**
* 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) {
this.groupId = groupId;
this.groupName = groupName;
this.arenas = new ArrayList<>(arenas);
}
/**
* Gets the id of this dropper arena group
*
* @return <p>The id of this group</p>
*/
public @NotNull UUID getGroupId() {
return this.groupId;
}
/**
* Gets the name of this dropper 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 dropper arena from this group
*
* @param arenaId <p>The id of the dropper 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);
}
}
/**
* 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(ArenaGameMode gameMode, Player player) {
DropperArenaHandler arenaHandler = Dropper.getInstance().getArenaHandler();
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
Dropper.getInstance().getLogger().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(ArenaGameMode 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 = Dropper.getInstance().getArenaHandler();
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
Dropper.getInstance().getLogger().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;
}
/**
* 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;
}
/**
* 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")).uuid();
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.uuid());
}
return new DropperArenaGroup(id, name, arenas);
}
@Override
public boolean equals(Object other) {
if (!(other instanceof DropperArenaGroup otherGroup)) {
return false;
}
return this.getGroupNameSanitized().equals(otherGroup.getGroupNameSanitized());
}
}

View File

@ -2,16 +2,16 @@ package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.util.ArenaStorageHelper;
import org.bukkit.entity.Player;
import net.knarcraft.dropper.util.StringSanitizer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
/**
@ -19,25 +19,96 @@ import java.util.logging.Level;
*/
public class DropperArenaHandler {
private static final File arenaFile = new File(Dropper.getInstance().getDataFolder(), "arenas.yml");
private List<DropperArena> arenas = new ArrayList<>();
private final Map<Player, Integer> stagesCleared = new HashMap<>();
private Map<UUID, DropperArena> arenas = new HashMap<>();
private Map<UUID, DropperArenaGroup> arenaGroups = new HashMap<>();
private Map<String, UUID> arenaNameLookup = new HashMap<>();
/**
* Tries to register the given stage as cleared
* Gets all arenas that are within a group
*
* @param player <p>The player that cleared a stage</p>
* @param stage <p>The stage the player cleared</p>
* @return <p>True if the player cleared a new stage</p>
* @return <p>All arenas in a group</p>
*/
public boolean registerStageCleared(@NotNull Player player, int stage) {
if ((!stagesCleared.containsKey(player) && stage == 1) || (stagesCleared.containsKey(player) &&
stage == stagesCleared.get(player) + 1)) {
stagesCleared.put(player, stage);
return true;
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 {
return false;
// 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);
}
}
@ -47,10 +118,21 @@ public class DropperArenaHandler {
* @param arena <p>The arena to add</p>
*/
public void addArena(@NotNull DropperArena arena) {
this.arenas.add(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
*
@ -58,13 +140,7 @@ public class DropperArenaHandler {
* @return <p>The arena with the given name, or null if not found</p>
*/
public @Nullable DropperArena getArena(@NotNull String arenaName) {
arenaName = ArenaStorageHelper.sanitizeArenaName(arenaName);
for (DropperArena arena : arenas) {
if (ArenaStorageHelper.sanitizeArenaName(arena.getArenaName()).equals(arenaName)) {
return arena;
}
}
return null;
return this.arenas.get(this.arenaNameLookup.get(StringSanitizer.sanitizeArenaName(arenaName)));
}
/**
@ -72,8 +148,8 @@ public class DropperArenaHandler {
*
* @return <p>All known arenas</p>
*/
public @NotNull List<DropperArena> getArenas() {
return new ArrayList<>(this.arenas);
public @NotNull Map<UUID, DropperArena> getArenas() {
return new HashMap<>(this.arenas);
}
/**
@ -83,19 +159,63 @@ public class DropperArenaHandler {
*/
public void removeArena(@NotNull DropperArena arena) {
Dropper.getInstance().getPlayerRegistry().removeForArena(arena);
this.arenas.remove(arena);
this.arenas.remove(arena.getArenaId());
this.arenaNameLookup.remove(arena.getArenaNameSanitized());
this.arenaGroups.remove(arena.getArenaId());
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.saveArenaData(this.arenas.get(arenaId).getData());
} catch (IOException e) {
Dropper.getInstance().getLogger().log(Level.SEVERE, "Unable to save arena data! Data loss can occur!");
Dropper.getInstance().getLogger().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) {
Dropper.getInstance().getLogger().log(Level.SEVERE, "Unable to save current arena groups! " +
"Data loss can occur!");
Dropper.getInstance().getLogger().log(Level.SEVERE, e.getMessage());
}
}
/**
* Loads all dropper groups from disk
*/
public 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.saveArenas(this.arenas, arenaFile);
ArenaStorageHelper.saveArenas(this.arenas);
} catch (IOException e) {
Dropper.getInstance().getLogger().log(Level.SEVERE, "Unable to save current arenas! " +
"Data loss can occur!");
Dropper.getInstance().getLogger().log(Level.SEVERE, e.getMessage());
}
}
@ -103,7 +223,14 @@ public class DropperArenaHandler {
* Loads all arenas from disk
*/
public void loadArenas() {
this.arenas = ArenaStorageHelper.loadArenas(arenaFile);
this.arenas = ArenaStorageHelper.loadArenas();
// 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

@ -52,7 +52,7 @@ public class DropperArenaPlayerRegistry {
for (Map.Entry<UUID, DropperArenaSession> entry : this.arenaPlayers.entrySet()) {
if (entry.getValue().getArena() == arena) {
// Kick the player gracefully
entry.getValue().triggerQuit();
entry.getValue().triggerQuit(false);
this.arenaPlayers.remove(entry.getKey());
}
}

View File

@ -1,30 +1,41 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.record.ArenaRecord;
import net.knarcraft.dropper.arena.record.IntegerRecord;
import net.knarcraft.dropper.arena.record.LongRecord;
import net.knarcraft.dropper.arena.record.SummableArenaRecord;
import net.knarcraft.dropper.container.SerializableUUID;
import net.knarcraft.dropper.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.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Stream;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
/**
* A registry keeping track of all records
*/
public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
private final Map<SerializableUUID, Integer> leastDeaths;
private final Map<SerializableUUID, Long> shortestTimeMilliSeconds;
private final UUID arenaId;
private final @NotNull Set<IntegerRecord> leastDeaths;
private final @NotNull Set<LongRecord> shortestTimeMilliSeconds;
/**
* Instantiates a new empty records registry
*/
public DropperArenaRecordsRegistry() {
this.leastDeaths = new HashMap<>();
this.shortestTimeMilliSeconds = new HashMap<>();
public DropperArenaRecordsRegistry(@NotNull UUID arenaId) {
this.arenaId = arenaId;
this.leastDeaths = new HashSet<>();
this.shortestTimeMilliSeconds = new HashSet<>();
}
/**
@ -33,10 +44,11 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
* @param leastDeaths <p>The existing least death records to use</p>
* @param shortestTimeMilliSeconds <p>The existing leash time records to use</p>
*/
public DropperArenaRecordsRegistry(@NotNull Map<SerializableUUID, Integer> leastDeaths,
@NotNull Map<SerializableUUID, Long> shortestTimeMilliSeconds) {
this.leastDeaths = new HashMap<>(leastDeaths);
this.shortestTimeMilliSeconds = new HashMap<>(shortestTimeMilliSeconds);
private DropperArenaRecordsRegistry(@NotNull UUID arenaId, @NotNull Set<IntegerRecord> leastDeaths,
@NotNull Set<LongRecord> shortestTimeMilliSeconds) {
this.arenaId = arenaId;
this.leastDeaths = new HashSet<>(leastDeaths);
this.shortestTimeMilliSeconds = new HashSet<>(shortestTimeMilliSeconds);
}
/**
@ -44,8 +56,8 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
*
* @return <p>Existing death records</p>
*/
public Map<SerializableUUID, Integer> getLeastDeathsRecords() {
return new HashMap<>(this.leastDeaths);
public Set<SummableArenaRecord<Integer>> getLeastDeathsRecords() {
return new HashSet<>(this.leastDeaths);
}
/**
@ -53,8 +65,8 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
*
* @return <p>Existing time records</p>
*/
public Map<SerializableUUID, Long> getShortestTimeMilliSecondsRecords() {
return new HashMap<>(this.shortestTimeMilliSeconds);
public Set<SummableArenaRecord<Long>> getShortestTimeMilliSecondsRecords() {
return new HashSet<>(this.shortestTimeMilliSeconds);
}
/**
@ -65,25 +77,9 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
* @return <p>The result explaining what type of record was achieved</p>
*/
public @NotNull RecordResult registerDeathRecord(@NotNull UUID playerId, int deaths) {
RecordResult result;
Stream<Map.Entry<SerializableUUID, Integer>> records = leastDeaths.entrySet().stream();
SerializableUUID serializableUUID = new SerializableUUID(playerId);
if (records.allMatch((entry) -> deaths < entry.getValue())) {
//If the given value is less than all other values, that's a world record!
result = RecordResult.WORLD_RECORD;
leastDeaths.put(serializableUUID, deaths);
save();
} else if (leastDeaths.containsKey(serializableUUID) && deaths < leastDeaths.get(serializableUUID)) {
//If the given value is less than the player's previous value, that's a personal best!
result = RecordResult.PERSONAL_BEST;
leastDeaths.put(serializableUUID, deaths);
save();
} else {
result = RecordResult.NONE;
}
return result;
Consumer<Integer> consumer = (value) -> leastDeaths.add(new IntegerRecord(playerId, value));
Set<ArenaRecord<Integer>> asInt = new HashSet<>(leastDeaths);
return registerRecord(asInt, consumer, playerId, deaths);
}
/**
@ -94,22 +90,51 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
* @return <p>The result explaining what type of record was achieved</p>
*/
public @NotNull RecordResult registerTimeRecord(@NotNull UUID playerId, long milliseconds) {
RecordResult result;
Stream<Map.Entry<SerializableUUID, Long>> records = shortestTimeMilliSeconds.entrySet().stream();
SerializableUUID serializableUUID = new SerializableUUID(playerId);
Consumer<Long> consumer = (value) -> shortestTimeMilliSeconds.add(new LongRecord(playerId, value));
Set<ArenaRecord<Long>> asLong = new HashSet<>(shortestTimeMilliSeconds);
return registerRecord(asLong, consumer, playerId, milliseconds);
}
if (records.allMatch((entry) -> milliseconds < entry.getValue())) {
//If the given value is less than all other values, that's a world record!
/**
* Saves changed records
*/
private void save() {
Dropper.getInstance().getArenaHandler().saveData(this.arenaId);
}
/**
* 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;
shortestTimeMilliSeconds.put(serializableUUID, milliseconds);
recordSetter.accept(amount);
save();
} else if (shortestTimeMilliSeconds.containsKey(serializableUUID) &&
milliseconds < shortestTimeMilliSeconds.get(serializableUUID)) {
//If the given value is less than the player's previous value, that's a personal best!
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;
shortestTimeMilliSeconds.put(serializableUUID, milliseconds);
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;
}
@ -117,16 +142,29 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
}
/**
* Saves changed records
* 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 void save() {
Dropper.getInstance().getArenaHandler().saveArenas();
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;
@ -140,12 +178,15 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
*/
@SuppressWarnings({"unused", "unchecked"})
public static DropperArenaRecordsRegistry deserialize(Map<String, Object> data) {
Map<SerializableUUID, Integer> leastDeathsData =
(Map<SerializableUUID, Integer>) data.getOrDefault("leastDeaths", new HashMap<>());
Map<SerializableUUID, Long> shortestTimeMillisecondsData =
(Map<SerializableUUID, Long>) data.getOrDefault("shortestTime", new HashMap<>());
UUID arenaId = ((SerializableUUID) data.get("arenaId")).uuid();
Set<IntegerRecord> leastDeaths =
(Set<IntegerRecord>) data.getOrDefault("leastDeaths", new HashMap<>());
Set<LongRecord> shortestTimeMilliseconds =
(Set<LongRecord>) data.getOrDefault("shortestTime", new HashMap<>());
return new DropperArenaRecordsRegistry(leastDeathsData, shortestTimeMillisecondsData);
leastDeaths.removeIf(Objects::isNull);
shortestTimeMilliseconds.removeIf(Objects::isNull);
return new DropperArenaRecordsRegistry(arenaId, leastDeaths, shortestTimeMilliseconds);
}
}

View File

@ -18,10 +18,9 @@ public class DropperArenaSession {
private final @NotNull DropperArena arena;
private final @NotNull Player player;
private final @NotNull ArenaGameMode gameMode;
private final @NotNull Location entryLocation;
private int deaths;
private final long startTime;
private final float playersOriginalFlySpeed;
private final PlayerEntryState entryState;
/**
* Instantiates a new dropper arena session
@ -37,13 +36,28 @@ public class DropperArenaSession {
this.gameMode = gameMode;
this.deaths = 0;
this.startTime = System.currentTimeMillis();
this.entryLocation = player.getLocation();
this.entryState = new PlayerEntryState(player, gameMode);
// Make the player fly to improve mobility in the air
player.setAllowFlight(true);
player.setFlying(true);
this.playersOriginalFlySpeed = player.getFlySpeed();
player.setFlySpeed((float) this.arena.getPlayerHorizontalVelocity());
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 ArenaGameMode 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 PlayerEntryState getEntryState() {
return this.entryState;
}
/**
@ -56,35 +70,28 @@ public class DropperArenaSession {
// Check for, and display, records
registerRecord();
//TODO: Give reward?
// Register and announce any cleared stages
Integer arenaStage = this.arena.getStage();
if (arenaStage != null) {
boolean clearedNewStage = Dropper.getInstance().getArenaHandler().registerStageCleared(this.player, arenaStage);
if (clearedNewStage) {
this.player.sendMessage("You cleared stage " + arenaStage + "!");
}
// Mark the arena as cleared
if (this.arena.getData().addCompleted(this.gameMode, this.player)) {
this.player.sendMessage("You cleared the arena!");
}
this.player.sendMessage("You won!");
// Teleport the player out of the arena
teleportToExit();
teleportToExit(false);
}
/**
* Teleports the playing player out of the arena
*/
private void teleportToExit() {
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.entryLocation;
exitLocation = this.entryState.getEntryLocation();
}
PlayerTeleporter.teleportPlayer(this.player, exitLocation, true);
PlayerTeleporter.teleportPlayer(this.player, exitLocation, true, immediately);
}
/**
@ -103,39 +110,57 @@ public class DropperArenaSession {
* Registers the player's record if necessary, and prints record information to the player
*/
private void registerRecord() {
DropperArenaRecordsRegistry recordsRegistry = this.arena.getRecordsRegistry();
RecordResult recordResult = switch (this.gameMode) {
case LEAST_TIME -> recordsRegistry.registerTimeRecord(this.player.getUniqueId(),
System.currentTimeMillis() - this.startTime);
case LEAST_DEATHS -> recordsRegistry.registerDeathRecord(this.player.getUniqueId(), this.deaths);
case DEFAULT -> RecordResult.NONE;
};
switch (recordResult) {
case WORLD_RECORD -> this.player.sendMessage("You just set a new record for this arena!");
case PERSONAL_BEST -> this.player.sendMessage("You just got a new personal record!");
DropperArenaRecordsRegistry recordsRegistry = this.arena.getData().recordRegistries().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() {
// Add to the death count if playing the least-deaths game-mode
if (this.gameMode == ArenaGameMode.LEAST_DEATHS) {
this.deaths++;
}
this.deaths++;
//Teleport the player back to the top
PlayerTeleporter.teleportPlayer(this.player, this.arena.getSpawnLocation(), true);
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() {
public void triggerQuit(boolean immediately) {
// Stop this session
stopSession();
// Teleport the player out of the arena
teleportToExit();
teleportToExit(immediately);
player.sendMessage("You quit the arena!");
}
@ -148,9 +173,7 @@ public class DropperArenaSession {
removeSession();
// Remove flight mode
this.player.setFlySpeed(this.playersOriginalFlySpeed);
this.player.setFlying(false);
this.player.setAllowFlight(false);
entryState.restore();
}
/**

View File

@ -0,0 +1,89 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.property.ArenaGameMode;
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 PlayerEntryState {
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 ArenaGameMode arenaGameMode;
/**
* Instantiates a new player state
*
* @param player <p>The player whose state should be stored</p>
*/
public PlayerEntryState(@NotNull Player player, @NotNull ArenaGameMode arenaGameMode) {
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();
}
/**
* 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);
this.player.setCollidable(false);
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 == ArenaGameMode.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);
this.player.setCollidable(this.originalCollideAble);
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.dropper.arena.record;
import net.knarcraft.dropper.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,38 @@
package net.knarcraft.dropper.arena.record;
import net.knarcraft.dropper.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);
}
/**
* 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")).uuid(), (Integer) data.get("record"));
}
}

View File

@ -0,0 +1,38 @@
package net.knarcraft.dropper.arena.record;
import net.knarcraft.dropper.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 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")).uuid(), ((Number) data.get("record")).longValue());
}
}

View File

@ -0,0 +1,28 @@
package net.knarcraft.dropper.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

@ -2,6 +2,8 @@ package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaHandler;
import net.knarcraft.dropper.util.StringSanitizer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -26,17 +28,24 @@ public class CreateArenaCommand implements CommandExecutor {
return false;
}
DropperArena existingArena = Dropper.getInstance().getArenaHandler().getArena(arguments[0]);
// 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 = Dropper.getInstance().getArenaHandler();
DropperArena existingArena = arenaHandler.getArena(arenaName);
if (existingArena != null) {
commandSender.sendMessage("There already exists a dropper arena with that name!");
return false;
}
// Remove known characters that are likely to cause trouble if used in an arena name
String arenaName = arguments[0].replaceAll("[§ :=&]", "");
DropperArena arena = new DropperArena(arenaName, player.getLocation());
Dropper.getInstance().getArenaHandler().addArena(arena);
DropperArena arena = new DropperArena(arenaName, player.getLocation(), arenaHandler);
arenaHandler.addArena(arena);
commandSender.sendMessage("The arena was successfully created!");
return true;
}

View File

@ -2,6 +2,9 @@ package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.property.ArenaEditableProperty;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -16,7 +19,7 @@ public class EditArenaCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
if (!(commandSender instanceof Player)) {
if (!(commandSender instanceof Player player)) {
commandSender.sendMessage("This command must be used by a player");
return false;
}
@ -31,9 +34,127 @@ public class EditArenaCommand implements CommandExecutor {
return false;
}
//TODO: If an arena name and a property is given, display the current value
//TODO: If an arena name, a property and a value is given, check if it's valid, and update the property
return false;
ArenaEditableProperty editableProperty = ArenaEditableProperty.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 ArenaEditableProperty 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 = 3.92;
}
// 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 = 1;
}
// Make sure the velocity isn't exactly 0
if (velocity == 0) {
velocity = 0.5f;
}
// 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

@ -1,11 +1,13 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.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;
/**
@ -16,9 +18,16 @@ public class EditArenaTabCompleter implements TabCompleter {
@Override
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String label, @NotNull String[] args) {
//TODO: Tab-complete existing arena names
//TODO: If an arena name is given, tab-complete change-able properties
return null;
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.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.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 = Dropper.getInstance().getArenaHandler();
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 : Dropper.getInstance().getArenaHandler().getAllGroups()) {
groupNames.add(group.getGroupName());
}
return groupNames;
} else {
return new ArrayList<>();
}
}
}

View File

@ -0,0 +1,79 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaHandler;
import net.knarcraft.dropper.util.StringSanitizer;
import net.knarcraft.dropper.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 = Dropper.getInstance().getArenaHandler();
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 : Dropper.getInstance().getArenaHandler().getAllGroups()) {
possibleValues.add(group.getGroupName());
}
return possibleValues;
} else {
return new ArrayList<>();
}
}
}

View File

@ -0,0 +1,102 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.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 = Dropper.getInstance().getArenaHandler();
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 = Dropper.getInstance().getArenaHandler();
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 = Dropper.getInstance().getArenaHandler();
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

@ -2,11 +2,11 @@ package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import net.knarcraft.dropper.arena.DropperArenaSession;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.util.PlayerTeleporter;
import org.bukkit.GameMode;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -30,12 +30,6 @@ public class JoinArenaCommand implements CommandExecutor {
return false;
}
if (player.isFlying() || player.getGameMode() == GameMode.CREATIVE ||
player.getGameMode() == GameMode.SPECTATOR) {
commandSender.sendMessage("You cannot join a dropper arena while able to fly!");
return false;
}
// Disallow joining if the player is already in a dropper arena
DropperArenaSession existingSession = Dropper.getInstance().getPlayerRegistry().getArenaSession(player.getUniqueId());
if (existingSession != null) {
@ -50,6 +44,24 @@ public class JoinArenaCommand implements CommandExecutor {
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
ArenaGameMode gameMode;
if (arguments.length > 1) {
@ -58,7 +70,18 @@ public class JoinArenaCommand implements CommandExecutor {
gameMode = ArenaGameMode.DEFAULT;
}
//TODO: Check if the arena has been beaten if the non-default game-mode has been chosen
// Make sure the player has beaten the necessary levels
DropperArenaGroup arenaGroup = Dropper.getInstance().getArenaHandler().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 (gameMode != ArenaGameMode.DEFAULT &&
specifiedArena.getData().hasNotCompleted(ArenaGameMode.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);
@ -66,15 +89,45 @@ public class JoinArenaCommand implements CommandExecutor {
playerRegistry.registerPlayer(player.getUniqueId(), newSession);
// Try to teleport the player to the arena
boolean teleported = PlayerTeleporter.teleportPlayer(player, specifiedArena.getSpawnLocation(), false);
boolean teleported = PlayerTeleporter.teleportPlayer(player, specifiedArena.getSpawnLocation(), false, false);
if (!teleported) {
commandSender.sendMessage("Unable to teleport you to the dropper arena. Make sure you're not in a vehicle," +
"and is not carrying a passenger!");
newSession.triggerQuit();
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 ArenaGameMode arenaGameMode, @NotNull Player player) {
// Require that players beat all arenas in the group in the normal game-mode before trying challenge modes
if (arenaGameMode != ArenaGameMode.DEFAULT) {
if (!arenaGroup.hasBeatenAll(ArenaGameMode.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 (!arenaGroup.canPlay(arenaGameMode, player, dropperArena.getArenaId())) {
player.sendMessage("You have not yet beaten the previous arena!");
return false;
}
return true;
}
}

View File

@ -23,8 +23,8 @@ public class JoinArenaTabCompleter implements TabCompleter {
} else if (arguments.length == 2) {
List<String> gameModes = new ArrayList<>();
gameModes.add("default");
gameModes.add("deaths");
gameModes.add("time");
gameModes.add("inverted");
gameModes.add("random");
return gameModes;
} else {
return new ArrayList<>();

View File

@ -32,7 +32,7 @@ public class LeaveArenaCommand implements TabExecutor {
return false;
}
existingSession.triggerQuit();
existingSession.triggerQuit(false);
return true;
}

View File

@ -22,8 +22,6 @@ public class ListArenaCommand implements TabExecutor {
for (String arenaName : TabCompleteHelper.getArenas()) {
sender.sendMessage(arenaName);
}
//TODO: Allow displaying information about each arena (possibly admin-only)
return true;
}

View File

@ -0,0 +1,46 @@
package net.knarcraft.dropper.listener;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.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 = Dropper.getInstance().getPlayerRegistry().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

@ -3,15 +3,19 @@ package net.knarcraft.dropper.listener;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import net.knarcraft.dropper.arena.DropperArenaSession;
import net.knarcraft.dropper.property.ArenaGameMode;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Tag;
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;
@ -40,33 +44,58 @@ public class MoveListener implements Listener {
return;
}
// Only do block type checking if the block beneath the player changes
if (event.getFrom().getBlock() != event.getTo().getBlock()) {
// 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
int depth = winBlockType == Material.WATER ? 0 : 1;
for (Block block : getBlocksBeneathLocation(event.getTo(), depth)) {
if (block.getType() == winBlockType) {
arenaSession.triggerWin();
return;
}
}
// Check if the player is about to hit a non-air and non-liquid block
for (Block block : getBlocksBeneathLocation(event.getTo(), 1)) {
if (!block.getType().isAir() && block.getType() != Material.STRUCTURE_VOID &&
block.getType() != Material.WATER && block.getType() != Material.LAVA) {
arenaSession.triggerLoss();
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) {
/* 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 decreases */
double liquidDepth = -0.8;
/* This decides the distance the player must be from the block below before a hit triggers. If too low, the
likelihood of detecting the hit decreases, but the immersion increases. */
double solidDepth = 0.2;
// 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
for (Block block : getBlocksBeneathLocation(toLocation, solidDepth)) {
Material blockType = block.getType();
if (!blockType.isAir() && blockType != Material.STRUCTURE_VOID && blockType != Material.WATER &&
blockType != Material.LAVA && !Tag.WALL_SIGNS.isTagged(blockType)) {
arenaSession.triggerLoss();
return true;
}
}
return false;
}
/**
* Gets the blocks at the given location that will be affected by the player's hit-box
*
@ -88,12 +117,40 @@ public class MoveListener implements Listener {
*
* @param session <p>The session to update the velocity for</p>
*/
private void updatePlayerVelocity(DropperArenaSession session) {
private void updatePlayerVelocity(@NotNull DropperArenaSession session) {
Player player = session.getPlayer();
Vector playerVelocity = player.getVelocity();
double arenaVelocity = session.getArena().getPlayerVerticalVelocity();
Vector newVelocity = new Vector(playerVelocity.getX(), -arenaVelocity, playerVelocity.getZ());
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() != ArenaGameMode.RANDOM_INVERTED) {
return;
}
Player player = session.getPlayer();
float horizontalVelocity = session.getArena().getPlayerHorizontalVelocity();
float secondsBetweenToggle = 7;
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

@ -44,7 +44,7 @@ public class PlayerLeaveListener implements Listener {
if (leftSessions.containsKey(playerId)) {
Dropper.getInstance().getLogger().log(Level.WARNING, "Found un-exited dropper session!");
Bukkit.getScheduler().runTaskLater(Dropper.getInstance(), () -> {
leftSessions.get(playerId).triggerQuit();
leftSessions.get(playerId).triggerQuit(false);
Dropper.getInstance().getLogger().log(Level.WARNING, "Triggered a quit!");
leftSessions.remove(playerId);
}, 80);
@ -66,7 +66,7 @@ public class PlayerLeaveListener implements Listener {
return;
}
arenaSession.triggerQuit();
arenaSession.triggerQuit(false);
}
/**

View File

@ -0,0 +1,238 @@
package net.knarcraft.dropper.placeholder;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaHandler;
import net.knarcraft.dropper.arena.DropperArenaRecordsRegistry;
import net.knarcraft.dropper.arena.record.ArenaRecord;
import net.knarcraft.dropper.placeholder.parsing.InfoType;
import net.knarcraft.dropper.placeholder.parsing.RecordType;
import net.knarcraft.dropper.placeholder.parsing.SelectionType;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
/**
* A placeholder expansion for dropper record placeholders
*/
public class DropperRecordExpansion extends PlaceholderExpansion {
private final Dropper plugin;
public DropperRecordExpansion(Dropper plugin) {
this.plugin = plugin;
}
@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]);
ArenaGameMode gameMode = ArenaGameMode.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.getArenaHandler();
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);
}
/**
* 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 ArenaGameMode 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 = getRecord(DropperGroupRecordHelper.getCombinedDeaths(group, gameMode), recordNumber);
} else {
record = getRecord(DropperGroupRecordHelper.getCombinedTime(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 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 ArenaGameMode 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, DropperArenaRecordsRegistry> registries = arena.getData().recordRegistries();
DropperArenaRecordsRegistry recordsRegistry = registries.get(gameMode);
ArenaRecord<?> record = getRecord(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,42 @@
package net.knarcraft.dropper.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.dropper.placeholder.parsing;
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,37 @@
package net.knarcraft.dropper.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,92 @@
package net.knarcraft.dropper.property;
import net.knarcraft.dropper.arena.DropperArena;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
/**
* All editable properties of a dropper arena
*/
public enum ArenaEditableProperty {
/**
* 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>
*/
ArenaEditableProperty(@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 ArenaEditableProperty getFromArgumentString(String argumentString) {
for (ArenaEditableProperty property : ArenaEditableProperty.values()) {
if (property.argumentString.equalsIgnoreCase(argumentString)) {
return property;
}
}
return null;
}
}

View File

@ -1,27 +1,30 @@
package net.knarcraft.dropper.property;
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 ArenaGameMode {
public enum ArenaGameMode implements ConfigurationSerializable {
/**
* The default game-mode. Failing once throws the player out.
* //TODO: Verify if we want the default game-mode to lock the player in the arena until they beat it
*/
DEFAULT,
/**
* The least-deaths game-mode. Player plays until they manage to win. The number of deaths is recorded.
* A game-mode where the player's directional buttons are inverted
*/
LEAST_DEATHS,
INVERTED,
/**
* The least-time game-mode. Player plays until they manage to win. The total time of the session is recorded.
* A game-mode which swaps between normal and inverted controls on a set schedule of a few seconds
*/
LEAST_TIME,
RANDOM_INVERTED,
;
/**
@ -32,13 +35,32 @@ public enum ArenaGameMode {
*/
public static @NotNull ArenaGameMode matchGamemode(@NotNull String gameMode) {
String sanitized = gameMode.trim().toLowerCase();
if (sanitized.matches("(least)?deaths?")) {
return ArenaGameMode.LEAST_DEATHS;
} else if (sanitized.matches("(least)?time")) {
return ArenaGameMode.LEAST_TIME;
if (sanitized.matches("(invert(ed)?|inverse)")) {
return ArenaGameMode.INVERTED;
} else if (sanitized.matches("rand(om)?")) {
return ArenaGameMode.RANDOM_INVERTED;
} else {
return ArenaGameMode.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 ArenaGameMode deserialize(Map<String, Object> data) {
return ArenaGameMode.valueOf((String) data.get("name"));
}
}

View File

@ -7,6 +7,11 @@ import org.jetbrains.annotations.NotNull;
*/
public enum ArenaStorageKey {
/**
* The key for an arena's id
*/
ID("arenaId"),
/**
* The key for an arena's name
*/
@ -32,20 +37,15 @@ public enum ArenaStorageKey {
*/
PLAYER_HORIZONTAL_VELOCITY("arenaPlayerHorizontalVelocity"),
/**
* The key for this arena's stage
*/
STAGE("arenaStage"),
/**
* The key for the type of this arena's win block
*/
WIN_BLOCK_TYPE("winBlockType"),
/**
* The hey for this arena's records
* The hey for this arena's data
*/
RECORDS("records"),
DATA("arenaData"),
;
private final @NotNull String key;

View File

@ -2,8 +2,12 @@ package net.knarcraft.dropper.util;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaData;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaRecordsRegistry;
import net.knarcraft.dropper.container.SerializableMaterial;
import net.knarcraft.dropper.container.SerializableUUID;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.property.ArenaStorageKey;
import org.bukkit.Location;
import org.bukkit.Material;
@ -14,9 +18,13 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
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;
import java.util.logging.Logger;
/**
* A helper class for saving and loading arenas
@ -24,54 +32,93 @@ import java.util.logging.Level;
public final class ArenaStorageHelper {
private final static String arenasConfigurationSection = "arenas";
private final static String groupsConfigurationSection = "groups";
private static final File arenaFile = new File(Dropper.getInstance().getDataFolder(), "arenas.yml");
private static final File groupFile = new File(Dropper.getInstance().getDataFolder(), "groups.yml");
private static final File arenaDataFolder = new File(Dropper.getInstance().getDataFolder(), "arena_data");
private ArenaStorageHelper() {
}
/**
* Saves the given arenas to the given file
* Saves the given dropper arena groups
*
* @param arenas <p>The arenas to save</p>
* @param arenaFile <p>The file to save the arenas to</p>
* @param arenaGroups <p>The arena groups to save</p>
* @throws IOException <p>If unable to write to the file</p>
*/
public static void saveArenas(@NotNull List<DropperArena> arenas, @NotNull File arenaFile) throws IOException {
public static void saveDropperArenaGroups(@NotNull Set<DropperArenaGroup> arenaGroups) throws IOException {
YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection groupSection = configuration.createSection(groupsConfigurationSection);
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(groupsConfigurationSection);
//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 saveArenas(@NotNull Map<UUID, DropperArena> arenas) throws IOException {
YamlConfiguration configuration = new YamlConfiguration();
ConfigurationSection arenaSection = configuration.createSection(arenasConfigurationSection);
for (DropperArena arena : arenas) {
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(sanitizeArenaName(
arena.getArenaName()));
@NotNull ConfigurationSection configSection = arenaSection.createSection(arena.getArenaId().toString());
configSection.set(ArenaStorageKey.ID.getKey(), new SerializableUUID(arena.getArenaId()));
configSection.set(ArenaStorageKey.NAME.getKey(), arena.getArenaName());
configSection.set(ArenaStorageKey.SPAWN_LOCATION.getKey(), arena.getSpawnLocation());
configSection.set(ArenaStorageKey.EXIT_LOCATION.getKey(), arena.getExitLocation());
configSection.set(ArenaStorageKey.PLAYER_VERTICAL_VELOCITY.getKey(), arena.getPlayerVerticalVelocity());
configSection.set(ArenaStorageKey.PLAYER_HORIZONTAL_VELOCITY.getKey(), arena.getPlayerHorizontalVelocity());
configSection.set(ArenaStorageKey.STAGE.getKey(), arena.getStage());
configSection.set(ArenaStorageKey.WIN_BLOCK_TYPE.getKey(), new SerializableMaterial(arena.getWinBlockType()));
configSection.set(ArenaStorageKey.RECORDS.getKey(), arena.getRecordsRegistry());
saveArenaData(arena.getData());
}
//TODO: Save records belonging to the arena
configuration.save(arenaFile);
}
/**
* Loads all arenas from the given file
* Loads all arenas
*
* @param arenaFile <p>The file used to store the arenas</p>
* @return <p>The loaded arenas, or null if the arenas configuration section is missing.</p>
*/
public static @NotNull List<DropperArena> loadArenas(@NotNull File arenaFile) {
public static @NotNull Map<UUID, DropperArena> loadArenas() {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(arenaFile);
ConfigurationSection arenaSection = configuration.getConfigurationSection(arenasConfigurationSection);
//If no such section exists, it must be the case that there is no data to load
if (arenaSection == null) {
return new ArrayList<>();
return new HashMap<>();
}
List<DropperArena> loadedArenas = new ArrayList<>();
Map<UUID, DropperArena> loadedArenas = new HashMap<>();
for (String sectionName : arenaSection.getKeys(false)) {
ConfigurationSection configurationSection = arenaSection.getConfigurationSection(sectionName);
@ -82,7 +129,7 @@ public final class ArenaStorageHelper {
DropperArena arena = loadArena(configurationSection);
if (arena != null) {
loadedArenas.add(arena);
loadedArenas.put(arena.getArenaId(), arena);
}
}
@ -96,41 +143,94 @@ public final class ArenaStorageHelper {
* @return <p>The loaded arena, or null if invalid</p>
*/
private static @Nullable DropperArena loadArena(@NotNull ConfigurationSection configurationSection) {
UUID arenaId = ((SerializableUUID) configurationSection.get(ArenaStorageKey.ID.getKey(),
new SerializableUUID(UUID.randomUUID()))).uuid();
String arenaName = configurationSection.getString(ArenaStorageKey.NAME.getKey());
Location spawnLocation = (Location) configurationSection.get(ArenaStorageKey.SPAWN_LOCATION.getKey());
Location exitLocation = (Location) configurationSection.get(ArenaStorageKey.EXIT_LOCATION.getKey());
double verticalVelocity = configurationSection.getDouble(ArenaStorageKey.PLAYER_VERTICAL_VELOCITY.getKey());
double horizontalVelocity = configurationSection.getDouble(ArenaStorageKey.PLAYER_HORIZONTAL_VELOCITY.getKey());
Integer stage = (Integer) configurationSection.get(ArenaStorageKey.STAGE.getKey());
float horizontalVelocity = sanitizeHorizontalVelocity((float) configurationSection.getDouble(
ArenaStorageKey.PLAYER_HORIZONTAL_VELOCITY.getKey()));
SerializableMaterial winBlockType = (SerializableMaterial) configurationSection.get(
ArenaStorageKey.WIN_BLOCK_TYPE.getKey());
DropperArenaRecordsRegistry recordsRegistry = (DropperArenaRecordsRegistry) configurationSection.get(
ArenaStorageKey.RECORDS.getKey());
Logger logger = Dropper.getInstance().getLogger();
if (arenaName == null || spawnLocation == null) {
Dropper.getInstance().getLogger().log(Level.SEVERE, "Could not load the arena at configuration " +
logger.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);
}
if (recordsRegistry == null) {
recordsRegistry = new DropperArenaRecordsRegistry();
DropperArenaData arenaData = loadArenaData(arenaId);
if (arenaData == null) {
logger.log(Level.SEVERE, "Unable to load arena data for " + arenaId);
Map<ArenaGameMode, DropperArenaRecordsRegistry> recordRegistries = new HashMap<>();
for (ArenaGameMode arenaGameMode : ArenaGameMode.values()) {
recordRegistries.put(arenaGameMode, new DropperArenaRecordsRegistry(arenaId));
}
arenaData = new DropperArenaData(arenaId, recordRegistries, new HashMap<>());
}
return new DropperArena(arenaName, spawnLocation, exitLocation, verticalVelocity, horizontalVelocity, stage,
winBlockType.material(), recordsRegistry);
return new DropperArena(arenaId, arenaName, spawnLocation, exitLocation, verticalVelocity, horizontalVelocity,
winBlockType.material(), arenaData, Dropper.getInstance().getArenaHandler());
}
/**
* Sanitizes an arena name for usage as a YAML key
* Stores the given arena data to a file
*
* @param arenaName <p>The arena name to sanitize</p>
* @return <p>The sanitized arena name</p>
* @param arenaData <p>The arena data to store</p>
*/
public static @NotNull String sanitizeArenaName(@NotNull String arenaName) {
return arenaName.toLowerCase().trim().replaceAll(" ", "_");
public static void saveArenaData(@NotNull DropperArenaData arenaData) throws IOException {
YamlConfiguration configuration = new YamlConfiguration();
configuration.set(ArenaStorageKey.DATA.getKey(), arenaData);
configuration.save(getArenaDataFile(arenaData.arenaId()));
}
/**
* 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 loadArenaData(@NotNull UUID arenaId) {
File arenaDataFile = getArenaDataFile(arenaId);
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(arenaDataFile);
return (DropperArenaData) configuration.get(ArenaStorageKey.DATA.getKey());
}
/**
* 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 getArenaDataFile(@NotNull UUID arenaId) {
File arenaDataFile = new File(arenaDataFolder, arenaId + ".yml");
if (!arenaDataFolder.exists() && !arenaDataFolder.mkdirs()) {
Dropper.getInstance().getLogger().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.dropper.util;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaHandler;
import net.knarcraft.dropper.arena.record.ArenaRecord;
import net.knarcraft.dropper.arena.record.SummableArenaRecord;
import net.knarcraft.dropper.property.ArenaGameMode;
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 ArenaGameMode gameMode) {
Map<UUID, SummableArenaRecord<Integer>> records = new HashMap<>();
@NotNull BiFunction<DropperArena, ArenaGameMode, Set<SummableArenaRecord<Integer>>> recordSupplier =
(arena, aGameMode) -> arena.getData().recordRegistries().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 ArenaGameMode gameMode) {
Map<UUID, SummableArenaRecord<Long>> records = new HashMap<>();
@NotNull BiFunction<DropperArena, ArenaGameMode, Set<SummableArenaRecord<Long>>> recordSupplier =
(arena, aGameMode) -> arena.getData().recordRegistries().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 ArenaGameMode gameMode,
@NotNull Map<UUID,
SummableArenaRecord<K>> records,
@NotNull BiFunction<DropperArena,
ArenaGameMode,
Set<SummableArenaRecord<K>>> recordSupplier) {
DropperArenaHandler arenaHandler = Dropper.getInstance().getArenaHandler();
// 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 ArenaGameMode gameMode,
@NotNull Map<UUID,
SummableArenaRecord<K>> combinedRecords,
@NotNull Map<UUID, Integer> recordsFound,
@NotNull BiFunction<DropperArena, ArenaGameMode,
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

@ -21,12 +21,13 @@ public final class PlayerTeleporter {
*
* <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 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) {
public static boolean teleportPlayer(Player player, Location location, boolean force, boolean immediately) {
if (!player.getPassengers().isEmpty()) {
if (force) {
for (Entity passenger : player.getPassengers()) {
@ -55,7 +56,11 @@ public final class PlayerTeleporter {
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
Bukkit.getScheduler().runTaskLater(Dropper.getInstance(), () -> player.setInvulnerable(false), 5);
if (!immediately) {
Bukkit.getScheduler().runTaskLater(Dropper.getInstance(), () -> player.setInvulnerable(false), 5);
} else {
player.setInvulnerable(false);
}
return true;
}

View File

@ -0,0 +1,37 @@
package net.knarcraft.dropper.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

@ -2,6 +2,7 @@ package net.knarcraft.dropper.util;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.property.ArenaEditableProperty;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@ -23,10 +24,23 @@ public final class TabCompleteHelper {
*/
public static @NotNull List<String> getArenas() {
List<String> arenaNames = new ArrayList<>();
for (DropperArena dropperArena : Dropper.getInstance().getArenaHandler().getArenas()) {
for (DropperArena dropperArena : Dropper.getInstance().getArenaHandler().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 (ArenaEditableProperty property : ArenaEditableProperty.values()) {
arenaProperties.add(property.getArgumentString());
}
return arenaProperties;
}
}

View File

@ -3,49 +3,80 @@ version: '${project.version}'
main: net.knarcraft.dropper.Dropper
api-version: 1.19
description: A plugin for dropper mini-games
softdepend:
- PlaceholderAPI
# Note to self: Aliases must be lowercase!
commands:
dropperreload:
dropperGroupSet:
aliases:
- dgset
permission: dropper.edit
usage: |
/<command> <arena> <group>
- The group will be created if it doesn't already exist
- Use "none" or "null" as the group to release the arena from its group
description: Sets the group of the given arena
dropperGroupSwap:
aliases:
- dgswap
permission: dropper.edit
usage: |
/<command> <arena1> <arena2>
- The two arenas must be in the same group
description: Swaps the order of two arenas in the same group
dropperGroupList:
aliases:
- dglist
permission: dropper.edit
usage: |
/<command> [group]
- Existing groups will be listed if used without an argument
- Supplying a group shows the group's arenas
description: Lists existing groups and their arenas
dropperReload:
aliases:
- dreload
permission: dropper.admin
usage: /<command>
description: Reloads all data from disk
dropperlist:
dropperList:
aliases:
- dlist
permission: dropper.join
usage: /<command>
description: Used to list all current dropper arenas
dropperjoin:
dropperJoin:
aliases:
- djoin
permission: dropper.join
usage: |
/<command> <arena> [mode]
Mode can be used to select challenge modes which can be played after beating the arena.
deaths = A least-deaths competitive game-mode
time = A shortest-time competitive game-mode
- Mode can be used to select challenge modes which can be played after beating the arena.
- inverted = A game-mode where the WASD buttons are inverted
- random = A game-mode where the WASD buttons toggle between being inverted and not
description: Used to join a dropper arena
dropperleave:
dropperLeave:
aliases:
- dleave
permission: dropper.join
usage: /<command>
description: Used to leave the current dropper arena
droppercreate:
dropperCreate:
aliases:
- dcreate
permission: dropper.create
usage: /<command> (Details not finalized)
usage: |
/<command> <arena> <property> [new value]
- Valid properties: name, spawnLocation, exitLocation, verticalVelocity, horizontalVelocity, winBlockType
description: Used to create a new dropper arena
dropperedit:
dropperEdit:
aliases:
- dedit
permission: dropper.edit
usage: /<command> (Details not finalized)
description: Used to edit an existing dropper arena
dropperremove:
dropperRemove:
aliases:
- dremove
permission: dropper.remove

View File

@ -0,0 +1,50 @@
package net.knarcraft.dropper.arena;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Tests for arena dropper groups
*/
public class DropperArenaGroupTest {
@Test
public void swapTest() {
/*
This test makes sure the order of arenas is as expected when the arenas are added to a group. It also makes
sure that swapping two items works as expected.
*/
DropperArenaGroup arenaGroup = new DropperArenaGroup("test");
UUID arena1Id = UUID.randomUUID();
UUID arena2Id = UUID.randomUUID();
UUID arena3Id = UUID.randomUUID();
UUID arena4Id = UUID.randomUUID();
arenaGroup.addArena(arena1Id);
arenaGroup.addArena(arena2Id);
arenaGroup.addArena(arena3Id);
arenaGroup.addArena(arena4Id);
List<UUID> initialOrder = new ArrayList<>();
initialOrder.add(arena1Id);
initialOrder.add(arena2Id);
initialOrder.add(arena3Id);
initialOrder.add(arena4Id);
Assertions.assertEquals(initialOrder, arenaGroup.getArenas());
arenaGroup.swapArenas(1, 3);
List<UUID> swapped = new ArrayList<>();
swapped.add(arena1Id);
swapped.add(arena4Id);
swapped.add(arena3Id);
swapped.add(arena2Id);
Assertions.assertEquals(swapped, arenaGroup.getArenas());
}
}