25 Commits

Author SHA1 Message Date
b1e86a928b Adds a config options section to the README 2023-04-11 20:26:08 +02:00
4de5ae469b Fixes a few problems
Makes sure to actually load the configuration when starting the plugin
Makes sure all numeric configuration values are within expected bounds
Adds improved descriptions of configuration values' bounds
Removes the option to not override the vertical speed
Adds options for whether sneaking and sprinting should be blocked while in an arena
Adds more materials to the default config's whitelist
Fixes reloading not properly loading the new config
Fixes config options not loading because the root node was missing
Prevents players combusting while in an arena
2023-04-11 20:04:04 +02:00
2f4d4ff4c6 Fixes a few problems
Makes sure to actually load the configuration when starting the plugin
Makes sure all numeric configuration values are within expected bounds
Adds improved descriptions of configuration values' bounds
2023-04-11 13:55:46 +02:00
50978d8baf Implements several configuration options #15 2023-04-11 13:25:45 +02:00
3bbf41206c Makes sure arena data is deleted with the arena 2023-04-09 18:22:45 +02:00
6a41664fef Adds some missing comments and annotations 2023-04-07 20:33:03 +02:00
d1964e9d7b Caches group records for 30 seconds 2023-04-07 20:23:46 +02:00
cd7d8eded0 Fixes a bug causing duplicate records to be stored 2023-04-07 18:51:07 +02:00
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
34 changed files with 1567 additions and 156 deletions

View File

@ -18,22 +18,22 @@ To modify
## Commands
| 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 | 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.
@ -44,7 +44,7 @@ This command is used for joining a dropper arena.
| 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.
@ -67,7 +67,7 @@ These are all the options that can be changed for an arena.
| 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
#### /dropperGroupSet
This command is used to set the group of an arena
@ -78,7 +78,7 @@ will be used again if you specify the "potato" group for another arena. You use
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
#### /dropperGroupSwap
This command is used for changing the order of arenas within a group.
@ -97,4 +97,55 @@ You could use `/droppergroupswap Sea Savanna` to change the order to:
1. Forest
2. Savanna
3. Nether
4. Sea
4. Sea
## Configuration options
| Name | Type | Default | Description |
|-----------------------------------|---------------------|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| blockSneaking | true/false | true | Whether to block using the shift key to drop faster than the intended drop speed |
| blockSprinting | true/false | true | Whether to block using the sprint key for slightly improved air speed |
| verticalVelocity | 0 < decimal <= 75 | 1.0 | The vertical velocity used as default for all arenas. Must be greater than 0. 3.92 is the max speed of a falling player. |
| horizontalVelocity | 0 < decimal <= 1 | 1.0 | The horizontal velocity used as default for all arenas (technically fly-speed). Must be between 0 (exclusive) and 1 (inclusive). |
| randomlyInvertedTimer | 0 < integer <= 3600 | 7 | The number of seconds before the randomly inverted game-mode switches between normal and inverted movement (0, 3600] |
| mustDoGroupedInSequence | true/false | true | Whether grouped dropper arenas must be played in the correct sequence |
| ignoreRecordsUntilGroupBeatenOnce | true/false | false | Whether records won't be registered unless the player has already beaten all arenas in a group. That means players are required to do a second play-through to register a record for a grouped arena. |
| mustDoNormalModeFirst | true/false | true | Whether a player must do the normal/default game-mode before playing any other game-modes |
| makePlayersInvisible | true/false | false | Whether players should be made invisible while playing in a dropper arena |
| disableHitCollision | true/false | true | Whether players should have their entity hit collision disabled while in an arena. This prevents players from pushing each-other if in the same arena. |
| liquidHitBoxDepth | -1 < decimal < 0 | -0.8 | This decides how far inside a non-solid block the player must go before detection triggers (-1, 0). The closer to -1 it is, the more accurate it will seem to the player, but the likelihood of not detecting the hit increases. |
| solidHitBoxDistance | 0 < decimal < 1 | 0.2 | This decides the distance the player must be from a block below them before a hit triggers (0, 1). If too low, the likelihood of detecting the hit decreases, but it won't look like the player hit the block without being near. |
| blockWhitelist | list | [see this](#blockwhitelist-default) | A whitelist for which blocks won't trigger a loss when hit/passed through. The win block check happens before the loss check, so even blocks on the whitelist can be used as the win-block. "+" denotes a material tag. |
#### blockWhitelist default:
- WATER
- LAVA
- +WALL_SIGNS
- +STANDING_SIGNS
- STRUCTURE_VOID
- WALL_TORCH
- SOUL_WALL_TORCH
- REDSTONE_WALL_TORCH
- +BANNERS
- +BUTTONS
- +CORALS
- +WALL_CORALS
## 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". |

10
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>
@ -79,5 +83,11 @@
<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,14 @@
package net.knarcraft.dropper;
import net.knarcraft.dropper.arena.ArenaGameMode;
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;
@ -19,13 +22,15 @@ import net.knarcraft.dropper.command.ListArenaCommand;
import net.knarcraft.dropper.command.ReloadCommand;
import net.knarcraft.dropper.command.RemoveArenaCommand;
import net.knarcraft.dropper.command.RemoveArenaTabCompleter;
import net.knarcraft.dropper.config.DropperConfiguration;
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.property.ArenaGameMode;
import net.knarcraft.dropper.placeholder.DropperRecordExpansion;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabCompleter;
@ -45,8 +50,10 @@ import java.util.logging.Level;
public final class Dropper extends JavaPlugin {
private static Dropper instance;
private DropperConfiguration configuration;
private DropperArenaHandler arenaHandler;
private DropperArenaPlayerRegistry playerRegistry;
private DropperRecordExpansion dropperRecordExpansion;
/**
* Gets an instance of this plugin
@ -75,6 +82,25 @@ public final class Dropper extends JavaPlugin {
return this.playerRegistry;
}
/**
* Gets the dropper configuration
*
* @return <p>The dropper configuration</p>
*/
public DropperConfiguration getDropperConfiguration() {
return this.configuration;
}
/**
* Logs a message
*
* @param level <p>The message level to log at</p>
* @param message <p>The message to log</p>
*/
public static void log(Level level, String message) {
Dropper.getInstance().getLogger().log(level, message);
}
/**
* Reloads all configurations and data from disk
*/
@ -82,6 +108,13 @@ public final class Dropper extends JavaPlugin {
// Load all arenas again
this.arenaHandler.loadArenas();
this.arenaHandler.loadGroups();
// Reload configuration
this.reloadConfig();
this.configuration.load(this.getConfig());
// Clear record caches
this.dropperRecordExpansion.clearCaches();
}
@Override
@ -94,12 +127,20 @@ public final class Dropper extends JavaPlugin {
ConfigurationSerialization.registerClass(DropperArenaData.class);
ConfigurationSerialization.registerClass(DropperArenaGroup.class);
ConfigurationSerialization.registerClass(ArenaGameMode.class);
ConfigurationSerialization.registerClass(LongRecord.class);
ConfigurationSerialization.registerClass(IntegerRecord.class);
}
@Override
public void onEnable() {
// Plugin startup logic
instance = this;
this.saveDefaultConfig();
getConfig().options().copyDefaults(true);
saveConfig();
reloadConfig();
this.configuration = new DropperConfiguration(this.getConfig());
this.configuration.load();
this.playerRegistry = new DropperArenaPlayerRegistry();
this.arenaHandler = new DropperArenaHandler();
this.arenaHandler.loadArenas();
@ -107,7 +148,7 @@ public final class Dropper extends JavaPlugin {
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new DamageListener(), this);
pluginManager.registerEvents(new MoveListener(), this);
pluginManager.registerEvents(new MoveListener(this.configuration), this);
pluginManager.registerEvents(new PlayerLeaveListener(), this);
pluginManager.registerEvents(new CommandListener(), this);
@ -116,11 +157,18 @@ public final class Dropper extends JavaPlugin {
registerCommand("dropperList", new ListArenaCommand(), null);
registerCommand("dropperJoin", new JoinArenaCommand(), new JoinArenaTabCompleter());
registerCommand("dropperLeave", new LeaveArenaCommand(), null);
registerCommand("dropperEdit", new EditArenaCommand(), new EditArenaTabCompleter());
registerCommand("dropperEdit", new EditArenaCommand(this.configuration), new EditArenaTabCompleter());
registerCommand("dropperRemove", new RemoveArenaCommand(), new RemoveArenaTabCompleter());
registerCommand("dropperGroupSet", new GroupSetCommand(), null);
registerCommand("dropperGroupSwap", new GroupSwapCommand(), null);
registerCommand("dropperGroupList", new GroupListCommand(), null);
if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null) {
this.dropperRecordExpansion = new DropperRecordExpansion(this);
if (!this.dropperRecordExpansion.register()) {
log(Level.WARNING, "Unable to register PlaceholderAPI expansion!");
}
}
}
@Override
@ -150,7 +198,7 @@ public final class Dropper extends JavaPlugin {
command.setTabCompleter(tabCompleter);
}
} else {
getLogger().log(Level.SEVERE, "Unable to register the command " + commandName);
log(Level.SEVERE, "Unable to register the command " + commandName);
}
}

