refactor(config): complete rewrite of configuration system
BREAKING CHANGE: New section-based configuration format New Features: - Type-safe BlockKey sealed interface (Vanilla, Oraxen, ItemsAdder) - Section-based config: RegionConfig, BlockConfig, ResetConfig, RewardConfig, UIConfig, TeleportConfig - Human-readable durations: 5m, 30s, 2h, 1d - YAML inheritance from defaults.yml - MiniMessage support for action bar and item names - Immutable config records with validation Removed: - Legacy MineConfig (17-field god object) - ConfigParser, ConfigValidator, ConfigSerializer split classes - Legacy FillMode enum (replaced with better version) - Old ConfigLoader with complex parsing logic New Files: - data/config/block/BlockKey.java - sealed interface for block types - data/config/block/BlockConfig.java - type-safe block weights - data/config/block/FillMode.java - fill strategies - data/config/region/RegionConfig.java - region management - data/config/reset/ResetConfig.java - reset behavior with Duration - data/config/reward/RewardConfig.java - rewards with ItemReward - data/config/teleport/TeleportConfig.java - teleport on reset - data/config/ui/UIConfig.java - action bar and messages - data/config/MineConfig.java - composed from sections - data/config/ConfigLoader.java - clean loader with defaults inheritance - defaults.yml - default configuration template Tests: - BlockKeyTest.java - serialization/deserialization - BlockConfigTest.java - weight normalization, validation - ResetConfigTest.java - duration parsing, defaults - UIConfigTest.java - timer formatting, action bar - MineConfigTest.java - builder, defaults, validation
This commit is contained in:
parent
f4e9458544
commit
f1060f34d4
22 changed files with 2012 additions and 896 deletions
|
|
@ -1,111 +1,427 @@
|
|||
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 dev.loki.lomines.data.config.block.BlockConfig;
|
||||
import dev.loki.lomines.data.config.block.BlockKey;
|
||||
import dev.loki.lomines.data.config.block.FillMode;
|
||||
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.reward.RewardConfig.RewardEntry;
|
||||
import dev.loki.lomines.data.config.reward.RewardConfig.RewardEntry.ItemReward;
|
||||
import dev.loki.lomines.data.config.teleport.TeleportConfig;
|
||||
import dev.loki.lomines.data.config.ui.UIConfig;
|
||||
import dev.loki.lomines.util.location.Cuboid;
|
||||
import dev.loki.lomines.util.location.LocationParser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Clean config loader for section-based configuration.
|
||||
* Supports YAML inheritance from defaults.
|
||||
*/
|
||||
public final class ConfigLoader {
|
||||
|
||||
private final ConfigParser parser;
|
||||
private final ConfigValidator validator;
|
||||
private final ConfigSerializer serializer;
|
||||
private final Path dataFolder;
|
||||
private YamlConfiguration defaults;
|
||||
|
||||
public ConfigLoader() {
|
||||
RewardParser rewardParser = new RewardParser();
|
||||
this.parser = new ConfigParser(rewardParser);
|
||||
this.validator = new ConfigValidator();
|
||||
this.serializer = new ConfigSerializer();
|
||||
public ConfigLoader(Path dataFolder) {
|
||||
this.dataFolder = dataFolder;
|
||||
loadDefaults();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Load or create defaults.yml.
|
||||
*/
|
||||
public MineConfig load(Path configFile) throws IOException, ConfigParseException {
|
||||
if (configFile == null) {
|
||||
throw new ConfigParseException("Config file path cannot be null");
|
||||
private void loadDefaults() {
|
||||
Path defaultsPath = dataFolder.resolve("mines").resolve("_defaults.yml");
|
||||
defaults = new YamlConfiguration();
|
||||
|
||||
if (Files.exists(defaultsPath)) {
|
||||
defaults = YamlConfiguration.loadConfiguration(defaultsPath.toFile());
|
||||
} else {
|
||||
// Create defaults with sensible values
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)) {
|
||||
throw new ConfigLoadException("Mine not found: " + mineName);
|
||||
}
|
||||
|
||||
if (!configFile.toFile().exists()) {
|
||||
throw new IOException("Config file does not exist: " + configFile);
|
||||
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(configPath.toFile());
|
||||
|
||||
// Merge with defaults (YAML inheritance)
|
||||
mergeDefaults(yaml);
|
||||
|
||||
try {
|
||||
return MineConfig.builder(mineName)
|
||||
.region(parseRegion(yaml))
|
||||
.blocks(parseBlocks(yaml))
|
||||
.reset(parseReset(yaml))
|
||||
.rewards(parseRewards(yaml))
|
||||
.teleport(parseTeleport(yaml))
|
||||
.ui(parseUI(yaml))
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
throw new ConfigLoadException("Failed to load mine '" + mineName + "': " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
// Save regions
|
||||
saveRegion(yaml, config.region());
|
||||
|
||||
// Save blocks
|
||||
saveBlocks(yaml, config.blocks());
|
||||
|
||||
// Save reset
|
||||
saveReset(yaml, config.reset());
|
||||
|
||||
// Save rewards
|
||||
saveRewards(yaml, config.rewards());
|
||||
|
||||
// Save teleport
|
||||
saveTeleport(yaml, config.teleport());
|
||||
|
||||
// Save UI
|
||||
saveUI(yaml, config.ui());
|
||||
|
||||
try {
|
||||
Files.createDirectories(configPath.getParent());
|
||||
yaml.save(configPath.toFile());
|
||||
} catch (IOException e) {
|
||||
throw new ConfigLoadException("Failed to save mine: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Parsers ---
|
||||
|
||||
private RegionConfig parseRegion(YamlConfiguration yaml) {
|
||||
List<Location> selections = new ArrayList<>();
|
||||
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
String key = "region.selection." + i;
|
||||
if (yaml.contains(key)) {
|
||||
String locStr = yaml.getString(key);
|
||||
if (locStr != null && !locStr.isBlank()) {
|
||||
selections.add(parseLocation(locStr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(configFile.toFile());
|
||||
if (selections.isEmpty()) {
|
||||
// Legacy fallback
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
String key = "selection." + i;
|
||||
if (yaml.contains(key)) {
|
||||
String locStr = yaml.getString(key);
|
||||
if (locStr != null && !locStr.isBlank()) {
|
||||
selections.add(parseLocation(locStr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
if (selections.size() < 2) {
|
||||
throw new IllegalArgumentException("Mine must have at least 2 selection points (1 region)");
|
||||
}
|
||||
|
||||
// Validate all data
|
||||
validator.validateSelections(selections);
|
||||
validator.validateBlocks(blocks);
|
||||
validator.validateMaskMarker(maskMarker);
|
||||
validator.validateRewards(rewards);
|
||||
return RegionConfig.fromSelections(selections);
|
||||
}
|
||||
|
||||
// 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", ""))
|
||||
private BlockConfig parseBlocks(YamlConfiguration yaml) {
|
||||
ConfigurationSection contents = yaml.getConfigurationSection("blocks.contents");
|
||||
if (contents == null) {
|
||||
contents = yaml.getConfigurationSection("contents"); // Legacy fallback
|
||||
}
|
||||
|
||||
if (contents == null) {
|
||||
throw new IllegalArgumentException("Missing 'blocks.contents' section");
|
||||
}
|
||||
|
||||
Map<BlockKey, Double> weights = new HashMap<>();
|
||||
for (String key : contents.getKeys(false)) {
|
||||
double weight = contents.getDouble(key);
|
||||
if (weight <= 0) continue;
|
||||
weights.put(BlockKey.deserialize(key), weight);
|
||||
}
|
||||
|
||||
FillMode fillMode = FillMode.valueOf(
|
||||
yaml.getString("blocks.fill-mode", "CUBOID").toUpperCase()
|
||||
);
|
||||
|
||||
// Parse mask config if applicable
|
||||
BlockConfig.MaskConfig mask = null;
|
||||
if (fillMode == FillMode.MASK) {
|
||||
String markerStr = yaml.getString("blocks.mask.marker", "pink_concrete");
|
||||
BlockKey marker = BlockKey.deserialize(markerStr);
|
||||
|
||||
Map<String, Boolean> positions = new HashMap<>();
|
||||
List<String> posList = yaml.getStringList("blocks.mask.positions");
|
||||
for (String pos : posList) {
|
||||
positions.put(pos, true);
|
||||
}
|
||||
|
||||
mask = new BlockConfig.MaskConfig(marker, positions);
|
||||
}
|
||||
|
||||
return new BlockConfig(weights, fillMode, mask);
|
||||
}
|
||||
|
||||
private ResetConfig parseReset(YamlConfiguration yaml) {
|
||||
String intervalStr = yaml.getString("reset.interval", defaults.getString("reset.interval", "5m"));
|
||||
|
||||
return ResetConfig.builder()
|
||||
.interval(intervalStr)
|
||||
.percentTrigger(yaml.getDouble("reset.percent-trigger", defaults.getDouble("reset.percent-trigger", 10.0)))
|
||||
.percentEnabled(yaml.getBoolean("reset.percent-enabled", defaults.getBoolean("reset.percent-enabled", false)))
|
||||
.commands(yaml.getStringList("reset.commands"))
|
||||
.broadcastMessage(yaml.getString("reset.broadcast", ""))
|
||||
.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");
|
||||
private RewardConfig parseRewards(YamlConfiguration yaml) {
|
||||
List<RewardEntry> entries = new ArrayList<>();
|
||||
List<Map<?, ?>> rewardList = yaml.getMapList("rewards");
|
||||
|
||||
if (rewardList.isEmpty()) {
|
||||
rewardList = yaml.getMapList("random-rewards"); // Legacy fallback
|
||||
}
|
||||
|
||||
YamlConfiguration yaml = new YamlConfiguration();
|
||||
serializer.serialize(config, yaml);
|
||||
yaml.save(configFile.toFile());
|
||||
for (Map<?, ?> map : rewardList) {
|
||||
entries.add(parseRewardEntry(map));
|
||||
}
|
||||
|
||||
return new RewardConfig(entries);
|
||||
}
|
||||
|
||||
private RewardEntry parseRewardEntry(Map<?, ?> map) {
|
||||
double chance = ((Number) map.getOrDefault("chance", 0)).doubleValue();
|
||||
|
||||
List<BlockKey> blocks = new ArrayList<>();
|
||||
List<?> blockList = (List<?>) map.get("blocks");
|
||||
if (blockList != null) {
|
||||
for (Object b : blockList) {
|
||||
blocks.add(BlockKey.deserialize(b.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
List<ItemReward> items = new ArrayList<>();
|
||||
List<?> itemList = (List<?>) map.get("items");
|
||||
if (itemList != null) {
|
||||
for (Object i : itemList) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> itemMap = (Map<String, Object>) i;
|
||||
items.add(parseItemReward(itemMap));
|
||||
}
|
||||
}
|
||||
|
||||
List<String> commands = (List<String>) map.getOrDefault("commands", List.of());
|
||||
boolean preventDrops = (Boolean) map.getOrDefault("prevent-drops", false);
|
||||
|
||||
return new RewardEntry(blocks, chance, items, commands, preventDrops);
|
||||
}
|
||||
|
||||
private ItemReward parseItemReward(Map<String, Object> map) {
|
||||
String typeStr = (String) map.get("type");
|
||||
Material material = Material.matchMaterial(typeStr);
|
||||
if (material == null) {
|
||||
material = Material.STONE;
|
||||
}
|
||||
|
||||
int amount = ((Number) map.getOrDefault("amount", 1)).intValue();
|
||||
String name = (String) map.get("name");
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> lore = (List<String>) map.getOrDefault("lore", List.of());
|
||||
|
||||
return new ItemReward(material, amount, name, lore);
|
||||
}
|
||||
|
||||
private TeleportConfig parseTeleport(YamlConfiguration yaml) {
|
||||
boolean enabled = yaml.getBoolean("teleport.enabled", false);
|
||||
if (!enabled) {
|
||||
return TeleportConfig.disabled();
|
||||
}
|
||||
|
||||
String locStr = yaml.getString("teleport.location");
|
||||
if (locStr == null) {
|
||||
return TeleportConfig.disabled();
|
||||
}
|
||||
|
||||
return TeleportConfig.at(parseLocation(locStr));
|
||||
}
|
||||
|
||||
private UIConfig parseUI(YamlConfiguration yaml) {
|
||||
boolean actionBarEnabled = yaml.getBoolean("ui.actionbar.enabled",
|
||||
defaults.getBoolean("ui.actionbar.enabled", true));
|
||||
|
||||
return new UIConfig(
|
||||
actionBarEnabled,
|
||||
yaml.getString("ui.actionbar.format", UIConfig.DEFAULT_ACTIONBAR_FORMAT),
|
||||
yaml.getDouble("ui.actionbar.range", 50.0),
|
||||
yaml.getString("ui.timer-format", UIConfig.DEFAULT_TIMER_FORMAT)
|
||||
);
|
||||
}
|
||||
|
||||
// --- Savers ---
|
||||
|
||||
private void saveRegion(YamlConfiguration yaml, RegionConfig region) {
|
||||
int i = 1;
|
||||
for (Cuboid cuboid : region.regions()) {
|
||||
yaml.set("region.selection." + i, LocationParser.format(cuboid.getMin()));
|
||||
i++;
|
||||
yaml.set("region.selection." + i, LocationParser.format(cuboid.getMax()));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveBlocks(YamlConfiguration yaml, BlockConfig blocks) {
|
||||
for (var entry : blocks.weights().entrySet()) {
|
||||
yaml.set("blocks.contents." + entry.getKey().serialize(), entry.getValue());
|
||||
}
|
||||
yaml.set("blocks.fill-mode", blocks.fillMode().name().toLowerCase());
|
||||
|
||||
if (blocks.fillMode() == FillMode.MASK && blocks.mask() != null) {
|
||||
yaml.set("blocks.mask.marker", blocks.mask().marker().serialize());
|
||||
yaml.set("blocks.mask.positions", new ArrayList<>(blocks.mask().positions().keySet()));
|
||||
}
|
||||
}
|
||||
|
||||
private void saveReset(YamlConfiguration yaml, ResetConfig reset) {
|
||||
yaml.set("reset.interval", reset.intervalDisplay());
|
||||
yaml.set("reset.percent-trigger", reset.percentTrigger());
|
||||
yaml.set("reset.percent-enabled", reset.percentEnabled());
|
||||
yaml.set("reset.commands", reset.commands());
|
||||
yaml.set("reset.broadcast", reset.broadcastMessage());
|
||||
}
|
||||
|
||||
private void saveRewards(YamlConfiguration yaml, RewardConfig rewards) {
|
||||
List<Map<String, Object>> list = new ArrayList<>();
|
||||
for (RewardEntry entry : rewards.entries()) {
|
||||
Map<String, Object> map = new LinkedHashMap<>();
|
||||
map.put("chance", entry.chance());
|
||||
map.put("blocks", entry.blocks().stream().map(BlockKey::serialize).toList());
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
for (ItemReward item : entry.items()) {
|
||||
Map<String, Object> itemMap = new LinkedHashMap<>();
|
||||
itemMap.put("type", item.material().name().toLowerCase());
|
||||
itemMap.put("amount", item.amount());
|
||||
if (item.displayName() != null) {
|
||||
itemMap.put("name", item.displayName());
|
||||
}
|
||||
if (!item.lore().isEmpty()) {
|
||||
itemMap.put("lore", item.lore());
|
||||
}
|
||||
items.add(itemMap);
|
||||
}
|
||||
map.put("items", items);
|
||||
map.put("commands", entry.commands());
|
||||
map.put("prevent-drops", entry.preventVanillaDrops());
|
||||
list.add(map);
|
||||
}
|
||||
yaml.set("rewards", list);
|
||||
}
|
||||
|
||||
private void saveTeleport(YamlConfiguration yaml, TeleportConfig teleport) {
|
||||
yaml.set("teleport.enabled", teleport.enabled());
|
||||
teleport.getLocation().ifPresent(loc ->
|
||||
yaml.set("teleport.location", LocationParser.format(loc)));
|
||||
}
|
||||
|
||||
private void saveUI(YamlConfiguration yaml, UIConfig ui) {
|
||||
yaml.set("ui.actionbar.enabled", ui.actionBarEnabled());
|
||||
yaml.set("ui.actionbar.format", ui.actionBarFormat());
|
||||
yaml.set("ui.actionbar.range", ui.actionBarRange());
|
||||
yaml.set("ui.timer-format", ui.timerFormat());
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
private Location parseLocation(String str) {
|
||||
String[] parts = str.split(";");
|
||||
if (parts.length < 4) {
|
||||
throw new IllegalArgumentException("Invalid location format: " + str);
|
||||
}
|
||||
|
||||
World world = Bukkit.getWorld(parts[0]);
|
||||
if (world == null) {
|
||||
throw new IllegalArgumentException("Unknown world: " + parts[0]);
|
||||
}
|
||||
|
||||
double x = Double.parseDouble(parts[1]);
|
||||
double y = Double.parseDouble(parts[2]);
|
||||
double z = Double.parseDouble(parts[3]);
|
||||
float yaw = parts.length > 4 ? Float.parseFloat(parts[4]) : 0;
|
||||
float pitch = parts.length > 5 ? Float.parseFloat(parts[5]) : 0;
|
||||
|
||||
return new Location(world, x, y, z, yaw, pitch);
|
||||
}
|
||||
|
||||
private void mergeDefaults(YamlConfiguration yaml) {
|
||||
// Simple shallow merge - could be enhanced for deep merging
|
||||
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);
|
||||
}
|
||||
public ConfigLoadException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,142 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -1,189 +1,126 @@
|
|||
package dev.loki.lomines.data.config;
|
||||
|
||||
import dev.loki.lomines.data.reward.Reward;
|
||||
import org.bukkit.Location;
|
||||
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.teleport.TeleportConfig;
|
||||
import dev.loki.lomines.data.config.ui.UIConfig;
|
||||
|
||||
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.
|
||||
* Clean, type-safe, section-based mine configuration.
|
||||
* Proper separation of concerns: each section handles one aspect.
|
||||
*/
|
||||
public final class MineConfig {
|
||||
public record MineConfig(
|
||||
String name,
|
||||
RegionConfig region,
|
||||
BlockConfig blocks,
|
||||
ResetConfig reset,
|
||||
RewardConfig rewards,
|
||||
TeleportConfig teleport,
|
||||
UIConfig ui
|
||||
) {
|
||||
|
||||
// Regions
|
||||
private final List<Location> selections;
|
||||
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();
|
||||
|
||||
// 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;
|
||||
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");
|
||||
}
|
||||
|
||||
/**
|
||||
* Material name for scanning marker blocks (vanilla), e.g. {@code pink_concrete}.
|
||||
* Total volume of the mine (for progress calculation).
|
||||
*/
|
||||
public String getMaskMarkerMaterial() {
|
||||
return maskMarkerMaterial;
|
||||
public int volume() {
|
||||
return region.totalVolume();
|
||||
}
|
||||
|
||||
public List<Location> getMaskPositions() {
|
||||
return maskPositions;
|
||||
/**
|
||||
* World name where the mine is located.
|
||||
*/
|
||||
public String worldName() {
|
||||
return region.worldName();
|
||||
}
|
||||
|
||||
public List<Reward> getRewards() {
|
||||
return rewards;
|
||||
/**
|
||||
* Creates builder for fluent construction.
|
||||
*/
|
||||
public static Builder builder(String name) {
|
||||
return new Builder(name);
|
||||
}
|
||||
|
||||
public int getResetTicks() {
|
||||
return resetTicks;
|
||||
/**
|
||||
* Creates config with sensible defaults.
|
||||
*/
|
||||
public static MineConfig defaults(String name, RegionConfig region, BlockConfig blocks) {
|
||||
return new MineConfig(
|
||||
name,
|
||||
region,
|
||||
blocks,
|
||||
ResetConfig.defaults(),
|
||||
RewardConfig.empty(),
|
||||
TeleportConfig.disabled(),
|
||||
UIConfig.defaults()
|
||||
);
|
||||
}
|
||||
|
||||
public double getResetPercent() {
|
||||
return resetPercent;
|
||||
}
|
||||
// --- Builder ---
|
||||
|
||||
public boolean isResetOnPercentEnabled() {
|
||||
return resetOnPercentEnabled;
|
||||
}
|
||||
public static class Builder {
|
||||
private final String name;
|
||||
private RegionConfig region;
|
||||
private BlockConfig blocks;
|
||||
private ResetConfig reset = ResetConfig.defaults();
|
||||
private RewardConfig rewards = RewardConfig.empty();
|
||||
private TeleportConfig teleport = TeleportConfig.disabled();
|
||||
private UIConfig ui = UIConfig.defaults();
|
||||
|
||||
public List<String> getResetCommands() {
|
||||
return resetCommands;
|
||||
}
|
||||
private Builder(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getBroadcastReset() {
|
||||
return broadcastReset;
|
||||
}
|
||||
public Builder region(RegionConfig region) {
|
||||
this.region = region;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isTeleportOnReset() {
|
||||
return teleportOnReset;
|
||||
}
|
||||
public Builder blocks(BlockConfig blocks) {
|
||||
this.blocks = blocks;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Location getTeleportLocation() {
|
||||
return teleportLocation;
|
||||
}
|
||||
public Builder reset(ResetConfig reset) {
|
||||
this.reset = reset;
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isActionBarEnabled() {
|
||||
return actionBarEnabled;
|
||||
}
|
||||
public Builder rewards(RewardConfig rewards) {
|
||||
this.rewards = rewards;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getActionBarMessage() {
|
||||
return actionBarMessage;
|
||||
}
|
||||
public Builder teleport(TeleportConfig teleport) {
|
||||
this.teleport = teleport;
|
||||
return this;
|
||||
}
|
||||
|
||||
public double getActionBarRange() {
|
||||
return actionBarRange;
|
||||
}
|
||||
public Builder ui(UIConfig ui) {
|
||||
this.ui = ui;
|
||||
return this;
|
||||
}
|
||||
|
||||
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);
|
||||
public MineConfig build() {
|
||||
return new MineConfig(name, region, blocks, reset, rewards, teleport, ui);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,129 +0,0 @@
|
|||
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,98 @@
|
|||
package dev.loki.lomines.data.config.block;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Configuration for mine block contents.
|
||||
* Type-safe, immutable, with validation.
|
||||
*/
|
||||
public record BlockConfig(
|
||||
Map<BlockKey, Double> weights,
|
||||
FillMode fillMode,
|
||||
MaskConfig mask
|
||||
) {
|
||||
|
||||
public BlockConfig {
|
||||
// Defensive copy and validation
|
||||
Map<BlockKey, Double> copy = new HashMap<>();
|
||||
double totalWeight = 0;
|
||||
|
||||
for (var entry : weights.entrySet()) {
|
||||
BlockKey key = Objects.requireNonNull(entry.getKey(), "Block key cannot be null");
|
||||
Double weight = entry.getValue();
|
||||
|
||||
if (weight == null || weight <= 0 || !Double.isFinite(weight)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid weight for " + key.serialize() + ": " + weight + " (must be positive finite number)"
|
||||
);
|
||||
}
|
||||
|
||||
totalWeight += weight;
|
||||
copy.put(key, weight);
|
||||
}
|
||||
|
||||
if (copy.isEmpty()) {
|
||||
throw new IllegalArgumentException("Block config must have at least one block");
|
||||
}
|
||||
|
||||
// Normalize weights to sum to 1.0
|
||||
if (Math.abs(totalWeight - 1.0) > 0.0001 && totalWeight > 0) {
|
||||
double finalTotal = totalWeight;
|
||||
copy.replaceAll((k, v) -> v / finalTotal);
|
||||
}
|
||||
|
||||
weights = Collections.unmodifiableMap(new HashMap<>(copy));
|
||||
fillMode = Objects.requireNonNull(fillMode, "FillMode cannot be null");
|
||||
mask = mask != null ? mask : new MaskConfig(null, Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simple vanilla-only config.
|
||||
*/
|
||||
public static BlockConfig vanilla(Map<Material, Double> materials) {
|
||||
Map<BlockKey, Double> keys = new HashMap<>();
|
||||
for (var entry : materials.entrySet()) {
|
||||
keys.put(new BlockKey.Vanilla(entry.getKey()), entry.getValue());
|
||||
}
|
||||
return new BlockConfig(keys, FillMode.CUBOID, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the weight for a specific block key.
|
||||
*/
|
||||
public double weightFor(BlockKey key) {
|
||||
return weights.getOrDefault(key, 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Total number of unique blocks.
|
||||
*/
|
||||
public int blockCount() {
|
||||
return weights.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this config uses custom blocks (Oraxen/ItemsAdder).
|
||||
*/
|
||||
public boolean hasCustomBlocks() {
|
||||
return weights.keySet().stream()
|
||||
.anyMatch(k -> k instanceof BlockKey.Oraxen || k instanceof BlockKey.ItemsAdder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for mask fill mode.
|
||||
*/
|
||||
public record MaskConfig(
|
||||
BlockKey marker,
|
||||
Map<String, Boolean> positions // serialized location -> enabled
|
||||
) {
|
||||
public MaskConfig {
|
||||
positions = positions != null
|
||||
? Collections.unmodifiableMap(new HashMap<>(positions))
|
||||
: Map.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
package dev.loki.lomines.data.config.block;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Type-safe key for identifying blocks across different plugins.
|
||||
* Supports vanilla blocks, Oraxen, and ItemsAdder.
|
||||
*/
|
||||
public sealed interface BlockKey permits BlockKey.Vanilla, BlockKey.Oraxen, BlockKey.ItemsAdder {
|
||||
|
||||
/**
|
||||
* Serializes this key to string format for YAML storage.
|
||||
* Format: "stone" (vanilla), "oraxen:my_block", "itemsadder:custom_block"
|
||||
*/
|
||||
String serialize();
|
||||
|
||||
/**
|
||||
* Deserializes a string to appropriate BlockKey type.
|
||||
*
|
||||
* @throws IllegalArgumentException if format is invalid
|
||||
*/
|
||||
static BlockKey deserialize(String key) {
|
||||
if (key == null || key.isBlank()) {
|
||||
throw new IllegalArgumentException("Block key cannot be null or empty");
|
||||
}
|
||||
|
||||
String trimmed = key.trim().toLowerCase();
|
||||
|
||||
if (trimmed.startsWith("oraxen:")) {
|
||||
return new Oraxen(trimmed.substring(7));
|
||||
}
|
||||
if (trimmed.startsWith("itemsadder:")) {
|
||||
return new ItemsAdder(trimmed.substring(11));
|
||||
}
|
||||
|
||||
// Vanilla block
|
||||
Material material = Material.matchMaterial(trimmed);
|
||||
if (material == null || !material.isBlock()) {
|
||||
throw new IllegalArgumentException("Unknown vanilla block: " + key);
|
||||
}
|
||||
return new Vanilla(material);
|
||||
}
|
||||
|
||||
// --- Implementations ---
|
||||
|
||||
record Vanilla(Material material) implements BlockKey {
|
||||
public Vanilla {
|
||||
Objects.requireNonNull(material, "Material cannot be null");
|
||||
if (!material.isBlock()) {
|
||||
throw new IllegalArgumentException(material + " is not a block");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String serialize() {
|
||||
return material.name().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
record Oraxen(String id) implements BlockKey {
|
||||
public Oraxen {
|
||||
Objects.requireNonNull(id, "Oraxen id cannot be null");
|
||||
if (id.isBlank()) {
|
||||
throw new IllegalArgumentException("Oraxen id cannot be blank");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String serialize() {
|
||||
return "oraxen:" + id;
|
||||
}
|
||||
}
|
||||
|
||||
record ItemsAdder(String id) implements BlockKey {
|
||||
public ItemsAdder {
|
||||
Objects.requireNonNull(id, "ItemsAdder id cannot be null");
|
||||
if (id.isBlank()) {
|
||||
throw new IllegalArgumentException("ItemsAdder id cannot be blank");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String serialize() {
|
||||
return "itemsadder:" + id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package dev.loki.lomines.data.config.block;
|
||||
|
||||
/**
|
||||
* Strategy for placing blocks when resetting a mine.
|
||||
*/
|
||||
public enum FillMode {
|
||||
/**
|
||||
* Fill entire cuboid region with blocks.
|
||||
*/
|
||||
CUBOID,
|
||||
|
||||
/**
|
||||
* Fill only specific positions marked by a block type.
|
||||
* Allows custom shapes (spheres, toruses, etc.).
|
||||
*/
|
||||
MASK
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package dev.loki.lomines.data.config.region;
|
||||
|
||||
import dev.loki.lomines.util.location.Cuboid;
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Configuration for mine regions.
|
||||
* Immutable, validates regions on construction.
|
||||
*/
|
||||
public record RegionConfig(List<Cuboid> regions) {
|
||||
|
||||
public RegionConfig {
|
||||
if (regions == null || regions.isEmpty()) {
|
||||
throw new IllegalArgumentException("Mine must have at least one region");
|
||||
}
|
||||
|
||||
// Defensive copy and validate
|
||||
List<Cuboid> copy = List.copyOf(regions);
|
||||
|
||||
// Check all regions are in same world
|
||||
String worldName = copy.get(0).getWorld().getName();
|
||||
for (Cuboid region : copy) {
|
||||
if (!region.getWorld().getName().equals(worldName)) {
|
||||
throw new IllegalArgumentException(
|
||||
"All regions must be in the same world. Expected: " + worldName
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
regions = Collections.unmodifiableList(copy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a single-region config.
|
||||
*/
|
||||
public static RegionConfig single(Cuboid region) {
|
||||
return new RegionConfig(List.of(region));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates from paired locations (selection.1 + selection.2 = region 1).
|
||||
* Selections must be even number.
|
||||
*/
|
||||
public static RegionConfig fromSelections(List<Location> selections) {
|
||||
if (selections.size() % 2 != 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Selections must be in pairs (even number). Got: " + selections.size()
|
||||
);
|
||||
}
|
||||
|
||||
var regions = new java.util.ArrayList<Cuboid>();
|
||||
for (int i = 0; i < selections.size(); i += 2) {
|
||||
Location loc1 = selections.get(i);
|
||||
Location loc2 = selections.get(i + 1);
|
||||
|
||||
if (!Objects.equals(loc1.getWorld(), loc2.getWorld())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Selection pair " + (i / 2 + 1) + " has locations in different worlds"
|
||||
);
|
||||
}
|
||||
|
||||
regions.add(new Cuboid(loc1, loc2));
|
||||
}
|
||||
|
||||
return new RegionConfig(regions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Total volume of all regions.
|
||||
*/
|
||||
public int totalVolume() {
|
||||
return regions.stream().mapToInt(Cuboid::getVolume).sum();
|
||||
}
|
||||
|
||||
/**
|
||||
* World name (all regions share the same world).
|
||||
*/
|
||||
public String worldName() {
|
||||
return regions.get(0).getWorld().getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if location is within any region.
|
||||
*/
|
||||
public boolean contains(Location location) {
|
||||
return regions.stream().anyMatch(r -> r.contains(location));
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of regions.
|
||||
*/
|
||||
public int regionCount() {
|
||||
return regions.size();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package dev.loki.lomines.data.config.reset;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Configuration for mine reset behavior.
|
||||
*/
|
||||
public record ResetConfig(
|
||||
Duration interval,
|
||||
double percentTrigger,
|
||||
boolean percentEnabled,
|
||||
List<String> commands,
|
||||
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;
|
||||
}
|
||||
|
||||
commands = commands != null ? List.copyOf(commands) : List.of();
|
||||
broadcastMessage = broadcastMessage != null ? broadcastMessage : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return seconds + "s";
|
||||
} else if (seconds < 3600) {
|
||||
return (seconds / 60) + "m";
|
||||
} else {
|
||||
return (seconds / 3600) + "h";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether reset should trigger based on percent mined.
|
||||
*/
|
||||
public boolean isPercentTriggerEnabled() {
|
||||
return percentEnabled && percentTrigger > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default config: 5 minutes, 10% trigger disabled.
|
||||
*/
|
||||
public static ResetConfig defaults() {
|
||||
return new ResetConfig(
|
||||
DEFAULT_INTERVAL,
|
||||
DEFAULT_PERCENT_TRIGGER,
|
||||
false,
|
||||
List.of(),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for fluent construction.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private Duration interval = DEFAULT_INTERVAL;
|
||||
private double percentTrigger = DEFAULT_PERCENT_TRIGGER;
|
||||
private boolean percentEnabled = false;
|
||||
private List<String> commands = List.of();
|
||||
private String broadcastMessage = "";
|
||||
|
||||
public Builder interval(Duration interval) {
|
||||
this.interval = interval;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder intervalTicks(long ticks) {
|
||||
this.interval = Duration.ofSeconds(ticks / 20);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder interval(String humanReadable) {
|
||||
this.interval = parseDuration(humanReadable);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder percentTrigger(double percent) {
|
||||
this.percentTrigger = percent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder percentEnabled(boolean enabled) {
|
||||
this.percentEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder commands(List<String> commands) {
|
||||
this.commands = commands;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder broadcastMessage(String message) {
|
||||
this.broadcastMessage = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResetConfig build() {
|
||||
return new ResetConfig(interval, percentTrigger, percentEnabled, commands, 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
package dev.loki.lomines.data.config.reward;
|
||||
|
||||
import dev.loki.lomines.data.config.block.BlockKey;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Configuration for mine rewards - drops when breaking specific blocks.
|
||||
*/
|
||||
public record RewardConfig(List<RewardEntry> entries) {
|
||||
|
||||
public RewardConfig {
|
||||
entries = entries != null ? List.copyOf(entries) : List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns rewards applicable for the given block.
|
||||
*/
|
||||
public List<RewardEntry> forBlock(BlockKey block) {
|
||||
return entries.stream()
|
||||
.filter(e -> e.blocks().contains(block))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any rewards are configured.
|
||||
*/
|
||||
public boolean hasRewards() {
|
||||
return !entries.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty reward config.
|
||||
*/
|
||||
public static RewardConfig empty() {
|
||||
return new RewardConfig(List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Single entry reward config builder.
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
// --- Reward Entry ---
|
||||
|
||||
public record RewardEntry(
|
||||
List<BlockKey> blocks, // Which blocks trigger this reward
|
||||
double chance, // 0.0-100.0 chance percentage
|
||||
List<ItemReward> items, // Item drops
|
||||
List<String> commands, // Commands to execute (%player% placeholder)
|
||||
boolean preventVanillaDrops // Cancel vanilla drops
|
||||
) {
|
||||
public RewardEntry {
|
||||
blocks = blocks != null ? List.copyOf(blocks) : List.of();
|
||||
items = items != null ? List.copyOf(items) : List.of();
|
||||
commands = commands != null ? List.copyOf(commands) : List.of();
|
||||
|
||||
if (chance < 0 || chance > 100) {
|
||||
throw new IllegalArgumentException("Chance must be between 0 and 100: " + chance);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this reward applies to the given block.
|
||||
*/
|
||||
public boolean appliesTo(BlockKey block) {
|
||||
return blocks.contains(block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Roll for this reward (0-100 random check).
|
||||
*/
|
||||
public boolean roll(java.util.Random random) {
|
||||
return random.nextDouble() * 100.0 < chance;
|
||||
}
|
||||
|
||||
public record ItemReward(
|
||||
Material material,
|
||||
int amount,
|
||||
String displayName, // MiniMessage format
|
||||
List<String> lore // MiniMessage format
|
||||
) {
|
||||
public ItemReward {
|
||||
Objects.requireNonNull(material, "Material cannot be null");
|
||||
amount = Math.max(1, Math.min(amount, 64));
|
||||
lore = lore != null ? List.copyOf(lore) : List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build ItemStack from this reward.
|
||||
*/
|
||||
public ItemStack toItemStack() {
|
||||
ItemStack item = new ItemStack(material, amount);
|
||||
if (displayName != null || !lore.isEmpty()) {
|
||||
item.editMeta(meta -> {
|
||||
if (displayName != null) {
|
||||
meta.displayName(net.kyori.adventure.text.minimessage.MiniMessage.miniMessage().deserialize(displayName));
|
||||
}
|
||||
if (!lore.isEmpty()) {
|
||||
var mm = net.kyori.adventure.text.minimessage.MiniMessage.miniMessage();
|
||||
meta.lore(lore.stream().map(mm::deserialize).toList());
|
||||
}
|
||||
});
|
||||
}
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Builder ---
|
||||
|
||||
public static class Builder {
|
||||
private final java.util.List<RewardEntry> entries = new java.util.ArrayList<>();
|
||||
|
||||
public Builder add(RewardEntry entry) {
|
||||
entries.add(entry);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder add(double chance, List<BlockKey> blocks, List<RewardEntry.ItemReward> items) {
|
||||
entries.add(new RewardEntry(blocks, chance, items, List.of(), false));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder add(double chance, BlockKey block, RewardEntry.ItemReward item) {
|
||||
entries.add(new RewardEntry(List.of(block), chance, List.of(item), List.of(), false));
|
||||
return this;
|
||||
}
|
||||
|
||||
public RewardConfig build() {
|
||||
return new RewardConfig(entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package dev.loki.lomines.data.config.teleport;
|
||||
|
||||
import org.bukkit.Location;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Configuration for mine teleportation on reset.
|
||||
*/
|
||||
public record TeleportConfig(
|
||||
boolean enabled,
|
||||
Location location
|
||||
) {
|
||||
|
||||
public TeleportConfig {
|
||||
if (!enabled) {
|
||||
location = null;
|
||||
} else if (location != null) {
|
||||
// Defensive copy
|
||||
location = location.clone();
|
||||
Objects.requireNonNull(location.getWorld(), "Teleport world cannot be null");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the teleport location if enabled.
|
||||
*/
|
||||
public Optional<Location> getLocation() {
|
||||
return Optional.ofNullable(enabled ? location : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disabled teleport config.
|
||||
*/
|
||||
public static TeleportConfig disabled() {
|
||||
return new TeleportConfig(false, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates enabled config with location.
|
||||
*/
|
||||
public static TeleportConfig at(Location location) {
|
||||
return new TeleportConfig(true, location);
|
||||
}
|
||||
}
|
||||
82
src/main/java/dev/loki/lomines/data/config/ui/UIConfig.java
Normal file
82
src/main/java/dev/loki/lomines/data/config/ui/UIConfig.java
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package dev.loki.lomines.data.config.ui;
|
||||
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Configuration for UI elements (action bar, messages).
|
||||
*/
|
||||
public record UIConfig(
|
||||
boolean actionBarEnabled,
|
||||
String actionBarFormat,
|
||||
double actionBarRange,
|
||||
String timerFormat
|
||||
) {
|
||||
|
||||
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 {
|
||||
actionBarFormat = actionBarFormat != null && !actionBarFormat.isBlank()
|
||||
? actionBarFormat
|
||||
: DEFAULT_ACTIONBAR_FORMAT;
|
||||
timerFormat = timerFormat != null && !timerFormat.isBlank()
|
||||
? timerFormat
|
||||
: DEFAULT_TIMER_FORMAT;
|
||||
actionBarRange = Math.max(1, actionBarRange);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the action bar format with placeholders.
|
||||
* Placeholders: {mine}, {percent}, {time}, {blocks}, {total}
|
||||
*/
|
||||
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("{time}", timeStr)
|
||||
.replace("{blocks}", String.valueOf(blocks))
|
||||
.replace("{total}", String.valueOf(total));
|
||||
|
||||
return MiniMessage.miniMessage().deserialize(parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a duration according to timerFormat.
|
||||
* Supports: mm:ss, m:ss, HH:mm:ss, H:mm:ss
|
||||
*/
|
||||
public String formatTimer(long seconds) {
|
||||
long hours = seconds / 3600;
|
||||
long mins = (seconds % 3600) / 60;
|
||||
long secs = seconds % 60;
|
||||
|
||||
return switch (timerFormat.toLowerCase()) {
|
||||
case "hh:mm:ss", "h:mm:ss" -> String.format("%d:%02d:%02d", hours, mins, secs);
|
||||
case "mm:ss", "m:ss" -> String.format("%02d:%02d", mins, secs);
|
||||
default -> String.format("%02d:%02d", mins, secs);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Range squared for efficient distance checks.
|
||||
*/
|
||||
public double actionBarRangeSquared() {
|
||||
return actionBarRange * actionBarRange;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default configuration.
|
||||
*/
|
||||
public static UIConfig defaults() {
|
||||
return new UIConfig(true, DEFAULT_ACTIONBAR_FORMAT, 50.0, DEFAULT_TIMER_FORMAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disabled configuration (no UI).
|
||||
*/
|
||||
public static UIConfig disabled() {
|
||||
return new UIConfig(false, "", 0, DEFAULT_TIMER_FORMAT);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue