refactor: migrate package from com.loki to dev.loki
- Change base package from com.loki to dev.loki across all Java files - Update plugin.yml main class path - Fix ConfigParseException imports in test files - Update build.gradle.kts relocate patterns - Add missing imports in ConfigLoaderTest.java - Update TODO.md with current status BREAKING CHANGE: Package namespace changed from com.loki to dev.loki
This commit is contained in:
parent
41924ea97f
commit
f4e9458544
87 changed files with 2185 additions and 1767 deletions
111
src/main/java/dev/loki/lomines/data/config/ConfigLoader.java
Normal file
111
src/main/java/dev/loki/lomines/data/config/ConfigLoader.java
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
package dev.loki.lomines.data.config;
|
||||
|
||||
import dev.loki.lomines.data.config.parser.ConfigParseException;
|
||||
import dev.loki.lomines.data.config.parser.ConfigParser;
|
||||
import dev.loki.lomines.data.reward.Reward;
|
||||
import dev.loki.lomines.data.reward.RewardParser;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Facade for loading and saving MineConfig objects from/to YAML files.
|
||||
* Coordinates the work between ConfigParser, ConfigValidator, and ConfigSerializer.
|
||||
* Follows the Single Responsibility Principle by delegating specific tasks.
|
||||
*/
|
||||
public final class ConfigLoader {
|
||||
|
||||
private final ConfigParser parser;
|
||||
private final ConfigValidator validator;
|
||||
private final ConfigSerializer serializer;
|
||||
|
||||
public ConfigLoader() {
|
||||
RewardParser rewardParser = new RewardParser();
|
||||
this.parser = new ConfigParser(rewardParser);
|
||||
this.validator = new ConfigValidator();
|
||||
this.serializer = new ConfigSerializer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a MineConfig from a YAML file.
|
||||
* Parses the file, validates all data, and builds the MineConfig object.
|
||||
*
|
||||
* @param configFile the path to the YAML configuration file
|
||||
* @return the loaded MineConfig object
|
||||
* @throws IOException if the file cannot be read
|
||||
* @throws ConfigParseException if the configuration is invalid
|
||||
*/
|
||||
public MineConfig load(Path configFile) throws IOException, ConfigParseException {
|
||||
if (configFile == null) {
|
||||
throw new ConfigParseException("Config file path cannot be null");
|
||||
}
|
||||
|
||||
if (!configFile.toFile().exists()) {
|
||||
throw new IOException("Config file does not exist: " + configFile);
|
||||
}
|
||||
|
||||
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(configFile.toFile());
|
||||
|
||||
// Parse all sections
|
||||
List<Location> selections = parser.parseSelections(yaml);
|
||||
Map<String, Double> blocks = parser.parseBlocks(yaml);
|
||||
FillMode fillMode = parser.parseFillMode(yaml);
|
||||
String maskMarker = parser.parseMaskMarker(yaml);
|
||||
List<Location> maskPositions = parser.parseMaskPositions(yaml);
|
||||
List<Reward> rewards = parser.parseRewards(yaml);
|
||||
Location teleportLocation = parser.parseTeleportLocation(yaml);
|
||||
|
||||
// Validate all data
|
||||
validator.validateSelections(selections);
|
||||
validator.validateBlocks(blocks);
|
||||
validator.validateMaskMarker(maskMarker);
|
||||
validator.validateRewards(rewards);
|
||||
|
||||
// Build and return config
|
||||
return MineConfig.builder()
|
||||
.selections(selections)
|
||||
.blocks(blocks)
|
||||
.fillMode(fillMode)
|
||||
.maskMarkerMaterial(maskMarker)
|
||||
.maskPositions(maskPositions)
|
||||
.rewards(rewards)
|
||||
.resetTicks(yaml.getInt("reset.ticks", 6000))
|
||||
.resetPercent(yaml.getDouble("reset.percent", 10.0))
|
||||
.resetOnPercentEnabled(yaml.getBoolean("reset-on-percent", false))
|
||||
.actionBarEnabled(yaml.getBoolean("actionbar.enabled", true))
|
||||
.actionBarMessage(yaml.getString("actionbar.message", ""))
|
||||
.actionBarRange(yaml.getDouble("actionbar.range", 50.0))
|
||||
.timerFormat(yaml.getString("timer-format", "mm:ss"))
|
||||
.teleportOnReset(yaml.getBoolean("teleport-on-reset", false))
|
||||
.teleportLocation(teleportLocation)
|
||||
.resetCommands(yaml.getStringList("reset-commands"))
|
||||
.broadcastReset(yaml.getString("broadcast-reset", ""))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a MineConfig to a YAML file.
|
||||
* Serializes the config and writes it to the specified file.
|
||||
*
|
||||
* @param config the MineConfig to save
|
||||
* @param configFile the path to the YAML configuration file
|
||||
* @throws IOException if the file cannot be written
|
||||
* @throws IllegalArgumentException if config or configFile is null
|
||||
*/
|
||||
public void save(MineConfig config, Path configFile) throws IOException {
|
||||
if (config == null) {
|
||||
throw new IllegalArgumentException("Config cannot be null");
|
||||
}
|
||||
if (configFile == null) {
|
||||
throw new IllegalArgumentException("Config file path cannot be null");
|
||||
}
|
||||
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
serializer.serialize(config, yaml);
|
||||
yaml.save(configFile.toFile());
|
||||
}
|
||||
}
|
||||
142
src/main/java/dev/loki/lomines/data/config/ConfigSerializer.java
Normal file
142
src/main/java/dev/loki/lomines/data/config/ConfigSerializer.java
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
package dev.loki.lomines.data.config;
|
||||
|
||||
import dev.loki.lomines.data.reward.Reward;
|
||||
import dev.loki.lomines.util.location.LocationParser;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Serializes MineConfig objects to YAML format.
|
||||
* Responsible only for writing MineConfig data to YAML configuration.
|
||||
*/
|
||||
final class ConfigSerializer {
|
||||
|
||||
/**
|
||||
* Serializes a MineConfig to YAML configuration.
|
||||
*
|
||||
* @param config the MineConfig to serialize
|
||||
* @param yaml the YAML configuration to write to
|
||||
*/
|
||||
void serialize(MineConfig config, YamlConfiguration yaml) {
|
||||
saveSelections(yaml, config.getSelections());
|
||||
saveBlocks(yaml, config.getBlocks());
|
||||
yaml.set("fill-mode", config.getFillMode().name().toLowerCase(Locale.ROOT));
|
||||
yaml.set("mask.marker", config.getMaskMarkerMaterial());
|
||||
saveMaskPositions(yaml, config.getMaskPositions());
|
||||
saveRewards(yaml, config.getRewards());
|
||||
|
||||
yaml.set("reset.ticks", config.getResetTicks());
|
||||
yaml.set("reset.percent", config.getResetPercent());
|
||||
yaml.set("reset-on-percent", config.isResetOnPercentEnabled());
|
||||
yaml.set("reset-commands", config.getResetCommands());
|
||||
yaml.set("broadcast-reset", config.getBroadcastReset());
|
||||
|
||||
yaml.set("teleport-on-reset", config.isTeleportOnReset());
|
||||
if (config.getTeleportLocation() != null) {
|
||||
yaml.set("teleport-location", LocationParser.format(config.getTeleportLocation()));
|
||||
}
|
||||
|
||||
yaml.set("actionbar.enabled", config.isActionBarEnabled());
|
||||
yaml.set("actionbar.message", config.getActionBarMessage());
|
||||
yaml.set("actionbar.range", config.getActionBarRange());
|
||||
|
||||
yaml.set("timer-format", config.getTimerFormat());
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves selection coordinates to the YAML configuration.
|
||||
*
|
||||
* @param yaml the YAML configuration
|
||||
* @param selections the list of selection locations
|
||||
*/
|
||||
private void saveSelections(YamlConfiguration yaml, List<Location> selections) {
|
||||
for (int i = 0; i < selections.size(); i++) {
|
||||
Location location = selections.get(i);
|
||||
String key = "selection." + (i + 1);
|
||||
yaml.set(key, LocationParser.format(location));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves block contents to the YAML configuration.
|
||||
*
|
||||
* @param yaml the YAML configuration
|
||||
* @param blocks the map of block string key to weight
|
||||
*/
|
||||
private void saveBlocks(YamlConfiguration yaml, Map<String, Double> blocks) {
|
||||
for (Map.Entry<String, Double> entry : blocks.entrySet()) {
|
||||
String blockKey = entry.getKey();
|
||||
Double weight = entry.getValue();
|
||||
yaml.set("contents." + blockKey, weight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves mask positions to the YAML configuration.
|
||||
*
|
||||
* @param yaml the YAML configuration
|
||||
* @param maskPositions the list of mask positions
|
||||
*/
|
||||
private void saveMaskPositions(YamlConfiguration yaml, List<Location> maskPositions) {
|
||||
if (maskPositions == null || maskPositions.isEmpty()) {
|
||||
yaml.set("mask.positions", new ArrayList<String>());
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> lines = new ArrayList<>();
|
||||
for (Location loc : maskPositions) {
|
||||
lines.add(LocationParser.format(loc));
|
||||
}
|
||||
yaml.set("mask.positions", lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves rewards to the YAML configuration.
|
||||
*
|
||||
* @param yaml the YAML configuration
|
||||
* @param rewards the list of rewards
|
||||
*/
|
||||
private void saveRewards(YamlConfiguration yaml, List<Reward> rewards) {
|
||||
if (rewards.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Map<String, Object>> rewardsList = new ArrayList<>();
|
||||
|
||||
for (Reward reward : rewards) {
|
||||
Map<String, Object> rewardMap = new HashMap<>();
|
||||
|
||||
rewardMap.put("chance", reward.getChance());
|
||||
rewardMap.put("prevent-drops", reward.isPreventDrops());
|
||||
|
||||
List<String> blockNames = new ArrayList<>();
|
||||
for (Material material : reward.getMaterials()) {
|
||||
blockNames.add(material.name().toLowerCase());
|
||||
}
|
||||
rewardMap.put("blocks", blockNames);
|
||||
|
||||
if (!reward.getItems().isEmpty()) {
|
||||
List<Map<String, Object>> itemsList = new ArrayList<>();
|
||||
for (ItemStack item : reward.getItems()) {
|
||||
Map<String, Object> itemMap = new HashMap<>();
|
||||
itemMap.put("type", item.getType().name().toLowerCase());
|
||||
itemMap.put("amount", item.getAmount());
|
||||
itemsList.add(itemMap);
|
||||
}
|
||||
rewardMap.put("items", itemsList);
|
||||
}
|
||||
|
||||
if (!reward.getCommands().isEmpty()) {
|
||||
rewardMap.put("commands", new ArrayList<>(reward.getCommands()));
|
||||
}
|
||||
|
||||
rewardsList.add(rewardMap);
|
||||
}
|
||||
|
||||
yaml.set("random-rewards", rewardsList);
|
||||
}
|
||||
}
|
||||
138
src/main/java/dev/loki/lomines/data/config/ConfigValidator.java
Normal file
138
src/main/java/dev/loki/lomines/data/config/ConfigValidator.java
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
package dev.loki.lomines.data.config;
|
||||
|
||||
import dev.loki.lomines.data.config.parser.ConfigParseException;
|
||||
import dev.loki.lomines.data.reward.Reward;
|
||||
import dev.loki.lomines.util.ValidationUtils;
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Validates configuration data according to business rules.
|
||||
* Responsible only for validation - throws ConfigParseException on invalid data.
|
||||
*/
|
||||
final class ConfigValidator {
|
||||
|
||||
/**
|
||||
* Validates selection coordinates.
|
||||
*
|
||||
* @param selections the list of selections to validate
|
||||
* @throws ConfigParseException if selections are invalid
|
||||
*/
|
||||
void validateSelections(List<Location> selections) throws ConfigParseException {
|
||||
if (selections.isEmpty()) {
|
||||
throw new ConfigParseException(
|
||||
"No selection coordinates found. At least one selection pair (2 points) is required"
|
||||
);
|
||||
}
|
||||
|
||||
if (selections.size() % 2 != 0) {
|
||||
throw new ConfigParseException(
|
||||
"Invalid number of selection coordinates: " + selections.size() +
|
||||
". Selections must be in pairs (even number)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates block contents configuration.
|
||||
*
|
||||
* @param blocks the map of block keys to weights
|
||||
* @throws ConfigParseException if blocks are invalid
|
||||
*/
|
||||
void validateBlocks(Map<String, Double> blocks) throws ConfigParseException {
|
||||
if (blocks.isEmpty()) {
|
||||
throw new ConfigParseException(
|
||||
"No blocks defined in 'contents' section. At least one block is required"
|
||||
);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, Double> entry : blocks.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Double weight = entry.getValue();
|
||||
|
||||
if (weight <= 0) {
|
||||
throw new ConfigParseException(
|
||||
"Block weight must be positive for '" + key + "', got: " + weight
|
||||
);
|
||||
}
|
||||
|
||||
// Validate vanilla materials (custom blocks are validated at runtime)
|
||||
if (!key.startsWith("oraxen:") && !key.startsWith("itemsadder:")) {
|
||||
validateMaterial(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a material name.
|
||||
*
|
||||
* @param materialName the material name to validate
|
||||
* @throws ConfigParseException if the material is invalid
|
||||
*/
|
||||
void validateMaterial(String materialName) throws ConfigParseException {
|
||||
try {
|
||||
ValidationUtils.validateMaterial(materialName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ConfigParseException(
|
||||
"Unknown material: '" + materialName + "'. Must be a valid Minecraft material name",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates mask marker material.
|
||||
*
|
||||
* @param marker the mask marker material name
|
||||
* @throws ConfigParseException if the marker is invalid
|
||||
*/
|
||||
void validateMaskMarker(String marker) throws ConfigParseException {
|
||||
try {
|
||||
ValidationUtils.validateMaterial(marker);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ConfigParseException("Invalid mask.marker material: '" + marker + "'", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a reward configuration.
|
||||
*
|
||||
* @param reward the reward to validate
|
||||
* @throws ConfigParseException if the reward is invalid
|
||||
*/
|
||||
void validateReward(Reward reward) throws ConfigParseException {
|
||||
double chance = reward.getChance();
|
||||
if (chance < 0 || chance > 100) {
|
||||
throw new ConfigParseException(
|
||||
"Reward chance must be between 0 and 100, got: " + chance
|
||||
);
|
||||
}
|
||||
|
||||
if (reward.getMaterials().isEmpty()) {
|
||||
throw new ConfigParseException("Reward 'blocks' list cannot be empty");
|
||||
}
|
||||
|
||||
// Validate item amounts
|
||||
for (org.bukkit.inventory.ItemStack item : reward.getItems()) {
|
||||
if (item.getAmount() <= 0) {
|
||||
throw new ConfigParseException(
|
||||
"Item amount must be positive, got: " + item.getAmount()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates all rewards in a list.
|
||||
*
|
||||
* @param rewards the list of rewards to validate
|
||||
* @throws ConfigParseException if any reward is invalid
|
||||
*/
|
||||
void validateRewards(List<Reward> rewards) throws ConfigParseException {
|
||||
for (Reward reward : rewards) {
|
||||
validateReward(reward);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/main/java/dev/loki/lomines/data/config/FillMode.java
Normal file
13
src/main/java/dev/loki/lomines/data/config/FillMode.java
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
package dev.loki.lomines.data.config;
|
||||
|
||||
/**
|
||||
* How mine contents are placed on reset.
|
||||
* <ul>
|
||||
* <li>{@link #CUBOID} — fill entire selection box (classic)</li>
|
||||
* <li>{@link #MASK} — only at stored block positions (painted with a marker block, then scanned)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public enum FillMode {
|
||||
CUBOID,
|
||||
MASK
|
||||
}
|
||||
189
src/main/java/dev/loki/lomines/data/config/MineConfig.java
Normal file
189
src/main/java/dev/loki/lomines/data/config/MineConfig.java
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
package dev.loki.lomines.data.config;
|
||||
|
||||
import dev.loki.lomines.data.reward.Reward;
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Immutable configuration data model for a mine.
|
||||
* All fields are final to ensure immutability.
|
||||
* Use the Builder pattern to create instances.
|
||||
*/
|
||||
public final class MineConfig {
|
||||
|
||||
// Regions
|
||||
private final List<Location> selections;
|
||||
|
||||
// Blocks
|
||||
private final Map<String, Double> blocks;
|
||||
|
||||
/**
|
||||
* How blocks are placed on reset.
|
||||
*/
|
||||
private final FillMode fillMode;
|
||||
/**
|
||||
* Vanilla material name for {@link FillMode#MASK} scan (e.g. pink_concrete).
|
||||
*/
|
||||
private final String maskMarkerMaterial;
|
||||
/**
|
||||
* Block centers for mask mode; ignored when {@link #fillMode} is {@link FillMode#CUBOID}.
|
||||
*/
|
||||
private final List<Location> maskPositions;
|
||||
|
||||
// Rewards
|
||||
private final List<Reward> rewards;
|
||||
|
||||
// Reset
|
||||
private final int resetTicks;
|
||||
private final double resetPercent;
|
||||
private final boolean resetOnPercentEnabled;
|
||||
private final List<String> resetCommands;
|
||||
private final String broadcastReset;
|
||||
|
||||
// Teleportation
|
||||
private final boolean teleportOnReset;
|
||||
private final Location teleportLocation;
|
||||
|
||||
// Action Bar
|
||||
private final boolean actionBarEnabled;
|
||||
private final String actionBarMessage;
|
||||
private final double actionBarRange;
|
||||
|
||||
// Timer
|
||||
private final String timerFormat;
|
||||
|
||||
MineConfig(List<Location> selections, Map<String, Double> blocks, FillMode fillMode,
|
||||
String maskMarkerMaterial, List<Location> maskPositions, List<Reward> rewards,
|
||||
int resetTicks, double resetPercent, boolean resetOnPercentEnabled,
|
||||
List<String> resetCommands, String broadcastReset, boolean teleportOnReset,
|
||||
Location teleportLocation, boolean actionBarEnabled, String actionBarMessage,
|
||||
double actionBarRange, String timerFormat) {
|
||||
this.selections = List.copyOf(selections);
|
||||
this.blocks = Map.copyOf(blocks);
|
||||
this.fillMode = fillMode;
|
||||
this.maskMarkerMaterial = maskMarkerMaterial;
|
||||
this.maskPositions = List.copyOf(maskPositions);
|
||||
this.rewards = List.copyOf(rewards);
|
||||
this.resetTicks = resetTicks;
|
||||
this.resetPercent = resetPercent;
|
||||
this.resetOnPercentEnabled = resetOnPercentEnabled;
|
||||
this.resetCommands = List.copyOf(resetCommands);
|
||||
this.broadcastReset = broadcastReset;
|
||||
this.teleportOnReset = teleportOnReset;
|
||||
this.teleportLocation = teleportLocation;
|
||||
this.actionBarEnabled = actionBarEnabled;
|
||||
this.actionBarMessage = actionBarMessage;
|
||||
this.actionBarRange = actionBarRange;
|
||||
this.timerFormat = timerFormat;
|
||||
}
|
||||
|
||||
public static MineConfigBuilder builder() {
|
||||
return new MineConfigBuilder();
|
||||
}
|
||||
|
||||
// Getters
|
||||
public List<Location> getSelections() {
|
||||
return selections;
|
||||
}
|
||||
|
||||
public Map<String, Double> getBlocks() {
|
||||
return blocks;
|
||||
}
|
||||
|
||||
public FillMode getFillMode() {
|
||||
return fillMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Material name for scanning marker blocks (vanilla), e.g. {@code pink_concrete}.
|
||||
*/
|
||||
public String getMaskMarkerMaterial() {
|
||||
return maskMarkerMaterial;
|
||||
}
|
||||
|
||||
public List<Location> getMaskPositions() {
|
||||
return maskPositions;
|
||||
}
|
||||
|
||||
public List<Reward> getRewards() {
|
||||
return rewards;
|
||||
}
|
||||
|
||||
public int getResetTicks() {
|
||||
return resetTicks;
|
||||
}
|
||||
|
||||
public double getResetPercent() {
|
||||
return resetPercent;
|
||||
}
|
||||
|
||||
public boolean isResetOnPercentEnabled() {
|
||||
return resetOnPercentEnabled;
|
||||
}
|
||||
|
||||
public List<String> getResetCommands() {
|
||||
return resetCommands;
|
||||
}
|
||||
|
||||
public String getBroadcastReset() {
|
||||
return broadcastReset;
|
||||
}
|
||||
|
||||
public boolean isTeleportOnReset() {
|
||||
return teleportOnReset;
|
||||
}
|
||||
|
||||
public Location getTeleportLocation() {
|
||||
return teleportLocation;
|
||||
}
|
||||
|
||||
public boolean isActionBarEnabled() {
|
||||
return actionBarEnabled;
|
||||
}
|
||||
|
||||
public String getActionBarMessage() {
|
||||
return actionBarMessage;
|
||||
}
|
||||
|
||||
public double getActionBarRange() {
|
||||
return actionBarRange;
|
||||
}
|
||||
|
||||
public String getTimerFormat() {
|
||||
return timerFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
MineConfig that = (MineConfig) o;
|
||||
return resetTicks == that.resetTicks &&
|
||||
Double.compare(that.resetPercent, resetPercent) == 0 &&
|
||||
resetOnPercentEnabled == that.resetOnPercentEnabled &&
|
||||
teleportOnReset == that.teleportOnReset &&
|
||||
actionBarEnabled == that.actionBarEnabled &&
|
||||
Double.compare(that.actionBarRange, actionBarRange) == 0 &&
|
||||
Objects.equals(selections, that.selections) &&
|
||||
Objects.equals(blocks, that.blocks) &&
|
||||
fillMode == that.fillMode &&
|
||||
Objects.equals(maskMarkerMaterial, that.maskMarkerMaterial) &&
|
||||
Objects.equals(maskPositions, that.maskPositions) &&
|
||||
Objects.equals(rewards, that.rewards) &&
|
||||
Objects.equals(resetCommands, that.resetCommands) &&
|
||||
Objects.equals(broadcastReset, that.broadcastReset) &&
|
||||
Objects.equals(teleportLocation, that.teleportLocation) &&
|
||||
Objects.equals(actionBarMessage, that.actionBarMessage) &&
|
||||
Objects.equals(timerFormat, that.timerFormat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(selections, blocks, fillMode, maskMarkerMaterial, maskPositions, rewards, resetTicks, resetPercent,
|
||||
resetOnPercentEnabled, resetCommands, broadcastReset, teleportOnReset,
|
||||
teleportLocation, actionBarEnabled, actionBarMessage, actionBarRange, timerFormat);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package dev.loki.lomines.data.config;
|
||||
|
||||
import dev.loki.lomines.data.reward.Reward;
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Builder for creating immutable MineConfig instances.
|
||||
*/
|
||||
public final class MineConfigBuilder {
|
||||
private List<Location> selections = new ArrayList<>();
|
||||
private Map<String, Double> blocks = new HashMap<>();
|
||||
private FillMode fillMode = FillMode.CUBOID;
|
||||
private String maskMarkerMaterial = "pink_concrete";
|
||||
private List<Location> maskPositions = new ArrayList<>();
|
||||
private List<Reward> rewards = new ArrayList<>();
|
||||
private int resetTicks = 6000;
|
||||
private double resetPercent = 10.0;
|
||||
private boolean resetOnPercentEnabled = false;
|
||||
private List<String> resetCommands = new ArrayList<>();
|
||||
private String broadcastReset = "";
|
||||
private boolean teleportOnReset = false;
|
||||
private Location teleportLocation = null;
|
||||
private boolean actionBarEnabled = true;
|
||||
private String actionBarMessage = "";
|
||||
private double actionBarRange = 50.0;
|
||||
private String timerFormat = "mm:ss";
|
||||
|
||||
MineConfigBuilder() {
|
||||
}
|
||||
|
||||
public MineConfigBuilder selections(List<Location> selections) {
|
||||
this.selections = new ArrayList<>(selections);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder blocks(Map<String, Double> blocks) {
|
||||
this.blocks = new HashMap<>(blocks);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder fillMode(FillMode fillMode) {
|
||||
this.fillMode = fillMode != null ? fillMode : FillMode.CUBOID;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder maskMarkerMaterial(String maskMarkerMaterial) {
|
||||
this.maskMarkerMaterial = maskMarkerMaterial != null ? maskMarkerMaterial : "pink_concrete";
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder maskPositions(List<Location> maskPositions) {
|
||||
this.maskPositions = maskPositions != null ? new ArrayList<>(maskPositions) : new ArrayList<>();
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder rewards(List<Reward> rewards) {
|
||||
this.rewards = new ArrayList<>(rewards);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder resetTicks(int resetTicks) {
|
||||
this.resetTicks = resetTicks;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder resetPercent(double resetPercent) {
|
||||
this.resetPercent = resetPercent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder resetOnPercentEnabled(boolean resetOnPercentEnabled) {
|
||||
this.resetOnPercentEnabled = resetOnPercentEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder resetCommands(List<String> resetCommands) {
|
||||
this.resetCommands = new ArrayList<>(resetCommands);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder broadcastReset(String broadcastReset) {
|
||||
this.broadcastReset = broadcastReset;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder teleportOnReset(boolean teleportOnReset) {
|
||||
this.teleportOnReset = teleportOnReset;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder teleportLocation(Location teleportLocation) {
|
||||
this.teleportLocation = teleportLocation;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder actionBarEnabled(boolean actionBarEnabled) {
|
||||
this.actionBarEnabled = actionBarEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder actionBarMessage(String actionBarMessage) {
|
||||
this.actionBarMessage = actionBarMessage;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder actionBarRange(double actionBarRange) {
|
||||
this.actionBarRange = actionBarRange;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfigBuilder timerFormat(String timerFormat) {
|
||||
this.timerFormat = timerFormat;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MineConfig build() {
|
||||
return new MineConfig(
|
||||
selections, blocks, fillMode, maskMarkerMaterial, maskPositions,
|
||||
rewards, resetTicks, resetPercent, resetOnPercentEnabled,
|
||||
resetCommands, broadcastReset, teleportOnReset, teleportLocation,
|
||||
actionBarEnabled, actionBarMessage, actionBarRange, timerFormat
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package dev.loki.lomines.data.config.parser;
|
||||
|
||||
/**
|
||||
* Exception thrown when configuration parsing fails.
|
||||
* Provides descriptive error messages for invalid configuration data.
|
||||
*/
|
||||
public final class ConfigParseException extends Exception {
|
||||
|
||||
public ConfigParseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ConfigParseException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
package dev.loki.lomines.data.config.parser;
|
||||
|
||||
import dev.loki.lomines.data.config.FillMode;
|
||||
import dev.loki.lomines.data.reward.Reward;
|
||||
import dev.loki.lomines.data.reward.RewardParser;
|
||||
import dev.loki.lomines.util.location.LocationParser;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Parses YAML configuration into intermediate data structures.
|
||||
* Responsible only for reading YAML and converting to Java objects.
|
||||
* Does not perform validation - that's handled by ConfigValidator.
|
||||
*/
|
||||
public final class ConfigParser {
|
||||
|
||||
private final RewardParser rewardParser;
|
||||
|
||||
public ConfigParser(RewardParser rewardParser) {
|
||||
this.rewardParser = rewardParser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses selection coordinates from the YAML configuration.
|
||||
* Reads selection.1 through selection.10 fields.
|
||||
*
|
||||
* @param yaml the YAML configuration
|
||||
* @return list of parsed Location objects
|
||||
* @throws ConfigParseException if location parsing fails
|
||||
*/
|
||||
public List<Location> parseSelections(YamlConfiguration yaml) throws ConfigParseException {
|
||||
List<Location> selections = new ArrayList<>();
|
||||
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
String key = "selection." + i;
|
||||
if (yaml.contains(key)) {
|
||||
String locationString = yaml.getString(key);
|
||||
if (locationString == null || locationString.trim().isEmpty()) {
|
||||
throw new ConfigParseException(
|
||||
"Selection coordinate at '" + key + "' is empty"
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
Location location = LocationParser.parse(locationString);
|
||||
selections.add(location);
|
||||
} catch (ConfigParseException e) {
|
||||
throw new ConfigParseException(
|
||||
"Invalid selection coordinate at '" + key + "': " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return selections;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses block contents from the YAML configuration.
|
||||
* Supports vanilla blocks, Oraxen blocks (oraxen: prefix), and ItemsAdder blocks (itemsadder: prefix).
|
||||
*
|
||||
* @param yaml the YAML configuration
|
||||
* @return map of block string key to weight
|
||||
* @throws ConfigParseException if block configuration is invalid
|
||||
*/
|
||||
public Map<String, Double> parseBlocks(YamlConfiguration yaml) throws ConfigParseException {
|
||||
ConfigurationSection contentsSection = yaml.getConfigurationSection("contents");
|
||||
|
||||
if (contentsSection == null) {
|
||||
throw new ConfigParseException(
|
||||
"Missing required 'contents' section in configuration"
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Double> blocks = new HashMap<>();
|
||||
|
||||
for (String key : contentsSection.getKeys(false)) {
|
||||
Object value = contentsSection.get(key);
|
||||
|
||||
double weight = parseWeight(key, value);
|
||||
|
||||
// Store block key (validation happens in ConfigValidator)
|
||||
String blockKey = normalizeBlockKey(key);
|
||||
blocks.put(blockKey, weight);
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses fill mode from the YAML configuration.
|
||||
*
|
||||
* @param yaml the YAML configuration
|
||||
* @return the parsed FillMode
|
||||
*/
|
||||
public FillMode parseFillMode(YamlConfiguration yaml) {
|
||||
String raw = yaml.getString("fill-mode", "cuboid");
|
||||
if (raw == null || raw.isBlank()) {
|
||||
return FillMode.CUBOID;
|
||||
}
|
||||
return switch (raw.trim().toLowerCase(Locale.ROOT)) {
|
||||
case "mask" -> FillMode.MASK;
|
||||
default -> FillMode.CUBOID;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses mask marker material from the YAML configuration.
|
||||
*
|
||||
* @param yaml the YAML configuration
|
||||
* @return the mask marker material name
|
||||
*/
|
||||
public String parseMaskMarker(YamlConfiguration yaml) {
|
||||
String marker = yaml.getString("mask.marker", "pink_concrete");
|
||||
if (marker == null || marker.isBlank()) {
|
||||
return "pink_concrete";
|
||||
}
|
||||
return marker.trim().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses mask positions from the YAML configuration.
|
||||
*
|
||||
* @param yaml the YAML configuration
|
||||
* @return list of mask positions
|
||||
* @throws ConfigParseException if position parsing fails
|
||||
*/
|
||||
public List<Location> parseMaskPositions(YamlConfiguration yaml) throws ConfigParseException {
|
||||
List<String> rawList = yaml.getStringList("mask.positions");
|
||||
if (rawList == null || rawList.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<Location> out = new ArrayList<>();
|
||||
for (int i = 0; i < rawList.size(); i++) {
|
||||
String line = rawList.get(i);
|
||||
if (line == null || line.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
out.add(LocationParser.parse(line.trim()));
|
||||
} catch (ConfigParseException e) {
|
||||
throw new ConfigParseException(
|
||||
"Invalid mask.positions entry at index " + i + ": " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses rewards from the YAML configuration.
|
||||
* Delegates to RewardParser for actual parsing logic.
|
||||
*
|
||||
* @param yaml the YAML configuration
|
||||
* @return list of parsed Reward objects
|
||||
* @throws ConfigParseException if reward configuration is invalid
|
||||
*/
|
||||
public List<Reward> parseRewards(YamlConfiguration yaml) throws ConfigParseException {
|
||||
return rewardParser.parseRewards(yaml);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the teleport location from the YAML configuration.
|
||||
*
|
||||
* @param yaml the YAML configuration
|
||||
* @return the parsed Location, or null if not configured
|
||||
* @throws ConfigParseException if the teleport location is invalid
|
||||
*/
|
||||
public Location parseTeleportLocation(YamlConfiguration yaml) throws ConfigParseException {
|
||||
if (!yaml.contains("teleport-location")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String locationString = yaml.getString("teleport-location");
|
||||
if (locationString == null || locationString.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return LocationParser.parse(locationString);
|
||||
} catch (ConfigParseException e) {
|
||||
throw new ConfigParseException(
|
||||
"Invalid teleport location: " + e.getMessage(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private double parseWeight(String key, Object value) throws ConfigParseException {
|
||||
try {
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
} else if (value instanceof String) {
|
||||
return Double.parseDouble((String) value);
|
||||
} else {
|
||||
throw new ConfigParseException(
|
||||
"Invalid weight type for block '" + key + "': expected number, got " +
|
||||
(value != null ? value.getClass().getSimpleName() : "null")
|
||||
);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ConfigParseException(
|
||||
"Invalid weight value for block '" + key + "': " + value,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeBlockKey(String key) {
|
||||
if (key.startsWith("oraxen:") || key.startsWith("itemsadder:")) {
|
||||
return key;
|
||||
}
|
||||
return key.toLowerCase();
|
||||
}
|
||||
}
|
||||
15
src/main/java/dev/loki/lomines/data/package-info.java
Normal file
15
src/main/java/dev/loki/lomines/data/package-info.java
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Data components for statistics and configuration.
|
||||
* <p>
|
||||
* This package contains classes for data management:
|
||||
* <ul>
|
||||
* <li>{@code MineConfig} - Configuration data model for mines</li>
|
||||
* <li>{@code Reward} - Reward configuration data model</li>
|
||||
* <li>{@code PlayerStats} - Player statistics tracking</li>
|
||||
* <li>{@code StatsManager} - Manager for all player statistics</li>
|
||||
* <li>{@code Leaderboard} - Leaderboard generation and caching</li>
|
||||
* <li>{@code ConfigLoader} - Configuration loading and saving</li>
|
||||
* <li>{@code ConfigParseException} - Exception for configuration parsing errors</li>
|
||||
* </ul>
|
||||
*/
|
||||
package dev.loki.lomines.data;
|
||||
62
src/main/java/dev/loki/lomines/data/reward/Reward.java
Normal file
62
src/main/java/dev/loki/lomines/data/reward/Reward.java
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package dev.loki.lomines.data.reward;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Immutable reward configuration for mine blocks.
|
||||
* All collections are copied to ensure immutability.
|
||||
*/
|
||||
public record Reward(List<Material> materials, double chance, List<ItemStack> items, List<String> commands,
|
||||
boolean preventDrops) {
|
||||
public Reward(List<Material> materials, double chance,
|
||||
List<ItemStack> items, List<String> commands,
|
||||
boolean preventDrops) {
|
||||
this.materials = List.copyOf(materials);
|
||||
this.chance = chance;
|
||||
this.items = List.copyOf(items);
|
||||
this.commands = List.copyOf(commands);
|
||||
this.preventDrops = preventDrops;
|
||||
}
|
||||
|
||||
public boolean matches(Material material) {
|
||||
return materials.contains(material);
|
||||
}
|
||||
|
||||
// JavaBean-style getters for compatibility
|
||||
public List<Material> getMaterials() {
|
||||
return materials;
|
||||
}
|
||||
|
||||
public double getChance() {
|
||||
return chance;
|
||||
}
|
||||
|
||||
public List<ItemStack> getItems() {
|
||||
return items;
|
||||
}
|
||||
|
||||
public List<String> getCommands() {
|
||||
return commands;
|
||||
}
|
||||
|
||||
public boolean isPreventDrops() {
|
||||
return preventDrops;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
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 &&
|
||||
Objects.equals(materials, reward.materials) &&
|
||||
Objects.equals(items, reward.items) &&
|
||||
Objects.equals(commands, reward.commands);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
package dev.loki.lomines.data.reward;
|
||||
|
||||
import dev.loki.lomines.data.config.parser.ConfigParseException;
|
||||
import dev.loki.lomines.util.ValidationUtils;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Parses ItemStack objects from YAML reward item configurations.
|
||||
* Handles material type, amount, and basic item properties.
|
||||
*/
|
||||
final class RewardItemParser {
|
||||
|
||||
/**
|
||||
* Parses a single reward item from a map configuration.
|
||||
*
|
||||
* @param itemMap the item configuration map
|
||||
* @param index the index of the item in the list (for error messages)
|
||||
* @return the parsed ItemStack
|
||||
* @throws ConfigParseException if the item configuration is invalid
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
String typeName = String.valueOf(itemMap.get("type"));
|
||||
Material material = parseMaterial(typeName);
|
||||
int amount = parseAmount(itemMap);
|
||||
|
||||
return new ItemStack(material, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a material from a string name.
|
||||
*
|
||||
* @param typeName the material name
|
||||
* @return the parsed Material
|
||||
* @throws ConfigParseException if the material is unknown
|
||||
*/
|
||||
private Material parseMaterial(String typeName) throws ConfigParseException {
|
||||
try {
|
||||
return ValidationUtils.validateMaterial(typeName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ConfigParseException(
|
||||
"Unknown material in item type: '" + typeName + "'",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the amount field from an item configuration.
|
||||
*
|
||||
* @param itemMap the item configuration map
|
||||
* @return the parsed amount (defaults to 1 if not specified)
|
||||
* @throws ConfigParseException if the amount value is invalid
|
||||
*/
|
||||
private int parseAmount(Map<String, Object> itemMap) throws ConfigParseException {
|
||||
if (!itemMap.containsKey("amount")) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
Object amountObj = itemMap.get("amount");
|
||||
try {
|
||||
if (amountObj instanceof Number) {
|
||||
return ((Number) amountObj).intValue();
|
||||
} else if (amountObj instanceof String) {
|
||||
return Integer.parseInt((String) amountObj);
|
||||
} else {
|
||||
throw new ConfigParseException(
|
||||
"Invalid 'amount' type: expected number, got " +
|
||||
(amountObj != null ? amountObj.getClass().getSimpleName() : "null")
|
||||
);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ConfigParseException("Invalid 'amount' value: " + amountObj, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
224
src/main/java/dev/loki/lomines/data/reward/RewardParser.java
Normal file
224
src/main/java/dev/loki/lomines/data/reward/RewardParser.java
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
package dev.loki.lomines.data.reward;
|
||||
|
||||
import dev.loki.lomines.data.config.parser.ConfigParseException;
|
||||
import dev.loki.lomines.util.ValidationUtils;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
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 {
|
||||
|
||||
private final RewardItemParser itemParser;
|
||||
|
||||
public RewardParser() {
|
||||
this.itemParser = new RewardItemParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses all rewards from the YAML configuration.
|
||||
*
|
||||
* @param yaml the YAML configuration
|
||||
* @return list of parsed Reward objects
|
||||
* @throws ConfigParseException if reward configuration is invalid
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a single reward from a map configuration.
|
||||
*
|
||||
* @param rewardMap the reward configuration map
|
||||
* @return the parsed Reward object
|
||||
* @throws ConfigParseException if the reward configuration is invalid
|
||||
*/
|
||||
private Reward parseReward(Map<String, Object> rewardMap) throws ConfigParseException {
|
||||
double chance = parseRewardField(rewardMap, "chance", Double.class);
|
||||
boolean preventDrops = parseRewardField(rewardMap, "prevent-drops", Boolean.class, false);
|
||||
|
||||
List<Material> materials = parseRewardMaterials(rewardMap);
|
||||
List<ItemStack> items = parseRewardItems(rewardMap);
|
||||
List<String> commands = parseRewardCommands(rewardMap);
|
||||
|
||||
return new Reward(materials, chance, items, commands, preventDrops);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <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");
|
||||
}
|
||||
Object value = map.get(key);
|
||||
if (type == Double.class) {
|
||||
return (T) parseDoubleValue(key, value);
|
||||
} else if (type == Boolean.class) {
|
||||
if (!(value instanceof Boolean)) {
|
||||
throw new ConfigParseException(
|
||||
"Invalid '" + key + "' value: expected boolean, got " +
|
||||
(value != null ? value.getClass().getSimpleName() : "null")
|
||||
);
|
||||
}
|
||||
return (T) value;
|
||||
}
|
||||
throw new ConfigParseException("Unsupported type: " + type);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T parseRewardField(Map<String, Object> map, String key, Class<T> type, T defaultValue) {
|
||||
if (!map.containsKey(key)) {
|
||||
return defaultValue;
|
||||
}
|
||||
Object value = map.get(key);
|
||||
if (type.isInstance(value)) {
|
||||
return (T) value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private Double parseDoubleValue(String key, Object value) throws ConfigParseException {
|
||||
try {
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).doubleValue();
|
||||
} else if (value instanceof String) {
|
||||
return Double.parseDouble((String) value);
|
||||
} else {
|
||||
throw new ConfigParseException(
|
||||
"Invalid '" + key + "' type: expected number, got " +
|
||||
(value != null ? value.getClass().getSimpleName() : "null")
|
||||
);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw new ConfigParseException("Invalid '" + key + "' value: " + value, e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Material> parseRewardMaterials(Map<String, Object> rewardMap)
|
||||
throws ConfigParseException {
|
||||
if (!rewardMap.containsKey("blocks")) {
|
||||
throw new ConfigParseException("Missing required 'blocks' field in reward");
|
||||
}
|
||||
Object blocksObj = rewardMap.get("blocks");
|
||||
if (!(blocksObj instanceof List)) {
|
||||
throw new ConfigParseException(
|
||||
"Invalid 'blocks' type: expected list, got " +
|
||||
(blocksObj != null ? blocksObj.getClass().getSimpleName() : "null")
|
||||
);
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> blocksList = (List<String>) blocksObj;
|
||||
List<Material> materials = new ArrayList<>();
|
||||
for (String blockName : blocksList) {
|
||||
if (blockName == null || blockName.trim().isEmpty()) {
|
||||
throw new ConfigParseException("Block name cannot be null or empty");
|
||||
}
|
||||
try {
|
||||
Material material = ValidationUtils.validateMaterial(blockName);
|
||||
materials.add(material);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ConfigParseException(
|
||||
"Unknown material in reward blocks: '" + blockName + "'",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
return materials;
|
||||
}
|
||||
|
||||
private List<ItemStack> parseRewardItems(Map<String, Object> rewardMap)
|
||||
throws ConfigParseException {
|
||||
List<ItemStack> items = new ArrayList<>();
|
||||
if (!rewardMap.containsKey("items")) {
|
||||
return items;
|
||||
}
|
||||
Object itemsObj = rewardMap.get("items");
|
||||
if (!(itemsObj instanceof List<?> itemsList)) {
|
||||
throw new ConfigParseException(
|
||||
"Invalid 'items' type: expected list, got " +
|
||||
(itemsObj != null ? itemsObj.getClass().getSimpleName() : "null")
|
||||
);
|
||||
}
|
||||
for (int i = 0; i < itemsList.size(); i++) {
|
||||
Object itemObj = itemsList.get(i);
|
||||
if (!(itemObj instanceof Map)) {
|
||||
throw new ConfigParseException(
|
||||
"Invalid item at index " + i + ": expected map, got " +
|
||||
(itemObj != null ? itemObj.getClass().getSimpleName() : "null")
|
||||
);
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> itemMap = (Map<String, Object>) itemObj;
|
||||
ItemStack item = itemParser.parseItem(itemMap, i);
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private List<String> parseRewardCommands(Map<String, Object> rewardMap)
|
||||
throws ConfigParseException {
|
||||
List<String> commands = new ArrayList<>();
|
||||
if (!rewardMap.containsKey("commands")) {
|
||||
return commands;
|
||||
}
|
||||
Object commandsObj = rewardMap.get("commands");
|
||||
if (!(commandsObj instanceof List)) {
|
||||
throw new ConfigParseException(
|
||||
"Invalid 'commands' type: expected list, got " +
|
||||
(commandsObj != null ? commandsObj.getClass().getSimpleName() : "null")
|
||||
);
|
||||
}
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> commandsList = (List<String>) commandsObj;
|
||||
for (String command : commandsList) {
|
||||
if (command == null || command.trim().isEmpty()) {
|
||||
throw new ConfigParseException("Command cannot be null or empty");
|
||||
}
|
||||
commands.add(command);
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
104
src/main/java/dev/loki/lomines/data/stats/Leaderboard.java
Normal file
104
src/main/java/dev/loki/lomines/data/stats/Leaderboard.java
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package dev.loki.lomines.data.stats;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Manages leaderboard functionality with caching.
|
||||
* Provides methods to retrieve top players by total blocks or by specific mine.
|
||||
* <p>
|
||||
* Thread-safety: Uses volatile cache and synchronized blocks for thread-safe operations.
|
||||
* Cache is invalidated when player statistics change.
|
||||
*/
|
||||
public final class Leaderboard {
|
||||
|
||||
private final StatsManager statsManager;
|
||||
private final Object cacheLock = new Object();
|
||||
// Cache for getTopTotal - volatile for visibility across threads
|
||||
private volatile List<LeaderboardEntry> cachedTopTotal = null;
|
||||
|
||||
/**
|
||||
* Creates a new Leaderboard instance.
|
||||
*
|
||||
* @param statsManager The stats manager to retrieve player statistics from
|
||||
*/
|
||||
public Leaderboard(StatsManager statsManager) {
|
||||
this.statsManager = statsManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates the cached top total leaderboard.
|
||||
* Should be called when player statistics change.
|
||||
*/
|
||||
public void invalidateCache() {
|
||||
synchronized (cacheLock) {
|
||||
cachedTopTotal = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top players by total blocks mined across all mines.
|
||||
* Results are cached and reused until invalidateCache() is called.
|
||||
*
|
||||
* @param limit Maximum number of entries to return
|
||||
* @return List of leaderboard entries sorted by descending block count
|
||||
*/
|
||||
public List<LeaderboardEntry> getTopTotal(int limit) {
|
||||
// Fast path: check cache without locking
|
||||
if (cachedTopTotal != null) {
|
||||
return cachedTopTotal.subList(0, Math.min(limit, cachedTopTotal.size()));
|
||||
}
|
||||
|
||||
// Slow path: build cache with double-checked locking
|
||||
synchronized (cacheLock) {
|
||||
// Double-check: another thread might have built the cache
|
||||
if (cachedTopTotal != null) {
|
||||
return cachedTopTotal.subList(0, Math.min(limit, cachedTopTotal.size()));
|
||||
}
|
||||
|
||||
// Build the cache
|
||||
cachedTopTotal = statsManager.getAllStats().stream()
|
||||
.filter(stats -> stats.getTotalBlocks() > 0)
|
||||
.sorted(Comparator.comparingLong(PlayerStats::getTotalBlocks).reversed())
|
||||
.map(stats -> new LeaderboardEntry(stats.getPlayerId(), stats.getTotalBlocks()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return cachedTopTotal.subList(0, Math.min(limit, cachedTopTotal.size()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top players for a specific mine.
|
||||
* This method does not use caching as mine-specific leaderboards are less frequently accessed.
|
||||
*
|
||||
* @param mineName The name of the mine
|
||||
* @param limit Maximum number of entries to return
|
||||
* @return List of leaderboard entries sorted by descending block count for the specified mine
|
||||
*/
|
||||
public List<LeaderboardEntry> getTopByMine(String mineName, int limit) {
|
||||
return statsManager.getAllStats().stream()
|
||||
.map(stats -> new LeaderboardEntry(stats.getPlayerId(), stats.getMineBlocks(mineName)))
|
||||
.filter(entry -> entry.count() > 0)
|
||||
.sorted(Comparator.comparingLong(LeaderboardEntry::count).reversed())
|
||||
.limit(limit)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the position of a player in the total leaderboard.
|
||||
*
|
||||
* @param playerId The UUID of the player
|
||||
* @return The 1-based position of the player, or -1 if the player is not in the leaderboard
|
||||
*/
|
||||
public int getPosition(UUID playerId) {
|
||||
List<LeaderboardEntry> top = getTopTotal(Integer.MAX_VALUE);
|
||||
for (int i = 0; i < top.size(); i++) {
|
||||
if (top.get(i).playerId().equals(playerId)) {
|
||||
return i + 1; // 1-based position
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package dev.loki.lomines.data.stats;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Immutable data class representing a single entry in the leaderboard.
|
||||
* Contains a player's UUID and their block count.
|
||||
*/
|
||||
public record LeaderboardEntry(UUID playerId, long count) {
|
||||
|
||||
public LeaderboardEntry(UUID playerId, long count) {
|
||||
this.playerId = Objects.requireNonNull(playerId, "playerId cannot be null");
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
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 String toString() {
|
||||
return "LeaderboardEntry{playerId=" + playerId + ", count=" + count + "}";
|
||||
}
|
||||
}
|
||||
57
src/main/java/dev/loki/lomines/data/stats/PlayerStats.java
Normal file
57
src/main/java/dev/loki/lomines/data/stats/PlayerStats.java
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package dev.loki.lomines.data.stats;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Stores statistics for a single player.
|
||||
* This is a placeholder implementation that will be completed in task 5.1.
|
||||
*/
|
||||
public final class PlayerStats {
|
||||
|
||||
private final UUID playerId;
|
||||
private final AtomicLong totalBlocks = new AtomicLong(0);
|
||||
private final ConcurrentHashMap<String, AtomicLong> mineStats = new ConcurrentHashMap<>();
|
||||
|
||||
public PlayerStats(UUID playerId) {
|
||||
this.playerId = playerId;
|
||||
}
|
||||
|
||||
public UUID getPlayerId() {
|
||||
return playerId;
|
||||
}
|
||||
|
||||
public long getTotalBlocks() {
|
||||
return totalBlocks.get();
|
||||
}
|
||||
|
||||
public void setTotalBlocks(long value) {
|
||||
totalBlocks.set(value);
|
||||
}
|
||||
|
||||
public long getMineBlocks(String mineName) {
|
||||
AtomicLong counter = mineStats.get(mineName.toLowerCase());
|
||||
return counter != null ? counter.get() : 0;
|
||||
}
|
||||
|
||||
public void incrementTotal() {
|
||||
totalBlocks.incrementAndGet();
|
||||
}
|
||||
|
||||
public void incrementMine(String mineName) {
|
||||
mineStats.computeIfAbsent(mineName.toLowerCase(), k -> new AtomicLong(0))
|
||||
.incrementAndGet();
|
||||
}
|
||||
|
||||
public void setMineBlocks(String mineName, long value) {
|
||||
mineStats.computeIfAbsent(mineName.toLowerCase(), k -> new AtomicLong(0))
|
||||
.set(value);
|
||||
}
|
||||
|
||||
public java.util.Map<String, Long> getMineStatsSnapshot() {
|
||||
java.util.Map<String, Long> snapshot = new java.util.HashMap<>();
|
||||
mineStats.forEach((mine, counter) -> snapshot.put(mine, counter.get()));
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
181
src/main/java/dev/loki/lomines/data/stats/StatsManager.java
Normal file
181
src/main/java/dev/loki/lomines/data/stats/StatsManager.java
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue