Add persistent score archives
This commit is contained in:
@@ -21,6 +21,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -31,6 +32,7 @@ import java.util.logging.Level;
|
||||
|
||||
import com.majinnaibu.minecraft.plugins.scorekeeper.commands.ScoreAddCommand;
|
||||
import com.majinnaibu.minecraft.plugins.scorekeeper.commands.ScoreArchiveCommand;
|
||||
import com.majinnaibu.minecraft.plugins.scorekeeper.commands.ScoreArchiveListCommand;
|
||||
import com.majinnaibu.minecraft.plugins.scorekeeper.commands.ScoreBucketCommand;
|
||||
import com.majinnaibu.minecraft.plugins.scorekeeper.commands.ScoreGetCommand;
|
||||
import com.majinnaibu.minecraft.plugins.scorekeeper.commands.ScoreResetCommand;
|
||||
@@ -51,6 +53,7 @@ public class ScoreKeeperPlugin extends JavaPlugin {
|
||||
private final Map<String, Map<UUID, Integer>> _bucketScores = new LinkedHashMap<>();
|
||||
private final Map<UUID, String> _playerBuckets = new LinkedHashMap<>();
|
||||
private final Map<String, HighScoreRun> _highScoreRuns = new LinkedHashMap<>();
|
||||
private final Map<String, ScoreArchive> _scoreArchives = new LinkedHashMap<>();
|
||||
private String _defaultBucketId = "points";
|
||||
public final String _logPrefix = "[ScoreKeeper] ";
|
||||
public final Component _messagePrefix =
|
||||
@@ -92,6 +95,7 @@ public class ScoreKeeperPlugin extends JavaPlugin {
|
||||
} catch (IOException ex) {
|
||||
logError(ex);
|
||||
}
|
||||
saveArchives();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,12 +105,14 @@ public class ScoreKeeperPlugin extends JavaPlugin {
|
||||
getCommand("score-subtract").setExecutor(new ScoreSubtractCommand(this));
|
||||
getCommand("score-reset").setExecutor(new ScoreResetCommand(this));
|
||||
getCommand("score-archive").setExecutor(new ScoreArchiveCommand(this));
|
||||
getCommand("score-archive-list").setExecutor(new ScoreArchiveListCommand(this));
|
||||
getCommand("score-bucket").setExecutor(new ScoreBucketCommand(this));
|
||||
getCommand("score-run").setExecutor(new ScoreRunCommand(this));
|
||||
|
||||
_buckets.put("points", new ScoreBucket("points", "point", "points", 0));
|
||||
_bucketScores.put("points", new LinkedHashMap<>());
|
||||
loadScores();
|
||||
loadArchives();
|
||||
checkHighScoreRuns();
|
||||
getServer().getScheduler().runTaskTimer(this, this::checkHighScoreRuns, 20L, 20L);
|
||||
|
||||
@@ -124,7 +130,30 @@ public class ScoreKeeperPlugin extends JavaPlugin {
|
||||
}
|
||||
|
||||
public void archiveScore(Player player) {
|
||||
logWarning("Unable to archive score for " + player.getName() + ".");
|
||||
archivePlayerScore(player);
|
||||
}
|
||||
|
||||
public String archivePlayerScore(Player player) {
|
||||
String bucketId = getPlayerBucket(player);
|
||||
return createScoreArchive(
|
||||
"player " + player.getName() + " in " + bucketId, List.of(bucketId), player);
|
||||
}
|
||||
|
||||
public String archiveBucketScores(String bucketId) {
|
||||
requireBucket(bucketId);
|
||||
return createScoreArchive("bucket " + bucketId, List.of(bucketId), null);
|
||||
}
|
||||
|
||||
public String archiveAllScores() {
|
||||
return createScoreArchive("all buckets", new ArrayList<>(_buckets.keySet()), null);
|
||||
}
|
||||
|
||||
public List<ScoreArchive> getScoreArchives() {
|
||||
return List.copyOf(_scoreArchives.values());
|
||||
}
|
||||
|
||||
public ScoreArchive getScoreArchive(String archiveId) {
|
||||
return _scoreArchives.get(archiveId);
|
||||
}
|
||||
|
||||
public int getScore(Player player) {
|
||||
@@ -298,6 +327,35 @@ public class ScoreKeeperPlugin extends JavaPlugin {
|
||||
"You are now tracking " + bucket.getId() + ". You have " + score + " " + unit + "."));
|
||||
}
|
||||
|
||||
private String createScoreArchive(String scope, List<String> bucketIds, Player player) {
|
||||
for (String bucketId : bucketIds) {
|
||||
if (_highScoreRuns.containsKey(bucketId)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Stop the active high-score run for " + bucketId + " before archiving it");
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Map<UUID, Integer>> archivedScores = new LinkedHashMap<>();
|
||||
UUID playerId = player == null ? null : player.getUniqueId();
|
||||
for (String bucketId : bucketIds) {
|
||||
ScoreBucket bucket = requireBucket(bucketId);
|
||||
Map<UUID, Integer> bucketScores = _bucketScores.get(bucketId);
|
||||
if (playerId == null) {
|
||||
archivedScores.put(bucketId, new LinkedHashMap<>(bucketScores));
|
||||
bucketScores.replaceAll((uuid, score) -> bucket.getInitialValue());
|
||||
} else {
|
||||
int score = getScore(player, bucketId);
|
||||
archivedScores.put(bucketId, Map.of(playerId, score));
|
||||
bucketScores.put(playerId, bucket.getInitialValue());
|
||||
}
|
||||
}
|
||||
|
||||
String archiveId = UUID.randomUUID().toString();
|
||||
_scoreArchives.put(
|
||||
archiveId, new ScoreArchive(archiveId, scope, System.currentTimeMillis(), archivedScores));
|
||||
return archiveId;
|
||||
}
|
||||
|
||||
private void startHighScoreRun(
|
||||
String bucketId,
|
||||
int topN,
|
||||
@@ -514,6 +572,62 @@ public class ScoreKeeperPlugin extends JavaPlugin {
|
||||
loadHighScoreRuns(scores);
|
||||
}
|
||||
|
||||
private void loadArchives() {
|
||||
File archiveFile = new File(getDataFolder(), "archives.yml");
|
||||
if (!archiveFile.isFile()) {
|
||||
return;
|
||||
}
|
||||
YamlConfiguration archives = YamlConfiguration.loadConfiguration(archiveFile);
|
||||
ConfigurationSection archiveSection = archives.getConfigurationSection("archives");
|
||||
if (archiveSection == null) {
|
||||
return;
|
||||
}
|
||||
for (String archiveId : archiveSection.getKeys(false)) {
|
||||
String path = "archives." + archiveId;
|
||||
Map<String, Map<UUID, Integer>> bucketScores = new LinkedHashMap<>();
|
||||
ConfigurationSection buckets = archives.getConfigurationSection(path + ".buckets");
|
||||
if (buckets != null) {
|
||||
for (String bucketId : buckets.getKeys(false)) {
|
||||
Map<UUID, Integer> playerScores = new LinkedHashMap<>();
|
||||
ConfigurationSection savedScores =
|
||||
archives.getConfigurationSection(path + ".buckets." + bucketId + ".scores");
|
||||
if (savedScores != null) {
|
||||
loadPlayerScores(savedScores, playerScores);
|
||||
}
|
||||
bucketScores.put(bucketId, playerScores);
|
||||
}
|
||||
}
|
||||
_scoreArchives.put(
|
||||
archiveId,
|
||||
new ScoreArchive(
|
||||
archiveId,
|
||||
archives.getString(path + ".scope", "unknown"),
|
||||
archives.getLong(path + ".created-at"),
|
||||
bucketScores));
|
||||
}
|
||||
}
|
||||
|
||||
private void saveArchives() {
|
||||
YamlConfiguration archives = new YamlConfiguration();
|
||||
for (ScoreArchive archive : _scoreArchives.values()) {
|
||||
String path = "archives." + archive.getId();
|
||||
archives.set(path + ".scope", archive.getScope());
|
||||
archives.set(path + ".created-at", archive.getCreatedAtMillis());
|
||||
for (var bucketEntry : archive.getBucketScores().entrySet()) {
|
||||
for (var scoreEntry : bucketEntry.getValue().entrySet()) {
|
||||
archives.set(
|
||||
path + ".buckets." + bucketEntry.getKey() + ".scores." + scoreEntry.getKey(),
|
||||
scoreEntry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
archives.save(new File(getDataFolder(), "archives.yml"));
|
||||
} catch (IOException ex) {
|
||||
logError(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadLegacyScores(YamlConfiguration scores) {
|
||||
loadPlayerScores(scores, _bucketScores.get("points"));
|
||||
}
|
||||
@@ -638,6 +752,44 @@ public class ScoreKeeperPlugin extends JavaPlugin {
|
||||
|
||||
private record ScoreEntry(UUID playerId, String playerName, int score) {}
|
||||
|
||||
public static final class ScoreArchive {
|
||||
private final String _id;
|
||||
private final String _scope;
|
||||
private final long _createdAtMillis;
|
||||
private final Map<String, Map<UUID, Integer>> _bucketScores;
|
||||
|
||||
private ScoreArchive(
|
||||
String id,
|
||||
String scope,
|
||||
long createdAtMillis,
|
||||
Map<String, Map<UUID, Integer>> bucketScores) {
|
||||
_id = id;
|
||||
_scope = scope;
|
||||
_createdAtMillis = createdAtMillis;
|
||||
Map<String, Map<UUID, Integer>> copiedScores = new LinkedHashMap<>();
|
||||
for (var entry : bucketScores.entrySet()) {
|
||||
copiedScores.put(entry.getKey(), Map.copyOf(entry.getValue()));
|
||||
}
|
||||
_bucketScores = Collections.unmodifiableMap(copiedScores);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return _id;
|
||||
}
|
||||
|
||||
public String getScope() {
|
||||
return _scope;
|
||||
}
|
||||
|
||||
public long getCreatedAtMillis() {
|
||||
return _createdAtMillis;
|
||||
}
|
||||
|
||||
public Map<String, Map<UUID, Integer>> getBucketScores() {
|
||||
return _bucketScores;
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Logging
|
||||
|
||||
@@ -20,10 +20,12 @@ package com.majinnaibu.minecraft.plugins.scorekeeper.commands;
|
||||
import com.majinnaibu.minecraft.plugins.scorekeeper.ScoreKeeperPlugin;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
public class ScoreArchiveCommand implements CommandExecutor {
|
||||
private final ScoreKeeperPlugin _plugin;
|
||||
@@ -34,7 +36,45 @@ public class ScoreArchiveCommand implements CommandExecutor {
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] split) {
|
||||
_plugin.sendMessage(sender, Component.text("archive command unimplemented"));
|
||||
if (!sender.hasPermission("scorekeeper.admin")) {
|
||||
sendError(sender, "You do not have permission to archive scores");
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
String archiveId;
|
||||
if (split.length == 1 && split[0].equalsIgnoreCase("all")) {
|
||||
archiveId = _plugin.archiveAllScores();
|
||||
} else if (split.length == 2 && split[0].equalsIgnoreCase("bucket")) {
|
||||
archiveId = _plugin.archiveBucketScores(split[1]);
|
||||
} else if (split.length == 0 && sender instanceof Player player) {
|
||||
archiveId = _plugin.archivePlayerScore(player);
|
||||
} else if (split.length == 1) {
|
||||
Player target = _plugin.getServer().getPlayerExact(split[0]);
|
||||
if (target == null) {
|
||||
sendError(sender, "Can't find a player with that name");
|
||||
return true;
|
||||
}
|
||||
archiveId = _plugin.archivePlayerScore(target);
|
||||
} else {
|
||||
sendUsage(sender);
|
||||
return true;
|
||||
}
|
||||
_plugin.sendMessage(sender, Component.text("Saved score archive " + archiveId + "."));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
sendError(sender, ex.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void sendError(CommandSender sender, String message) {
|
||||
_plugin.sendMessage(sender, Component.text(message).color(NamedTextColor.RED));
|
||||
}
|
||||
|
||||
private void sendUsage(CommandSender sender) {
|
||||
_plugin.sendMessage(
|
||||
sender,
|
||||
Component.text("Usage: /score-archive [playerName|all] | /score-archive bucket <bucketId>")
|
||||
.color(NamedTextColor.YELLOW));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
This file is part of ScoreKeeper.
|
||||
|
||||
ScoreKeeper is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
ScoreKeeper is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with ScoreKeeper. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
|
||||
*/
|
||||
|
||||
package com.majinnaibu.minecraft.plugins.scorekeeper.commands;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.majinnaibu.minecraft.plugins.scorekeeper.ScoreBucket;
|
||||
import com.majinnaibu.minecraft.plugins.scorekeeper.ScoreKeeperPlugin;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandExecutor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
public class ScoreArchiveListCommand implements CommandExecutor {
|
||||
private final ScoreKeeperPlugin _plugin;
|
||||
|
||||
public ScoreArchiveListCommand(ScoreKeeperPlugin plugin) {
|
||||
_plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
if (args.length == 0) {
|
||||
var archives = _plugin.getScoreArchives();
|
||||
if (archives.isEmpty()) {
|
||||
_plugin.sendMessage(sender, Component.text("No score archives have been saved."));
|
||||
} else {
|
||||
archives.stream()
|
||||
.sorted(
|
||||
Comparator.comparingLong(
|
||||
(ScoreKeeperPlugin.ScoreArchive archive) -> archive.getCreatedAtMillis())
|
||||
.reversed())
|
||||
.forEach(
|
||||
archive ->
|
||||
_plugin.sendMessage(
|
||||
sender,
|
||||
Component.text(
|
||||
archive.getId()
|
||||
+ " | "
|
||||
+ archive.getScope()
|
||||
+ " | "
|
||||
+ Instant.ofEpochMilli(archive.getCreatedAtMillis()))));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.length != 1) {
|
||||
sendUsage(sender);
|
||||
return true;
|
||||
}
|
||||
ScoreKeeperPlugin.ScoreArchive archive = _plugin.getScoreArchive(args[0]);
|
||||
if (archive == null) {
|
||||
_plugin.sendMessage(
|
||||
sender,
|
||||
Component.text("No score archive exists with ID " + args[0] + ".")
|
||||
.color(NamedTextColor.RED));
|
||||
return true;
|
||||
}
|
||||
|
||||
_plugin.sendMessage(
|
||||
sender,
|
||||
Component.text(
|
||||
"Archive "
|
||||
+ archive.getId()
|
||||
+ " ("
|
||||
+ archive.getScope()
|
||||
+ ", "
|
||||
+ Instant.ofEpochMilli(archive.getCreatedAtMillis())
|
||||
+ ")"));
|
||||
for (var bucketEntry : archive.getBucketScores().entrySet()) {
|
||||
ScoreBucket bucket = _plugin.getBucket(bucketEntry.getKey());
|
||||
_plugin.sendMessage(sender, Component.text("Bucket " + bucketEntry.getKey() + ":"));
|
||||
bucketEntry.getValue().entrySet().stream()
|
||||
.sorted(
|
||||
Map.Entry.<UUID, Integer>comparingByValue()
|
||||
.reversed()
|
||||
.thenComparing(entry -> entry.getKey().toString()))
|
||||
.forEach(
|
||||
entry ->
|
||||
_plugin.sendMessage(
|
||||
sender,
|
||||
Component.text(
|
||||
displayName(entry.getKey())
|
||||
+ " - "
|
||||
+ entry.getValue()
|
||||
+ " "
|
||||
+ scoreUnit(bucket, entry.getValue()))));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String displayName(UUID playerId) {
|
||||
OfflinePlayer player = _plugin.getServer().getOfflinePlayer(playerId);
|
||||
return player.getName() == null ? playerId.toString() : player.getName();
|
||||
}
|
||||
|
||||
private String scoreUnit(ScoreBucket bucket, int score) {
|
||||
if (bucket == null) {
|
||||
return "scores";
|
||||
}
|
||||
return score == 1 ? bucket.getSingular() : bucket.getPlural();
|
||||
}
|
||||
|
||||
private void sendUsage(CommandSender sender) {
|
||||
_plugin.sendMessage(
|
||||
sender,
|
||||
Component.text("Usage: /score-archive-list [archiveId]").color(NamedTextColor.YELLOW));
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,9 @@ commands:
|
||||
score-archive:
|
||||
description: Archives and resets the player's score
|
||||
usage: /score-archive [playername]
|
||||
score-archive-list:
|
||||
description: Lists saved score archives or displays one archive.
|
||||
usage: /score-archive-list [archiveId]
|
||||
score-bucket:
|
||||
description: Selects or manages score buckets.
|
||||
usage: /score-bucket [bucketId] | create <id> <singular> <plural> <initialValue> [admin|global|player|none] | <player|all> <bucketId>
|
||||
|
||||
Reference in New Issue
Block a user