9 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
8e9b274fc0 Adds unfinished code for group record placeholders 2023-04-07 00:17:57 +02:00
12 changed files with 485 additions and 109 deletions

View File

@ -19,21 +19,21 @@ To modify
## Commands ## Commands
| Command | Alias | Arguments | Description | | Command | Alias | Arguments | Description |
|-----------------------------------------|----------|-----------------------------|-------------------------------------------------------------------------------------| |----------------------------------------|----------|-----------------------------|-------------------------------------------------------------------------------------|
| /dropperList | /dlist | | Lists available dropper arenas. | | /dropperList | /dlist | | Lists available dropper arenas. |
| [/dropperJoin](#/dropperJoin) | /djoin | \<arena> \[mode] | Joins the selected arena. | | [/dropperJoin](#dropperJoin) | /djoin | \<arena> \[mode] | Joins the selected arena. |
| /dropperLeave | /dleave | | Leaves the current dropper 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. | | /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. | | /dropperRemove | /dremove | \<arena> | Removes the specified dropper arena. |
| [/dropperEdit](#/dropperEdit) | /dedit | \<arena> \<option> \[value] | Gets or sets a dropper arena option. | | [/dropperEdit](#dropperEdit) | /dedit | \<arena> \<option> \[value] | Gets or sets a dropper arena option. |
| /dropperReload | /dreload | | Reloads all data from disk. | | /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. | | [/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. | | /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. | | [/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. 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. | | 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. | | 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.
@ -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. | | 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. | | 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 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 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. moved to the new group.
### /dropperGroupSwap #### /dropperGroupSwap
This command is used for changing the order of arenas within a group. This command is used for changing the order of arenas within a group.
@ -99,10 +99,11 @@ You could use `/droppergroupswap Sea Savanna` to change the order to:
3. Nether 3. Nether
4. Sea 4. Sea
## Record display placeholders ## Record placeholders
Player records can be displayed on a leaderboard by using PlaceholderAPI. The format for the built-in placeholders is as Player records can be displayed on a leaderboard by using PlaceholderAPI. If you want to display a sign-based
follows: 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%` `%dropper_record_recordType_gameModeType_identifierType_identifier_recordPlacing_infoType%`
@ -111,7 +112,7 @@ follows:
| dropper_record | | Denotes that it's a placeholder for a dropper record. Must be present as-is. | | 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). | | 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. | | gameModeType | default / inverted / random | Selects the game-mode to get the record for. |
| identifierType | arena | This specifies that the following string is an arena identifier. | | 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). | | 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.). | | 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". | | 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". |

View File

@ -1,12 +1,13 @@
package net.knarcraft.dropper; package net.knarcraft.dropper;
import net.knarcraft.dropper.arena.ArenaRecord;
import net.knarcraft.dropper.arena.DropperArenaData; import net.knarcraft.dropper.arena.DropperArenaData;
import net.knarcraft.dropper.arena.DropperArenaGroup; import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaHandler; import net.knarcraft.dropper.arena.DropperArenaHandler;
import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry; import net.knarcraft.dropper.arena.DropperArenaPlayerRegistry;
import net.knarcraft.dropper.arena.DropperArenaRecordsRegistry; import net.knarcraft.dropper.arena.DropperArenaRecordsRegistry;
import net.knarcraft.dropper.arena.DropperArenaSession; 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.CreateArenaCommand;
import net.knarcraft.dropper.command.EditArenaCommand; import net.knarcraft.dropper.command.EditArenaCommand;
import net.knarcraft.dropper.command.EditArenaTabCompleter; import net.knarcraft.dropper.command.EditArenaTabCompleter;
@ -97,7 +98,8 @@ public final class Dropper extends JavaPlugin {
ConfigurationSerialization.registerClass(DropperArenaData.class); ConfigurationSerialization.registerClass(DropperArenaData.class);
ConfigurationSerialization.registerClass(DropperArenaGroup.class); ConfigurationSerialization.registerClass(DropperArenaGroup.class);
ConfigurationSerialization.registerClass(ArenaGameMode.class); ConfigurationSerialization.registerClass(ArenaGameMode.class);
ConfigurationSerialization.registerClass(ArenaRecord.class); ConfigurationSerialization.registerClass(LongRecord.class);
ConfigurationSerialization.registerClass(IntegerRecord.class);
} }
@Override @Override

View File

@ -1,52 +0,0 @@
package net.knarcraft.dropper.arena;
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.UUID;
/**
* A record stored for an arena
*
* @param userId <p>The id of the player that achieved the record</p>
* @param record <p>The record achieved</p>
* @param <K> <p>The comparable type of the record</p>
*/
public record ArenaRecord<K extends Comparable<K>>(UUID userId, K record) implements Comparable<ArenaRecord<K>>,
ConfigurationSerializable {
@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(userId()));
data.put("record", record);
return data;
}
/**
* Deserializes the saved arena record
*
* @param data <p>The data to deserialize</p>
* @param <K> <p>The type of the deserialized record</p>
* @return <p>The deserialized data</p>
*/
@SuppressWarnings({"unused", "unchecked"})
public static <K extends Comparable<K>> ArenaRecord<K> deserialize(@NotNull Map<String, Object> data) {
return new ArenaRecord<>(((SerializableUUID) data.get("userId")).uuid(), (K) data.get("record"));
}
}

View File

@ -103,6 +103,12 @@ public record DropperArenaData(@NotNull UUID arenaId,
Map<ArenaGameMode, Set<SerializableUUID>> playersCompletedData = Map<ArenaGameMode, Set<SerializableUUID>> playersCompletedData =
(Map<ArenaGameMode, Set<SerializableUUID>>) data.get("playersCompleted"); (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 // Convert the serializable UUIDs to normal UUIDs
Map<ArenaGameMode, Set<UUID>> allPlayersCompleted = new HashMap<>(); Map<ArenaGameMode, Set<UUID>> allPlayersCompleted = new HashMap<>();
for (ArenaGameMode arenaGameMode : playersCompletedData.keySet()) { for (ArenaGameMode arenaGameMode : playersCompletedData.keySet()) {
@ -111,6 +117,10 @@ public record DropperArenaData(@NotNull UUID arenaId,
playersCompleted.add(completedId.uuid()); playersCompleted.add(completedId.uuid());
} }
allPlayersCompleted.put(arenaGameMode, playersCompleted); 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); return new DropperArenaData(serializableUUID.uuid(), recordsRegistry, allPlayersCompleted);
} }

View File

@ -160,7 +160,7 @@ public class DropperArenaGroup implements ConfigurationSerializable {
for (UUID anArenaId : this.getArenas()) { for (UUID anArenaId : this.getArenas()) {
// If the target arena is reached, allow, as all previous arenas must have been cleared // If the target arena is reached, allow, as all previous arenas must have been cleared
if (arenaId == anArenaId) { if (arenaId.equals(anArenaId)) {
return true; return true;
} }

View File

@ -1,17 +1,24 @@
package net.knarcraft.dropper.arena; package net.knarcraft.dropper.arena;
import net.knarcraft.dropper.Dropper; 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.container.SerializableUUID;
import net.knarcraft.dropper.property.RecordResult; import net.knarcraft.dropper.property.RecordResult;
import org.bukkit.configuration.serialization.ConfigurationSerializable; import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
/** /**
* A registry keeping track of all records * A registry keeping track of all records
@ -19,8 +26,8 @@ import java.util.concurrent.atomic.AtomicReference;
public class DropperArenaRecordsRegistry implements ConfigurationSerializable { public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
private final UUID arenaId; private final UUID arenaId;
private final @NotNull Set<ArenaRecord<Integer>> leastDeaths; private final @NotNull Set<IntegerRecord> leastDeaths;
private final @NotNull Set<ArenaRecord<Long>> shortestTimeMilliSeconds; private final @NotNull Set<LongRecord> shortestTimeMilliSeconds;
/** /**
* Instantiates a new empty records registry * Instantiates a new empty records registry
@ -37,8 +44,8 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
* @param leastDeaths <p>The existing least death records to use</p> * @param leastDeaths <p>The existing least death records to use</p>
* @param shortestTimeMilliSeconds <p>The existing leash time records to use</p> * @param shortestTimeMilliSeconds <p>The existing leash time records to use</p>
*/ */
private DropperArenaRecordsRegistry(@NotNull UUID arenaId, @NotNull Set<ArenaRecord<Integer>> leastDeaths, private DropperArenaRecordsRegistry(@NotNull UUID arenaId, @NotNull Set<IntegerRecord> leastDeaths,
@NotNull Set<ArenaRecord<Long>> shortestTimeMilliSeconds) { @NotNull Set<LongRecord> shortestTimeMilliSeconds) {
this.arenaId = arenaId; this.arenaId = arenaId;
this.leastDeaths = new HashSet<>(leastDeaths); this.leastDeaths = new HashSet<>(leastDeaths);
this.shortestTimeMilliSeconds = new HashSet<>(shortestTimeMilliSeconds); this.shortestTimeMilliSeconds = new HashSet<>(shortestTimeMilliSeconds);
@ -49,7 +56,7 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
* *
* @return <p>Existing death records</p> * @return <p>Existing death records</p>
*/ */
public Set<ArenaRecord<Integer>> getLeastDeathsRecords() { public Set<SummableArenaRecord<Integer>> getLeastDeathsRecords() {
return new HashSet<>(this.leastDeaths); return new HashSet<>(this.leastDeaths);
} }
@ -58,7 +65,7 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
* *
* @return <p>Existing time records</p> * @return <p>Existing time records</p>
*/ */
public Set<ArenaRecord<Long>> getShortestTimeMilliSecondsRecords() { public Set<SummableArenaRecord<Long>> getShortestTimeMilliSecondsRecords() {
return new HashSet<>(this.shortestTimeMilliSeconds); return new HashSet<>(this.shortestTimeMilliSeconds);
} }
@ -70,7 +77,9 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
* @return <p>The result explaining what type of record was achieved</p> * @return <p>The result explaining what type of record was achieved</p>
*/ */
public @NotNull RecordResult registerDeathRecord(@NotNull UUID playerId, int deaths) { public @NotNull RecordResult registerDeathRecord(@NotNull UUID playerId, int deaths) {
return registerRecord(leastDeaths, playerId, deaths); Consumer<Integer> consumer = (value) -> leastDeaths.add(new IntegerRecord(playerId, value));
Set<ArenaRecord<Integer>> asInt = new HashSet<>(leastDeaths);
return registerRecord(asInt, consumer, playerId, deaths);
} }
/** /**
@ -81,7 +90,9 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
* @return <p>The result explaining what type of record was achieved</p> * @return <p>The result explaining what type of record was achieved</p>
*/ */
public @NotNull RecordResult registerTimeRecord(@NotNull UUID playerId, long milliseconds) { public @NotNull RecordResult registerTimeRecord(@NotNull UUID playerId, long milliseconds) {
return registerRecord(shortestTimeMilliSeconds, playerId, milliseconds); Consumer<Long> consumer = (value) -> shortestTimeMilliSeconds.add(new LongRecord(playerId, value));
Set<ArenaRecord<Long>> asLong = new HashSet<>(shortestTimeMilliSeconds);
return registerRecord(asLong, consumer, playerId, milliseconds);
} }
/** /**
@ -95,30 +106,33 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
* Registers a new record if applicable * Registers a new record if applicable
* *
* @param existingRecords <p>The map of existing records to use</p> * @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 playerId <p>The id of the player that potentially achieved a record</p>
* @param amount <p>The amount of whatever the player achieved</p> * @param amount <p>The amount of whatever the player achieved</p>
* @return <p>The result of the player's record attempt</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, private <T extends Comparable<T>> @NotNull RecordResult registerRecord(@NotNull Set<ArenaRecord<T>> existingRecords,
@NotNull Consumer<T> recordSetter,
@NotNull UUID playerId, T amount) { @NotNull UUID playerId, T amount) {
RecordResult result; RecordResult result;
if (existingRecords.stream().allMatch((entry) -> amount.compareTo(entry.record()) < 0)) { 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! // If the given value is less than all other values, that's a world record!
result = RecordResult.WORLD_RECORD; result = RecordResult.WORLD_RECORD;
existingRecords.add(new ArenaRecord<>(playerId, amount)); recordSetter.accept(amount);
save(); save();
return result; return result;
} }
ArenaRecord<T> playerRecord = getRecord(existingRecords, playerId); ArenaRecord<T> playerRecord = getRecord(existingRecords, playerId);
if (playerRecord != null && amount.compareTo(playerRecord.record()) < 0) { 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! // If the given value is less than the player's previous value, that's a personal best!
result = RecordResult.PERSONAL_BEST; result = RecordResult.PERSONAL_BEST;
existingRecords.add(new ArenaRecord<>(playerId, amount)); recordSetter.accept(amount);
save(); save();
} else { } else {
// Make sure to save the record if this is the user's first attempt // Make sure to save the record if this is the user's first attempt
if (playerRecord == null) { if (playerRecord == null) {
recordSetter.accept(amount);
save(); save();
} }
result = RecordResult.NONE; result = RecordResult.NONE;
@ -127,11 +141,19 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
return result; return result;
} }
private <T extends Comparable<T>> ArenaRecord<T> getRecord(@NotNull Set<ArenaRecord<T>> existingRecords, /**
* 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) { @NotNull UUID playerId) {
AtomicReference<ArenaRecord<T>> record = new AtomicReference<>(); AtomicReference<ArenaRecord<T>> record = new AtomicReference<>();
existingRecords.forEach((item) -> { existingRecords.forEach((item) -> {
if (item.userId().equals(playerId)) { if (item.getUserId().equals(playerId)) {
record.set(item); record.set(item);
} }
}); });
@ -157,10 +179,13 @@ public class DropperArenaRecordsRegistry implements ConfigurationSerializable {
@SuppressWarnings({"unused", "unchecked"}) @SuppressWarnings({"unused", "unchecked"})
public static DropperArenaRecordsRegistry deserialize(Map<String, Object> data) { public static DropperArenaRecordsRegistry deserialize(Map<String, Object> data) {
UUID arenaId = ((SerializableUUID) data.get("arenaId")).uuid(); UUID arenaId = ((SerializableUUID) data.get("arenaId")).uuid();
Set<ArenaRecord<Integer>> leastDeaths = Set<IntegerRecord> leastDeaths =
(Set<ArenaRecord<Integer>>) data.getOrDefault("leastDeaths", new HashMap<>()); (Set<IntegerRecord>) data.getOrDefault("leastDeaths", new HashMap<>());
Set<ArenaRecord<Long>> shortestTimeMilliseconds = Set<LongRecord> shortestTimeMilliseconds =
(Set<ArenaRecord<Long>>) data.getOrDefault("shortestTime", new HashMap<>()); (Set<LongRecord>) data.getOrDefault("shortestTime", new HashMap<>());
leastDeaths.removeIf(Objects::isNull);
shortestTimeMilliseconds.removeIf(Objects::isNull);
return new DropperArenaRecordsRegistry(arenaId, leastDeaths, shortestTimeMilliseconds); return new DropperArenaRecordsRegistry(arenaId, leastDeaths, shortestTimeMilliseconds);
} }

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,22 +2,24 @@ package net.knarcraft.dropper.placeholder;
import me.clip.placeholderapi.expansion.PlaceholderExpansion; import me.clip.placeholderapi.expansion.PlaceholderExpansion;
import net.knarcraft.dropper.Dropper; import net.knarcraft.dropper.Dropper;
import net.knarcraft.dropper.arena.ArenaRecord;
import net.knarcraft.dropper.arena.DropperArena; import net.knarcraft.dropper.arena.DropperArena;
import net.knarcraft.dropper.arena.DropperArenaGroup;
import net.knarcraft.dropper.arena.DropperArenaHandler; import net.knarcraft.dropper.arena.DropperArenaHandler;
import net.knarcraft.dropper.arena.DropperArenaRecordsRegistry; 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.InfoType;
import net.knarcraft.dropper.placeholder.parsing.RecordType; import net.knarcraft.dropper.placeholder.parsing.RecordType;
import net.knarcraft.dropper.placeholder.parsing.SelectionType; import net.knarcraft.dropper.placeholder.parsing.SelectionType;
import net.knarcraft.dropper.property.ArenaGameMode; import net.knarcraft.dropper.property.ArenaGameMode;
import net.knarcraft.dropper.util.DropperGroupRecordHelper;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -75,13 +77,55 @@ public class DropperRecordExpansion extends PlaceholderExpansion {
String info = null; String info = null;
DropperArenaHandler arenaHandler = plugin.getArenaHandler(); DropperArenaHandler arenaHandler = plugin.getArenaHandler();
if (selectionType == SelectionType.ARENA) { 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); info = getArenaRecord(arenaHandler, identifier, gameMode, recordType, recordNumber, infoType);
} }
return Objects.requireNonNullElse(info, parameters); 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 * Gets a piece of record information from a dropper arena
* *
@ -130,8 +174,8 @@ public class DropperRecordExpansion extends PlaceholderExpansion {
private @Nullable ArenaRecord<?> getRecord(@NotNull DropperArenaRecordsRegistry recordsRegistry, private @Nullable ArenaRecord<?> getRecord(@NotNull DropperArenaRecordsRegistry recordsRegistry,
@NotNull RecordType recordType, int recordNumber) { @NotNull RecordType recordType, int recordNumber) {
return switch (recordType) { return switch (recordType) {
case TIME -> getRecord(recordsRegistry.getShortestTimeMilliSecondsRecords(), recordNumber); case TIME -> getRecord(new HashSet<>(recordsRegistry.getShortestTimeMilliSecondsRecords()), recordNumber);
case DEATHS -> getRecord(recordsRegistry.getLeastDeathsRecords(), recordNumber); case DEATHS -> getRecord(new HashSet<>(recordsRegistry.getLeastDeathsRecords()), recordNumber);
}; };
} }
@ -161,9 +205,9 @@ public class DropperRecordExpansion extends PlaceholderExpansion {
*/ */
private String getRecordData(@NotNull InfoType infoType, @NotNull ArenaRecord<?> arenaRecord) { private String getRecordData(@NotNull InfoType infoType, @NotNull ArenaRecord<?> arenaRecord) {
return switch (infoType) { return switch (infoType) {
case PLAYER -> getPlayerName(arenaRecord.userId()); case PLAYER -> getPlayerName(arenaRecord.getUserId());
case VALUE -> arenaRecord.record().toString(); case VALUE -> arenaRecord.getRecord().toString();
case COMBINED -> getPlayerName(arenaRecord.userId()) + ": " + arenaRecord.record().toString(); case COMBINED -> getPlayerName(arenaRecord.getUserId()) + ": " + arenaRecord.getRecord().toString();
}; };
} }
@ -188,12 +232,7 @@ public class DropperRecordExpansion extends PlaceholderExpansion {
* @return <p>The name of the player, or a string representation of the UUID if not found</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) { private String getPlayerName(@NotNull UUID playerId) {
Player player = Bukkit.getPlayer(playerId); return Bukkit.getOfflinePlayer(playerId).getName();
if (player != null) {
return player.getName();
} else {
return playerId.toString();
}
} }
} }

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