Add configurable score buckets

This commit is contained in:
2026-09-26 18:18:00 -07:00
parent 46c93685ad
commit 1773361716
6 changed files with 367 additions and 30 deletions

View File

@@ -0,0 +1,57 @@
/*
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;
public final class ScoreBucket {
private final String _id;
private final String _singular;
private final String _plural;
private final int _initialValue;
public ScoreBucket(String id, String singular, String plural, int initialValue) {
if (id == null || !id.matches("[A-Za-z0-9_-]+")) {
throw new IllegalArgumentException("Bucket ID must contain only letters, numbers, _ or -");
}
if (singular == null || singular.isBlank()) {
throw new IllegalArgumentException("Bucket singular label cannot be blank");
}
if (plural == null || plural.isBlank()) {
throw new IllegalArgumentException("Bucket plural label cannot be blank");
}
_id = id;
_singular = singular;
_plural = plural;
_initialValue = initialValue;
}
public String getId() {
return _id;
}
public String getSingular() {
return _singular;
}
public String getPlural() {
return _plural;
}
public int getInitialValue() {
return _initialValue;
}
}

View File

@@ -19,12 +19,14 @@ package com.majinnaibu.minecraft.plugins.scorekeeper;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
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.ScoreBucketCommand;
import com.majinnaibu.minecraft.plugins.scorekeeper.commands.ScoreGetCommand;
import com.majinnaibu.minecraft.plugins.scorekeeper.commands.ScoreResetCommand;
import com.majinnaibu.minecraft.plugins.scorekeeper.commands.ScoreSubtractCommand;
@@ -33,12 +35,16 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
public class ScoreKeeperPlugin extends JavaPlugin {
private final HashMap<UUID, Integer> _playerScores = new HashMap<UUID, Integer>();
private final Map<String, ScoreBucket> _buckets = new LinkedHashMap<>();
private final Map<String, Map<UUID, Integer>> _bucketScores = new LinkedHashMap<>();
private final Map<UUID, String> _playerBuckets = new LinkedHashMap<>();
private String _defaultBucketId = "points";
public final String _logPrefix = "[ScoreKeeper] ";
public final Component _messagePrefix =
Component.text("[")
@@ -48,8 +54,19 @@ public class ScoreKeeperPlugin extends JavaPlugin {
@Override
public void onDisable() {
YamlConfiguration scores = new YamlConfiguration();
for (var entry : _playerScores.entrySet()) {
scores.set(entry.getKey().toString(), entry.getValue());
scores.set("version", 2);
scores.set("default-bucket", _defaultBucketId);
for (ScoreBucket bucket : _buckets.values()) {
String path = "buckets." + bucket.getId();
scores.set(path + ".singular", bucket.getSingular());
scores.set(path + ".plural", bucket.getPlural());
scores.set(path + ".initial-value", bucket.getInitialValue());
for (var entry : _bucketScores.get(bucket.getId()).entrySet()) {
scores.set(path + ".scores." + entry.getKey(), entry.getValue());
}
}
for (var entry : _playerBuckets.entrySet()) {
scores.set("player-buckets." + entry.getKey(), entry.getValue());
}
try {
scores.save(new File(getDataFolder(), "scores.yml"));
@@ -65,18 +82,11 @@ 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-bucket").setExecutor(new ScoreBucketCommand(this));
File scoreFile = new File(getDataFolder(), "scores.yml");
if (scoreFile.isFile()) {
YamlConfiguration scores = YamlConfiguration.loadConfiguration(scoreFile);
for (String key : scores.getKeys(false)) {
try {
_playerScores.put(UUID.fromString(key), scores.getInt(key));
} catch (IllegalArgumentException ex) {
logWarning("Ignoring score with invalid player UUID: " + key);
}
}
}
_buckets.put("points", new ScoreBucket("points", "point", "points", 0));
_bucketScores.put("points", new LinkedHashMap<>());
loadScores();
logInfo(
getPluginMeta().getName() + " version " + getPluginMeta().getVersion() + " is enabled.");
@@ -84,8 +94,11 @@ public class ScoreKeeperPlugin extends JavaPlugin {
// region Commands
public void addScore(Player player, int amount) {
int oldScore = getPlayerScore(player);
setPlayerScore(player, oldScore + amount);
addScore(player, getPlayerBucket(player), amount);
}
public void addScore(Player player, String bucketId, int amount) {
setScore(player, bucketId, getScore(player, bucketId) + amount);
}
public void archiveScore(Player player) {
@@ -93,20 +106,77 @@ public class ScoreKeeperPlugin extends JavaPlugin {
}
public int getScore(Player player) {
return getPlayerScore(player);
return getScore(player, getPlayerBucket(player));
}
public int getScore(Player player, String bucketId) {
ScoreBucket bucket = requireBucket(bucketId);
return _bucketScores
.get(bucketId)
.computeIfAbsent(player.getUniqueId(), uuid -> bucket.getInitialValue());
}
public void resetScore(Player player) {
setPlayerScore(player, 0);
resetScore(player, getPlayerBucket(player));
}
public void resetScore(Player player, String bucketId) {
setScore(player, bucketId, requireBucket(bucketId).getInitialValue());
}
public void setScore(Player player, int score) {
setPlayerScore(player, score);
setScore(player, getPlayerBucket(player), score);
}
public void setScore(Player player, String bucketId, int score) {
requireBucket(bucketId);
_bucketScores.get(bucketId).put(player.getUniqueId(), score);
}
public void subtractScore(Player player, int amount) {
int oldScore = getPlayerScore(player);
setPlayerScore(player, oldScore - amount);
subtractScore(player, getPlayerBucket(player), amount);
}
public void subtractScore(Player player, String bucketId, int amount) {
setScore(player, bucketId, getScore(player, bucketId) - amount);
}
public ScoreBucket createBucket(String id, String singular, String plural, int initialValue) {
ScoreBucket bucket = new ScoreBucket(id, singular, plural, initialValue);
if (_buckets.containsKey(id)) {
throw new IllegalArgumentException("A score bucket with ID " + id + " already exists");
}
_buckets.put(id, bucket);
_bucketScores.put(id, new LinkedHashMap<>());
return bucket;
}
public ScoreBucket getBucket(String bucketId) {
return _buckets.get(bucketId);
}
public Map<String, ScoreBucket> getBuckets() {
return Map.copyOf(_buckets);
}
public String getPlayerBucket(Player player) {
return _playerBuckets.getOrDefault(player.getUniqueId(), _defaultBucketId);
}
public void switchPlayerBucket(Player player, String bucketId) {
requireBucket(bucketId);
UUID playerId = player.getUniqueId();
if (bucketId.equals(_defaultBucketId)) {
_playerBuckets.remove(playerId);
} else {
_playerBuckets.put(playerId, bucketId);
}
}
public void switchAllPlayers(String bucketId) {
requireBucket(bucketId);
_defaultBucketId = bucketId;
_playerBuckets.clear();
}
// endregion
@@ -116,16 +186,84 @@ public class ScoreKeeperPlugin extends JavaPlugin {
reciever.sendMessage(_messagePrefix.append(message));
}
private int getPlayerScore(Player player) {
UUID uuid = player.getUniqueId();
if (!_playerScores.containsKey(uuid)) {
_playerScores.put(uuid, 0);
private ScoreBucket requireBucket(String bucketId) {
ScoreBucket bucket = _buckets.get(bucketId);
if (bucket == null) {
throw new IllegalArgumentException("Unknown score bucket: " + bucketId);
}
return _playerScores.get(uuid);
return bucket;
}
private void setPlayerScore(Player player, int score) {
_playerScores.put(player.getUniqueId(), score);
private void loadScores() {
File scoreFile = new File(getDataFolder(), "scores.yml");
if (!scoreFile.isFile()) {
return;
}
YamlConfiguration scores = YamlConfiguration.loadConfiguration(scoreFile);
ConfigurationSection bucketSection = scores.getConfigurationSection("buckets");
if (bucketSection == null) {
loadLegacyScores(scores);
return;
}
_buckets.clear();
_bucketScores.clear();
for (String id : bucketSection.getKeys(false)) {
try {
createBucket(
id,
bucketSection.getString(id + ".singular", id),
bucketSection.getString(id + ".plural", id),
bucketSection.getInt(id + ".initial-value"));
} catch (IllegalArgumentException ex) {
logWarning("Ignoring invalid score bucket " + id + ": " + ex.getMessage());
}
}
if (!_buckets.containsKey("points")) {
createBucket("points", "point", "points", 0);
}
_defaultBucketId = scores.getString("default-bucket", "points");
if (!_buckets.containsKey(_defaultBucketId)) {
logWarning("Unknown default score bucket; using points instead.");
_defaultBucketId = "points";
}
for (String id : _buckets.keySet()) {
ConfigurationSection bucketScores =
scores.getConfigurationSection("buckets." + id + ".scores");
if (bucketScores != null) {
loadPlayerScores(bucketScores, _bucketScores.get(id));
}
}
ConfigurationSection playerBuckets = scores.getConfigurationSection("player-buckets");
if (playerBuckets != null) {
for (String key : playerBuckets.getKeys(false)) {
try {
UUID playerId = UUID.fromString(key);
String bucketId = playerBuckets.getString(key);
if (_buckets.containsKey(bucketId) && !bucketId.equals(_defaultBucketId)) {
_playerBuckets.put(playerId, bucketId);
}
} catch (IllegalArgumentException ex) {
logWarning("Ignoring invalid player bucket UUID: " + key);
}
}
}
}
private void loadLegacyScores(YamlConfiguration scores) {
loadPlayerScores(scores, _bucketScores.get("points"));
}
private void loadPlayerScores(ConfigurationSection scores, Map<UUID, Integer> target) {
for (String key : scores.getKeys(false)) {
try {
target.put(UUID.fromString(key), scores.getInt(key));
} catch (IllegalArgumentException ex) {
logWarning("Ignoring score with invalid player UUID: " + key);
}
}
}
// endregion

View File

@@ -0,0 +1,130 @@
/*
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 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.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class ScoreBucketCommand implements CommandExecutor {
private final ScoreKeeperPlugin _plugin;
public ScoreBucketCommand(ScoreKeeperPlugin plugin) {
_plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0) {
if (sender instanceof Player player) {
String bucketId = _plugin.getPlayerBucket(player);
ScoreBucket bucket = _plugin.getBucket(bucketId);
_plugin.sendMessage(
sender,
Component.text(
"You are tracking " + bucket.getId() + " (" + bucket.getPlural() + ")."));
} else {
sendUsage(sender);
}
return true;
}
if (args.length == 5 && args[0].equalsIgnoreCase("create")) {
if (!requireAdmin(sender)) {
return true;
}
try {
_plugin.createBucket(args[1], args[2], args[3], Integer.parseInt(args[4]));
_plugin.sendMessage(sender, Component.text("Created score bucket " + args[1] + "."));
} catch (IllegalArgumentException ex) {
sendError(sender, ex.getMessage());
}
return true;
}
if (args.length == 1 && sender instanceof Player player) {
switchPlayerBucket(sender, player, args[0]);
return true;
}
if (args.length == 2) {
if (!requireAdmin(sender)) {
return true;
}
if (args[0].equalsIgnoreCase("all")) {
try {
_plugin.switchAllPlayers(args[1]);
_plugin.sendMessage(
sender, Component.text("All players are now tracking " + args[1] + "."));
} catch (IllegalArgumentException ex) {
sendError(sender, ex.getMessage());
}
return true;
}
Player target = _plugin.getServer().getPlayerExact(args[0]);
if (target == null) {
sendError(sender, "Can't find a player with that name");
} else {
switchPlayerBucket(sender, target, args[1]);
}
return true;
}
sendUsage(sender);
return true;
}
private void switchPlayerBucket(CommandSender sender, Player target, String bucketId) {
try {
_plugin.switchPlayerBucket(target, bucketId);
_plugin.sendMessage(
sender, Component.text(target.getName() + " is now tracking " + bucketId + "."));
} catch (IllegalArgumentException ex) {
sendError(sender, ex.getMessage());
}
}
private boolean requireAdmin(CommandSender sender) {
if (sender.hasPermission("scorekeeper.admin")) {
return true;
}
sendError(sender, "You do not have permission to manage score buckets");
return false;
}
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-bucket [bucketId] | create <id> <singular> <plural> <initialValue>"
+ " | <player|all> <bucketId>")
.color(NamedTextColor.YELLOW));
}
}

View File

@@ -18,4 +18,11 @@ commands:
score-archive:
description: Archives and resets the player's score
usage: /score-archive [playername]
score-bucket:
description: Selects or manages score buckets.
usage: /score-bucket [bucketId] | create <id> <singular> <plural> <initialValue> | <player|all> <bucketId>
permissions:
scorekeeper.admin:
description: Allows managing score buckets.
default: op