View File

@ -1,6 +1,5 @@
package net.knarcraft.dropper.property;
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.arena.DropperArena;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

View File

@ -1,4 +1,4 @@
package net.knarcraft.dropper.property;
package net.knarcraft.dropper.arena;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull;

View File

@ -1,4 +1,4 @@
package net.knarcraft.dropper.property;
package net.knarcraft.dropper.arena;
import org.jetbrains.annotations.NotNull;

View File

@ -1,7 +1,7 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.config.DropperConfiguration;
import net.knarcraft.dropper.util.StringSanitizer;
import org.bukkit.Location;
import org.bukkit.Material;
@ -61,7 +61,7 @@ public class DropperArena {
*/
private final DropperArenaData dropperArenaData;
private static DropperArenaHandler dropperArenaHandler = null;
private final DropperArenaHandler dropperArenaHandler;
/**
* Instantiates a new dropper arena
@ -74,10 +74,12 @@ public class DropperArena {
* @param playerHorizontalVelocity <p>The velocity to use for players' horizontal velocity (-1 to 1)</p>
* @param winBlockType <p>The material of the block players have to hit to win this dropper arena</p>
* @param dropperArenaData <p>The arena data keeping track of which players have done what in this arena</p>
* @param arenaHandler <p>The arena handler used for saving any changes</p>
*/
public DropperArena(@NotNull UUID arenaId, @NotNull String arenaName, @NotNull Location spawnLocation,
@Nullable Location exitLocation, double playerVerticalVelocity, float playerHorizontalVelocity,
@NotNull Material winBlockType, @NotNull DropperArenaData dropperArenaData) {
@NotNull Material winBlockType, @NotNull DropperArenaData dropperArenaData,
@NotNull DropperArenaHandler arenaHandler) {
this.arenaId = arenaId;
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
@ -86,10 +88,7 @@ public class DropperArena {
this.playerHorizontalVelocity = playerHorizontalVelocity;
this.winBlockType = winBlockType;
this.dropperArenaData = dropperArenaData;
if (dropperArenaHandler == null) {
dropperArenaHandler = Dropper.getInstance().getArenaHandler();
}
this.dropperArenaHandler = arenaHandler;
}
/**
@ -100,14 +99,17 @@ 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) {
DropperConfiguration configuration = Dropper.getInstance().getDropperConfiguration();
this.arenaId = UUID.randomUUID();
this.arenaName = arenaName;
this.spawnLocation = spawnLocation;
this.exitLocation = null;
this.playerVerticalVelocity = 3.92;
this.playerHorizontalVelocity = 1;
this.playerVerticalVelocity = configuration.getVerticalVelocity();
this.playerHorizontalVelocity = configuration.getHorizontalVelocity();
Map<ArenaGameMode, DropperArenaRecordsRegistry> recordRegistries = new HashMap<>();
for (ArenaGameMode arenaGameMode : ArenaGameMode.values()) {
@ -116,6 +118,7 @@ public class DropperArena {
this.dropperArenaData = new DropperArenaData(this.arenaId, recordRegistries, new HashMap<>());
this.winBlockType = Material.WATER;
this.dropperArenaHandler = arenaHandler;
}
/**

View File

@ -2,7 +2,6 @@ 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;
@ -103,6 +102,12 @@ public record DropperArenaData(@NotNull UUID arenaId,
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()) {
@ -111,6 +116,10 @@ public record DropperArenaData(@NotNull UUID arenaId,
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

@ -2,7 +2,6 @@ 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;
@ -130,7 +129,7 @@ public class DropperArenaGroup implements ConfigurationSerializable {
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() +
Dropper.log(Level.WARNING, "The dropper group " + this.getGroupName() +
" contains the arena id " + anArenaId + " which is not a valid arena id!");
continue;
}
@ -160,14 +159,14 @@ public class DropperArenaGroup implements ConfigurationSerializable {
for (UUID anArenaId : this.getArenas()) {
// If the target arena is reached, allow, as all previous arenas must have been cleared
if (arenaId == anArenaId) {
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" +
Dropper.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;
}

View File

@ -158,10 +158,15 @@ public class DropperArenaHandler {
* @param arena <p>The arena to remove</p>
*/
public void removeArena(@NotNull DropperArena arena) {
UUID arenaId = arena.getArenaId();
Dropper.getInstance().getPlayerRegistry().removeForArena(arena);
this.arenas.remove(arena.getArenaId());
this.arenas.remove(arenaId);
this.arenaNameLookup.remove(arena.getArenaNameSanitized());
this.arenaGroups.remove(arena.getArenaId());
this.arenaGroups.remove(arenaId);
if (!ArenaStorageHelper.removeArenaData(arenaId)) {
Dropper.log(Level.WARNING, "Unable to remove dropper arena data file " + arenaId + ".yml. " +
"You must remove it manually!");
}
this.saveArenas();
}
@ -174,8 +179,8 @@ public class DropperArenaHandler {
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());
Dropper.log(Level.SEVERE, "Unable to save arena data! Data loss can occur!");
Dropper.log(Level.SEVERE, e.getMessage());
}
}
@ -186,9 +191,9 @@ public class DropperArenaHandler {
try {
ArenaStorageHelper.saveDropperArenaGroups(new HashSet<>(this.arenaGroups.values()));
} catch (IOException e) {
Dropper.getInstance().getLogger().log(Level.SEVERE, "Unable to save current arena groups! " +
Dropper.log(Level.SEVERE, "Unable to save current arena groups! " +
"Data loss can occur!");
Dropper.getInstance().getLogger().log(Level.SEVERE, e.getMessage());
Dropper.log(Level.SEVERE, e.getMessage());
}
}
@ -213,9 +218,9 @@ public class DropperArenaHandler {
try {
ArenaStorageHelper.saveArenas(this.arenas);
} catch (IOException e) {
Dropper.getInstance().getLogger().log(Level.SEVERE, "Unable to save current arenas! " +
Dropper.log(Level.SEVERE, "Unable to save current arenas! " +
"Data loss can occur!");
Dropper.getInstance().getLogger().log(Level.SEVERE, e.getMessage());
Dropper.log(Level.SEVERE, e.getMessage());
}
}

View File

@ -1,15 +1,24 @@
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
@ -17,16 +26,16 @@ import java.util.stream.Stream;
public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
private final UUID arenaId;
private final @NotNull Map<UUID, Number> leastDeaths;
private final @NotNull Map<UUID, Number> shortestTimeMilliSeconds;
private final @NotNull Set<IntegerRecord> leastDeaths;
private final @NotNull Set<LongRecord> shortestTimeMilliSeconds;
/**
* Instantiates a new empty records registry
*/
public DropperArenaRecordsRegistry(@NotNull UUID arenaId) {
this.arenaId = arenaId;
this.leastDeaths = new HashMap<>();
this.shortestTimeMilliSeconds = new HashMap<>();
this.leastDeaths = new HashSet<>();
this.shortestTimeMilliSeconds = new HashSet<>();
}
/**
@ -35,11 +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>
*/
private DropperArenaRecordsRegistry(@NotNull UUID arenaId, @NotNull Map<UUID, Integer> leastDeaths,
@NotNull Map<UUID, Long> shortestTimeMilliSeconds) {
private DropperArenaRecordsRegistry(@NotNull UUID arenaId, @NotNull Set<IntegerRecord> leastDeaths,
@NotNull Set<LongRecord> shortestTimeMilliSeconds) {
this.arenaId = arenaId;
this.leastDeaths = new HashMap<>(leastDeaths);
this.shortestTimeMilliSeconds = new HashMap<>(shortestTimeMilliSeconds);
this.leastDeaths = new HashSet<>(leastDeaths);
this.shortestTimeMilliSeconds = new HashSet<>(shortestTimeMilliSeconds);
}
/**
@ -47,12 +56,8 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
*
* @return <p>Existing death records</p>
*/
public Map<UUID, Integer> getLeastDeathsRecords() {
Map<UUID, Integer> leastDeathRecords = new HashMap<>();
for (Map.Entry<UUID, Number> entry : this.leastDeaths.entrySet()) {
leastDeathRecords.put(entry.getKey(), entry.getValue().intValue());
}
return leastDeathRecords;
public Set<SummableArenaRecord<Integer>> getLeastDeathsRecords() {
return new HashSet<>(this.leastDeaths);
}
/**
@ -60,12 +65,8 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
*
* @return <p>Existing time records</p>
*/
public Map<UUID, Long> getShortestTimeMilliSecondsRecords() {
Map<UUID, Long> leastTimeRecords = new HashMap<>();
for (Map.Entry<UUID, Number> entry : this.shortestTimeMilliSeconds.entrySet()) {
leastTimeRecords.put(entry.getKey(), entry.getValue().longValue());
}
return leastTimeRecords;
public Set<SummableArenaRecord<Long>> getShortestTimeMilliSecondsRecords() {
return new HashSet<>(this.shortestTimeMilliSeconds);
}
/**
@ -76,7 +77,12 @@ 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) {
return registerRecord(leastDeaths, playerId, deaths);
Consumer<Integer> consumer = (value) -> {
leastDeaths.removeIf((item) -> item.getUserId().equals(playerId));
leastDeaths.add(new IntegerRecord(playerId, value));
};
Set<ArenaRecord<Integer>> asInt = new HashSet<>(leastDeaths);
return registerRecord(asInt, consumer, playerId, deaths);
}
/**
@ -87,7 +93,12 @@ 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) {
return registerRecord(shortestTimeMilliSeconds, playerId, milliseconds);
Consumer<Long> consumer = (value) -> {
shortestTimeMilliSeconds.removeIf((item) -> item.getUserId().equals(playerId));
shortestTimeMilliSeconds.add(new LongRecord(playerId, value));
};
Set<ArenaRecord<Long>> asLong = new HashSet<>(shortestTimeMilliSeconds);
return registerRecord(asLong, consumer, playerId, milliseconds);
}
/**
@ -101,29 +112,33 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
* 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 @NotNull RecordResult registerRecord(@NotNull Map<UUID, Number> existingRecords, @NotNull UUID playerId,
Number amount) {
private <T extends Comparable<T>> @NotNull RecordResult registerRecord(@NotNull Set<ArenaRecord<T>> existingRecords,
@NotNull Consumer<T> recordSetter,
@NotNull UUID playerId, T amount) {
RecordResult result;
Stream<Map.Entry<UUID, Number>> records = existingRecords.entrySet().stream();
long amountLong = amount.longValue();
if (records.allMatch((entry) -> amountLong < entry.getValue().longValue())) {
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;
existingRecords.put(playerId, amount);
recordSetter.accept(amount);
save();
} else if (existingRecords.containsKey(playerId) && amountLong < existingRecords.get(playerId).longValue()) {
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;
existingRecords.put(playerId, amount);
recordSetter.accept(amount);
save();
} else {
// Make sure to save the record if this is the user's first attempt
if (!existingRecords.containsKey(playerId)) {
if (playerRecord == null) {
recordSetter.accept(amount);
save();
}
result = RecordResult.NONE;
@ -132,23 +147,32 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
return result;
}
/**
* Gets the record stored for the given player
*
* @param existingRecords <p>The existing records to look through</p>
* @param playerId <p>The id of the player to look for</p>
* @param <T> <p>The type of the stored record</p>
* @return <p>The record, or null if not found</p>
*/
private <T extends Comparable<T>> @Nullable ArenaRecord<T> getRecord(@NotNull Set<ArenaRecord<T>> existingRecords,
@NotNull UUID playerId) {
AtomicReference<ArenaRecord<T>> record = new AtomicReference<>();
existingRecords.forEach((item) -> {
if (item.getUserId().equals(playerId)) {
record.set(item);
}
});
return record.get();
}
@NotNull
@Override
public Map<String, Object> serialize() {
Map<String, Object> data = new HashMap<>();
data.put("arenaId", new SerializableUUID(this.arenaId));
Map<SerializableUUID, Number> leastDeaths = new HashMap<>();
for (Map.Entry<UUID, Number> entry : this.leastDeaths.entrySet()) {
leastDeaths.put(new SerializableUUID(entry.getKey()), entry.getValue());
}
data.put("leastDeaths", leastDeaths);
Map<SerializableUUID, Number> shortestTimeMilliSeconds = new HashMap<>();
for (Map.Entry<UUID, Number> entry : this.shortestTimeMilliSeconds.entrySet()) {
shortestTimeMilliSeconds.put(new SerializableUUID(entry.getKey()), entry.getValue());
}
data.put("shortestTime", shortestTimeMilliSeconds);
data.put("leastDeaths", this.leastDeaths);
data.put("shortestTime", this.shortestTimeMilliSeconds);
return data;
}
@ -161,20 +185,13 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
@SuppressWarnings({"unused", "unchecked"})
public static DropperArenaRecordsRegistry deserialize(Map<String, Object> data) {
UUID arenaId = ((SerializableUUID) data.get("arenaId")).uuid();
Map<SerializableUUID, Integer> leastDeathsData =
(Map<SerializableUUID, Integer>) data.getOrDefault("leastDeaths", new HashMap<>());
Map<UUID, Integer> leastDeaths = new HashMap<>();
for (Map.Entry<SerializableUUID, Integer> entry : leastDeathsData.entrySet()) {
leastDeaths.put(entry.getKey().uuid(), entry.getValue());
}
Map<SerializableUUID, Number> shortestTimeMillisecondsData =
(Map<SerializableUUID, Number>) data.getOrDefault("shortestTime", new HashMap<>());
Map<UUID, Long> shortestTimeMilliseconds = new HashMap<>();
for (Map.Entry<SerializableUUID, Number> entry : shortestTimeMillisecondsData.entrySet()) {
shortestTimeMilliseconds.put(entry.getKey().uuid(), entry.getValue().longValue());
}
Set<IntegerRecord> leastDeaths =
(Set<IntegerRecord>) data.getOrDefault("leastDeaths", new HashMap<>());
Set<LongRecord> shortestTimeMilliseconds =
(Set<LongRecord>) data.getOrDefault("shortestTime", new HashMap<>());
leastDeaths.removeIf(Objects::isNull);
shortestTimeMilliseconds.removeIf(Objects::isNull);
return new DropperArenaRecordsRegistry(arenaId, leastDeaths, shortestTimeMilliseconds);
}

View File

@ -1,7 +1,7 @@
package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.config.DropperConfiguration;
import net.knarcraft.dropper.property.RecordResult;
import net.knarcraft.dropper.util.PlayerTeleporter;
import org.bukkit.Location;
@ -37,7 +37,10 @@ public class DropperArenaSession {
this.deaths = 0;
this.startTime = System.currentTimeMillis();
this.entryState = new PlayerEntryState(player, gameMode);
DropperConfiguration configuration = Dropper.getInstance().getDropperConfiguration();
boolean makeInvisible = configuration.makePlayersInvisible();
boolean disableCollision = configuration.disableHitCollision();
this.entryState = new PlayerEntryState(player, gameMode, makeInvisible, disableCollision);
// Make the player fly to improve mobility in the air
this.entryState.setArenaState(this.arena.getPlayerHorizontalVelocity());
}
@ -68,7 +71,12 @@ public class DropperArenaSession {
stopSession();
// Check for, and display, records
registerRecord();
Dropper dropper = Dropper.getInstance();
boolean ignore = dropper.getDropperConfiguration().ignoreRecordsUntilGroupBeatenOnce();
DropperArenaGroup group = dropper.getArenaHandler().getGroup(this.arena.getArenaId());
if (!ignore || group == null || group.hasBeatenAll(this.gameMode, this.player)) {
registerRecord();
}
// Mark the arena as cleared
if (this.arena.getData().addCompleted(this.gameMode, this.player)) {
@ -101,8 +109,8 @@ public class DropperArenaSession {
// Remove this session for game sessions to stop listeners from fiddling more with the player
boolean removedSession = Dropper.getInstance().getPlayerRegistry().removePlayer(player.getUniqueId());
if (!removedSession) {
Dropper.getInstance().getLogger().log(Level.SEVERE, "Unable to remove dropper arena session for " +
player.getName() + ". This will have unintended consequences.");
Dropper.log(Level.SEVERE, "Unable to remove dropper arena session for " + player.getName() + ". " +
"This will have unintended consequences.");
}
}

View File

@ -1,9 +1,10 @@
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;
/**
@ -19,6 +20,9 @@ public class PlayerEntryState {
private final boolean originalAllowFlight;
private final boolean originalInvulnerable;
private final boolean originalIsSwimming;
private final boolean originalCollideAble;
private final boolean makePlayerInvisible;
private final boolean disableHitCollision;
private final ArenaGameMode arenaGameMode;
/**
@ -26,7 +30,8 @@ public class PlayerEntryState {
*
* @param player <p>The player whose state should be stored</p>
*/
public PlayerEntryState(@NotNull Player player, @NotNull ArenaGameMode arenaGameMode) {
public PlayerEntryState(@NotNull Player player, @NotNull ArenaGameMode arenaGameMode, boolean makePlayerInvisible,
boolean disableHitCollision) {
this.player = player;
this.entryLocation = player.getLocation().clone();
this.originalFlySpeed = player.getFlySpeed();
@ -36,6 +41,9 @@ public class PlayerEntryState {
this.originalInvulnerable = player.isInvulnerable();
this.originalIsSwimming = player.isSwimming();
this.arenaGameMode = arenaGameMode;
this.originalCollideAble = player.isCollidable();
this.makePlayerInvisible = makePlayerInvisible;
this.disableHitCollision = disableHitCollision;
}
/**
@ -48,6 +56,13 @@ public class PlayerEntryState {
this.player.setFlying(true);
this.player.setGameMode(GameMode.ADVENTURE);
this.player.setSwimming(false);
if (this.disableHitCollision) {
this.player.setCollidable(false);
}
if (this.makePlayerInvisible) {
this.player.addPotionEffect(new PotionEffect(PotionEffectType.INVISIBILITY,
PotionEffect.INFINITE_DURATION, 3));
}
// If playing on the inverted game-mode, negate the horizontal velocity to swap the controls
if (arenaGameMode == ArenaGameMode.INVERTED) {
@ -67,6 +82,12 @@ public class PlayerEntryState {
this.player.setFlySpeed(this.originalFlySpeed);
this.player.setInvulnerable(this.originalInvulnerable);
this.player.setSwimming(this.originalIsSwimming);
if (this.disableHitCollision) {
this.player.setCollidable(this.originalCollideAble);
}
if (this.makePlayerInvisible) {
this.player.removePotionEffect(PotionEffectType.INVISIBILITY);
}
}
/**

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,43 @@
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);
}
@Override
public boolean equals(Object other) {
return other instanceof IntegerRecord && this.getUserId().equals(((IntegerRecord) other).getUserId());
}
/**
* Deserializes the saved arena record
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized data</p>
*/
@SuppressWarnings("unused")
public static IntegerRecord deserialize(@NotNull Map<String, Object> data) {
return new IntegerRecord(((SerializableUUID) data.get("userId")).uuid(), (Integer) data.get("record"));
}
}

View File

@ -0,0 +1,43 @@
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 boolean equals(Object other) {
return other instanceof LongRecord && this.getUserId().equals(((LongRecord) other).getUserId());
}
@Override
public SummableArenaRecord<Long> sum(Long value) {
return new LongRecord(this.getUserId(), this.getRecord() + value);
}
/**
* Deserializes the saved arena record
*
* @param data <p>The data to deserialize</p>
* @return <p>The deserialized data</p>
*/
@SuppressWarnings("unused")
public static LongRecord deserialize(@NotNull Map<String, Object> data) {
return new LongRecord(((SerializableUUID) data.get("userId")).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,7 @@ 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;
@ -35,14 +36,16 @@ public class CreateArenaCommand implements CommandExecutor {
return false;
}
DropperArena existingArena = Dropper.getInstance().getArenaHandler().getArena(arenaName);
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;
}
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

@ -1,8 +1,9 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.ArenaEditableProperty;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.property.ArenaEditableProperty;
import net.knarcraft.dropper.config.DropperConfiguration;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.command.Command;
@ -16,6 +17,17 @@ import org.jetbrains.annotations.NotNull;
*/
public class EditArenaCommand implements CommandExecutor {
private final DropperConfiguration configuration;
/**
* Instantiates a new edit arena command
*
* @param configuration <p>The configuration to use</p>
*/
public EditArenaCommand(DropperConfiguration configuration) {
this.configuration = configuration;
}
@Override
public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String s,
@NotNull String[] arguments) {
@ -92,7 +104,7 @@ public class EditArenaCommand implements CommandExecutor {
try {
velocity = Double.parseDouble(velocityString);
} catch (NumberFormatException exception) {
velocity = 3.92;
velocity = configuration.getVerticalVelocity();
}
// Require at least speed of 0.001, and at most 75 blocks/s
@ -111,12 +123,7 @@ public class EditArenaCommand implements CommandExecutor {
try {
velocity = Float.parseFloat(velocityString);
} catch (NumberFormatException exception) {
velocity = 1;
}
// Make sure the velocity isn't exactly 0
if (velocity == 0) {
velocity = 0.5f;
velocity = configuration.getHorizontalVelocity();
}
// If outside bonds, choose the most extreme value

View File

@ -1,11 +1,12 @@
package net.knarcraft.dropper.command;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.ArenaGameMode;
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.config.DropperConfiguration;
import net.knarcraft.dropper.util.PlayerTeleporter;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
@ -77,7 +78,8 @@ public class JoinArenaCommand implements CommandExecutor {
}
// Make sure the player has beaten the arena once in normal mode before playing another mode
if (gameMode != ArenaGameMode.DEFAULT &&
if (Dropper.getInstance().getDropperConfiguration().mustDoNormalModeFirst() &&
gameMode != ArenaGameMode.DEFAULT &&
specifiedArena.getData().hasNotCompleted(ArenaGameMode.DEFAULT, player)) {
player.sendMessage("You must complete this arena in normal mode first!");
return false;
@ -113,16 +115,17 @@ public class JoinArenaCommand implements CommandExecutor {
*/
private boolean doGroupChecks(@NotNull DropperArena dropperArena, @NotNull DropperArenaGroup arenaGroup,
@NotNull ArenaGameMode arenaGameMode, @NotNull Player player) {
DropperConfiguration configuration = Dropper.getInstance().getDropperConfiguration();
// 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;
}
if (configuration.mustDoNormalModeFirst() && arenaGameMode != ArenaGameMode.DEFAULT &&
!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())) {
if (configuration.mustDoGroupedInSequence() &&
!arenaGroup.canPlay(arenaGameMode, player, dropperArena.getArenaId())) {
player.sendMessage("You have not yet beaten the previous arena!");
return false;
}

View File

@ -0,0 +1,298 @@
package net.knarcraft.dropper.config;
import net.knarcraft.dropper.Dropper;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Tag;
import org.bukkit.configuration.file.FileConfiguration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The configuration keeping track of the player's current configuration
*/
public class DropperConfiguration {
private FileConfiguration configuration;
private final static String rootNode = "dropper.";
private double verticalVelocity;
private float horizontalVelocity;
private int randomlyInvertedTimer;
private boolean mustDoGroupedInSequence;
private boolean ignoreRecordsUntilGroupBeatenOnce;
private boolean mustDoNormalModeFirst;
private boolean makePlayersInvisible;
private boolean disableHitCollision;
private double liquidHitBoxDepth;
private double solidHitBoxDistance;
private boolean blockSneaking;
private boolean blockSprinting;
private Set<Material> blockWhitelist;
/**
* Instantiates a new dropper configuration
*
* @param configuration <p>The YAML configuration to use internally</p>
*/
public DropperConfiguration(FileConfiguration configuration) {
this.configuration = configuration;
}
/**
* Gets the default vertical velocity
*
* @return <p>The default vertical velocity</p>
*/
public double getVerticalVelocity() {
return this.verticalVelocity;
}
/**
* Gets the default horizontal velocity
*
* @return <p>The default horizontal velocity</p>
*/
public float getHorizontalVelocity() {
return this.horizontalVelocity;
}
/**
* Gets the number of seconds before the randomly inverted game-mode toggles
*
* @return <p>Number of seconds before the inversion toggles</p>
*/
public int getRandomlyInvertedTimer() {
return this.randomlyInvertedTimer;
}
/**
* Gets whether grouped arenas must be done in the set sequence
*
* @return <p>Whether grouped arenas must be done in sequence</p>
*/
public boolean mustDoGroupedInSequence() {
return this.mustDoGroupedInSequence;
}
/**
* Gets whether the normal/default mode must be beaten before playing another game-mode
*
* @return <p>Whether the normal game-mode must be beaten first</p>
*/
public boolean mustDoNormalModeFirst() {
return this.mustDoNormalModeFirst;
}
/**
* Gets the types of block which should not trigger a loss
*
* @return <p>The materials that should not trigger a loss</p>
*/
public Set<Material> getBlockWhitelist() {
return new HashSet<>(this.blockWhitelist);
}
/**
* Gets whether records should be discarded, unless the player has already beaten all arenas in the group
*
* @return <p>Whether to ignore records on the first play-through</p>
*/
public boolean ignoreRecordsUntilGroupBeatenOnce() {
return this.ignoreRecordsUntilGroupBeatenOnce;
}
/**
* Gets whether players should be made invisible while in an arena
*
* @return <p>Whether players should be made invisible</p>
*/
public boolean makePlayersInvisible() {
return this.makePlayersInvisible;
}
/**
* Gets whether entity hit-collision of players in an arena should be disabled
*
* @return <p>Whether to disable hit collision</p>
*/
public boolean disableHitCollision() {
return this.disableHitCollision;
}
/**
* Gets the negative depth a player must reach in a liquid block for fail/win detection to trigger
*
* <p>This decides how far inside a non-solid block the player must go before detection triggers. The closer to -1
* it is, the more accurate it will seem to the player, but the likelihood of not detecting the hit increases.</p>
*
* @return <p>The liquid hit box depth to use</p>
*/
public double getLiquidHitBoxDepth() {
return this.liquidHitBoxDepth;
}
/**
* Gets the positive distance a player must at most be from a block for fail/win detection to trigger
*
* <p>This decides the distance the player must be from a block below them before a hit triggers. If too low, the
* likelihood of detecting the hit decreases, but it won't look like the player hit the block without being near.</p>
*
* @return <p>The solid hit box distance to use</p>
*/
public double getSolidHitBoxDistance() {
return this.solidHitBoxDistance;
}
/**
* Gets whether players trying to sneak while in a dropper arena to increase their downwards speed should be blocked
*
* @return <p>Whether to block sneak to speed up</p>
*/
public boolean blockSneaking() {
return blockSneaking;
}
/**
* Gets whether players trying to sprint to improve their horizontal speed while in a dropper arena should be blocked
*
* @return <p>Whether to block sprint to speed up</p>
*/
public boolean blockSprinting() {
return this.blockSprinting;
}
/**
* Loads all configuration values from disk
*
* @param configuration <p>The configuration to load</p>
*/
public void load(FileConfiguration configuration) {
this.configuration = configuration;
this.load();
}
/**
* Loads all configuration values from disk
*/
public void load() {
this.verticalVelocity = configuration.getDouble(rootNode + "verticalVelocity", 1.0);
this.horizontalVelocity = (float) configuration.getDouble(rootNode + "horizontalVelocity", 1.0);
this.randomlyInvertedTimer = configuration.getInt(rootNode + "randomlyInvertedTimer", 7);
this.mustDoGroupedInSequence = configuration.getBoolean(rootNode + "mustDoGroupedInSequence", true);
this.ignoreRecordsUntilGroupBeatenOnce = configuration.getBoolean(rootNode + "ignoreRecordsUntilGroupBeatenOnce", false);
this.mustDoNormalModeFirst = configuration.getBoolean(rootNode + "mustDoNormalModeFirst", true);
this.makePlayersInvisible = configuration.getBoolean(rootNode + "makePlayersInvisible", false);
this.disableHitCollision = configuration.getBoolean(rootNode + "disableHitCollision", true);
this.liquidHitBoxDepth = configuration.getDouble(rootNode + "liquidHitBoxDepth", -0.8);
this.solidHitBoxDistance = configuration.getDouble(rootNode + "solidHitBoxDistance", 0.2);
this.blockSprinting = configuration.getBoolean(rootNode + "blockSprinting", true);
this.blockSneaking = configuration.getBoolean(rootNode + "blockSneaking", true);
sanitizeValues();
loadBlockWhitelist();
}
/**
* Sanitizes configuration values to ensure they are within expected bounds
*/
private void sanitizeValues() {
if (this.liquidHitBoxDepth <= -1 || this.liquidHitBoxDepth > 0) {
this.liquidHitBoxDepth = -0.8;
}
if (this.solidHitBoxDistance <= 0 || this.solidHitBoxDistance > 1) {
this.solidHitBoxDistance = 0.2;
}
if (this.horizontalVelocity > 1 || this.horizontalVelocity <= 0) {
this.horizontalVelocity = 1;
}
if (this.verticalVelocity <= 0 || this.verticalVelocity > 75) {
this.verticalVelocity = 1;
}
if (this.randomlyInvertedTimer <= 0 || this.randomlyInvertedTimer > 3600) {
this.randomlyInvertedTimer = 7;
}
}
/**
* Loads the materials specified in the block whitelist
*/
private void loadBlockWhitelist() {
this.blockWhitelist = new HashSet<>();
List<?> blockWhitelist = configuration.getList(rootNode + "blockWhitelist", new ArrayList<>());
for (Object value : blockWhitelist) {
if (!(value instanceof String string)) {
continue;
}
// Try to parse a material tag first
if (parseMaterialTag(string)) {
continue;
}
// Try to parse a material name
Material matched = Material.matchMaterial(string);
if (matched != null) {
this.blockWhitelist.add(matched);
} else {
Dropper.log(Level.WARNING, "Unable to parse: " + string);
}
}
}
/**
* Tries to parse the material tag in the specified material name
*
* @param materialName <p>The material name that might be a material tag</p>
* @return <p>True if a tag was found</p>
*/
private boolean parseMaterialTag(String materialName) {
Pattern pattern = Pattern.compile("^\\+([a-zA-Z_]+)");
Matcher matcher = pattern.matcher(materialName);
if (matcher.find()) {
// The material is a material tag
Tag<Material> tag = Bukkit.getTag(Tag.REGISTRY_BLOCKS, NamespacedKey.minecraft(
matcher.group(1).toLowerCase()), Material.class);
if (tag != null) {
this.blockWhitelist.addAll(tag.getValues());
} else {
Dropper.log(Level.WARNING, "Unable to parse: " + materialName);
}
return true;
}
return false;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder(
"Current configuration:" +
"\n" + "Vertical velocity: " + verticalVelocity +
"\n" + "Horizontal velocity: " + horizontalVelocity +
"\n" + "Randomly inverted timer: " + randomlyInvertedTimer +
"\n" + "Must do groups in sequence: " + mustDoGroupedInSequence +
"\n" + "Ignore records until group beaten once: " + ignoreRecordsUntilGroupBeatenOnce +
"\n" + "Must do normal mode first: " + mustDoNormalModeFirst +
"\n" + "Make players invisible: " + makePlayersInvisible +
"\n" + "Disable hit collision: " + disableHitCollision +
"\n" + "Liquid hit box depth: " + liquidHitBoxDepth +
"\n" + "Solid hit box distance: " + solidHitBoxDistance +
"\n" + "Block whitelist: ");
for (Material material : blockWhitelist) {
builder.append("\n - ").append(material.name());
}
return builder.toString();
}
}

View File

@ -1,11 +1,13 @@
package net.knarcraft.dropper.listener;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import net.knarcraft.dropper.arena.DropperArenaSession;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityCombustEvent;
import org.bukkit.event.entity.EntityDamageEvent;
/**
@ -36,4 +38,18 @@ public class DamageListener implements Listener {
}
}
@EventHandler(ignoreCancelled = true)
public void onPlayerCombustion(EntityCombustEvent event) {
if (event.getEntityType() != EntityType.PLAYER) {
return;
}
DropperArenaPlayerRegistry registry = Dropper.getInstance().getPlayerRegistry();
DropperArenaSession arenaSession = registry.getArenaSession(event.getEntity().getUniqueId());
if (arenaSession != null) {
// Cancel combustion for any player in an arena
event.setCancelled(true);
}
}
}

View File

@ -1,9 +1,10 @@
package net.knarcraft.dropper.listener;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.ArenaGameMode;
import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import net.knarcraft.dropper.arena.DropperArenaSession;
import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.config.DropperConfiguration;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.block.Block;
@ -23,6 +24,17 @@ import java.util.Set;
*/
public class MoveListener implements Listener {
private final DropperConfiguration configuration;
/**
* Instantiates a new move listener
*
* @param configuration <p>The configuration to use</p>
*/
public MoveListener(DropperConfiguration configuration) {
this.configuration = configuration;
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
// Ignore if no actual movement is happening
@ -36,9 +48,11 @@ public class MoveListener implements Listener {
if (arenaSession == null) {
return;
}
// Prevent the player from flying upwards while in flight mode
if (event.getFrom().getY() < event.getTo().getY()) {
if (event.getFrom().getY() < event.getTo().getY() ||
(configuration.blockSneaking() && event.getPlayer().isSneaking()) ||
(configuration.blockSprinting() && event.getPlayer().isSprinting())) {
event.setCancelled(true);
return;
}
@ -64,12 +78,8 @@ public class MoveListener implements Listener {
* @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;
double liquidDepth = configuration.getLiquidHitBoxDepth();
double solidDepth = configuration.getSolidHitBoxDistance();
// Check if the player enters water
Material winBlockType = arenaSession.getArena().getWinBlockType();
@ -83,9 +93,10 @@ public class MoveListener implements Listener {
}
// Check if the player is about to hit a non-air and non-liquid block
Set<Material> whitelisted = configuration.getBlockWhitelist();
for (Block block : getBlocksBeneathLocation(toLocation, solidDepth)) {
if (!block.getType().isAir() && block.getType() != Material.STRUCTURE_VOID &&
block.getType() != Material.WATER && block.getType() != Material.LAVA) {
Material blockType = block.getType();
if (!blockType.isAir() && !whitelisted.contains(blockType)) {
arenaSession.triggerLoss();
return true;
}
@ -116,10 +127,11 @@ public class MoveListener implements Listener {
* @param session <p>The session to update the velocity for</p>
*/
private void updatePlayerVelocity(@NotNull DropperArenaSession session) {
// Override the vertical velocity
Player player = session.getPlayer();
Vector playerVelocity = player.getVelocity();
double arenaVelocity = session.getArena().getPlayerVerticalVelocity();
Vector newVelocity = new Vector(playerVelocity.getX(), -arenaVelocity, playerVelocity.getZ());
Vector newVelocity = new Vector(playerVelocity.getX() * 5, -arenaVelocity, playerVelocity.getZ() * 5);
player.setVelocity(newVelocity);
// Toggle the direction of the player's flying, as necessary
@ -137,7 +149,7 @@ public class MoveListener implements Listener {
}
Player player = session.getPlayer();
float horizontalVelocity = session.getArena().getPlayerHorizontalVelocity();
float secondsBetweenToggle = 7;
float secondsBetweenToggle = configuration.getRandomlyInvertedTimer();
int seconds = Calendar.getInstance().get(Calendar.SECOND);
/*

View File

@ -32,7 +32,7 @@ public class PlayerLeaveListener implements Listener {
return;
}
Dropper.getInstance().getLogger().log(Level.WARNING, "Found player " + player.getUniqueId() +
Dropper.log(Level.WARNING, "Found player " + player.getUniqueId() +
" leaving in the middle of a session!");
leftSessions.put(player.getUniqueId(), arenaSession);
}
@ -42,10 +42,10 @@ public class PlayerLeaveListener implements Listener {
UUID playerId = event.getPlayer().getUniqueId();
// Force the player to quit from the session once they re-join
if (leftSessions.containsKey(playerId)) {
Dropper.getInstance().getLogger().log(Level.WARNING, "Found un-exited dropper session!");
Dropper.log(Level.WARNING, "Found un-exited dropper session!");
Bukkit.getScheduler().runTaskLater(Dropper.getInstance(), () -> {
leftSessions.get(playerId).triggerQuit(false);
Dropper.getInstance().getLogger().log(Level.WARNING, "Triggered a quit!");
Dropper.log(Level.WARNING, "Triggered a quit!");
leftSessions.remove(playerId);
}, 80);
}

View File

@ -0,0 +1,333 @@
package net.knarcraft.dropper.placeholder;
import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.ArenaGameMode;
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.SelectionType;
import net.knarcraft.dropper.property.RecordType;
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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.function.Supplier;
/**
* A placeholder expansion for dropper record placeholders
*/
public class DropperRecordExpansion extends PlaceholderExpansion {
private final Dropper plugin;
private final Map<UUID, Set<GroupRecordCache<Integer>>> groupRecordDeathsCache;
private final Map<UUID, Set<GroupRecordCache<Long>>> groupRecordTimeCache;
/**
* Initializes a new record expansion
*
* @param plugin <p>A reference to the dropper plugin</p>
*/
public DropperRecordExpansion(Dropper plugin) {
this.plugin = plugin;
this.groupRecordDeathsCache = new HashMap<>();
this.groupRecordTimeCache = new HashMap<>();
}
@Override
public String getIdentifier() {
return "dropper";
}
@Override
public String getAuthor() {
return "EpicKnarvik97";
}
@Override
public String getVersion() {
return "1.0.0";
}
@Override
public boolean persist() {
return true;
}
@Override
public String onRequest(OfflinePlayer player, String parameters) {
String[] parts = parameters.split("_");
// Record is used as the prefix for all record placeholders in case more placeholder types are added
if (parts.length < 7 || !parts[0].equals("record")) {
return parameters;
}
RecordType recordType = RecordType.getFromString(parts[1]);
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);
}
/**
* Clears all record caches
*/
public void clearCaches() {
this.groupRecordDeathsCache.clear();
this.groupRecordTimeCache.clear();
}
/**
* Gets a piece of record information from a dropper arena group
*
* @param arenaHandler <p>The arena handler to get the group from</p>
* @param identifier <p>The identifier (name/uuid) selecting the group</p>
* @param gameMode <p>The game-mode to get a record for</p>
* @param recordType <p>The type of record to get</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @param infoType <p>The type of info (player, value, combined) to get</p>
* @return <p>The selected information about the record, or null if not found</p>
*/
private @Nullable String getGroupRecord(@NotNull DropperArenaHandler arenaHandler, @NotNull String identifier,
@NotNull 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 = getGroupDeathRecord(group, gameMode, recordNumber);
} else {
record = getGroupTimeRecord(group, gameMode, recordNumber);
}
// If a record number is not found, leave it blank, so it looks neat
if (record == null) {
return "";
}
return getRecordData(infoType, record);
}
/**
* Gets a time record from a group, using the cache if possible
*
* @param group <p>The group to get the record from</p>
* @param gameMode <p>The game-mode to get the record from</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @return <p>The record, or null if not found</p>
*/
private @Nullable ArenaRecord<?> getGroupTimeRecord(@NotNull DropperArenaGroup group,
@NotNull ArenaGameMode gameMode, int recordNumber) {
return getCachedGroupRecord(group, gameMode, RecordType.TIME, recordNumber, groupRecordTimeCache,
() -> DropperGroupRecordHelper.getCombinedTime(group, gameMode));
}
/**
* Gets a death record from a group, using the cache if possible
*
* @param group <p>The group to get the record from</p>
* @param gameMode <p>The game-mode to get the record from</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @return <p>The record, or null if not found</p>
*/
private @Nullable ArenaRecord<?> getGroupDeathRecord(@NotNull DropperArenaGroup group,
@NotNull ArenaGameMode gameMode, int recordNumber) {
return getCachedGroupRecord(group, gameMode, RecordType.DEATHS, recordNumber, groupRecordDeathsCache,
() -> DropperGroupRecordHelper.getCombinedDeaths(group, gameMode));
}
/**
* Gets a group record, fetching from a cache if possible
*
* @param group <p>The group to get the record for</p>
* @param gameMode <p>The game-mode to get the record for</p>
* @param recordType <p>The type of record to get</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @param caches <p>The caches to use for looking for and saving the record</p>
* @param recordProvider <p>The provider of records if the cache cannot provide the record</p>
* @param <K> <p>The type of the provided records</p>
* @return <p>The specified record, or null if not found</p>
*/
private <K extends Comparable<K>> @Nullable ArenaRecord<?> getCachedGroupRecord(@NotNull DropperArenaGroup group,
@NotNull ArenaGameMode gameMode,
@NotNull RecordType recordType,
int recordNumber,
@NotNull Map<UUID, Set<GroupRecordCache<K>>> caches,
@NotNull Supplier<Set<ArenaRecord<K>>> recordProvider) {
UUID groupId = group.getGroupId();
if (!caches.containsKey(groupId)) {
caches.put(groupId, new HashSet<>());
}
Set<GroupRecordCache<K>> existingCaches = caches.get(groupId);
Set<GroupRecordCache<K>> expired = new HashSet<>();
Set<ArenaRecord<K>> cachedRecords = null;
for (GroupRecordCache<K> cache : existingCaches) {
// Expire caches after 30 seconds
if (System.currentTimeMillis() - cache.createdTime() > 30000) {
expired.add(cache);
}
// If of the correct type, and not expired, use the cache
if (cache.gameMode() == gameMode && cache.recordType() == recordType) {
cachedRecords = cache.records();
break;
}
}
existingCaches.removeAll(expired);
// If not found, generate and cache the specified record
if (cachedRecords == null) {
cachedRecords = recordProvider.get();
existingCaches.add(new GroupRecordCache<>(gameMode, recordType, cachedRecords, System.currentTimeMillis()));
}
return getRecord(cachedRecords, recordNumber);
}
/**
* Gets a piece of record information from a dropper arena
*
* @param arenaHandler <p>The arena handler to get the arena from</p>
* @param identifier <p>The identifier (name/uuid) selecting the arena</p>
* @param gameMode <p>The game-mode to get a record for</p>
* @param recordType <p>The type of record to get</p>
* @param recordNumber <p>The placing of the record to get (1st place, 2nd place, etc.)</p>
* @param infoType <p>The type of info (player, value, combined) to get</p>
* @return <p>The selected information about the record, or null if not found</p>
*/
private @Nullable String getArenaRecord(@NotNull DropperArenaHandler arenaHandler, @NotNull String identifier,
@NotNull 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,21 @@
package net.knarcraft.dropper.placeholder;
import net.knarcraft.dropper.arena.ArenaGameMode;
import net.knarcraft.dropper.arena.record.ArenaRecord;
import net.knarcraft.dropper.property.RecordType;
import org.jetbrains.annotations.NotNull;
import java.util.Set;
/**
* A record for keeping track of records for a dropper group
*
* @param gameMode <p>The game-mode this cache is storing records for</p>
* @param recordType <p>The type of record stored</p>
* @param records <p>The stored records</p>
* @param createdTime <p>The time this cache was created</p>
*/
public record GroupRecordCache<K extends Comparable<K>>(@NotNull ArenaGameMode gameMode, @NotNull RecordType recordType,
@NotNull Set<ArenaRecord<K>> records,
@NotNull Long createdTime) {
}

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

View File

@ -1,14 +1,14 @@
package net.knarcraft.dropper.util;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.ArenaGameMode;
import net.knarcraft.dropper.arena.ArenaStorageKey;
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;
import org.bukkit.configuration.ConfigurationSection;
@ -154,7 +154,7 @@ public final class ArenaStorageHelper {
ArenaStorageKey.WIN_BLOCK_TYPE.getKey());
if (arenaName == null || spawnLocation == null) {
Dropper.getInstance().getLogger().log(Level.SEVERE, "Could not load the arena at configuration " +
Dropper.log(Level.SEVERE, "Could not load the arena at configuration " +
"section " + configurationSection.getName() + ". Please check the arenas storage file for issues.");
return null;
}
@ -164,7 +164,7 @@ public final class ArenaStorageHelper {
DropperArenaData arenaData = loadArenaData(arenaId);
if (arenaData == null) {
Dropper.getInstance().getLogger().log(Level.SEVERE, "Unable to load arena data for " + arenaId);
Dropper.log(Level.SEVERE, "Unable to load arena data for " + arenaId);
Map<ArenaGameMode, DropperArenaRecordsRegistry> recordRegistries = new HashMap<>();
for (ArenaGameMode arenaGameMode : ArenaGameMode.values()) {
@ -174,7 +174,7 @@ public final class ArenaStorageHelper {
}
return new DropperArena(arenaId, arenaName, spawnLocation, exitLocation, verticalVelocity, horizontalVelocity,
winBlockType.material(), arenaData);
winBlockType.material(), arenaData, Dropper.getInstance().getArenaHandler());
}
/**
@ -201,6 +201,16 @@ public final class ArenaStorageHelper {
return (DropperArenaData) configuration.get(ArenaStorageKey.DATA.getKey());
}
/**
* Removes data for the arena with the given id
*
* @param arenaId <p>The id of the arena to remove data for</p>
* @return <p>True if the data was successfully removed</p>
*/
public static boolean removeArenaData(@NotNull UUID arenaId) {
return getArenaDataFile(arenaId).delete();
}
/**
* Gets the file used to store the given arena id's data
*
@ -210,7 +220,7 @@ public final class ArenaStorageHelper {
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");
Dropper.log(Level.SEVERE, "Unable to create the arena data directories");
}
return arenaDataFile;
}

View File

@ -0,0 +1,171 @@
package net.knarcraft.dropper.util;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.ArenaGameMode;
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 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

@ -1,8 +1,8 @@
package net.knarcraft.dropper.util;
import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.ArenaEditableProperty;
import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.property.ArenaEditableProperty;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;

View File

@ -0,0 +1,59 @@
# Configuration values for droppers
dropper:
# Whether to block using the shift key to drop faster than the intended drop speed
blockSneaking: true
# Whether to block using the sprint key for slightly improved air speed
blockSprinting: true
# The vertical velocity used as default for all arenas. Must be greater than 0. 3.92 is the max speed of a falling
# player.
verticalVelocity: 1.0
# The horizontal velocity used as default for all arenas (technically fly-speed). Must be between 0 (exclusive) and 1
# (inclusive).
horizontalVelocity: 1.0
# The number of seconds before the randomly inverted game-mode switches between normal and inverted movement (0, 3600]
randomlyInvertedTimer: 7
# Whether grouped dropper arenas must be played in the correct sequence
mustDoGroupedInSequence: true
# Whether records won't be registered unless the player has already beaten all arenas in a group. That means players
# are required to do a second play-through to register a record for a grouped arena.
ignoreRecordsUntilGroupBeatenOnce: false
# Whether a player must do the normal/default game-mode before playing any other game-modes
mustDoNormalModeFirst: true
# Whether players should be made invisible while playing in a dropper arena
makePlayersInvisible: false
# Whether players should have their entity hit collision disabled while in an arena. This prevents players from
# pushing each-other if in the same arena.
disableHitCollision: true
# This decides how far inside a non-solid block the player must go before detection triggers (-1, 0). The closer to -1
# it is, the more accurate it will seem to the player, but the likelihood of not detecting the hit increases.
liquidHitBoxDepth: -0.8
# This decides the distance the player must be from a block below them before a hit triggers (0, 1). If too low, the
# likelihood of detecting the hit decreases, but it won't look like the player hit the block without being near.
solidHitBoxDistance: 0.2
# A whitelist for which blocks won't trigger a loss when hit/passed through. The win block check happens before the
# loss check, so even blocks on the whitelist can be used as the win-block. "+" denotes a material tag.
blockWhitelist:
- WATER
- LAVA
- +WALL_SIGNS
- +STANDING_SIGNS
- STRUCTURE_VOID
- WALL_TORCH
- SOUL_WALL_TORCH
- REDSTONE_WALL_TORCH
- +BANNERS
- +BUTTONS
- +CORALS
- +WALL_CORALS

View File

@ -3,6 +3,8 @@ 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: