Update dependencies and improve configuration management
- Updated LoLib dependency to version 3.0.0 in build.gradle.kts - Added HikariCP and MySQL connector dependencies for improved database handling - Enhanced jar minimization process to exclude JDBC pool and driver - Refactored configuration management by introducing ConfigAccessor and ConfigLoader for better file handling and validation - Removed deprecated GenerationOptions class and integrated its functionality into the new structure - Improved command error handling and added cooldown hints for admin commands
This commit is contained in:
parent
978f739045
commit
3100c53229
86 changed files with 4324 additions and 3248 deletions
|
|
@ -1,22 +1,15 @@
|
|||
package dev.loki.loparkour.config;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.reward.Rewards;
|
||||
import dev.loki.loparkour.schematic.Schematics;
|
||||
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
/**
|
||||
* Config management class.
|
||||
* Refactored Config enum using composition pattern.
|
||||
* Delegates functionality to specialized components.
|
||||
*/
|
||||
public enum Config {
|
||||
|
||||
|
|
@ -25,252 +18,102 @@ public enum Config {
|
|||
REWARDS("rewards-v2.yml", List.of("score-rewards", "interval-rewards", "one-time-rewards")),
|
||||
SCHEMATICS("schematics/schematics.yml", List.of("difficulty"));
|
||||
|
||||
/**
|
||||
* The path to this file, incl. plugin folder.
|
||||
* Not final — resolved lazily on first reload() call after plugin instance is set.
|
||||
*/
|
||||
public File path;
|
||||
/**
|
||||
* The name of this file, e.g. config.yml
|
||||
*/
|
||||
public final String fileName;
|
||||
/**
|
||||
* The sections in the file that will be ignored when updating the keys.
|
||||
*/
|
||||
public final List<String> ignoredSections;
|
||||
/**
|
||||
* The {@link FileConfiguration} instance associated with this config file.
|
||||
*/
|
||||
public FileConfiguration fileConfiguration;
|
||||
|
||||
private ConfigAccessor accessor;
|
||||
|
||||
Config(String fileName, @Nullable List<String> ignoredSections) {
|
||||
this.fileName = fileName;
|
||||
this.ignoredSections = ignoredSections;
|
||||
// path resolved lazily via getPath() — do NOT call LoParkour.getInFolder() here,
|
||||
// because enum constants are initialized before onEnable() sets the instance.
|
||||
this.path = null; // placeholder; real path resolved on first access
|
||||
this.path = null; // Resolved lazily
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reloads all config files.
|
||||
* On first call (initialLoad=true) also resolves file paths and saves defaults.
|
||||
* Reload all configuration files.
|
||||
*/
|
||||
public static void reload(boolean initialLoad) {
|
||||
ConfigLoader.reloadAllConfigs(initialLoad);
|
||||
|
||||
// Update accessors for all configs
|
||||
for (Config config : values()) {
|
||||
if (initialLoad) {
|
||||
// Resolve path now that instance is guaranteed to be set
|
||||
config.path = LoParkour.getInFolder(config.fileName);
|
||||
if (!config.path.exists()) {
|
||||
LoParkour.getPlugin().saveResource(config.fileName, false);
|
||||
}
|
||||
config.update();
|
||||
if (config.fileConfiguration != null) {
|
||||
config.accessor = new ConfigAccessor(config.fileConfiguration, config.fileName);
|
||||
}
|
||||
config.load();
|
||||
}
|
||||
|
||||
// Validate configuration values
|
||||
validateConfigs();
|
||||
|
||||
// read config stuff
|
||||
Rewards.init();
|
||||
Locales.init();
|
||||
Schematics.init();
|
||||
Option.init(initialLoad);
|
||||
// Now that accessors exist, initialize systems that depend on Config getters
|
||||
ConfigLoader.initializeDependentSystems(initialLoad);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates critical configuration values to prevent crashes and unexpected behavior.
|
||||
*/
|
||||
private static void validateConfigs() {
|
||||
var logger = LoParkour.getPlugin().getLogger();
|
||||
|
||||
// Validate border-size
|
||||
if (CONFIG.isPath("world.border-size")) {
|
||||
int borderSize = CONFIG.getInt("world.border-size");
|
||||
if (borderSize <= 0) {
|
||||
logger.severe("Invalid config: world.border-size must be > 0, found: " + borderSize);
|
||||
logger.severe("Using default value: 1000");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate world dimensions
|
||||
if (CONFIG.isPath("world.max-y") && CONFIG.isPath("world.min-y")) {
|
||||
int maxY = CONFIG.getInt("world.max-y");
|
||||
int minY = CONFIG.getInt("world.min-y");
|
||||
if (maxY <= minY) {
|
||||
logger.severe("Invalid config: world.max-y (" + maxY + ") must be > world.min-y (" + minY + ")");
|
||||
logger.severe("Using default values: max-y=320, min-y=-64");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate section dimensions
|
||||
if (CONFIG.isPath("world.section.width") && CONFIG.isPath("world.section.length")) {
|
||||
int width = CONFIG.getInt("world.section.width");
|
||||
int length = CONFIG.getInt("world.section.length");
|
||||
if (width <= 0 || length <= 0) {
|
||||
logger.severe("Invalid config: world.section dimensions must be > 0, found: width=" + width + ", length=" + length);
|
||||
logger.severe("Using default values: width=50, length=50");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate generation chances (must sum to reasonable values)
|
||||
if (GENERATION.isPath("chances.height")) {
|
||||
double sum = GENERATION.getDouble("chances.height.1") +
|
||||
GENERATION.getDouble("chances.height.0") +
|
||||
GENERATION.getDouble("chances.height.-1") +
|
||||
GENERATION.getDouble("chances.height.-2");
|
||||
if (sum <= 0) {
|
||||
logger.severe("Invalid config: generation height chances sum to " + sum + ", must be > 0");
|
||||
logger.severe("Check generation.yml chances.height section");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate ghost mode settings
|
||||
if (CONFIG.isPath("ghost-mode.show-top")) {
|
||||
int showTop = CONFIG.getInt("ghost-mode.show-top");
|
||||
if (showTop < 0 || showTop > 10) {
|
||||
logger.warning("Config: ghost-mode.show-top should be between 0-10, found: " + showTop);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate cleanup settings
|
||||
if (GENERATION.isPath("advanced.cleanup-distance")) {
|
||||
int cleanupDistance = GENERATION.getInt("advanced.cleanup-distance");
|
||||
if (cleanupDistance <= 0) {
|
||||
logger.severe("Invalid config: advanced.cleanup-distance must be > 0, found: " + cleanupDistance);
|
||||
logger.severe("Using default value: 100");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the file from disk.
|
||||
* Load this configuration file from disk.
|
||||
*/
|
||||
public void load() {
|
||||
if (path == null) return; // not yet initialized, skip
|
||||
this.fileConfiguration = YamlConfiguration.loadConfiguration(path);
|
||||
if (path == null) return;
|
||||
|
||||
this.fileConfiguration = ConfigLoader.loadConfig(path);
|
||||
this.accessor = new ConfigAccessor(fileConfiguration, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the file so all keys are present.
|
||||
* Any key that exists in the bundled default but is missing on disk
|
||||
* will be appended to the correct section automatically.
|
||||
* Update this configuration file with new keys.
|
||||
*/
|
||||
public void update() {
|
||||
try {
|
||||
ConfigUpdater.update(LoParkour.getPlugin(), fileName, path, ignoredSections);
|
||||
} catch (Exception ex) {
|
||||
LoParkour.getPlugin().getLogger().log(java.util.logging.Level.SEVERE,
|
||||
"Error while trying to update config file: " + fileName, ex);
|
||||
if (path != null) {
|
||||
ConfigLoader.updateConfig(fileName, path, ignoredSections);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return True when path exists, false if not.
|
||||
*/
|
||||
// Delegate all accessor methods to ConfigAccessor
|
||||
|
||||
public boolean isPath(@NotNull String path) {
|
||||
return fileConfiguration.isSet(path);
|
||||
return accessor != null && accessor.isPath(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The value at path.
|
||||
*/
|
||||
public Object get(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.get(path);
|
||||
return accessor.get(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The boolean value at path.
|
||||
*/
|
||||
public boolean getBoolean(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.getBoolean(path);
|
||||
return accessor.getBoolean(path);
|
||||
}
|
||||
|
||||
public boolean getBoolean(@NotNull String path, boolean defaultValue) {
|
||||
return accessor != null ? accessor.getBoolean(path, defaultValue) : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The int value at path.
|
||||
*/
|
||||
public int getInt(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.getInt(path);
|
||||
return accessor.getInt(path);
|
||||
}
|
||||
|
||||
public int getInt(@NotNull String path, int defaultValue) {
|
||||
return accessor != null ? accessor.getInt(path, defaultValue) : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The double value at path.
|
||||
*/
|
||||
public double getDouble(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.getDouble(path);
|
||||
return accessor.getDouble(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The String value at path.
|
||||
*/
|
||||
@NotNull
|
||||
public String getString(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.getString(path, "");
|
||||
return accessor.getString(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The String list value at path.
|
||||
*/
|
||||
@NotNull
|
||||
public List<String> getStringList(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.getStringList(path);
|
||||
return accessor.getStringList(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @return The int list value at path.
|
||||
*/
|
||||
@NotNull
|
||||
public List<Integer> getIntList(@NotNull String path) {
|
||||
check(path);
|
||||
|
||||
return fileConfiguration.getIntegerList(path);
|
||||
return accessor.getIntList(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path The path.
|
||||
* @param deep Whether search should include children of children as well.
|
||||
* @return The children nodes from path.
|
||||
*/
|
||||
@NotNull
|
||||
public List<String> getChildren(@NotNull String path, boolean... deep) {
|
||||
check(path);
|
||||
|
||||
ConfigurationSection section = fileConfiguration.getConfigurationSection(path);
|
||||
|
||||
if (section == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
boolean isDeep = deep.length > 0 && deep[0];
|
||||
return new ArrayList<>(section.getKeys(isDeep));
|
||||
return accessor.getChildren(path, deep);
|
||||
}
|
||||
|
||||
// checks if the specified path exists to avoid developer error
|
||||
private void check(@NotNull String path) {
|
||||
if (!isPath(path)) {
|
||||
throw new NoSuchElementException("Unknown path %s in %s".formatted(path, fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
141
src/main/java/dev/loki/loparkour/config/ConfigAccessor.java
Normal file
141
src/main/java/dev/loki/loparkour/config/ConfigAccessor.java
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package dev.loki.loparkour.config;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Provides safe access to configuration values with validation.
|
||||
*/
|
||||
public class ConfigAccessor {
|
||||
|
||||
private final FileConfiguration config;
|
||||
private final String fileName;
|
||||
private final Set<String> warnedMissingPaths = ConcurrentHashMap.newKeySet();
|
||||
|
||||
public ConfigAccessor(@NotNull FileConfiguration config, @NotNull String fileName) {
|
||||
this.config = config;
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if path exists in configuration.
|
||||
*/
|
||||
public boolean isPath(@NotNull String path) {
|
||||
return config.isSet(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw object value at path.
|
||||
*/
|
||||
public Object get(@NotNull String path) {
|
||||
warnIfMissing(path);
|
||||
return config.get(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get boolean value at path.
|
||||
*/
|
||||
public boolean getBoolean(@NotNull String path) {
|
||||
warnIfMissing(path);
|
||||
return config.getBoolean(path, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Boolean at path, or default when the path is absent (no missing-path warning).
|
||||
*/
|
||||
public boolean getBoolean(@NotNull String path, boolean defaultValue) {
|
||||
if (!isPath(path)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return config.getBoolean(path, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get integer value at path.
|
||||
*/
|
||||
public int getInt(@NotNull String path) {
|
||||
warnIfMissing(path);
|
||||
return config.getInt(path, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get integer value at path with default.
|
||||
*/
|
||||
public int getInt(@NotNull String path, int defaultValue) {
|
||||
if (!isPath(path)) {
|
||||
return defaultValue;
|
||||
}
|
||||
return config.getInt(path, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get double value at path.
|
||||
*/
|
||||
public double getDouble(@NotNull String path) {
|
||||
warnIfMissing(path);
|
||||
return config.getDouble(path, 0.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get string value at path.
|
||||
*/
|
||||
@NotNull
|
||||
public String getString(@NotNull String path) {
|
||||
warnIfMissing(path);
|
||||
return config.getString(path, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get string list at path.
|
||||
*/
|
||||
@NotNull
|
||||
public List<String> getStringList(@NotNull String path) {
|
||||
warnIfMissing(path);
|
||||
return config.getStringList(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get integer list at path.
|
||||
*/
|
||||
@NotNull
|
||||
public List<Integer> getIntList(@NotNull String path) {
|
||||
warnIfMissing(path);
|
||||
return config.getIntegerList(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get child keys from configuration section.
|
||||
*/
|
||||
@NotNull
|
||||
public List<String> getChildren(@NotNull String path, boolean... deep) {
|
||||
if (!isPath(path)) {
|
||||
warnIfMissing(path);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
ConfigurationSection section = config.getConfigurationSection(path);
|
||||
if (section == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
boolean isDeep = deep.length > 0 && deep[0];
|
||||
return new ArrayList<>(section.getKeys(isDeep));
|
||||
}
|
||||
|
||||
private void warnIfMissing(@NotNull String path) {
|
||||
if (isPath(path)) {
|
||||
return;
|
||||
}
|
||||
if (warnedMissingPaths.add(path)) {
|
||||
LoParkour.getPlugin().getLogger().warning(
|
||||
"Missing config path '" + path + "' in " + fileName + ". Using a safe default value.");
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/main/java/dev/loki/loparkour/config/ConfigLoader.java
Normal file
87
src/main/java/dev/loki/loparkour/config/ConfigLoader.java
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package dev.loki.loparkour.config;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Handles loading, updating, and basic validation of configuration files.
|
||||
*/
|
||||
public class ConfigLoader {
|
||||
|
||||
@NotNull
|
||||
public static FileConfiguration loadConfig(@NotNull File configFile) {
|
||||
return YamlConfiguration.loadConfiguration(configFile);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static File initializeConfigFile(@NotNull String fileName) {
|
||||
File configFile = LoParkour.getInFolder(fileName);
|
||||
if (!configFile.exists()) {
|
||||
LoParkour.getPlugin().saveResource(fileName, false);
|
||||
}
|
||||
return configFile;
|
||||
}
|
||||
|
||||
public static void updateConfig(@NotNull String fileName, @NotNull File configFile, @Nullable List<String> ignoredSections) {
|
||||
try {
|
||||
var plugin = LoParkour.getPlugin();
|
||||
var resourceStream = plugin.getResource(fileName);
|
||||
if (resourceStream != null) {
|
||||
ConfigUpdater.update(configFile, resourceStream, "1.0", ignoredSections, plugin.getLogger());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LoParkour.getPlugin().getLogger().log(java.util.logging.Level.SEVERE,
|
||||
"Error while trying to update config file: " + fileName, ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void reloadAllConfigs(boolean initialLoad) {
|
||||
for (Config config : Config.values()) {
|
||||
if (initialLoad) {
|
||||
config.path = initializeConfigFile(config.fileName);
|
||||
updateConfig(config.fileName, config.path, config.ignoredSections);
|
||||
}
|
||||
config.fileConfiguration = loadConfig(config.path);
|
||||
}
|
||||
|
||||
validateConfigs();
|
||||
}
|
||||
|
||||
public static void initializeDependentSystems(boolean firstLoad) {
|
||||
dev.loki.loparkour.reward.Rewards.init();
|
||||
Locales.init();
|
||||
dev.loki.loparkour.schematic.Schematics.init();
|
||||
Option.init(firstLoad);
|
||||
}
|
||||
|
||||
// ── inline validation (was ConfigValidatorRefactored) ─────────────────────
|
||||
|
||||
private static void validateConfigs() {
|
||||
Logger log = LoParkour.getPlugin().getLogger();
|
||||
|
||||
if (Config.CONFIG.isPath("world.max-y") && Config.CONFIG.isPath("world.min-y")) {
|
||||
int maxY = Config.CONFIG.getInt("world.max-y");
|
||||
int minY = Config.CONFIG.getInt("world.min-y");
|
||||
if (maxY <= minY) {
|
||||
log.severe("Invalid config: world.max-y (%d) must be > world.min-y (%d)".formatted(maxY, minY));
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.GENERATION.isPath("generation.normal.height")) {
|
||||
double sum = Config.GENERATION.getDouble("generation.normal.height.1")
|
||||
+ Config.GENERATION.getDouble("generation.normal.height.0")
|
||||
+ Config.GENERATION.getDouble("generation.normal.height.-1")
|
||||
+ Config.GENERATION.getDouble("generation.normal.height.-2");
|
||||
if (sum <= 0) {
|
||||
log.severe("Invalid config: generation height chances sum to %.1f, must be > 0".formatted(sum));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,287 +1,197 @@
|
|||
package dev.loki.loparkour.config;
|
||||
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Automatically adds missing keys from the default (bundled) config
|
||||
* into the player's config file on disk, preserving all existing values,
|
||||
* comments, and blank lines.
|
||||
*
|
||||
* <p>Algorithm:
|
||||
* <ol>
|
||||
* <li>Load the default config from the jar as a list of raw lines.</li>
|
||||
* <li>Load the on-disk config as a flat key→value map.</li>
|
||||
* <li>Walk the default lines; for every key that is missing on disk,
|
||||
* emit the default line (with its comment header) into the output.</li>
|
||||
* <li>Rewrite the on-disk file with the merged result.</li>
|
||||
* </ol>
|
||||
*
|
||||
* Sections listed in {@code ignoredSections} are never touched — their
|
||||
* default content is never injected (user manages them freely).
|
||||
* Merges a resource-template YAML with the user's existing file,
|
||||
* preserving user values while adding any new keys from the template.
|
||||
* Uses full YAML paths (e.g. "sql.enabled") to avoid key collisions.
|
||||
*/
|
||||
public final class ConfigUpdater {
|
||||
public class ConfigUpdater {
|
||||
|
||||
private ConfigUpdater() {}
|
||||
private final Logger logger;
|
||||
|
||||
private ConfigUpdater(@NotNull Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates {@code diskFile} by injecting any keys that exist in the
|
||||
* bundled default but are absent on disk.
|
||||
*
|
||||
* @param plugin the plugin (used to read the bundled resource)
|
||||
* @param resourceName path inside the jar, e.g. {@code "config.yml"}
|
||||
* @param diskFile the on-disk file to update
|
||||
* @param ignoredSections top-level sections whose keys should never be added
|
||||
* (pass {@code null} or empty list to ignore nothing)
|
||||
*/
|
||||
public static void update(
|
||||
@NotNull Plugin plugin,
|
||||
@NotNull String resourceName,
|
||||
@NotNull File diskFile,
|
||||
@Nullable List<String> ignoredSections
|
||||
) throws IOException {
|
||||
@NotNull File configFile,
|
||||
@NotNull InputStream templateStream,
|
||||
@NotNull String expectedVersion,
|
||||
@Nullable List<String> ignoredKeys,
|
||||
@NotNull Logger logger) throws IOException {
|
||||
|
||||
List<String> defaultLines = readResource(plugin, resourceName);
|
||||
if (defaultLines == null) {
|
||||
plugin.getLogger().warning("[ConfigUpdater] Resource not found in jar: " + resourceName);
|
||||
return;
|
||||
}
|
||||
new ConfigUpdater(logger).performUpdate(configFile, templateStream, ignoredKeys);
|
||||
}
|
||||
|
||||
// Flat key set of what already exists on disk (dot-separated paths)
|
||||
Set<String> diskKeys = flatKeys(diskFile);
|
||||
// ── core ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// Ignored section prefixes (e.g. "styles" → skip any path starting with "styles.")
|
||||
Set<String> ignored = normalizedIgnored(ignoredSections);
|
||||
private void performUpdate(
|
||||
@NotNull File configFile,
|
||||
@NotNull InputStream templateStream,
|
||||
@Nullable List<String> ignoredKeys) throws IOException {
|
||||
|
||||
List<String> diskLines = readLines(diskFile);
|
||||
List<String> outputLines = merge(defaultLines, diskLines, diskKeys, ignored);
|
||||
|
||||
// Only write if something actually changed
|
||||
if (!outputLines.equals(diskLines)) {
|
||||
writeLines(diskFile, outputLines);
|
||||
plugin.getLogger().info("[ConfigUpdater] Updated " + resourceName
|
||||
+ " — added " + countNew(diskLines, outputLines) + " new key(s).");
|
||||
File backup = createBackup(configFile);
|
||||
try {
|
||||
List<String> merged = merge(templateStream, configFile, ignoredKeys);
|
||||
Files.write(configFile.toPath(), merged);
|
||||
logger.info("Configuration file synced: " + configFile.getName());
|
||||
} catch (Exception e) {
|
||||
if (backup != null && backup.exists()) {
|
||||
Files.copy(backup.toPath(), configFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
logger.severe("Config sync failed, restored backup: " + e.getMessage());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Merge ─────────────────────────────────────────────────────────────────
|
||||
// ── merge ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Merges default lines into disk lines.
|
||||
*
|
||||
* <p>Strategy: walk disk lines first, emit them as-is. Then walk default
|
||||
* lines; for each key block (comment + key line) whose key is absent on
|
||||
* disk and not ignored, append it at the correct indentation level.
|
||||
*/
|
||||
private static List<String> merge(
|
||||
List<String> defaultLines,
|
||||
List<String> diskLines,
|
||||
Set<String> diskKeys,
|
||||
Set<String> ignored
|
||||
) {
|
||||
// We need to insert missing keys at the right position.
|
||||
// Build a copy of disk lines we will extend.
|
||||
List<String> result = new ArrayList<>(diskLines);
|
||||
@NotNull
|
||||
private List<String> merge(
|
||||
@NotNull InputStream templateStream,
|
||||
@NotNull File existingFile,
|
||||
@Nullable List<String> ignoredKeys) throws IOException {
|
||||
|
||||
// Collect blocks from the default file: each block = list of comment
|
||||
// lines + the key line itself. We track the "path stack" to know the
|
||||
// full dotted path of every key.
|
||||
Deque<String> pathStack = new ArrayDeque<>();
|
||||
List<String> pendingComments = new ArrayList<>();
|
||||
List<String> templateLines = readLines(templateStream);
|
||||
List<String> existingLines = existingFile.exists() ? readLines(existingFile) : List.of();
|
||||
|
||||
for (int i = 0; i < defaultLines.size(); i++) {
|
||||
String raw = defaultLines.get(i);
|
||||
String trimmed = raw.stripLeading();
|
||||
Set<String> ignored = normalizeIgnored(ignoredKeys);
|
||||
Map<String, String> existingValues = extractFullPathValues(existingLines);
|
||||
|
||||
// Blank line → flush pending comments, keep as separator
|
||||
if (trimmed.isBlank()) {
|
||||
pendingComments.add(raw);
|
||||
List<String> result = new ArrayList<>();
|
||||
Deque<String> stack = new ArrayDeque<>();
|
||||
int prevIndent = -1;
|
||||
|
||||
for (String line : templateLines) {
|
||||
String trimmed = line.trim();
|
||||
|
||||
if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("-")) {
|
||||
result.add(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Comment line → accumulate
|
||||
if (trimmed.startsWith("#")) {
|
||||
pendingComments.add(raw);
|
||||
continue;
|
||||
int colon = trimmed.indexOf(':');
|
||||
if (colon == -1) { result.add(line); continue; }
|
||||
|
||||
String leaf = trimmed.substring(0, colon).trim();
|
||||
String after = trimmed.substring(colon + 1).trim();
|
||||
int indent = indentOf(line);
|
||||
|
||||
while (prevIndent >= 0 && indent <= prevIndent) {
|
||||
if (!stack.isEmpty()) stack.pop();
|
||||
prevIndent -= 2;
|
||||
}
|
||||
|
||||
// Key line
|
||||
int indent = raw.length() - trimmed.length();
|
||||
int depth = indent / 2; // assume 2-space indentation
|
||||
String fullPath = stack.isEmpty() ? leaf : String.join(".", stack) + "." + leaf;
|
||||
boolean section = after.isEmpty() || after.startsWith("#");
|
||||
|
||||
// Pop stack back to current depth
|
||||
while (pathStack.size() > depth) pathStack.pollLast();
|
||||
|
||||
// Parse key name (everything before ':')
|
||||
String keyPart = trimmed.split(":")[0].trim();
|
||||
pathStack.addLast(keyPart);
|
||||
String fullPath = String.join(".", pathStack);
|
||||
|
||||
boolean isSection = trimmed.endsWith(":") || trimmed.matches(".*:\\s*$");
|
||||
boolean isIgnored = isIgnored(fullPath, ignored);
|
||||
|
||||
if (!isIgnored && !diskKeys.contains(fullPath) && !isSection) {
|
||||
// This key is missing on disk — inject it
|
||||
// Find the best insertion point: after the last line that
|
||||
// belongs to the parent section on disk
|
||||
int insertAt = findInsertionPoint(result, pathStack, depth);
|
||||
|
||||
// Insert blank separator + comments + key line
|
||||
List<String> block = new ArrayList<>();
|
||||
if (insertAt == result.size() || !result.get(Math.max(0, insertAt - 1)).isBlank()) {
|
||||
block.add("");
|
||||
}
|
||||
block.addAll(pendingComments);
|
||||
block.add(raw);
|
||||
result.addAll(insertAt, block);
|
||||
if (section) {
|
||||
stack.push(leaf);
|
||||
prevIndent = indent;
|
||||
result.add(line);
|
||||
} else if (isIgnored(fullPath, ignored)) {
|
||||
result.add(line);
|
||||
prevIndent = indent;
|
||||
} else {
|
||||
String existing = existingValues.get(fullPath);
|
||||
result.add(existing != null
|
||||
? line.substring(0, line.indexOf(':') + 1) + " " + existing
|
||||
: line);
|
||||
prevIndent = indent;
|
||||
}
|
||||
|
||||
// Clear pending comments regardless
|
||||
pendingComments.clear();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the line index where a missing key should be inserted.
|
||||
* Inserts after the last line of its parent section.
|
||||
*/
|
||||
private static int findInsertionPoint(List<String> lines, Deque<String> pathStack, int depth) {
|
||||
if (depth == 0) {
|
||||
// Top-level key → append at end
|
||||
return lines.size();
|
||||
}
|
||||
@NotNull
|
||||
private Map<String, String> extractFullPathValues(@NotNull List<String> lines) {
|
||||
Map<String, String> values = new LinkedHashMap<>();
|
||||
Deque<String> stack = new ArrayDeque<>();
|
||||
int prevIndent = -1;
|
||||
|
||||
// Parent path
|
||||
List<String> parts = new ArrayList<>(pathStack);
|
||||
String parentKey = parts.get(depth - 1);
|
||||
int parentIndent = (depth - 1) * 2;
|
||||
for (String line : lines) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.isEmpty() || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
||||
|
||||
// Find the parent key line in the result
|
||||
int parentLine = -1;
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
String l = lines.get(i);
|
||||
int ind = l.length() - l.stripLeading().length();
|
||||
if (ind == parentIndent && l.stripLeading().startsWith(parentKey + ":")) {
|
||||
parentLine = i;
|
||||
break;
|
||||
int colon = trimmed.indexOf(':');
|
||||
if (colon == -1) continue;
|
||||
|
||||
String leaf = trimmed.substring(0, colon).trim();
|
||||
String after = trimmed.substring(colon + 1).trim();
|
||||
int indent = indentOf(line);
|
||||
|
||||
while (prevIndent >= 0 && indent <= prevIndent) {
|
||||
if (!stack.isEmpty()) stack.pop();
|
||||
prevIndent -= 2;
|
||||
}
|
||||
|
||||
String fullPath = stack.isEmpty() ? leaf : String.join(".", stack) + "." + leaf;
|
||||
|
||||
if (after.isEmpty() || after.startsWith("#")) {
|
||||
stack.push(leaf);
|
||||
} else {
|
||||
values.put(fullPath, after);
|
||||
}
|
||||
prevIndent = indent;
|
||||
}
|
||||
|
||||
if (parentLine < 0) return lines.size();
|
||||
|
||||
// Find end of parent section: next line with same or lower indent that isn't blank/comment
|
||||
for (int i = parentLine + 1; i < lines.size(); i++) {
|
||||
String l = lines.get(i);
|
||||
if (l.isBlank() || l.stripLeading().startsWith("#")) continue;
|
||||
int ind = l.length() - l.stripLeading().length();
|
||||
if (ind <= parentIndent) return i;
|
||||
}
|
||||
|
||||
return lines.size();
|
||||
return values;
|
||||
}
|
||||
|
||||
// ── IO helpers ─────────────────────────────────────────────────────────────
|
||||
// ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static int indentOf(@NotNull String line) {
|
||||
int i = 0;
|
||||
while (i < line.length() && line.charAt(i) == ' ') i++;
|
||||
return i;
|
||||
}
|
||||
|
||||
/** Reads a resource from the jar as a list of lines. Returns null if not found. */
|
||||
@Nullable
|
||||
private static List<String> readResource(Plugin plugin, String name) {
|
||||
try (InputStream in = plugin.getResource(name)) {
|
||||
if (in == null) return null;
|
||||
return readLines(in);
|
||||
} catch (IOException ex) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to read resource: " + name, ex);
|
||||
private File createBackup(@NotNull File file) {
|
||||
if (!file.exists()) return null;
|
||||
try {
|
||||
File bak = new File(file.getParent(), file.getName() + ".bak");
|
||||
Files.copy(file.toPath(), bak.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
return bak;
|
||||
} catch (IOException e) {
|
||||
logger.warning("Failed to create backup: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> readLines(File file) throws IOException {
|
||||
try (BufferedReader r = new BufferedReader(
|
||||
new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||
return readLines(r);
|
||||
@NotNull
|
||||
private static List<String> readLines(@NotNull InputStream is) throws IOException {
|
||||
try (var r = new BufferedReader(new InputStreamReader(is))) {
|
||||
return r.lines().toList();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> readLines(InputStream in) throws IOException {
|
||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
|
||||
return readLines(r);
|
||||
}
|
||||
@NotNull
|
||||
private static List<String> readLines(@NotNull File file) throws IOException {
|
||||
return Files.readAllLines(file.toPath());
|
||||
}
|
||||
|
||||
private static List<String> readLines(BufferedReader reader) throws IOException {
|
||||
List<String> lines = new ArrayList<>();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) lines.add(line);
|
||||
return lines;
|
||||
@NotNull
|
||||
private static Set<String> normalizeIgnored(@Nullable List<String> keys) {
|
||||
if (keys == null) return Set.of();
|
||||
Set<String> s = new HashSet<>();
|
||||
for (String k : keys) s.add(k.trim().toLowerCase());
|
||||
return s;
|
||||
}
|
||||
|
||||
private static void writeLines(File file, List<String> lines) throws IOException {
|
||||
try (BufferedWriter w = new BufferedWriter(
|
||||
new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
w.write(lines.get(i));
|
||||
if (i < lines.size() - 1) w.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Key extraction ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns the flat set of dotted-path keys present in the file.
|
||||
* Only leaf keys (not sections) are returned.
|
||||
*/
|
||||
private static Set<String> flatKeys(File file) throws IOException {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
Deque<String> stack = new ArrayDeque<>();
|
||||
|
||||
List<String> lines = readLines(file);
|
||||
for (String raw : lines) {
|
||||
String trimmed = raw.stripLeading();
|
||||
if (trimmed.isBlank() || trimmed.startsWith("#")) continue;
|
||||
|
||||
int indent = raw.length() - trimmed.length();
|
||||
int depth = indent / 2;
|
||||
|
||||
while (stack.size() > depth) stack.pollLast();
|
||||
|
||||
if (!trimmed.contains(":")) continue;
|
||||
String keyPart = trimmed.split(":")[0].trim();
|
||||
stack.addLast(keyPart);
|
||||
|
||||
boolean isSection = trimmed.matches("[^:]+:\\s*$");
|
||||
if (!isSection) {
|
||||
keys.add(String.join(".", stack));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static Set<String> normalizedIgnored(@Nullable List<String> list) {
|
||||
if (list == null || list.isEmpty()) return Collections.emptySet();
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
for (String s : list) result.add(s.toLowerCase(Locale.ROOT).trim());
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean isIgnored(String fullPath, Set<String> ignored) {
|
||||
String lower = fullPath.toLowerCase(Locale.ROOT);
|
||||
for (String prefix : ignored) {
|
||||
if (lower.equals(prefix) || lower.startsWith(prefix + ".")) return true;
|
||||
private static boolean isIgnored(@NotNull String fullPath, @NotNull Set<String> ignored) {
|
||||
String lower = fullPath.toLowerCase();
|
||||
for (String ign : ignored) {
|
||||
if (lower.equals(ign) || lower.startsWith(ign + ".")) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int countNew(List<String> before, List<String> after) {
|
||||
return Math.max(0, after.size() - before.size());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,117 +0,0 @@
|
|||
package dev.loki.loparkour.config;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
|
||||
/**
|
||||
* Generation and jump-validation settings. Populated by {@link Option#init}.
|
||||
*/
|
||||
public final class GenerationOptions {
|
||||
|
||||
// Block type chances
|
||||
public static double TYPE_NORMAL;
|
||||
public static double TYPE_SPECIAL;
|
||||
public static double TYPE_SCHEMATICS;
|
||||
|
||||
// Special block chances
|
||||
public static double SPECIAL_ICE;
|
||||
public static double SPECIAL_SLAB;
|
||||
public static double SPECIAL_PANE;
|
||||
public static double SPECIAL_FENCE;
|
||||
|
||||
// Distance weights
|
||||
public static double NORMAL_DISTANCE_1;
|
||||
public static double NORMAL_DISTANCE_2;
|
||||
public static double NORMAL_DISTANCE_3;
|
||||
public static double NORMAL_DISTANCE_4;
|
||||
|
||||
// Height weights
|
||||
public static double NORMAL_HEIGHT_1;
|
||||
public static double NORMAL_HEIGHT_0;
|
||||
public static double NORMAL_HEIGHT_NEG1;
|
||||
public static double NORMAL_HEIGHT_NEG2;
|
||||
|
||||
// World bounds
|
||||
public static int MAX_Y;
|
||||
public static int MIN_Y;
|
||||
|
||||
// Jump validation
|
||||
public static boolean JUMP_VALIDATION_ENABLED;
|
||||
public static double MAX_JUMP_DISTANCE;
|
||||
public static double MAX_HORIZONTAL_DISTANCE;
|
||||
public static double MAX_VERTICAL_UP;
|
||||
public static double MAX_VERTICAL_DOWN;
|
||||
|
||||
// Jump types
|
||||
public static boolean JUMP_TYPES_ENABLED;
|
||||
public static java.util.Map<String, Boolean> JUMP_TYPE_ENABLED;
|
||||
public static java.util.Map<String, Double> JUMP_TYPE_CHANCE;
|
||||
|
||||
// Memory optimisation
|
||||
public static int BLOCK_CLEANUP_DISTANCE;
|
||||
public static int CLEANUP_INTERVAL;
|
||||
|
||||
// Ghost mode
|
||||
public static boolean GHOST_MODE_ENABLED;
|
||||
public static int GHOST_SHOW_TOP;
|
||||
public static double GHOST_TRANSPARENCY;
|
||||
|
||||
static void init() {
|
||||
// Jump validation
|
||||
JUMP_VALIDATION_ENABLED = Config.CONFIG.isPath("jump-validation.enabled") && Config.CONFIG.getBoolean("jump-validation.enabled");
|
||||
MAX_JUMP_DISTANCE = Config.CONFIG.isPath("jump-validation.max-distance") ? Config.CONFIG.getDouble("jump-validation.max-distance") : 5.0;
|
||||
MAX_HORIZONTAL_DISTANCE = Config.CONFIG.isPath("jump-validation.max-horizontal") ? Config.CONFIG.getDouble("jump-validation.max-horizontal") : 4.0;
|
||||
MAX_VERTICAL_UP = Config.CONFIG.isPath("jump-validation.max-vertical-up") ? Config.CONFIG.getDouble("jump-validation.max-vertical-up") : 1.0;
|
||||
MAX_VERTICAL_DOWN = Config.CONFIG.isPath("jump-validation.max-vertical-down") ? Config.CONFIG.getDouble("jump-validation.max-vertical-down") : 3.0;
|
||||
|
||||
// Jump types
|
||||
JUMP_TYPES_ENABLED = Config.CONFIG.isPath("jump-types.enabled") && Config.CONFIG.getBoolean("jump-types.enabled");
|
||||
JUMP_TYPE_ENABLED = new java.util.HashMap<>();
|
||||
JUMP_TYPE_CHANCE = new java.util.HashMap<>();
|
||||
if (Config.CONFIG.isPath("jump-types.types")) {
|
||||
for (String type : Config.CONFIG.getChildren("jump-types.types")) {
|
||||
String path = "jump-types.types." + type;
|
||||
JUMP_TYPE_ENABLED.put(type, Config.CONFIG.isPath(path + ".enabled") && Config.CONFIG.getBoolean(path + ".enabled"));
|
||||
JUMP_TYPE_CHANCE.put(type, Config.CONFIG.isPath(path + ".chance") ? Config.CONFIG.getDouble(path + ".chance") : 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Memory
|
||||
BLOCK_CLEANUP_DISTANCE = Config.CONFIG.isPath("memory.block-cleanup-distance") ? Config.CONFIG.getInt("memory.block-cleanup-distance") : 100;
|
||||
CLEANUP_INTERVAL = Config.CONFIG.isPath("memory.cleanup-interval") ? Config.CONFIG.getInt("memory.cleanup-interval") : 100;
|
||||
|
||||
// Ghost
|
||||
GHOST_MODE_ENABLED = Config.CONFIG.isPath("ghost-mode.enabled") && Config.CONFIG.getBoolean("ghost-mode.enabled");
|
||||
GHOST_SHOW_TOP = Config.CONFIG.isPath("ghost-mode.show-top") ? Config.CONFIG.getInt("ghost-mode.show-top") : 3;
|
||||
GHOST_TRANSPARENCY = Config.CONFIG.isPath("ghost-mode.transparency") ? Config.CONFIG.getDouble("ghost-mode.transparency") : 0.5;
|
||||
|
||||
// Generation chances
|
||||
TYPE_NORMAL = Config.GENERATION.getInt("generation.type.normal") / 100.0;
|
||||
TYPE_SPECIAL = Config.GENERATION.getInt("generation.type.special") / 100.0;
|
||||
TYPE_SCHEMATICS = Config.GENERATION.getInt("generation.type.schematic") / 100.0;
|
||||
|
||||
SPECIAL_ICE = Config.GENERATION.getInt("generation.special.ice") / 100.0;
|
||||
SPECIAL_SLAB = Config.GENERATION.getInt("generation.special.slab") / 100.0;
|
||||
SPECIAL_PANE = Config.GENERATION.getInt("generation.special.pane") / 100.0;
|
||||
SPECIAL_FENCE = Config.GENERATION.getInt("generation.special.fence") / 100.0;
|
||||
|
||||
NORMAL_DISTANCE_1 = Config.GENERATION.getInt("generation.normal.distance.1") / 100.0;
|
||||
NORMAL_DISTANCE_2 = Config.GENERATION.getInt("generation.normal.distance.2") / 100.0;
|
||||
NORMAL_DISTANCE_3 = Config.GENERATION.getInt("generation.normal.distance.3") / 100.0;
|
||||
NORMAL_DISTANCE_4 = Config.GENERATION.getInt("generation.normal.distance.4") / 100.0;
|
||||
|
||||
NORMAL_HEIGHT_1 = Config.GENERATION.getInt("generation.normal.height.1") / 100.0;
|
||||
NORMAL_HEIGHT_0 = Config.GENERATION.getInt("generation.normal.height.0") / 100.0;
|
||||
NORMAL_HEIGHT_NEG1 = Config.GENERATION.getInt("generation.normal.height.-1") / 100.0;
|
||||
NORMAL_HEIGHT_NEG2 = Config.GENERATION.getInt("generation.normal.height.-2") / 100.0;
|
||||
|
||||
MAX_Y = Config.GENERATION.getInt("generation.settings.max-y");
|
||||
MIN_Y = Config.GENERATION.getInt("generation.settings.min-y");
|
||||
|
||||
if (MIN_Y >= MAX_Y) {
|
||||
MIN_Y = 100; MAX_Y = 200;
|
||||
LoParkour.getPlugin().getLogger().severe("min-y >= max-y in generation.yml — using defaults 100/200.");
|
||||
}
|
||||
}
|
||||
|
||||
private GenerationOptions() {}
|
||||
}
|
||||
|
|
@ -1,13 +1,10 @@
|
|||
package dev.loki.loparkour.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import dev.loki.loparkour.util.Item;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.player.ParkourUser;
|
||||
|
||||
import dev.loki.loparkour.util.Item;
|
||||
import dev.loki.loparkour.util.Materials;
|
||||
import dev.lolib.scheduler.Scheduler;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.configuration.ConfigurationSection;
|
||||
|
|
@ -30,244 +27,194 @@ import java.util.regex.Pattern;
|
|||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Locale message/item handler.
|
||||
* Locale system: loading, caching, validation, and access.
|
||||
*/
|
||||
public class Locales {
|
||||
|
||||
// a list of all nodes, used to check against missing nodes
|
||||
private static List<String> resourceNodes;
|
||||
private static final Map<String, FileConfiguration> locales = new HashMap<>();
|
||||
private static final Pattern REPLACEMENT_PATTERN = Pattern.compile("%[a-z]");
|
||||
|
||||
/**
|
||||
* A map of all locales with their respective yml trees
|
||||
*/
|
||||
public static final Map<String, FileConfiguration> locales = new HashMap<>();
|
||||
// ── public API ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Initializes this Locale handler.
|
||||
*/
|
||||
public static void init() {
|
||||
loadLocalesAsync();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String getString(@NotNull Player player, @NotNull String path) {
|
||||
return getString(getPlayerLocale(player), path);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static String getString(@NotNull String locale, @NotNull String path) {
|
||||
return cachedValue(locale, c -> c.getString(path), "");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<String> getStringList(@NotNull String locale, @NotNull String path) {
|
||||
return cachedValue(locale, c -> c.getStringList(path), Collections.emptyList());
|
||||
}
|
||||
|
||||
public static int getLocaleCount() {
|
||||
synchronized (locales) { return locales.size(); }
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Set<String> getLocaleKeys() {
|
||||
synchronized (locales) { return new HashSet<>(locales.keySet()); }
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Item getItem(@NotNull Player player, @NotNull String path, String... replace) {
|
||||
return getItem(getPlayerLocale(player), path, replace);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Item getItem(@NotNull String locale, @NotNull String path, String... replace) {
|
||||
synchronized (locales) {
|
||||
if (locales.isEmpty()) return new Item(Material.STONE, "");
|
||||
FileConfiguration config = locales.get(locale);
|
||||
if (config == null) return new Item(Material.STONE, "");
|
||||
return buildItem(config, path, replace);
|
||||
}
|
||||
}
|
||||
|
||||
// ── loading (was LocaleLoader) ────────────────────────────────────────────
|
||||
|
||||
private static void loadLocalesAsync() {
|
||||
Plugin plugin = LoParkour.getPlugin();
|
||||
|
||||
// Create a temporary map to avoid clearing locales before new ones are loaded
|
||||
Map<String, FileConfiguration> newLocales = new HashMap<>();
|
||||
|
||||
Scheduler.get(plugin).runAsync(() -> {
|
||||
FileConfiguration embedded = YamlConfiguration.loadConfiguration(new InputStreamReader(plugin.getResource("locales/en.yml"), StandardCharsets.UTF_8));
|
||||
try {
|
||||
FileConfiguration embedded = YamlConfiguration.loadConfiguration(
|
||||
new InputStreamReader(plugin.getResource("locales/en.yml"), StandardCharsets.UTF_8));
|
||||
|
||||
// get all nodes from the plugin's english resource, aka the most updated version
|
||||
resourceNodes = getChildren(embedded, "", true);
|
||||
Map<String, FileConfiguration> loaded = loadFromDisk(embedded);
|
||||
|
||||
File folder = LoParkour.getInFolder("locales");
|
||||
|
||||
// download files to locales folder
|
||||
if (!folder.exists()) {
|
||||
folder.mkdirs();
|
||||
}
|
||||
|
||||
String[] files = folder.list();
|
||||
|
||||
// create non-existent files
|
||||
if (files != null && files.length == 0) {
|
||||
plugin.saveResource("locales/en.yml", false);
|
||||
plugin.saveResource("locales/ru.yml", false);
|
||||
}
|
||||
|
||||
// get all files in locales folder
|
||||
try (Stream<Path> stream = Files.list(folder.toPath())) {
|
||||
stream.forEach(path -> {
|
||||
File file = path.toFile();
|
||||
|
||||
// get locale from file name
|
||||
String locale = file.getName().split("\\.")[0];
|
||||
|
||||
LoParkour.log("Found locale " + locale);
|
||||
|
||||
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
|
||||
validate(embedded, config, file);
|
||||
|
||||
newLocales.put(locale, config);
|
||||
Scheduler.get(plugin).run(() -> {
|
||||
synchronized (locales) {
|
||||
locales.clear();
|
||||
locales.putAll(loaded);
|
||||
}
|
||||
LoParkour.log("Locales reloaded successfully (" + loaded.size() + " locales)");
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
LoParkour.getPlugin().getLogger().severe("Error while trying to read locale files - restart/reload your server - " + ex.getMessage());
|
||||
plugin.getLogger().severe("Error while loading locale files: " + ex.getMessage());
|
||||
}
|
||||
|
||||
// Only clear and replace locales after all new ones are loaded
|
||||
Scheduler.get(plugin).run(() -> {
|
||||
locales.clear();
|
||||
locales.putAll(newLocales);
|
||||
LoParkour.log("Locales reloaded successfully (" + locales.size() + " locales)");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// validates whether a lang file contains all required keys.
|
||||
// if it doesn't, automatically add them
|
||||
private static void validate(FileConfiguration provided, FileConfiguration user, File localPath) {
|
||||
List<String> userNodes = getChildren(user, "", true);
|
||||
|
||||
for (String node : resourceNodes) {
|
||||
if (userNodes.contains(node)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LoParkour.log("Fixing missing config node %s in %s".formatted(node, localPath.getName()));
|
||||
|
||||
user.set(node, provided.get(node));
|
||||
}
|
||||
|
||||
LoParkour.log("Validated locale " + localPath.getName());
|
||||
|
||||
try {
|
||||
user.save(localPath);
|
||||
} catch (IOException ex) {
|
||||
LoParkour.getPlugin().getLogger().severe("Error while trying to save fixed config file %s - delete this file and restart your server - ".formatted(localPath) + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a String from the provided path in the provided player's locale.
|
||||
* If the player is a {@link ParkourUser}, their locale value will be used.
|
||||
* If not, the default locale will be used.
|
||||
*
|
||||
* @param player The player
|
||||
* @param path The path
|
||||
* @return a String
|
||||
*/
|
||||
public static String getString(Player player, String path) {
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
|
||||
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
|
||||
|
||||
return getString(locale, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a coloured String from the provided path in the provided locale file
|
||||
*
|
||||
* @param locale The locale
|
||||
* @param path The path
|
||||
* @return a String
|
||||
*/
|
||||
public static String getString(String locale, String path) {
|
||||
return getValue(locale, config -> config.getString(path), "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an uncoloured String list from the provided path in the provided locale file
|
||||
*
|
||||
* @param locale The locale
|
||||
* @param path The path
|
||||
* @return a String list
|
||||
*/
|
||||
public static List<String> getStringList(String locale, String path) {
|
||||
return getValue(locale, config -> config.getStringList(path), Collections.emptyList());
|
||||
}
|
||||
|
||||
private static <T> T getValue(String locale, Function<FileConfiguration, T> f, T def) {
|
||||
if (locales.isEmpty()) {
|
||||
return def;
|
||||
}
|
||||
|
||||
FileConfiguration config = locales.get(locale);
|
||||
|
||||
return config != null ? f.apply(config) : def;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an item from a json locale file.
|
||||
* The locale is derived from the player.
|
||||
* If the player is a {@link ParkourUser}, their locale value will be used.
|
||||
* If not, the default locale will be used.
|
||||
*
|
||||
* @param player The player
|
||||
* @param path The full path of the item in the locale file
|
||||
* @return a non-null {@link Item} instance built from the description in the locale file
|
||||
*/
|
||||
@NotNull
|
||||
public static Item getItem(@NotNull Player player, String path, String... replace) {
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
String locale = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
|
||||
private static Map<String, FileConfiguration> loadFromDisk(@NotNull FileConfiguration embedded) throws Exception {
|
||||
Map<String, FileConfiguration> result = new HashMap<>();
|
||||
File folder = LoParkour.getInFolder("locales");
|
||||
ensureFolder(folder);
|
||||
|
||||
return getItem(locale, path, replace);
|
||||
try (Stream<Path> stream = Files.list(folder.toPath())) {
|
||||
stream.forEach(path -> {
|
||||
File file = path.toFile();
|
||||
if (!file.getName().endsWith(".yml")) return;
|
||||
|
||||
String locale = file.getName().split("\\.")[0];
|
||||
LoParkour.log("Found locale " + locale);
|
||||
|
||||
FileConfiguration config = YamlConfiguration.loadConfiguration(file);
|
||||
validateAndFix(embedded, config, file);
|
||||
result.put(locale, config);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final Pattern pattern = Pattern.compile("%[a-z]");
|
||||
private static void ensureFolder(@NotNull File folder) {
|
||||
if (!folder.exists()) folder.mkdirs();
|
||||
String[] files = folder.list();
|
||||
if (files == null || files.length == 0) {
|
||||
Plugin plugin = LoParkour.getPlugin();
|
||||
plugin.saveResource("locales/en.yml", false);
|
||||
plugin.saveResource("locales/ru.yml", false);
|
||||
}
|
||||
}
|
||||
|
||||
// ── validation (was LocaleValidator) ──────────────────────────────────────
|
||||
|
||||
private static void validateAndFix(@NotNull FileConfiguration reference, @NotNull FileConfiguration locale, @NotNull File file) {
|
||||
List<String> refNodes = getChildren(reference);
|
||||
List<String> locNodes = getChildren(locale);
|
||||
|
||||
boolean modified = false;
|
||||
for (String node : refNodes) {
|
||||
if (!locNodes.contains(node)) {
|
||||
LoParkour.log("Fixing missing config node %s in %s".formatted(node, file.getName()));
|
||||
locale.set(node, reference.get(node));
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
if (modified) {
|
||||
try { locale.save(file); }
|
||||
catch (IOException ex) {
|
||||
LoParkour.getPlugin().getLogger().severe(
|
||||
"Error saving fixed locale %s — delete and restart — %s".formatted(file.getName(), ex.getMessage()));
|
||||
}
|
||||
}
|
||||
LoParkour.log("Validated locale " + file.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an item from a provided json locale file with possible replacements.
|
||||
*
|
||||
* @param locale The locale
|
||||
* @param path The path in the json file
|
||||
* @param replace The Strings that will replace any appearances of a String following the regex "%[a-z]"
|
||||
* @return a non-null {@link Item} instance built from the description in the locale file
|
||||
*/
|
||||
@NotNull
|
||||
public static Item getItem(String locale, String path, String... replace) {
|
||||
if (locales.isEmpty()) { // during reloading
|
||||
return new Item(Material.STONE, "");
|
||||
private static List<String> getChildren(@NotNull FileConfiguration config) {
|
||||
ConfigurationSection section = config.getConfigurationSection("");
|
||||
return section != null ? new ArrayList<>(section.getKeys(true)) : Collections.emptyList();
|
||||
}
|
||||
|
||||
// ── cache access (was LocaleCache) ────────────────────────────────────────
|
||||
|
||||
@NotNull
|
||||
private static <T> T cachedValue(@NotNull String locale, @NotNull Function<FileConfiguration, T> extractor, @NotNull T defaultValue) {
|
||||
synchronized (locales) {
|
||||
if (locales.isEmpty()) return defaultValue;
|
||||
FileConfiguration config = locales.get(locale);
|
||||
if (config == null) return defaultValue;
|
||||
try {
|
||||
T result = extractor.apply(config);
|
||||
return result != null ? result : defaultValue;
|
||||
} catch (Exception e) { return defaultValue; }
|
||||
}
|
||||
}
|
||||
|
||||
FileConfiguration base = locales.get(locale);
|
||||
// ── item builder ──────────────────────────────────────────────────────────
|
||||
|
||||
String material = base.getString("%s.material".formatted(path));
|
||||
String name = base.getString("%s.name".formatted(path));
|
||||
String lore = base.getString("%s.lore".formatted(path));
|
||||
int modelId = base.getInt("%s.model_id".formatted(path), -1);
|
||||
@NotNull
|
||||
private static Item buildItem(@NotNull FileConfiguration config, @NotNull String path, String... replace) {
|
||||
String material = config.getString("%s.material".formatted(path), "STONE");
|
||||
String name = applyReplacements(config.getString("%s.name".formatted(path), ""), replace);
|
||||
String lore = applyReplacements(config.getString("%s.lore".formatted(path), ""), replace);
|
||||
|
||||
if (material == null) {
|
||||
material = "";
|
||||
}
|
||||
if (name == null) {
|
||||
name = "";
|
||||
}
|
||||
if (lore == null) {
|
||||
lore = "";
|
||||
}
|
||||
|
||||
int idx = 0;
|
||||
Matcher matcher = pattern.matcher(name);
|
||||
while (matcher.find()) {
|
||||
if (idx == replace.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
name = name.replaceFirst(matcher.group(), replace[idx]);
|
||||
idx++;
|
||||
}
|
||||
|
||||
matcher = pattern.matcher(lore);
|
||||
|
||||
while (matcher.find()) {
|
||||
if (idx == replace.length) {
|
||||
break;
|
||||
}
|
||||
|
||||
lore = lore.replaceFirst(matcher.group(), replace[idx]);
|
||||
idx++;
|
||||
}
|
||||
|
||||
Material mat = Material.getMaterial(material.toUpperCase());
|
||||
Material mat = Materials.parse(material);
|
||||
if (mat == null) {
|
||||
LoParkour.getPlugin().getLogger().warning("Invalid material '%s' for locale path '%s', using STONE as fallback".formatted(material, path));
|
||||
LoParkour.getPlugin().getLogger().warning("Invalid material '%s' for locale path '%s', using STONE".formatted(material, path));
|
||||
mat = Material.STONE;
|
||||
}
|
||||
|
||||
Item item = new Item(mat, name);
|
||||
|
||||
if (!lore.isEmpty()) {
|
||||
item.lore(lore.split("\\|\\|"));
|
||||
}
|
||||
|
||||
if (modelId != -1) {
|
||||
// TODO: item.setCustomModelData(modelId);
|
||||
}
|
||||
|
||||
if (!lore.isEmpty()) item.lore(lore.split("\\|\\|"));
|
||||
return item;
|
||||
}
|
||||
|
||||
public static List<String> getChildren(FileConfiguration file, String path, boolean deep) {
|
||||
ConfigurationSection section = file.getConfigurationSection(path);
|
||||
@NotNull
|
||||
private static String applyReplacements(@NotNull String text, String... replacements) {
|
||||
if (replacements.length == 0) return text;
|
||||
String result = text;
|
||||
int idx = 0;
|
||||
Matcher matcher = REPLACEMENT_PATTERN.matcher(result);
|
||||
while (matcher.find() && idx < replacements.length) {
|
||||
result = result.replaceFirst(matcher.group(), replacements[idx++]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return section != null ? new ArrayList<>(section.getKeys(deep)) : Collections.emptyList();
|
||||
@NotNull
|
||||
private static String getPlayerLocale(@NotNull Player player) {
|
||||
ParkourUser user = ParkourUser.getUser(player);
|
||||
String loc = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
|
||||
return (loc != null && !loc.isBlank()) ? loc : "en";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package dev.loki.loparkour.config;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.util.Materials;
|
||||
import dev.loki.loparkour.api.Registry;
|
||||
import dev.loki.loparkour.menu.ParkourOption;
|
||||
import dev.loki.loparkour.style.RandomStyle;
|
||||
|
|
@ -9,24 +10,12 @@ import dev.loki.loparkour.util.ParticleData;
|
|||
import org.bukkit.*;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.util.Vector;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
/**
|
||||
* Top-level config constants used throughout the plugin.
|
||||
*
|
||||
* Heavy sub-sections are delegated to focused classes:
|
||||
* <ul>
|
||||
* <li>{@link SqlOptions} – SQL connection settings</li>
|
||||
* <li>{@link ParticleOptions} – particle & sound settings</li>
|
||||
* <li>{@link GenerationOptions} – generation & jump-validation settings</li>
|
||||
* </ul>
|
||||
*
|
||||
* Those classes expose their own static fields. Option re-exports the most
|
||||
* commonly accessed ones via forwarding fields so existing call-sites compile
|
||||
* without changes.
|
||||
* All config-derived constants used at runtime.
|
||||
*/
|
||||
public class Option {
|
||||
|
||||
|
|
@ -41,9 +30,8 @@ public class Option {
|
|||
|
||||
public static Location GO_BACK_LOC;
|
||||
|
||||
// ── Forwarded from sub-classes (backwards compat) ─────────────────────────
|
||||
// ── SQL ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** @see SqlOptions#SQL */
|
||||
public static boolean SQL;
|
||||
public static String SQL_URL;
|
||||
public static int SQL_PORT;
|
||||
|
|
@ -52,38 +40,49 @@ public class Option {
|
|||
public static String SQL_PASSWORD;
|
||||
public static String SQL_PREFIX;
|
||||
|
||||
/** @see ParticleOptions#SHAPE */
|
||||
public static ParticleShape PARTICLE_SHAPE;
|
||||
/** @see ParticleOptions#SOUND_TYPE */
|
||||
public static Sound SOUND_TYPE;
|
||||
/** @see ParticleOptions#SOUND_PITCH */
|
||||
public static int SOUND_PITCH;
|
||||
/** @see ParticleOptions#SOUND_VOLUME */
|
||||
public static int SOUND_VOLUME;
|
||||
/** @see ParticleOptions#PARTICLE_TYPE */
|
||||
public static Particle PARTICLE_TYPE;
|
||||
/** @see ParticleOptions#PARTICLE_DATA */
|
||||
// ── Particles / Sound ─────────────────────────────────────────────────────
|
||||
|
||||
public static ParticleShape PARTICLE_SHAPE;
|
||||
public static Sound SOUND_TYPE;
|
||||
public static int SOUND_PITCH;
|
||||
public static int SOUND_VOLUME;
|
||||
public static Particle PARTICLE_TYPE;
|
||||
public static ParticleData<?> PARTICLE_DATA;
|
||||
|
||||
/** @see GenerationOptions */
|
||||
// ── Generation ────────────────────────────────────────────────────────────
|
||||
|
||||
public static double TYPE_NORMAL, TYPE_SPECIAL, TYPE_SCHEMATICS;
|
||||
public static double SPECIAL_ICE, SPECIAL_SLAB, SPECIAL_PANE, SPECIAL_FENCE;
|
||||
public static double NORMAL_DISTANCE_1, NORMAL_DISTANCE_2, NORMAL_DISTANCE_3, NORMAL_DISTANCE_4;
|
||||
public static double NORMAL_HEIGHT_1, NORMAL_HEIGHT_0, NORMAL_HEIGHT_NEG1, NORMAL_HEIGHT_NEG2;
|
||||
public static int MAX_Y, MIN_Y;
|
||||
public static int BLOCK_CLEANUP_DISTANCE, CLEANUP_INTERVAL;
|
||||
/** Block indices to keep behind the rearmost player before trail air-cleanup. */
|
||||
public static int TRAIL_KEEP_BEHIND;
|
||||
public static boolean GHOST_MODE_ENABLED;
|
||||
public static int GHOST_SHOW_TOP;
|
||||
public static double GHOST_TRANSPARENCY;
|
||||
|
||||
// ── Jump validation ───────────────────────────────────────────────────────
|
||||
|
||||
public static boolean JUMP_VALIDATION_ENABLED;
|
||||
public static double MAX_JUMP_DISTANCE;
|
||||
public static double MAX_HORIZONTAL_DISTANCE;
|
||||
public static double MAX_VERTICAL_UP;
|
||||
public static double MAX_VERTICAL_DOWN;
|
||||
|
||||
// ── Jump types ────────────────────────────────────────────────────────────
|
||||
|
||||
public static boolean JUMP_TYPES_ENABLED;
|
||||
public static Map<String, Boolean> JUMP_TYPE_ENABLED;
|
||||
public static Map<String, Double> JUMP_TYPE_CHANCE;
|
||||
|
||||
// ── init ──────────────────────────────────────────────────────────────────
|
||||
|
||||
public static void init(boolean firstLoad) {
|
||||
SqlOptions.init();
|
||||
ParticleOptions.init();
|
||||
GenerationOptions.init();
|
||||
|
||||
syncForwardedFields();
|
||||
initSql();
|
||||
initParticles();
|
||||
initGeneration();
|
||||
initGeneral(firstLoad);
|
||||
initOptions();
|
||||
|
||||
|
|
@ -91,60 +90,139 @@ public class Option {
|
|||
.forEach(Registry::register);
|
||||
}
|
||||
|
||||
/** Copy sub-class fields to this class so existing code still compiles. */
|
||||
private static void syncForwardedFields() {
|
||||
SQL = SqlOptions.SQL;
|
||||
SQL_URL = SqlOptions.URL;
|
||||
SQL_PORT = SqlOptions.PORT;
|
||||
SQL_DB = SqlOptions.DB;
|
||||
SQL_USERNAME = SqlOptions.USERNAME;
|
||||
SQL_PASSWORD = SqlOptions.PASSWORD;
|
||||
SQL_PREFIX = SqlOptions.PREFIX;
|
||||
// ── SQL ───────────────────────────────────────────────────────────────────
|
||||
|
||||
PARTICLE_SHAPE = ParticleOptions.SHAPE;
|
||||
SOUND_TYPE = ParticleOptions.SOUND_TYPE;
|
||||
SOUND_PITCH = ParticleOptions.SOUND_PITCH;
|
||||
SOUND_VOLUME = ParticleOptions.SOUND_VOLUME;
|
||||
PARTICLE_TYPE = ParticleOptions.PARTICLE_TYPE;
|
||||
PARTICLE_DATA = ParticleOptions.PARTICLE_DATA;
|
||||
|
||||
TYPE_NORMAL = GenerationOptions.TYPE_NORMAL;
|
||||
TYPE_SPECIAL = GenerationOptions.TYPE_SPECIAL;
|
||||
TYPE_SCHEMATICS = GenerationOptions.TYPE_SCHEMATICS;
|
||||
|
||||
SPECIAL_ICE = GenerationOptions.SPECIAL_ICE;
|
||||
SPECIAL_SLAB = GenerationOptions.SPECIAL_SLAB;
|
||||
SPECIAL_PANE = GenerationOptions.SPECIAL_PANE;
|
||||
SPECIAL_FENCE = GenerationOptions.SPECIAL_FENCE;
|
||||
|
||||
NORMAL_DISTANCE_1 = GenerationOptions.NORMAL_DISTANCE_1;
|
||||
NORMAL_DISTANCE_2 = GenerationOptions.NORMAL_DISTANCE_2;
|
||||
NORMAL_DISTANCE_3 = GenerationOptions.NORMAL_DISTANCE_3;
|
||||
NORMAL_DISTANCE_4 = GenerationOptions.NORMAL_DISTANCE_4;
|
||||
|
||||
NORMAL_HEIGHT_1 = GenerationOptions.NORMAL_HEIGHT_1;
|
||||
NORMAL_HEIGHT_0 = GenerationOptions.NORMAL_HEIGHT_0;
|
||||
NORMAL_HEIGHT_NEG1 = GenerationOptions.NORMAL_HEIGHT_NEG1;
|
||||
NORMAL_HEIGHT_NEG2 = GenerationOptions.NORMAL_HEIGHT_NEG2;
|
||||
|
||||
MAX_Y = GenerationOptions.MAX_Y;
|
||||
MIN_Y = GenerationOptions.MIN_Y;
|
||||
|
||||
BLOCK_CLEANUP_DISTANCE = GenerationOptions.BLOCK_CLEANUP_DISTANCE;
|
||||
CLEANUP_INTERVAL = GenerationOptions.CLEANUP_INTERVAL;
|
||||
|
||||
GHOST_MODE_ENABLED = GenerationOptions.GHOST_MODE_ENABLED;
|
||||
GHOST_SHOW_TOP = GenerationOptions.GHOST_SHOW_TOP;
|
||||
GHOST_TRANSPARENCY = GenerationOptions.GHOST_TRANSPARENCY;
|
||||
private static void initSql() {
|
||||
SQL = Config.CONFIG.getBoolean("sql.enabled");
|
||||
SQL_PORT = Config.CONFIG.getInt("sql.port");
|
||||
SQL_DB = Config.CONFIG.getString("sql.database");
|
||||
SQL_URL = Config.CONFIG.getString("sql.url");
|
||||
SQL_USERNAME = Config.CONFIG.getString("sql.username");
|
||||
String envPassword = System.getenv("LOPARKOUR_SQL_PASSWORD");
|
||||
SQL_PASSWORD = (envPassword != null && !envPassword.isEmpty())
|
||||
? envPassword
|
||||
: Config.CONFIG.getString("sql.password");
|
||||
SQL_PREFIX = Config.CONFIG.getString("sql.prefix");
|
||||
}
|
||||
|
||||
// ── General init ──────────────────────────────────────────────────────────
|
||||
// ── Particles / Sound ─────────────────────────────────────────────────────
|
||||
|
||||
private static void initParticles() {
|
||||
SOUND_TYPE = resolveSound(Config.CONFIG.getString("particles.sound-type"));
|
||||
PARTICLE_TYPE = resolveParticle(Config.CONFIG.getString("particles.particle-type"));
|
||||
PARTICLE_SHAPE = resolveShape(Config.CONFIG.getString("particles.particle-shape"));
|
||||
|
||||
SOUND_PITCH = Config.CONFIG.getInt("particles.sound-pitch");
|
||||
SOUND_VOLUME = Config.CONFIG.getInt("particles.sound-volume");
|
||||
PARTICLE_DATA = new ParticleData<>(PARTICLE_TYPE, null, 10);
|
||||
}
|
||||
|
||||
private static Sound resolveSound(String name) {
|
||||
if (name != null && !name.isEmpty()) {
|
||||
try { return Sound.valueOf(name.toUpperCase()); }
|
||||
catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
for (String fb : new String[]{"BLOCK_NOTE_BLOCK_GUITAR", "BLOCK_NOTE_BLOCK_PLING", "BLOCK_NOTE_PLING"}) {
|
||||
try { return Sound.valueOf(fb); }
|
||||
catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
LoParkour.getPlugin().getLogger().warning("Could not resolve any sound, using first available.");
|
||||
return Sound.values()[0];
|
||||
}
|
||||
|
||||
private static Particle resolveParticle(String name) {
|
||||
if (name != null && !name.isEmpty()) {
|
||||
try { return Particle.valueOf(name.toUpperCase()); }
|
||||
catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
for (String fb : new String[]{"INSTANT_EFFECT", "SPELL_INSTANT", "CRIT"}) {
|
||||
try { return Particle.valueOf(fb); }
|
||||
catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
LoParkour.getPlugin().getLogger().warning("Could not resolve any particle, using first available.");
|
||||
return Particle.values()[0];
|
||||
}
|
||||
|
||||
private static ParticleShape resolveShape(String name) {
|
||||
if (name != null && !name.isEmpty()) {
|
||||
try { return ParticleShape.valueOf(name.toUpperCase()); }
|
||||
catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
return ParticleShape.BOX;
|
||||
}
|
||||
|
||||
// ── Generation ────────────────────────────────────────────────────────────
|
||||
|
||||
private static void initGeneration() {
|
||||
JUMP_VALIDATION_ENABLED = Config.CONFIG.isPath("jump-validation.enabled") && Config.CONFIG.getBoolean("jump-validation.enabled");
|
||||
MAX_JUMP_DISTANCE = Config.CONFIG.isPath("jump-validation.max-distance") ? Config.CONFIG.getDouble("jump-validation.max-distance") : 5.0;
|
||||
MAX_HORIZONTAL_DISTANCE = Config.CONFIG.isPath("jump-validation.max-horizontal") ? Config.CONFIG.getDouble("jump-validation.max-horizontal") : 4.0;
|
||||
MAX_VERTICAL_UP = Config.CONFIG.isPath("jump-validation.max-vertical-up") ? Config.CONFIG.getDouble("jump-validation.max-vertical-up") : 1.0;
|
||||
MAX_VERTICAL_DOWN = Config.CONFIG.isPath("jump-validation.max-vertical-down") ? Config.CONFIG.getDouble("jump-validation.max-vertical-down") : 3.0;
|
||||
|
||||
JUMP_TYPES_ENABLED = Config.CONFIG.isPath("jump-types.enabled") && Config.CONFIG.getBoolean("jump-types.enabled");
|
||||
JUMP_TYPE_ENABLED = new HashMap<>();
|
||||
JUMP_TYPE_CHANCE = new HashMap<>();
|
||||
if (Config.CONFIG.isPath("jump-types.types")) {
|
||||
for (String type : Config.CONFIG.getChildren("jump-types.types")) {
|
||||
String p = "jump-types.types." + type;
|
||||
JUMP_TYPE_ENABLED.put(type, Config.CONFIG.isPath(p + ".enabled") && Config.CONFIG.getBoolean(p + ".enabled"));
|
||||
JUMP_TYPE_CHANCE.put(type, Config.CONFIG.isPath(p + ".chance") ? Config.CONFIG.getDouble(p + ".chance") : 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
BLOCK_CLEANUP_DISTANCE = Config.CONFIG.isPath("memory.block-cleanup-distance") ? Config.CONFIG.getInt("memory.block-cleanup-distance") : 100;
|
||||
CLEANUP_INTERVAL = Config.CONFIG.isPath("memory.cleanup-interval") ? Config.CONFIG.getInt("memory.cleanup-interval") : 100;
|
||||
TRAIL_KEEP_BEHIND = Config.CONFIG.isPath("memory.trail-keep-behind") ? Config.CONFIG.getInt("memory.trail-keep-behind") : 10;
|
||||
|
||||
GHOST_MODE_ENABLED = Config.CONFIG.isPath("ghost-mode.enabled") && Config.CONFIG.getBoolean("ghost-mode.enabled");
|
||||
GHOST_SHOW_TOP = Config.CONFIG.isPath("ghost-mode.show-top") ? Config.CONFIG.getInt("ghost-mode.show-top") : 3;
|
||||
GHOST_TRANSPARENCY = Config.CONFIG.isPath("ghost-mode.transparency") ? Config.CONFIG.getDouble("ghost-mode.transparency") : 0.5;
|
||||
|
||||
TYPE_NORMAL = Config.GENERATION.getInt("generation.type.normal") / 100.0;
|
||||
TYPE_SPECIAL = Config.GENERATION.getInt("generation.type.special") / 100.0;
|
||||
TYPE_SCHEMATICS = Config.GENERATION.getInt("generation.type.schematic") / 100.0;
|
||||
|
||||
SPECIAL_ICE = Config.GENERATION.getInt("generation.special.ice") / 100.0;
|
||||
SPECIAL_SLAB = Config.GENERATION.getInt("generation.special.slab") / 100.0;
|
||||
SPECIAL_PANE = Config.GENERATION.getInt("generation.special.pane") / 100.0;
|
||||
SPECIAL_FENCE = Config.GENERATION.getInt("generation.special.fence") / 100.0;
|
||||
|
||||
NORMAL_DISTANCE_1 = Config.GENERATION.getInt("generation.normal.distance.1") / 100.0;
|
||||
NORMAL_DISTANCE_2 = Config.GENERATION.getInt("generation.normal.distance.2") / 100.0;
|
||||
NORMAL_DISTANCE_3 = Config.GENERATION.getInt("generation.normal.distance.3") / 100.0;
|
||||
NORMAL_DISTANCE_4 = Config.GENERATION.getInt("generation.normal.distance.4") / 100.0;
|
||||
|
||||
NORMAL_HEIGHT_1 = Config.GENERATION.getInt("generation.normal.height.1") / 100.0;
|
||||
NORMAL_HEIGHT_0 = Config.GENERATION.getInt("generation.normal.height.0") / 100.0;
|
||||
NORMAL_HEIGHT_NEG1 = Config.GENERATION.getInt("generation.normal.height.-1") / 100.0;
|
||||
NORMAL_HEIGHT_NEG2 = Config.GENERATION.getInt("generation.normal.height.-2") / 100.0;
|
||||
|
||||
MAX_Y = Config.GENERATION.getInt("generation.settings.max-y");
|
||||
MIN_Y = Config.GENERATION.getInt("generation.settings.min-y");
|
||||
|
||||
if (MIN_Y >= MAX_Y) {
|
||||
MIN_Y = 100; MAX_Y = 200;
|
||||
LoParkour.getPlugin().getLogger().severe("min-y >= max-y in generation.yml — using defaults 100/200.");
|
||||
}
|
||||
}
|
||||
|
||||
// ── General ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static void initGeneral(boolean firstLoad) {
|
||||
GO_BACK_LOC = parseLocation(Config.CONFIG.getString("bungeecord.go-back"));
|
||||
String[] axes = Config.CONFIG.getString("bungeecord.go-back-axes").split(",");
|
||||
GO_BACK_LOC.setPitch(Float.parseFloat(axes[0]));
|
||||
GO_BACK_LOC.setYaw(Float.parseFloat(axes[1]));
|
||||
try {
|
||||
String axesRaw = Config.CONFIG.getString("bungeecord.go-back-axes");
|
||||
if (!axesRaw.isEmpty()) {
|
||||
String[] axes = axesRaw.split(",");
|
||||
if (axes.length >= 2) {
|
||||
GO_BACK_LOC.setPitch(Float.parseFloat(axes[0].trim()));
|
||||
GO_BACK_LOC.setYaw(Float.parseFloat(axes[1].trim()));
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LoParkour.getPlugin().getLogger().warning("Invalid bungeecord.go-back-axes, using 0,0");
|
||||
}
|
||||
|
||||
POSSIBLE_LEADS = Config.CONFIG.getIntList("options.leads.amount");
|
||||
for (int lead : new ArrayList<>(POSSIBLE_LEADS)) {
|
||||
|
|
@ -193,20 +271,31 @@ public class Option {
|
|||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static Location parseLocation(String location) {
|
||||
String[] values = location.replaceAll("[()]", "").replaceAll("[, ]", " ").split(" ");
|
||||
World world = Bukkit.getWorld(values[3]);
|
||||
if (world == null) world = Bukkit.getWorlds().get(0);
|
||||
return new Location(world, Double.parseDouble(values[0]),
|
||||
Double.parseDouble(values[1]), Double.parseDouble(values[2]));
|
||||
try {
|
||||
String[] values = location.replaceAll("[()]", "").replaceAll("[, ]", " ").trim().split("\\s+");
|
||||
if (values.length < 4) {
|
||||
throw new IllegalArgumentException("Expected 4 values (x,y,z,world), got " + values.length);
|
||||
}
|
||||
World world = Bukkit.getWorld(values[3]);
|
||||
if (world == null) world = Bukkit.getWorlds().get(0);
|
||||
return new Location(world, Double.parseDouble(values[0]),
|
||||
Double.parseDouble(values[1]), Double.parseDouble(values[2]));
|
||||
} catch (Exception ex) {
|
||||
LoParkour.getPlugin().getLogger().warning(
|
||||
"Invalid bungeecord.go-back value '" + location + "'. Falling back to spawn world origin.");
|
||||
World fallback = Bukkit.getWorlds().isEmpty() ? null : Bukkit.getWorlds().get(0);
|
||||
return new Location(fallback, 0, 100, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static Set<Style> initStyles(String path, FileConfiguration config, BiFunction<String, List<Material>, Style> fn) {
|
||||
var styles = new HashSet<Style>();
|
||||
for (String style : Locales.getChildren(config, path, false)) {
|
||||
ConfigAccessor accessor = new ConfigAccessor(config, "config.yml");
|
||||
for (String style : accessor.getChildren(path, false)) {
|
||||
styles.add(fn.apply(style,
|
||||
config.getStringList("%s.%s".formatted(path, style)).stream()
|
||||
.map(name -> {
|
||||
Material m = Material.getMaterial(name.toUpperCase());
|
||||
Material m = Materials.parse(name);
|
||||
if (m == null) {
|
||||
LoParkour.getPlugin().getLogger().severe("Invalid material %s in style %s".formatted(name, style));
|
||||
return Material.STONE;
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
package dev.loki.loparkour.config;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.util.ParticleData;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
|
||||
/**
|
||||
* Particle and sound settings. Populated by {@link Option#init}.
|
||||
*/
|
||||
public final class ParticleOptions {
|
||||
|
||||
public static Option.ParticleShape SHAPE;
|
||||
public static Particle PARTICLE_TYPE;
|
||||
public static ParticleData<?> PARTICLE_DATA;
|
||||
public static Sound SOUND_TYPE;
|
||||
public static int SOUND_PITCH;
|
||||
public static int SOUND_VOLUME;
|
||||
|
||||
static void init() {
|
||||
String sound = Config.CONFIG.getString("particles.sound-type").toUpperCase();
|
||||
try {
|
||||
SOUND_TYPE = Sound.valueOf(sound);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
SOUND_TYPE = Sound.valueOf("BLOCK_NOTE_PLING");
|
||||
LoParkour.getPlugin().getLogger().severe("Invalid sound: " + sound);
|
||||
}
|
||||
|
||||
String particle = Config.CONFIG.getString("particles.particle-type");
|
||||
try {
|
||||
PARTICLE_TYPE = Particle.valueOf(particle);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
PARTICLE_TYPE = Particle.valueOf("SPELL_INSTANT");
|
||||
LoParkour.getPlugin().getLogger().severe("Invalid particle: " + particle);
|
||||
}
|
||||
|
||||
SOUND_PITCH = Config.CONFIG.getInt("particles.sound-pitch");
|
||||
SOUND_VOLUME = Config.CONFIG.getInt("particles.sound-volume");
|
||||
SHAPE = Option.ParticleShape.valueOf(Config.CONFIG.getString("particles.particle-shape").toUpperCase());
|
||||
PARTICLE_DATA = new ParticleData<>(PARTICLE_TYPE, null, 10);
|
||||
}
|
||||
|
||||
private ParticleOptions() {}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package dev.loki.loparkour.config;
|
||||
|
||||
/**
|
||||
* SQL connection settings. Populated by {@link Option#init}.
|
||||
*/
|
||||
public final class SqlOptions {
|
||||
|
||||
public static boolean SQL;
|
||||
public static int PORT;
|
||||
public static String URL;
|
||||
public static String DB;
|
||||
public static String USERNAME;
|
||||
public static String PASSWORD;
|
||||
public static String PREFIX;
|
||||
|
||||
static void init() {
|
||||
SQL = Config.CONFIG.getBoolean("sql.enabled");
|
||||
PORT = Config.CONFIG.getInt("sql.port");
|
||||
DB = Config.CONFIG.getString("sql.database");
|
||||
URL = Config.CONFIG.getString("sql.url");
|
||||
USERNAME = Config.CONFIG.getString("sql.username");
|
||||
PASSWORD = Config.CONFIG.getString("sql.password");
|
||||
PREFIX = Config.CONFIG.getString("sql.prefix");
|
||||
}
|
||||
|
||||
private SqlOptions() {}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue