chore: unify checkstyle, fix violations, cleanup bin/

This commit is contained in:
loki5512344 2026-07-12 00:59:11 +02:00
parent 1d8c0ca483
commit a0c1ab8ca1
Signed by: boba
GPG key ID: 253067914055423B
156 changed files with 5222 additions and 4348 deletions

View file

@ -8,24 +8,18 @@ import dev.loki.lomines.data.config.loader.region.WorldGuardConfigLoader;
import dev.loki.lomines.data.config.loader.reward.RewardConfigLoader;
import dev.loki.lomines.data.config.loader.system.ResetConfigLoader;
import dev.loki.lomines.data.config.loader.system.UIConfigLoader;
import dev.loki.lomines.data.config.model.MineConfig;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
/**
* Clean config loader for section-based configuration.
* Supports YAML inheritance from defaults.
* Delegates to specialized loaders for each section.
*/
public final class ConfigLoader {
private final Path dataFolder;
private YamlConfiguration defaults;
private final DefaultsMerger defaultsMerger;
// Section loaders
private RegionConfigLoader regionLoader;
private BlockConfigLoader blockLoader;
private ResetConfigLoader resetLoader;
@ -37,69 +31,29 @@ public final class ConfigLoader {
public ConfigLoader(Path dataFolder) {
this.dataFolder = dataFolder;
loadDefaults();
this.defaultsMerger = new DefaultsMerger(dataFolder);
initLoaders();
}
private void loadDefaults() {
Path defaultsPath = dataFolder.resolve("mines").resolve("_defaults.yml");
defaults = new YamlConfiguration();
if (Files.exists(defaultsPath)) {
defaults = YamlConfiguration.loadConfiguration(defaultsPath.toFile());
} else {
setDefaults(defaults);
try {
Files.createDirectories(defaultsPath.getParent());
defaults.save(defaultsPath.toFile());
} catch (IOException e) {
// Ignore, use in-memory defaults
}
}
}
private void setDefaults(YamlConfiguration yaml) {
yaml.set("reset.interval", "5m");
yaml.set("reset.percent-trigger", 10.0);
yaml.set("reset.percent-enabled", false);
yaml.set("reset.commands", List.of());
yaml.set("reset.broadcast", "");
yaml.set("ui.actionbar.enabled", true);
yaml.set("ui.actionbar.format", "<green>{mine}</green> <gray>{percent}%</gray> <dark_gray>({time})");
yaml.set("ui.actionbar.range", 50.0);
yaml.set("ui.timer-format", "mm:ss");
yaml.set("teleport.enabled", false);
yaml.set("rewards", List.of());
worldGuardLoader.setDefaults(yaml);
playerSpawnLoader.setDefaults(yaml);
}
private void initLoaders() {
regionLoader = new RegionConfigLoader();
blockLoader = new BlockConfigLoader();
resetLoader = new ResetConfigLoader(defaults);
resetLoader = new ResetConfigLoader(defaultsMerger.getDefaults());
rewardLoader = new RewardConfigLoader();
teleportLoader = new TeleportConfigLoader();
uiLoader = new UIConfigLoader(defaults);
worldGuardLoader = new WorldGuardConfigLoader(defaults);
uiLoader = new UIConfigLoader(defaultsMerger.getDefaults());
worldGuardLoader = new WorldGuardConfigLoader(defaultsMerger.getDefaults());
playerSpawnLoader = new PlayerSpawnConfigLoader();
}
/**
* Load a mine configuration from file.
*/
public MineConfig load(String mineName) throws ConfigLoadException {
Path configPath = dataFolder.resolve("mines").resolve(mineName + ".yml");
if (!Files.exists(configPath)) {
if (!Files.exists(dataFolder.resolve("mines").resolve(mineName + ".yml"))) {
throw new ConfigLoadException("Mine not found: " + mineName);
}
Path configPath = dataFolder.resolve("mines").resolve(mineName + ".yml");
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(configPath.toFile());
mergeDefaults(yaml);
defaultsMerger.mergeDefaults(yaml);
try {
return MineConfig.builder(mineName)
@ -117,11 +71,7 @@ public final class ConfigLoader {
}
}
/**
* Save a mine configuration to file.
*/
public void save(MineConfig config) throws ConfigLoadException {
Path configPath = dataFolder.resolve("mines").resolve(config.name() + ".yml");
YamlConfiguration yaml = new YamlConfiguration();
regionLoader.save(yaml, config.region());
@ -134,6 +84,7 @@ public final class ConfigLoader {
playerSpawnLoader.save(yaml, config.playerSpawn());
try {
Path configPath = dataFolder.resolve("mines").resolve(config.name() + ".yml");
Files.createDirectories(configPath.getParent());
yaml.save(configPath.toFile());
} catch (IOException e) {
@ -141,17 +92,6 @@ public final class ConfigLoader {
}
}
private void mergeDefaults(YamlConfiguration yaml) {
for (String key : defaults.getKeys(true)) {
if (!yaml.contains(key)) {
yaml.set(key, defaults.get(key));
}
}
}
/**
* Exception for config loading errors.
*/
public static class ConfigLoadException extends Exception {
public ConfigLoadException(String message) {
super(message);

View file

@ -0,0 +1,71 @@
package dev.loki.lomines.data.config;
import dev.loki.lomines.data.config.loader.region.WorldGuardConfigLoader;
import dev.loki.lomines.data.config.loader.entity.PlayerSpawnConfigLoader;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public final class DefaultsMerger {
private final Path dataFolder;
private YamlConfiguration defaults;
public DefaultsMerger(Path dataFolder) {
this.dataFolder = dataFolder;
loadDefaults();
}
public YamlConfiguration getDefaults() {
return defaults;
}
private void loadDefaults() {
Path defaultsPath = dataFolder.resolve("mines").resolve("_defaults.yml");
defaults = new YamlConfiguration();
if (Files.exists(defaultsPath)) {
defaults = YamlConfiguration.loadConfiguration(defaultsPath.toFile());
} else {
setDefaults(defaults);
try {
Files.createDirectories(defaultsPath.getParent());
defaults.save(defaultsPath.toFile());
} catch (IOException e) {
// Ignore, use in-memory defaults
}
}
}
private void setDefaults(YamlConfiguration yaml) {
yaml.set("reset.interval", "5m");
yaml.set("reset.percent-trigger", 10.0);
yaml.set("reset.percent-enabled", false);
yaml.set("reset.commands", List.of());
yaml.set("reset.broadcast", "");
yaml.set("ui.actionbar.enabled", true);
yaml.set("ui.actionbar.format", "<green>{mine}</green> <gray>{percent}%</gray> <dark_gray>({time})");
yaml.set("ui.actionbar.range", 50.0);
yaml.set("ui.timer-format", "mm:ss");
yaml.set("teleport.enabled", false);
yaml.set("rewards", List.of());
WorldGuardConfigLoader wgLoader = new WorldGuardConfigLoader(new YamlConfiguration());
wgLoader.setDefaults(yaml);
PlayerSpawnConfigLoader psLoader = new PlayerSpawnConfigLoader();
psLoader.setDefaults(yaml);
}
public void mergeDefaults(YamlConfiguration yaml) {
for (String key : defaults.getKeys(true)) {
if (!yaml.contains(key)) {
yaml.set(key, defaults.get(key));
}
}
}
}

View file

@ -32,7 +32,9 @@ public final class BlockConfigLoader {
Map<BlockKey, Double> weights = new HashMap<>();
for (String key : contents.getKeys(false)) {
double weight = contents.getDouble(key);
if (weight <= 0) continue;
if (weight <= 0) {
continue;
}
weights.put(BlockKey.deserialize(key), weight);
}

View file

@ -66,7 +66,9 @@ public final class RegionConfigLoader {
break;
}
}
if (!hasMore) break;
if (!hasMore) {
break;
}
}
}
}

View file

@ -1,6 +1,6 @@
package dev.loki.lomines.data.config.loader.region;
import dev.loki.lomines.integration.worldguard.WorldGuardConfig;
import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
@ -38,9 +38,6 @@ public final class WorldGuardConfigLoader {
return WorldGuardConfig.disabled();
}
String template = yaml.getString("worldguard.region-template",
defaults.getString("worldguard.region-template", "{mine_name}_{random_4}"));
List<String> owners = yaml.getStringList("worldguard.owners");
if (owners.isEmpty()) {
owners = defaults.getStringList("worldguard.owners");
@ -63,9 +60,12 @@ public final class WorldGuardConfigLoader {
boolean protect = yaml.getBoolean("worldguard.protect-on-create",
defaults.getBoolean("worldguard.protect-on-create", true));
String tmpl = yaml.getString("worldguard.region-template",
defaults.getString("worldguard.region-template", "{mine_name}_{random_4}"));
return WorldGuardConfig.builder()
.enabled(true)
.template(template)
.template(tmpl)
.owners(owners)
.members(members)
.flags(flags)

View file

@ -36,9 +36,6 @@ public final class RewardConfigLoader {
}
private RewardEntry parseRewardEntry(Map<?, ?> map) {
Object chanceObj = map.get("chance");
double chance = chanceObj instanceof Number n ? n.doubleValue() : 0.0;
List<BlockKey> blocks = new ArrayList<>();
List<?> blockList = (List<?>) map.get("blocks");
if (blockList != null) {
@ -69,6 +66,8 @@ public final class RewardConfigLoader {
Object preventDropsObj = map.get("prevent-drops");
boolean preventDrops = preventDropsObj instanceof Boolean b && b;
Object chanceObj = map.get("chance");
double chance = chanceObj instanceof Number n ? n.doubleValue() : 0.0;
return new RewardEntry(blocks, chance, items, commands, preventDrops);
}

View file

@ -0,0 +1,64 @@
package dev.loki.lomines.data.config.model;
import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.region.RegionConfig;
import dev.loki.lomines.data.config.reset.ResetConfig;
import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig;
import dev.loki.lomines.data.config.teleport.TeleportConfig;
import dev.loki.lomines.data.config.ui.UIConfig;
import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig;
import org.bukkit.Location;
import java.util.Objects;
import java.util.Optional;
public record MineConfig(
String name,
RegionConfig region,
BlockConfig blocks,
ResetConfig reset,
RewardConfig rewards,
TeleportConfig teleport,
UIConfig ui,
WorldGuardConfig worldGuard,
PlayerSpawnConfig playerSpawn
) {
public MineConfig {
Objects.requireNonNull(name, "Mine name cannot be null");
if (name.isBlank()) {
throw new IllegalArgumentException("Mine name cannot be blank");
}
name = name.toLowerCase().trim();
Objects.requireNonNull(region, "Region config cannot be null");
Objects.requireNonNull(blocks, "Block config cannot be null");
Objects.requireNonNull(reset, "Reset config cannot be null");
Objects.requireNonNull(rewards, "Reward config cannot be null");
Objects.requireNonNull(teleport, "Teleport config cannot be null");
Objects.requireNonNull(ui, "UI config cannot be null");
if (playerSpawn == null) {
playerSpawn = PlayerSpawnConfig.disabled();
}
}
public static MineConfigDefaults.Builder builder(String name) {
return new MineConfigDefaults.Builder(name);
}
public int volume() {
return region.totalVolume();
}
public String worldName() {
return region.worldName();
}
public Optional<Location> getSpawnForStuckPlayer() {
return playerSpawn.enabled()
? playerSpawn.getLocation()
: teleport.getLocation();
}
}

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.config;
package dev.loki.lomines.data.config.model;
import dev.loki.lomines.data.config.block.BlockConfig;
import dev.loki.lomines.data.config.region.RegionConfig;
@ -7,58 +7,13 @@ import dev.loki.lomines.data.config.reward.RewardConfig;
import dev.loki.lomines.data.config.spawn.PlayerSpawnConfig;
import dev.loki.lomines.data.config.teleport.TeleportConfig;
import dev.loki.lomines.data.config.ui.UIConfig;
import dev.loki.lomines.integration.worldguard.WorldGuardConfig;
import dev.loki.lomines.integration.worldguard.config.WorldGuardConfig;
import org.bukkit.Location;
public final class MineConfigDefaults {
import java.util.Objects;
import java.util.Optional;
/**
* Clean, type-safe, section-based mine configuration.
* Proper separation of concerns: each section handles one aspect.
*/
public record MineConfig(
String name,
RegionConfig region,
BlockConfig blocks,
ResetConfig reset,
RewardConfig rewards,
TeleportConfig teleport,
UIConfig ui,
WorldGuardConfig worldGuard,
PlayerSpawnConfig playerSpawn
) {
public MineConfig {
Objects.requireNonNull(name, "Mine name cannot be null");
if (name.isBlank()) {
throw new IllegalArgumentException("Mine name cannot be blank");
}
name = name.toLowerCase().trim();
Objects.requireNonNull(region, "Region config cannot be null");
Objects.requireNonNull(blocks, "Block config cannot be null");
Objects.requireNonNull(reset, "Reset config cannot be null");
Objects.requireNonNull(rewards, "Reward config cannot be null");
Objects.requireNonNull(teleport, "Teleport config cannot be null");
Objects.requireNonNull(ui, "UI config cannot be null");
// worldGuard and playerSpawn can be null (disabled by default)
if (playerSpawn == null) {
playerSpawn = PlayerSpawnConfig.disabled();
}
private MineConfigDefaults() {
}
/**
* Creates builder for fluent construction.
*/
public static Builder builder(String name) {
return new Builder(name);
}
/**
* Creates config with sensible defaults.
*/
public static MineConfig defaults(String name, RegionConfig region, BlockConfig blocks) {
return new MineConfig(
name,
@ -73,32 +28,6 @@ public record MineConfig(
);
}
/**
* Total volume of the mine (for progress calculation).
*/
public int volume() {
return region.totalVolume();
}
/**
* World name where the mine is located.
*/
public String worldName() {
return region.worldName();
}
/**
* Returns the spawn location for stuck players.
* If playerSpawn is not set, falls back to teleport location.
*/
public Optional<Location> getSpawnForStuckPlayer() {
return playerSpawn.enabled()
? playerSpawn.getLocation()
: teleport.getLocation();
}
// --- Builder ---
public static class Builder {
private final String name;
private RegionConfig region;
@ -110,7 +39,7 @@ public record MineConfig(
private WorldGuardConfig worldGuard = WorldGuardConfig.disabled();
private PlayerSpawnConfig playerSpawn = PlayerSpawnConfig.disabled();
private Builder(String name) {
public Builder(String name) {
this.name = name;
}

View file

@ -1,6 +1,6 @@
package dev.loki.lomines.data.config.region;
import dev.loki.lomines.util.location.Cuboid;
import dev.loki.lomines.util.location.geo.Cuboid;
import org.bukkit.Location;
import java.util.Collections;

View file

@ -3,9 +3,6 @@ package dev.loki.lomines.data.config.reset;
import java.time.Duration;
import java.util.List;
/**
* Configuration for mine reset behavior.
*/
public record ResetConfig(
Duration interval,
double percentTrigger,
@ -14,34 +11,16 @@ public record ResetConfig(
String broadcastMessage
) {
public static final Duration DEFAULT_INTERVAL = Duration.ofMinutes(5);
public static final double DEFAULT_PERCENT_TRIGGER = 10.0;
public ResetConfig {
// Normalize and validate interval
if (interval == null || interval.isZero() || interval.isNegative()) {
interval = DEFAULT_INTERVAL;
}
// Ensure interval is reasonable (1 second to 24 hours)
if (interval.getSeconds() < 1) {
interval = Duration.ofSeconds(1);
} else if (interval.getSeconds() > 86400) {
interval = Duration.ofHours(24);
}
// Validate percent trigger
if (percentTrigger < 0 || percentTrigger > 100) {
percentTrigger = DEFAULT_PERCENT_TRIGGER;
}
interval = ResetConfigValidator.validateInterval(interval);
percentTrigger = ResetConfigValidator.validatePercentTrigger(percentTrigger);
commands = commands != null ? List.copyOf(commands) : List.of();
broadcastMessage = broadcastMessage != null ? broadcastMessage : "";
}
/**
* Default config: 5 minutes, 10% trigger disabled.
*/
public static final Duration DEFAULT_INTERVAL = ResetConfigValidator.DEFAULT_INTERVAL;
public static final double DEFAULT_PERCENT_TRIGGER = ResetConfigValidator.DEFAULT_PERCENT_TRIGGER;
public static ResetConfig defaults() {
return new ResetConfig(
DEFAULT_INTERVAL,
@ -52,23 +31,14 @@ public record ResetConfig(
);
}
/**
* Builder for fluent construction.
*/
public static Builder builder() {
return new Builder();
}
/**
* Interval in ticks (20 ticks = 1 second).
*/
public long intervalTicks() {
return interval.getSeconds() * 20;
}
/**
* Human-readable interval string.
*/
public String intervalDisplay() {
long seconds = interval.getSeconds();
if (seconds < 60) {
@ -80,9 +50,6 @@ public record ResetConfig(
}
}
/**
* Whether reset should trigger based on percent mined.
*/
public boolean isPercentTriggerEnabled() {
return percentEnabled && percentTrigger > 0;
}
@ -94,38 +61,18 @@ public record ResetConfig(
private List<String> commands = List.of();
private String broadcastMessage = "";
private static Duration parseDuration(String s) {
s = s.trim().toLowerCase();
try {
if (s.endsWith("s")) {
return Duration.ofSeconds(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("m")) {
return Duration.ofMinutes(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("h")) {
return Duration.ofHours(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("d")) {
return Duration.ofDays(Long.parseLong(s.substring(0, s.length() - 1)));
} else {
// Assume seconds if no suffix
return Duration.ofSeconds(Long.parseLong(s));
}
} catch (NumberFormatException e) {
return DEFAULT_INTERVAL;
}
}
public Builder interval(Duration interval) {
this.interval = interval;
return this;
}
public Builder intervalTicks(long ticks) {
this.interval = Duration.ofSeconds(ticks / 20);
public Builder interval(String humanReadable) {
this.interval = ResetConfigValidator.parseDuration(humanReadable);
return this;
}
public Builder interval(String humanReadable) {
this.interval = parseDuration(humanReadable);
public Builder intervalTicks(long ticks) {
this.interval = Duration.ofSeconds(ticks / 20);
return this;
}

View file

@ -0,0 +1,50 @@
package dev.loki.lomines.data.config.reset;
import java.time.Duration;
public final class ResetConfigValidator {
public static final Duration DEFAULT_INTERVAL = Duration.ofMinutes(5);
public static final double DEFAULT_PERCENT_TRIGGER = 10.0;
private ResetConfigValidator() {
}
public static Duration validateInterval(Duration interval) {
if (interval == null || interval.isZero() || interval.isNegative()) {
interval = DEFAULT_INTERVAL;
}
if (interval.getSeconds() < 1) {
interval = Duration.ofSeconds(1);
} else if (interval.getSeconds() > 86400) {
interval = Duration.ofHours(24);
}
return interval;
}
public static double validatePercentTrigger(double percentTrigger) {
if (percentTrigger < 0 || percentTrigger > 100) {
return DEFAULT_PERCENT_TRIGGER;
}
return percentTrigger;
}
public static Duration parseDuration(String s) {
s = s.trim().toLowerCase();
try {
if (s.endsWith("s")) {
return Duration.ofSeconds(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("m")) {
return Duration.ofMinutes(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("h")) {
return Duration.ofHours(Long.parseLong(s.substring(0, s.length() - 1)));
} else if (s.endsWith("d")) {
return Duration.ofDays(Long.parseLong(s.substring(0, s.length() - 1)));
} else {
return Duration.ofSeconds(Long.parseLong(s));
}
} catch (NumberFormatException e) {
return DEFAULT_INTERVAL;
}
}
}

View file

@ -1,8 +1,10 @@
package dev.loki.lomines.data.config.ui;
import dev.loki.lomines.util.format.ColorUtils;
import dev.loki.lomines.util.format.color.ColorUtils;
import net.kyori.adventure.text.Component;
import java.util.Locale;
/**
* Configuration for UI elements (action bar, messages).
*/
@ -16,6 +18,7 @@ public record UIConfig(
public static final String DEFAULT_ACTIONBAR_FORMAT = "<green>{mine}</green> <gray>{percent}%</gray> <dark_gray>({time})";
public static final String DEFAULT_TIMER_FORMAT = "mm:ss";
public UIConfig {
hologram = hologram != null ? hologram : HologramConfig.disabled();
actionBarFormat = actionBarFormat != null && !actionBarFormat.isBlank()
@ -49,7 +52,7 @@ public record UIConfig(
public Component formatActionBar(String mineName, double percent, String timeStr, int blocks, int total) {
String parsed = actionBarFormat
.replace("{mine}", mineName)
.replace("{percent}", String.format("%.1f", percent))
.replace("{percent}", String.format(Locale.US, "%.1f", percent))
.replace("{time}", timeStr)
.replace("{blocks}", String.valueOf(blocks))
.replace("{total}", String.valueOf(total));

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.reward;
package dev.loki.lomines.data.reward.entity;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
@ -49,8 +49,12 @@ public record Reward(List<Material> materials, double chance, List<ItemStack> it
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Reward reward = (Reward) o;
return Double.compare(reward.chance, chance) == 0 &&
preventDrops == reward.preventDrops &&
@ -59,4 +63,8 @@ public record Reward(List<Material> materials, double chance, List<ItemStack> it
Objects.equals(commands, reward.commands);
}
@Override
public int hashCode() {
return Objects.hash(materials, chance, items, commands, preventDrops);
}
}

View file

@ -1,69 +1,29 @@
package dev.loki.lomines.data.reward;
package dev.loki.lomines.data.reward.parse;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import org.bukkit.configuration.file.YamlConfiguration;
import dev.loki.lomines.data.reward.entity.Reward;
import dev.loki.lomines.data.reward.parse.command.RewardCommandParser;
import dev.loki.lomines.data.reward.parse.item.RewardItemParser;
import dev.loki.lomines.data.reward.parse.item.RewardMaterialParser;
import org.bukkit.inventory.ItemStack;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Parses reward configurations from YAML.
* Handles reward chance, materials, items, commands, and prevent-drops settings.
*/
public final class RewardParser {
public final class RewardEntryParser {
private final RewardItemParser itemParser;
private final RewardMaterialParser materialParser;
private final RewardCommandParser commandParser;
public RewardParser() {
public RewardEntryParser() {
this.itemParser = new RewardItemParser();
this.materialParser = new RewardMaterialParser();
this.commandParser = new RewardCommandParser();
}
public List<Reward> parseRewards(YamlConfiguration yaml) throws ConfigParseException {
List<Reward> rewards = new ArrayList<>();
if (!yaml.contains("random-rewards")) {
return rewards;
}
List<?> rewardsList = yaml.getList("random-rewards");
if (rewardsList == null) {
return rewards;
}
for (int i = 0; i < rewardsList.size(); i++) {
Object rewardObj = rewardsList.get(i);
if (!(rewardObj instanceof Map)) {
throw new ConfigParseException(
"Invalid reward at index " + i + ": expected map, got " +
(rewardObj != null ? rewardObj.getClass().getSimpleName() : "null")
);
}
@SuppressWarnings("unchecked")
Map<String, Object> rewardMap = (Map<String, Object>) rewardObj;
try {
Reward reward = parseReward(rewardMap);
rewards.add(reward);
} catch (ConfigParseException e) {
throw new ConfigParseException(
"Error parsing reward at index " + i + ": " + e.getMessage(),
e
);
}
}
return rewards;
}
private Reward parseReward(Map<String, Object> rewardMap) throws ConfigParseException {
public Reward parseReward(Map<String, Object> rewardMap) throws ConfigParseException {
double chance = parseRewardField(rewardMap, "chance", Double.class);
boolean preventDrops = parseRewardField(rewardMap, "prevent-drops", Boolean.class, false);
@ -75,7 +35,7 @@ public final class RewardParser {
}
@SuppressWarnings("unchecked")
private <T> T parseRewardField(Map<String, Object> map, String key, Class<T> type)
public <T> T parseRewardField(Map<String, Object> map, String key, Class<T> type)
throws ConfigParseException {
if (!map.containsKey(key)) {
throw new ConfigParseException("Missing required '" + key + "' field in reward");
@ -96,7 +56,7 @@ public final class RewardParser {
}
@SuppressWarnings("unchecked")
private <T> T parseRewardField(Map<String, Object> map, String key, Class<T> type, T defaultValue) {
public <T> T parseRewardField(Map<String, Object> map, String key, Class<T> type, T defaultValue) {
if (!map.containsKey(key)) {
return defaultValue;
}
@ -107,7 +67,7 @@ public final class RewardParser {
return defaultValue;
}
private Double parseDoubleValue(String key, Object value) throws ConfigParseException {
public Double parseDoubleValue(String key, Object value) throws ConfigParseException {
try {
if (value instanceof Number) {
return ((Number) value).doubleValue();
@ -124,7 +84,7 @@ public final class RewardParser {
}
}
private List<ItemStack> parseRewardItems(Map<String, Object> rewardMap)
public List<ItemStack> parseRewardItems(Map<String, Object> rewardMap)
throws ConfigParseException {
List<ItemStack> items = new ArrayList<>();
if (!rewardMap.containsKey("items")) {

View file

@ -0,0 +1,57 @@
package dev.loki.lomines.data.reward.parse;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.data.reward.entity.Reward;
import org.bukkit.configuration.file.YamlConfiguration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public final class RewardParser {
private final RewardEntryParser entryParser;
public RewardParser() {
this.entryParser = new RewardEntryParser();
}
public List<Reward> parseRewards(YamlConfiguration yaml) throws ConfigParseException {
List<Reward> rewards = new ArrayList<>();
if (!yaml.contains("random-rewards")) {
return rewards;
}
List<?> rewardsList = yaml.getList("random-rewards");
if (rewardsList == null) {
return rewards;
}
for (int i = 0; i < rewardsList.size(); i++) {
Object rewardObj = rewardsList.get(i);
if (!(rewardObj instanceof Map)) {
throw new ConfigParseException(
"Invalid reward at index " + i + ": expected map, got " +
(rewardObj != null ? rewardObj.getClass().getSimpleName() : "null")
);
}
@SuppressWarnings("unchecked")
Map<String, Object> rewardMap = (Map<String, Object>) rewardObj;
try {
Reward reward = entryParser.parseReward(rewardMap);
rewards.add(reward);
} catch (ConfigParseException e) {
throw new ConfigParseException(
"Error parsing reward at index " + i + ": " + e.getMessage(),
e
);
}
}
return rewards;
}
}

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.reward;
package dev.loki.lomines.data.reward.parse.command;
import dev.loki.lomines.data.config.parser.ConfigParseException;
@ -9,10 +9,10 @@ import java.util.Map;
/**
* Parses reward commands from configuration.
*/
final class RewardCommandParser {
public final class RewardCommandParser {
@SuppressWarnings("unchecked")
List<String> parseCommands(Map<String, Object> rewardMap) throws ConfigParseException {
public List<String> parseCommands(Map<String, Object> rewardMap) throws ConfigParseException {
List<String> commands = new ArrayList<>();
if (!rewardMap.containsKey("commands")) {
return commands;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.reward;
package dev.loki.lomines.data.reward.parse.item;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.util.ValidationUtils;
@ -12,7 +12,7 @@ import java.util.Map;
* Parses ItemStack objects from YAML reward item configurations.
* Handles material type, amount, and basic item properties.
*/
final class RewardItemParser {
public final class RewardItemParser {
/**
* Parses a single reward item from a map configuration.
@ -22,7 +22,7 @@ final class RewardItemParser {
* @return the parsed ItemStack
* @throws ConfigParseException if the item configuration is invalid
*/
ItemStack parseItem(Map<String, Object> itemMap, int index) throws ConfigParseException {
public ItemStack parseItem(Map<String, Object> itemMap, int index) throws ConfigParseException {
if (!itemMap.containsKey("type")) {
throw new ConfigParseException("Missing required 'type' field in item at index " + index);
}

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.reward;
package dev.loki.lomines.data.reward.parse.item;
import dev.loki.lomines.data.config.parser.ConfigParseException;
import dev.loki.lomines.util.ValidationUtils;
@ -11,10 +11,10 @@ import java.util.Map;
/**
* Parses reward materials (blocks) from configuration.
*/
final class RewardMaterialParser {
public final class RewardMaterialParser {
@SuppressWarnings("unchecked")
List<Material> parseMaterials(Map<String, Object> rewardMap) throws ConfigParseException {
public List<Material> parseMaterials(Map<String, Object> rewardMap) throws ConfigParseException {
if (!rewardMap.containsKey("blocks")) {
throw new ConfigParseException("Missing required 'blocks' field in reward");
}

View file

@ -1,181 +0,0 @@
package dev.loki.lomines.data.stats;
import dev.loki.lomines.LoMinesPlugin;
import dev.lolib.scheduler.ScheduledTask;
import dev.lolib.scheduler.Scheduler;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manages player statistics for all mines.
* Handles loading, saving, and auto-saving of player statistics.
*/
public final class StatsManager {
private final Map<UUID, PlayerStats> stats = new ConcurrentHashMap<>();
private final LoMinesPlugin plugin;
private final Path statsFile;
private final Leaderboard leaderboard;
private ScheduledTask saveTask;
public StatsManager(LoMinesPlugin plugin) {
this.plugin = plugin;
this.statsFile = plugin.getDataFolder().toPath().resolve("stats.yml");
this.leaderboard = new Leaderboard(this);
}
/**
* Loads statistics from file.
*/
public void load() throws IOException {
if (!Files.exists(statsFile)) {
plugin.loLogger().info("Stats file does not exist, starting with empty statistics");
return;
}
File file = statsFile.toFile();
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
ConfigurationSection playersSection = yaml.getConfigurationSection("players");
if (playersSection == null) {
plugin.loLogger().info("No players section found in stats.yml");
return;
}
for (String uuidString : playersSection.getKeys(false)) {
try {
UUID playerId = UUID.fromString(uuidString);
ConfigurationSection playerSection = playersSection.getConfigurationSection(uuidString);
if (playerSection == null) {
continue;
}
PlayerStats playerStats = getOrCreate(playerId);
// Load total blocks
long totalBlocks = playerSection.getLong("total", 0);
playerStats.setTotalBlocks(totalBlocks);
// Load mine-specific stats
ConfigurationSection minesSection = playerSection.getConfigurationSection("mines");
if (minesSection != null) {
for (String mineName : minesSection.getKeys(false)) {
long mineBlocks = minesSection.getLong(mineName, 0);
playerStats.setMineBlocks(mineName, mineBlocks);
}
}
} catch (IllegalArgumentException e) {
plugin.loLogger().warn("Invalid UUID in stats.yml: " + uuidString);
}
}
plugin.loLogger().info("Loaded statistics for " + stats.size() + " player(s)");
}
/**
* Saves statistics to file.
*/
public void save() throws IOException {
YamlConfiguration yaml = new YamlConfiguration();
for (Map.Entry<UUID, PlayerStats> entry : stats.entrySet()) {
UUID playerId = entry.getKey();
PlayerStats playerStats = entry.getValue();
String path = "players." + playerId.toString();
// Save total blocks
yaml.set(path + ".total", playerStats.getTotalBlocks());
// Save mine-specific stats
Map<String, Long> mineStats = playerStats.getMineStatsSnapshot();
if (!mineStats.isEmpty()) {
for (Map.Entry<String, Long> mineEntry : mineStats.entrySet()) {
yaml.set(path + ".mines." + mineEntry.getKey(), mineEntry.getValue());
}
}
}
// Ensure parent directory exists
if (!Files.exists(statsFile.getParent())) {
Files.createDirectories(statsFile.getParent());
}
yaml.save(statsFile.toFile());
}
/**
* Starts auto-save task.
*/
public void startAutoSave() {
// Auto-save every 5 minutes (20 ticks/second * 60 seconds * 5 minutes)
long interval = 20L * 60L * 5L;
saveTask = Scheduler.get(plugin).runTimerAsync(
() -> {
try {
save();
plugin.loLogger().info("Statistics auto-saved");
} catch (IOException e) {
plugin.loLogger().error("Failed to auto-save statistics: " + e.getMessage());
}
},
interval,
interval
);
}
/**
* Stops auto-save task.
*/
public void stopAutoSave() {
if (saveTask != null) {
saveTask.cancel();
saveTask = null;
}
}
/**
* Gets or creates player statistics.
*/
public PlayerStats getOrCreate(UUID playerId) {
return stats.computeIfAbsent(playerId, PlayerStats::new);
}
/**
* Increments block count for a player in a specific mine.
*/
public void incrementBlocks(UUID playerId, String mineName) {
PlayerStats playerStats = getOrCreate(playerId);
playerStats.incrementTotal();
playerStats.incrementMine(mineName);
// Invalidate leaderboard cache
leaderboard.invalidateCache();
}
/**
* Returns all player statistics.
*/
public Collection<PlayerStats> getAllStats() {
return Collections.unmodifiableCollection(stats.values());
}
/**
* Returns the leaderboard instance.
*/
public Leaderboard getLeaderboard() {
return leaderboard;
}
}

View file

@ -1,4 +1,6 @@
package dev.loki.lomines.data.stats;
package dev.loki.lomines.data.stats.model;
import dev.loki.lomines.data.stats.service.StatsManager;
import java.util.Comparator;
import java.util.List;

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.stats;
package dev.loki.lomines.data.stats.model;
import java.util.Objects;
import java.util.UUID;
@ -16,12 +16,21 @@ public record LeaderboardEntry(UUID playerId, long count) {
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
LeaderboardEntry that = (LeaderboardEntry) o;
return count == that.count && playerId.equals(that.playerId);
}
@Override
public int hashCode() {
return Objects.hash(playerId, count);
}
@Override
public String toString() {
return "LeaderboardEntry{playerId=" + playerId + ", count=" + count + "}";

View file

@ -1,4 +1,4 @@
package dev.loki.lomines.data.stats;
package dev.loki.lomines.data.stats.model;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

View file

@ -0,0 +1,72 @@
package dev.loki.lomines.data.stats.service;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.data.stats.model.Leaderboard;
import dev.loki.lomines.data.stats.model.PlayerStats;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class StatsManager {
private final Map<UUID, PlayerStats> stats = new ConcurrentHashMap<>();
private final LoMinesPlugin plugin;
private final Leaderboard leaderboard;
private final StatsPersistence persistence;
public StatsManager(LoMinesPlugin plugin) {
this.plugin = plugin;
this.leaderboard = new Leaderboard(this);
this.persistence = new StatsPersistence(plugin, this);
}
public StatsPersistence getPersistence() {
return persistence;
}
public void load() throws IOException {
persistence.load(stats);
}
public void save() throws IOException {
persistence.save(stats);
}
public void startAutoSave() {
persistence.startAutoSave(() -> {
try {
save();
plugin.getLogger().info("Statistics auto-saved");
} catch (IOException e) {
plugin.getLogger().severe("Failed to auto-save statistics: " + e.getMessage());
}
});
}
public void stopAutoSave() {
persistence.stopAutoSave();
}
public PlayerStats getOrCreate(UUID playerId) {
return stats.computeIfAbsent(playerId, PlayerStats::new);
}
public void incrementBlocks(UUID playerId, String mineName) {
PlayerStats playerStats = getOrCreate(playerId);
playerStats.incrementTotal();
playerStats.incrementMine(mineName);
leaderboard.invalidateCache();
}
public Collection<PlayerStats> getAllStats() {
return Collections.unmodifiableCollection(stats.values());
}
public Leaderboard getLeaderboard() {
return leaderboard;
}
}

View file

@ -0,0 +1,110 @@
package dev.loki.lomines.data.stats.service;
import dev.loki.lomines.LoMinesPlugin;
import dev.loki.lomines.data.stats.model.PlayerStats;
import org.bukkit.Bukkit;
import org.bukkit.scheduler.BukkitTask;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.UUID;
public final class StatsPersistence {
private final LoMinesPlugin plugin;
private final Path statsFile;
private final StatsManager statsManager;
private BukkitTask saveTask;
public StatsPersistence(LoMinesPlugin plugin, StatsManager statsManager) {
this.plugin = plugin;
this.statsFile = plugin.getDataFolder().toPath().resolve("stats.yml");
this.statsManager = statsManager;
}
public void load(Map<UUID, PlayerStats> stats) throws IOException {
if (!Files.exists(statsFile)) {
plugin.getLogger().info("Stats file does not exist, starting with empty statistics");
return;
}
File file = statsFile.toFile();
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
ConfigurationSection playersSection = yaml.getConfigurationSection("players");
if (playersSection == null) {
plugin.getLogger().info("No players section found in stats.yml");
return;
}
for (String uuidString : playersSection.getKeys(false)) {
try {
UUID playerId = UUID.fromString(uuidString);
ConfigurationSection playerSection = playersSection.getConfigurationSection(uuidString);
if (playerSection == null) {
continue;
}
PlayerStats playerStats = stats.computeIfAbsent(playerId, PlayerStats::new);
long totalBlocks = playerSection.getLong("total", 0);
playerStats.setTotalBlocks(totalBlocks);
ConfigurationSection minesSection = playerSection.getConfigurationSection("mines");
if (minesSection != null) {
for (String mineName : minesSection.getKeys(false)) {
long mineBlocks = minesSection.getLong(mineName, 0);
playerStats.setMineBlocks(mineName, mineBlocks);
}
}
} catch (IllegalArgumentException e) {
plugin.getLogger().warning("Invalid UUID in stats.yml: " + uuidString);
}
}
plugin.getLogger().info("Loaded statistics for " + stats.size() + " player(s)");
}
public void save(Map<UUID, PlayerStats> stats) throws IOException {
YamlConfiguration yaml = new YamlConfiguration();
for (Map.Entry<UUID, PlayerStats> entry : stats.entrySet()) {
UUID playerId = entry.getKey();
PlayerStats playerStats = entry.getValue();
String path = "players." + playerId.toString();
yaml.set(path + ".total", playerStats.getTotalBlocks());
Map<String, Long> mineStats = playerStats.getMineStatsSnapshot();
if (!mineStats.isEmpty()) {
for (Map.Entry<String, Long> mineEntry : mineStats.entrySet()) {
yaml.set(path + ".mines." + mineEntry.getKey(), mineEntry.getValue());
}
}
}
if (!Files.exists(statsFile.getParent())) {
Files.createDirectories(statsFile.getParent());
}
yaml.save(statsFile.toFile());
}
public void startAutoSave(Runnable saveAction) {
long interval = 20L * 60L * 5L;
saveTask = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, saveAction, interval, interval);
}
public void stopAutoSave() {
if (saveTask != null) {
saveTask.cancel();
saveTask = null;
}
}
}