refactor(config): encapsulate option sections, drop the Option mirror
Option was a facade of 45 public static fields kept in sync with the four sections by syncFields(); the sections themselves also exposed public static mutable fields. 88 VisibilityModifier violations in one place and two owners for every value. - Option keeps only init(boolean) (the reload entry ConfigLoader calls); the mirrors, syncFields() and the unused @Deprecated initStyles are gone - sections hold private static state behind fluent accessors (OptionSQL.enabled(), OptionGeneration.cleanupInterval(), OptionGeneral.goBackLocation()); the two Option.SQL = false writes become OptionSQL.disable() - collection/location accessors hand out unmodifiable views and a Location copy, so no caller can mutate loaded config state - ParticleShape promoted from nested enum to config/options/ParticleShape - delete 17 never-read generation probability fields and initGenerationProbabilities(): live weights come from advanced.* via GeneratorProfileManager/GenerationProfileLoader; min-y/max-y guard moved 1:1 into initBounds() - rewrite 63 Option.<FIELD> reads in 19 files to the section accessors - validateConfigs() now checks the paths the code actually consumes (advanced.distance/height/type weights) instead of the dead generation.normal.* - ParkourRestrictionListener.onChat: getOrDefault(CHAT, true) instead of unboxing a Boolean that may be absent checkstyle 0 errors / 329 -> 240 warnings (VisibilityModifier 163 -> 75); config/options itself is clean. Dead config keys found on the way are filed as TODO items 164-166. ./gradlew check green: PMD 0, 94 tests, NullAway unchanged.
This commit is contained in:
parent
4263df50a2
commit
c120309663
27 changed files with 386 additions and 359 deletions
|
|
@ -7,7 +7,7 @@ import dev.loki.loparkour.adaptive.model.SkillRating;
|
|||
import dev.loki.loparkour.adaptive.storage.FileStatsStorage;
|
||||
import dev.loki.loparkour.adaptive.storage.SQLStatsStorage;
|
||||
import dev.loki.loparkour.adaptive.storage.StatsRepository;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.section.OptionSQL;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.core.model.Profile;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
|
|
@ -42,7 +42,7 @@ public final class AdaptiveServices {
|
|||
}
|
||||
|
||||
final Runnable setup = () -> startCollector(createRepository());
|
||||
if (Option.SQL) {
|
||||
if (OptionSQL.enabled()) {
|
||||
Storage.runWhenReady(setup);
|
||||
} else {
|
||||
setup.run();
|
||||
|
|
@ -54,7 +54,7 @@ public final class AdaptiveServices {
|
|||
AdaptiveConfig.reload();
|
||||
if (plugin != null && AdaptiveConfig.isEnabled()) {
|
||||
final Runnable setup = () -> startCollector(createRepository());
|
||||
if (Option.SQL) {
|
||||
if (OptionSQL.enabled()) {
|
||||
Storage.runWhenReady(setup);
|
||||
} else {
|
||||
setup.run();
|
||||
|
|
@ -147,7 +147,7 @@ public final class AdaptiveServices {
|
|||
|
||||
@NotNull
|
||||
private static StatsRepository createRepository() {
|
||||
if (Option.SQL && StorageSQL.isConnected()) {
|
||||
if (OptionSQL.enabled() && StorageSQL.isConnected()) {
|
||||
return new SQLStatsStorage(StorageSQL.getConnectionManager());
|
||||
}
|
||||
return new FileStatsStorage();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import dev.loki.loparkour.api.core.Registry;
|
|||
import dev.loki.loparkour.command.core.LoParkourCommand;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.section.OptionSQL;
|
||||
import dev.loki.loparkour.duels.DuelsMode;
|
||||
import dev.loki.loparkour.ghost.service.GhostService;
|
||||
import dev.loki.loparkour.hook.holo.HoloHook;
|
||||
|
|
@ -141,7 +141,7 @@ public final class PluginBootstrap {
|
|||
|
||||
private static void setupMetrics(@NotNull LoParkour plugin) {
|
||||
final Metrics metrics = new Metrics(plugin, BSTATS_PLUGIN_ID);
|
||||
metrics.addCustomChart(new SimplePie("using_sql", () -> Boolean.toString(Option.SQL)));
|
||||
metrics.addCustomChart(new SimplePie("using_sql", () -> Boolean.toString(OptionSQL.enabled())));
|
||||
metrics.addCustomChart(new SimplePie("using_rewards", () -> Boolean.toString(Rewards.REWARDS_ENABLED)));
|
||||
metrics.addCustomChart(new SimplePie("locale_count", () -> Integer.toString(Locales.getLocaleCount())));
|
||||
metrics.addCustomChart(new SingleLineChart("player_joins", () ->
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package dev.loki.loparkour.command.schematic.wand;
|
|||
|
||||
import dev.loki.loparkour.command.schematic.SchematicCommandHandler;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.OptionGeneral;
|
||||
import dev.loki.loparkour.menu.core.screen.ParkourOption;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
|
@ -41,7 +41,7 @@ public final class SchematicWand {
|
|||
return cachedWand;
|
||||
}
|
||||
|
||||
String locale = Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
|
||||
String locale = OptionGeneral.optionsDefaults().get(ParkourOption.LANG);
|
||||
if (locale == null || locale.isBlank()) {
|
||||
locale = "en";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,14 +79,36 @@ public final class ConfigLoader {
|
|||
}
|
||||
}
|
||||
|
||||
if (Config.GENERATION.isPath("generation.normal.height")) {
|
||||
final 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));
|
||||
if (Config.GENERATION.isPath("advanced.height.min") && Config.GENERATION.isPath("advanced.height.max")) {
|
||||
final int heightMax = Config.GENERATION.getInt("advanced.height.max");
|
||||
final int heightMin = Config.GENERATION.getInt("advanced.height.min");
|
||||
if (heightMax < heightMin) {
|
||||
log.severe("Invalid config: advanced.height.max (%d) must be >= advanced.height.min (%d)"
|
||||
.formatted(heightMax, heightMin));
|
||||
}
|
||||
}
|
||||
|
||||
validateWeights(log, "advanced.distance.chances");
|
||||
validateWeights(log, "advanced.height.chances");
|
||||
validateWeights(log, "advanced.type");
|
||||
}
|
||||
|
||||
/**
|
||||
* Block/jump weights are divided by their sum, so a section that adds up to
|
||||
* zero silently produces no blocks at all.
|
||||
*/
|
||||
private static void validateWeights(@NotNull Logger log, @NotNull String section) {
|
||||
if (!Config.GENERATION.isPath(section)) {
|
||||
return;
|
||||
}
|
||||
|
||||
double sum = 0.0;
|
||||
for (final String key : Config.GENERATION.getChildren(section)) {
|
||||
sum += Config.GENERATION.getDouble(section + "." + key);
|
||||
}
|
||||
|
||||
if (sum <= 0.0) {
|
||||
log.severe("Invalid config: %s weights sum to %.1f, must be > 0".formatted(section, sum));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package dev.loki.loparkour.config.locale;
|
||||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.OptionGeneral;
|
||||
import dev.loki.loparkour.menu.core.screen.ParkourOption;
|
||||
import dev.loki.loparkour.player.core.ParkourUser;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
|
|
@ -56,8 +56,8 @@ public final class Locales {
|
|||
if (sender instanceof Player player) {
|
||||
return getString(player, path);
|
||||
}
|
||||
String locale = Option.OPTIONS_DEFAULTS != null
|
||||
? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG)
|
||||
String locale = OptionGeneral.optionsDefaults() != null
|
||||
? OptionGeneral.optionsDefaults().get(ParkourOption.LANG)
|
||||
: null;
|
||||
if (locale == null || locale.isBlank()) {
|
||||
locale = "en";
|
||||
|
|
@ -170,11 +170,11 @@ public final class Locales {
|
|||
@NotNull
|
||||
private static String getPlayerLocale(@NotNull Player player) {
|
||||
final ParkourUser user = ParkourUser.getUser(player);
|
||||
String loc = user == null ? Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG) : user.locale;
|
||||
String loc = user == null ? OptionGeneral.optionsDefaults().get(ParkourOption.LANG) : user.locale;
|
||||
|
||||
// Fix legacy Boolean "true" values from old config parsing
|
||||
if ("true".equals(loc) || "false".equals(loc)) {
|
||||
loc = Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
|
||||
loc = OptionGeneral.optionsDefaults().get(ParkourOption.LANG);
|
||||
if (user != null) {
|
||||
user.locale = loc; // Update user's locale to fixed value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,37 +2,25 @@ package dev.loki.loparkour.config.options;
|
|||
|
||||
import dev.loki.loparkour.api.core.Registry;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.menu.core.screen.ParkourOption;
|
||||
import dev.loki.loparkour.style.core.RandomStyle;
|
||||
import dev.loki.loparkour.style.core.Style;
|
||||
import dev.loki.loparkour.util.particle.ParticleData;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import dev.loki.loparkour.config.options.section.OptionGeneration;
|
||||
import dev.loki.loparkour.config.options.section.OptionParticles;
|
||||
import dev.loki.loparkour.config.options.section.OptionSQL;
|
||||
import dev.loki.loparkour.config.options.section.OptionStyles;
|
||||
import java.util.function.BiFunction;
|
||||
import dev.loki.loparkour.style.core.RandomStyle;
|
||||
|
||||
/**
|
||||
* Facade for all config-derived constants.
|
||||
* Delegates to specialized option classes in config.options package.
|
||||
* Reload entry point for the option sections.
|
||||
*
|
||||
* <p>Each section owns its values and exposes them through accessors
|
||||
* ({@link OptionSQL}, {@link OptionParticles}, {@link OptionGeneration},
|
||||
* {@link OptionGeneral}); this class does not mirror them.</p>
|
||||
*/
|
||||
public final class Option {
|
||||
|
||||
private Option() {
|
||||
}
|
||||
|
||||
// ── Initialization ────────────────────────────────────────────────────────
|
||||
|
||||
/** (Re)read every option section and re-register the configured styles. */
|
||||
public static void init(boolean firstLoad) {
|
||||
OptionSQL.init();
|
||||
OptionParticles.init();
|
||||
|
|
@ -40,129 +28,7 @@ public final class Option {
|
|||
OptionGeneral.init(firstLoad);
|
||||
OptionGeneral.initOptions();
|
||||
|
||||
// Sync all fields to Option facade
|
||||
syncFields();
|
||||
|
||||
OptionStyles.initStyles("styles.list", Config.CONFIG.fileConfiguration, RandomStyle::new)
|
||||
.forEach(Registry::register);
|
||||
}
|
||||
|
||||
// ── Direct field re-exports for backward compatibility ───────────────────
|
||||
|
||||
// SQL
|
||||
public static boolean SQL;
|
||||
public static String SQL_URL;
|
||||
public static int SQL_PORT;
|
||||
public static String SQL_DB;
|
||||
public static String SQL_USERNAME;
|
||||
public static String SQL_PASSWORD;
|
||||
public static String SQL_PREFIX;
|
||||
|
||||
// Particles
|
||||
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;
|
||||
|
||||
// Generation
|
||||
public static double TYPE_NORMAL;
|
||||
public static double TYPE_SPECIAL;
|
||||
public static double TYPE_SCHEMATICS;
|
||||
public static double SPECIAL_ICE;
|
||||
public static double SPECIAL_SLAB;
|
||||
public static double SPECIAL_PANE;
|
||||
public static double SPECIAL_FENCE;
|
||||
public static double NORMAL_DISTANCE_1;
|
||||
public static double NORMAL_DISTANCE_2;
|
||||
public static double NORMAL_DISTANCE_3;
|
||||
public static double NORMAL_DISTANCE_4;
|
||||
public static double NORMAL_HEIGHT_1;
|
||||
public static double NORMAL_HEIGHT_0;
|
||||
public static double NORMAL_HEIGHT_NEG1;
|
||||
public static double NORMAL_HEIGHT_NEG2;
|
||||
public static int MAX_Y;
|
||||
public static int MIN_Y;
|
||||
public static int BLOCK_CLEANUP_DISTANCE;
|
||||
public static int CLEANUP_INTERVAL;
|
||||
public static int TRAIL_KEEP_BEHIND;
|
||||
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;
|
||||
|
||||
// General
|
||||
public static double BORDER_SIZE;
|
||||
public static List<Integer> POSSIBLE_LEADS;
|
||||
public static BlockFace HEADING;
|
||||
public static Location GO_BACK_LOC;
|
||||
public static Map<ParkourOption, Boolean> OPTIONS_ENABLED;
|
||||
public static Map<ParkourOption, String> OPTIONS_DEFAULTS;
|
||||
|
||||
private static void syncFields() {
|
||||
// SQL
|
||||
SQL = OptionSQL.SQL;
|
||||
SQL_URL = OptionSQL.SQL_URL;
|
||||
SQL_PORT = OptionSQL.SQL_PORT;
|
||||
SQL_DB = OptionSQL.SQL_DB;
|
||||
SQL_USERNAME = OptionSQL.SQL_USERNAME;
|
||||
SQL_PASSWORD = OptionSQL.SQL_PASSWORD;
|
||||
SQL_PREFIX = OptionSQL.SQL_PREFIX;
|
||||
|
||||
// Particles
|
||||
PARTICLE_SHAPE = OptionParticles.PARTICLE_SHAPE;
|
||||
SOUND_TYPE = OptionParticles.SOUND_TYPE;
|
||||
SOUND_PITCH = OptionParticles.SOUND_PITCH;
|
||||
SOUND_VOLUME = OptionParticles.SOUND_VOLUME;
|
||||
PARTICLE_TYPE = OptionParticles.PARTICLE_TYPE;
|
||||
PARTICLE_DATA = OptionParticles.PARTICLE_DATA;
|
||||
|
||||
// Generation
|
||||
TYPE_NORMAL = OptionGeneration.TYPE_NORMAL;
|
||||
TYPE_SPECIAL = OptionGeneration.TYPE_SPECIAL;
|
||||
TYPE_SCHEMATICS = OptionGeneration.TYPE_SCHEMATICS;
|
||||
SPECIAL_ICE = OptionGeneration.SPECIAL_ICE;
|
||||
SPECIAL_SLAB = OptionGeneration.SPECIAL_SLAB;
|
||||
SPECIAL_PANE = OptionGeneration.SPECIAL_PANE;
|
||||
SPECIAL_FENCE = OptionGeneration.SPECIAL_FENCE;
|
||||
NORMAL_DISTANCE_1 = OptionGeneration.NORMAL_DISTANCE_1;
|
||||
NORMAL_DISTANCE_2 = OptionGeneration.NORMAL_DISTANCE_2;
|
||||
NORMAL_DISTANCE_3 = OptionGeneration.NORMAL_DISTANCE_3;
|
||||
NORMAL_DISTANCE_4 = OptionGeneration.NORMAL_DISTANCE_4;
|
||||
NORMAL_HEIGHT_1 = OptionGeneration.NORMAL_HEIGHT_1;
|
||||
NORMAL_HEIGHT_0 = OptionGeneration.NORMAL_HEIGHT_0;
|
||||
NORMAL_HEIGHT_NEG1 = OptionGeneration.NORMAL_HEIGHT_NEG1;
|
||||
NORMAL_HEIGHT_NEG2 = OptionGeneration.NORMAL_HEIGHT_NEG2;
|
||||
MAX_Y = OptionGeneration.MAX_Y;
|
||||
MIN_Y = OptionGeneration.MIN_Y;
|
||||
BLOCK_CLEANUP_DISTANCE = OptionGeneration.BLOCK_CLEANUP_DISTANCE;
|
||||
CLEANUP_INTERVAL = OptionGeneration.CLEANUP_INTERVAL;
|
||||
TRAIL_KEEP_BEHIND = OptionGeneration.TRAIL_KEEP_BEHIND;
|
||||
JUMP_VALIDATION_ENABLED = OptionGeneration.JUMP_VALIDATION_ENABLED;
|
||||
MAX_JUMP_DISTANCE = OptionGeneration.MAX_JUMP_DISTANCE;
|
||||
MAX_HORIZONTAL_DISTANCE = OptionGeneration.MAX_HORIZONTAL_DISTANCE;
|
||||
MAX_VERTICAL_UP = OptionGeneration.MAX_VERTICAL_UP;
|
||||
MAX_VERTICAL_DOWN = OptionGeneration.MAX_VERTICAL_DOWN;
|
||||
|
||||
// General
|
||||
BORDER_SIZE = OptionGeneral.BORDER_SIZE;
|
||||
POSSIBLE_LEADS = OptionGeneral.POSSIBLE_LEADS;
|
||||
HEADING = OptionGeneral.HEADING;
|
||||
GO_BACK_LOC = OptionGeneral.GO_BACK_LOC;
|
||||
OPTIONS_ENABLED = OptionGeneral.OPTIONS_ENABLED;
|
||||
OPTIONS_DEFAULTS = OptionGeneral.OPTIONS_DEFAULTS;
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link OptionStyles#initStyles(String, FileConfiguration, BiFunction)} */
|
||||
@Deprecated
|
||||
public static Set<Style> initStyles(String path, FileConfiguration config,
|
||||
BiFunction<String, List<Material>, Style> fn) {
|
||||
return OptionStyles.initStyles(path, config, fn);
|
||||
}
|
||||
|
||||
// ── Inner types ───────────────────────────────────────────────────────────
|
||||
|
||||
public enum ParticleShape { DOT, CIRCLE, BOX }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import org.bukkit.block.BlockFace;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
@ -27,13 +28,12 @@ public final class OptionGeneral {
|
|||
private OptionGeneral() {
|
||||
}
|
||||
|
||||
public static double BORDER_SIZE;
|
||||
public static List<Integer> POSSIBLE_LEADS;
|
||||
public static BlockFace HEADING;
|
||||
public static Location GO_BACK_LOC;
|
||||
|
||||
public static Map<ParkourOption, Boolean> OPTIONS_ENABLED;
|
||||
public static Map<ParkourOption, String> OPTIONS_DEFAULTS;
|
||||
private static double borderSize;
|
||||
private static List<Integer> possibleLeads;
|
||||
private static BlockFace heading;
|
||||
private static Location goBackLocation;
|
||||
private static Map<ParkourOption, Boolean> optionsEnabled;
|
||||
private static Map<ParkourOption, String> optionsDefaults;
|
||||
|
||||
public static void init(boolean firstLoad) {
|
||||
initGoBackLocation();
|
||||
|
|
@ -41,7 +41,7 @@ public final class OptionGeneral {
|
|||
initHeading();
|
||||
|
||||
if (firstLoad) {
|
||||
BORDER_SIZE = Config.GENERATION.getDouble("advanced.border-size");
|
||||
borderSize = Config.GENERATION.getDouble("advanced.border-size");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,17 +50,45 @@ public final class OptionGeneral {
|
|||
options.remove(ParkourOption.JOIN);
|
||||
options.remove(ParkourOption.ADMIN);
|
||||
|
||||
OPTIONS_DEFAULTS = new HashMap<>();
|
||||
OPTIONS_ENABLED = new HashMap<>();
|
||||
optionsDefaults = new HashMap<>();
|
||||
optionsEnabled = new HashMap<>();
|
||||
|
||||
for (final ParkourOption option : options) {
|
||||
applyOption(option);
|
||||
}
|
||||
}
|
||||
|
||||
public static double borderSize() {
|
||||
return borderSize;
|
||||
}
|
||||
|
||||
/** Read-only view; the live list is pruned on reload by {@link #initLeads()}. */
|
||||
public static List<Integer> possibleLeads() {
|
||||
return possibleLeads == null ? null : Collections.unmodifiableList(possibleLeads);
|
||||
}
|
||||
|
||||
public static BlockFace heading() {
|
||||
return heading;
|
||||
}
|
||||
|
||||
/** Copy of the configured location, so callers cannot mutate the stored one. */
|
||||
public static Location goBackLocation() {
|
||||
return goBackLocation == null ? null : goBackLocation.clone();
|
||||
}
|
||||
|
||||
/** Read-only view of the option switches. */
|
||||
public static Map<ParkourOption, Boolean> optionsEnabled() {
|
||||
return optionsEnabled == null ? null : Collections.unmodifiableMap(optionsEnabled);
|
||||
}
|
||||
|
||||
/** Read-only view of the option defaults. */
|
||||
public static Map<ParkourOption, String> optionsDefaults() {
|
||||
return optionsDefaults == null ? null : Collections.unmodifiableMap(optionsDefaults);
|
||||
}
|
||||
|
||||
private static void applyOption(ParkourOption option) {
|
||||
final String parent = "default-values." + option.path;
|
||||
OPTIONS_ENABLED.put(option, Config.CONFIG.getBoolean(parent + ".enabled"));
|
||||
optionsEnabled.put(option, Config.CONFIG.getBoolean(parent + ".enabled"));
|
||||
|
||||
final String defaultPath = parent + ".default";
|
||||
if (Config.CONFIG.isPath(defaultPath)) {
|
||||
|
|
@ -88,7 +116,7 @@ public final class OptionGeneral {
|
|||
}
|
||||
|
||||
if (value != null && !value.isEmpty()) {
|
||||
OPTIONS_DEFAULTS.put(option, value);
|
||||
optionsDefaults.put(option, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,14 +129,14 @@ public final class OptionGeneral {
|
|||
}
|
||||
|
||||
private static void initGoBackLocation() {
|
||||
GO_BACK_LOC = parseLocation(Config.CONFIG.getString("bungeecord.go-back"));
|
||||
goBackLocation = parseLocation(Config.CONFIG.getString("bungeecord.go-back"));
|
||||
try {
|
||||
final String axesRaw = Config.CONFIG.getString("bungeecord.go-back-axes");
|
||||
if (!axesRaw.isEmpty()) {
|
||||
final 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()));
|
||||
goBackLocation.setPitch(Float.parseFloat(axes[0].trim()));
|
||||
goBackLocation.setYaw(Float.parseFloat(axes[1].trim()));
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
|
|
@ -117,25 +145,25 @@ public final class OptionGeneral {
|
|||
}
|
||||
|
||||
private static void initLeads() {
|
||||
POSSIBLE_LEADS = Config.CONFIG.getIntList("options.leads.amount");
|
||||
for (final int lead : new ArrayList<>(POSSIBLE_LEADS)) {
|
||||
possibleLeads = Config.CONFIG.getIntList("options.leads.amount");
|
||||
for (final int lead : new ArrayList<>(possibleLeads)) {
|
||||
if (lead < 1 || lead > MAX_LEAD) {
|
||||
LoParkour.getPlugin().getLogger().severe(
|
||||
"Invalid lead: %d. Must be 1–128.".formatted(lead));
|
||||
POSSIBLE_LEADS.remove((Object) lead);
|
||||
possibleLeads.remove((Object) lead);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void initHeading() {
|
||||
final String heading = Config.GENERATION.getString("advanced.island.parkour.heading");
|
||||
HEADING = switch (heading.toLowerCase()) {
|
||||
final String headingName = Config.GENERATION.getString("advanced.island.parkour.heading");
|
||||
heading = switch (headingName.toLowerCase()) {
|
||||
case "north" -> BlockFace.NORTH;
|
||||
case "south" -> BlockFace.SOUTH;
|
||||
case "west" -> BlockFace.WEST;
|
||||
case "east" -> BlockFace.EAST;
|
||||
default -> {
|
||||
LoParkour.getPlugin().getLogger().severe("Invalid heading: " + heading);
|
||||
LoParkour.getPlugin().getLogger().severe("Invalid heading: " + headingName);
|
||||
yield BlockFace.NORTH;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package dev.loki.loparkour.config.options;
|
||||
|
||||
/**
|
||||
* Shape used when spawning trail particles.
|
||||
* Loaded from {@code particles.particle-shape} (see {@code OptionParticles}).
|
||||
*/
|
||||
public enum ParticleShape {
|
||||
|
||||
DOT,
|
||||
CIRCLE,
|
||||
BOX
|
||||
|
||||
}
|
||||
|
|
@ -4,110 +4,121 @@ import dev.loki.loparkour.LoParkour;
|
|||
import dev.loki.loparkour.config.core.Config;
|
||||
|
||||
/**
|
||||
* Generation-related configuration options (validation, memory).
|
||||
* Generation-related configuration options (bounds, memory, jump validation).
|
||||
*
|
||||
* <p>Block-type weights are not read here — they live in
|
||||
* {@code GeneratorProfileManager} / {@code GenerationProfileLoader}, which resolve
|
||||
* the {@code advanced.*} paths (see TODO: {@code generation.type|normal|special}
|
||||
* in generation.yml is currently ignored).</p>
|
||||
*/
|
||||
public final class OptionGeneration {
|
||||
|
||||
private static final double DEFAULT_MAX_JUMP_DISTANCE = 5.0;
|
||||
private static final double DEFAULT_MAX_HORIZONTAL_DISTANCE = 4.0;
|
||||
private static final double DEFAULT_MAX_VERTICAL_UP = 1.0;
|
||||
private static final double DEFAULT_MAX_VERTICAL_DOWN = 3.0;
|
||||
private static final int DEFAULT_BLOCK_CLEANUP_DISTANCE = 100;
|
||||
private static final int DEFAULT_CLEANUP_INTERVAL = 100;
|
||||
private static final int DEFAULT_TRAIL_KEEP_BEHIND = 10;
|
||||
private static final double PERCENT_DIVISOR = 100.0;
|
||||
private static final int DEFAULT_MIN_Y = 100;
|
||||
private static final int DEFAULT_MAX_Y = 200;
|
||||
|
||||
private OptionGeneration() {
|
||||
}
|
||||
|
||||
// ── Block generation probabilities ────────────────────────────────────────
|
||||
public static double TYPE_NORMAL;
|
||||
public static double TYPE_SPECIAL;
|
||||
public static double TYPE_SCHEMATICS;
|
||||
public static double SPECIAL_ICE;
|
||||
public static double SPECIAL_SLAB;
|
||||
public static double SPECIAL_PANE;
|
||||
public static double SPECIAL_FENCE;
|
||||
public static double NORMAL_DISTANCE_1;
|
||||
public static double NORMAL_DISTANCE_2;
|
||||
public static double NORMAL_DISTANCE_3;
|
||||
public static double NORMAL_DISTANCE_4;
|
||||
public static double NORMAL_HEIGHT_1;
|
||||
public static double NORMAL_HEIGHT_0;
|
||||
public static double NORMAL_HEIGHT_NEG1;
|
||||
public static double NORMAL_HEIGHT_NEG2;
|
||||
public static int MAX_Y;
|
||||
public static int MIN_Y;
|
||||
// ── World bounds ───────────────────────────────────────────────────────────
|
||||
private static int maxY;
|
||||
private static int minY;
|
||||
|
||||
// ── Memory / cleanup ──────────────────────────────────────────────────────
|
||||
public static int BLOCK_CLEANUP_DISTANCE;
|
||||
public static int CLEANUP_INTERVAL;
|
||||
public static int TRAIL_KEEP_BEHIND;
|
||||
// ── Memory / cleanup ───────────────────────────────────────────────────────
|
||||
private static int blockCleanupDistance;
|
||||
private static int cleanupInterval;
|
||||
private static int trailKeepBehind;
|
||||
|
||||
// ── 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 validation ────────────────────────────────────────────────────────
|
||||
private static boolean jumpValidationEnabled;
|
||||
private static double maxJumpDistance;
|
||||
private static double maxHorizontalDistance;
|
||||
private static double maxVerticalUp;
|
||||
private static double maxVerticalDown;
|
||||
|
||||
public static void init() {
|
||||
initBounds();
|
||||
initJumpValidation();
|
||||
initMemory();
|
||||
initGenerationProbabilities();
|
||||
}
|
||||
|
||||
private static void initJumpValidation() {
|
||||
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") : DEFAULT_MAX_JUMP_DISTANCE;
|
||||
MAX_HORIZONTAL_DISTANCE = Config.CONFIG.isPath("jump-validation.max-horizontal")
|
||||
? Config.CONFIG.getDouble("jump-validation.max-horizontal") : DEFAULT_MAX_HORIZONTAL_DISTANCE;
|
||||
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") : DEFAULT_MAX_VERTICAL_DOWN;
|
||||
}
|
||||
private static void initBounds() {
|
||||
maxY = Config.GENERATION.getInt("generation.settings.max-y");
|
||||
minY = Config.GENERATION.getInt("generation.settings.min-y");
|
||||
|
||||
private static void initMemory() {
|
||||
BLOCK_CLEANUP_DISTANCE = Config.CONFIG.isPath("memory.block-cleanup-distance")
|
||||
? Config.CONFIG.getInt("memory.block-cleanup-distance") : DEFAULT_BLOCK_CLEANUP_DISTANCE;
|
||||
CLEANUP_INTERVAL = Config.CONFIG.isPath("memory.cleanup-interval")
|
||||
? Config.CONFIG.getInt("memory.cleanup-interval") : DEFAULT_CLEANUP_INTERVAL;
|
||||
TRAIL_KEEP_BEHIND = Config.CONFIG.isPath("memory.trail-keep-behind")
|
||||
? Config.CONFIG.getInt("memory.trail-keep-behind") : DEFAULT_TRAIL_KEEP_BEHIND;
|
||||
}
|
||||
|
||||
private static void initGenerationProbabilities() {
|
||||
TYPE_NORMAL = Config.GENERATION.getInt("generation.type.normal") / PERCENT_DIVISOR;
|
||||
TYPE_SPECIAL = Config.GENERATION.getInt("generation.type.special") / PERCENT_DIVISOR;
|
||||
TYPE_SCHEMATICS = Config.GENERATION.getInt("generation.type.schematic") / PERCENT_DIVISOR;
|
||||
|
||||
SPECIAL_ICE = Config.GENERATION.getInt("generation.special.ice") / PERCENT_DIVISOR;
|
||||
SPECIAL_SLAB = Config.GENERATION.getInt("generation.special.slab") / PERCENT_DIVISOR;
|
||||
SPECIAL_PANE = Config.GENERATION.getInt("generation.special.pane") / PERCENT_DIVISOR;
|
||||
SPECIAL_FENCE = Config.GENERATION.getInt("generation.special.fence") / PERCENT_DIVISOR;
|
||||
|
||||
NORMAL_DISTANCE_1 = Config.GENERATION.getInt("generation.normal.distance.1") / PERCENT_DIVISOR;
|
||||
NORMAL_DISTANCE_2 = Config.GENERATION.getInt("generation.normal.distance.2") / PERCENT_DIVISOR;
|
||||
NORMAL_DISTANCE_3 = Config.GENERATION.getInt("generation.normal.distance.3") / PERCENT_DIVISOR;
|
||||
NORMAL_DISTANCE_4 = Config.GENERATION.getInt("generation.normal.distance.4") / PERCENT_DIVISOR;
|
||||
|
||||
NORMAL_HEIGHT_1 = Config.GENERATION.getInt("generation.normal.height.1") / PERCENT_DIVISOR;
|
||||
NORMAL_HEIGHT_0 = Config.GENERATION.getInt("generation.normal.height.0") / PERCENT_DIVISOR;
|
||||
NORMAL_HEIGHT_NEG1 = Config.GENERATION.getInt("generation.normal.height.-1") / PERCENT_DIVISOR;
|
||||
NORMAL_HEIGHT_NEG2 = Config.GENERATION.getInt("generation.normal.height.-2") / PERCENT_DIVISOR;
|
||||
|
||||
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 = DEFAULT_MIN_Y;
|
||||
MAX_Y = DEFAULT_MAX_Y;
|
||||
if (minY >= maxY) {
|
||||
minY = DEFAULT_MIN_Y;
|
||||
maxY = DEFAULT_MAX_Y;
|
||||
LoParkour.getPlugin().getLogger().severe(
|
||||
"min-y >= max-y in generation.yml — using defaults 100/200.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void initJumpValidation() {
|
||||
jumpValidationEnabled = Config.CONFIG.isPath("jump-validation.enabled")
|
||||
&& Config.CONFIG.getBoolean("jump-validation.enabled");
|
||||
maxJumpDistance = Config.CONFIG.isPath("jump-validation.max-distance")
|
||||
? Config.CONFIG.getDouble("jump-validation.max-distance") : DEFAULT_MAX_JUMP_DISTANCE;
|
||||
maxHorizontalDistance = Config.CONFIG.isPath("jump-validation.max-horizontal")
|
||||
? Config.CONFIG.getDouble("jump-validation.max-horizontal") : DEFAULT_MAX_HORIZONTAL_DISTANCE;
|
||||
maxVerticalUp = Config.CONFIG.isPath("jump-validation.max-vertical-up")
|
||||
? Config.CONFIG.getDouble("jump-validation.max-vertical-up") : DEFAULT_MAX_VERTICAL_UP;
|
||||
maxVerticalDown = Config.CONFIG.isPath("jump-validation.max-vertical-down")
|
||||
? Config.CONFIG.getDouble("jump-validation.max-vertical-down") : DEFAULT_MAX_VERTICAL_DOWN;
|
||||
}
|
||||
|
||||
private static void initMemory() {
|
||||
blockCleanupDistance = Config.CONFIG.isPath("memory.block-cleanup-distance")
|
||||
? Config.CONFIG.getInt("memory.block-cleanup-distance") : DEFAULT_BLOCK_CLEANUP_DISTANCE;
|
||||
cleanupInterval = Config.CONFIG.isPath("memory.cleanup-interval")
|
||||
? Config.CONFIG.getInt("memory.cleanup-interval") : DEFAULT_CLEANUP_INTERVAL;
|
||||
trailKeepBehind = Config.CONFIG.isPath("memory.trail-keep-behind")
|
||||
? Config.CONFIG.getInt("memory.trail-keep-behind") : DEFAULT_TRAIL_KEEP_BEHIND;
|
||||
}
|
||||
|
||||
public static int maxY() {
|
||||
return maxY;
|
||||
}
|
||||
|
||||
public static int minY() {
|
||||
return minY;
|
||||
}
|
||||
|
||||
public static int blockCleanupDistance() {
|
||||
return blockCleanupDistance;
|
||||
}
|
||||
|
||||
public static int cleanupInterval() {
|
||||
return cleanupInterval;
|
||||
}
|
||||
|
||||
public static int trailKeepBehind() {
|
||||
return trailKeepBehind;
|
||||
}
|
||||
|
||||
public static boolean jumpValidationEnabled() {
|
||||
return jumpValidationEnabled;
|
||||
}
|
||||
|
||||
public static double maxJumpDistance() {
|
||||
return maxJumpDistance;
|
||||
}
|
||||
|
||||
public static double maxHorizontalDistance() {
|
||||
return maxHorizontalDistance;
|
||||
}
|
||||
|
||||
public static double maxVerticalUp() {
|
||||
return maxVerticalUp;
|
||||
}
|
||||
|
||||
public static double maxVerticalDown() {
|
||||
return maxVerticalDown;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package dev.loki.loparkour.config.options.section;
|
|||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.ParticleShape;
|
||||
import dev.loki.loparkour.util.particle.ParticleData;
|
||||
import org.bukkit.Particle;
|
||||
import org.bukkit.Sound;
|
||||
|
|
@ -17,21 +17,45 @@ public final class OptionParticles {
|
|||
private OptionParticles() {
|
||||
}
|
||||
|
||||
public static Option.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;
|
||||
private static ParticleShape shape;
|
||||
private static Sound soundType;
|
||||
private static int soundPitch;
|
||||
private static int soundVolume;
|
||||
private static Particle particleType;
|
||||
private static ParticleData<?> particleData;
|
||||
|
||||
public static void init() {
|
||||
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"));
|
||||
soundType = resolveSound(Config.CONFIG.getString("particles.sound-type"));
|
||||
particleType = resolveParticle(Config.CONFIG.getString("particles.particle-type"));
|
||||
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, PARTICLE_SIZE);
|
||||
soundPitch = Config.CONFIG.getInt("particles.sound-pitch");
|
||||
soundVolume = Config.CONFIG.getInt("particles.sound-volume");
|
||||
particleData = new ParticleData<>(particleType, null, PARTICLE_SIZE);
|
||||
}
|
||||
|
||||
public static ParticleShape shape() {
|
||||
return shape;
|
||||
}
|
||||
|
||||
public static Sound soundType() {
|
||||
return soundType;
|
||||
}
|
||||
|
||||
public static int soundPitch() {
|
||||
return soundPitch;
|
||||
}
|
||||
|
||||
public static int soundVolume() {
|
||||
return soundVolume;
|
||||
}
|
||||
|
||||
public static Particle particleType() {
|
||||
return particleType;
|
||||
}
|
||||
|
||||
public static ParticleData<?> particleData() {
|
||||
return particleData;
|
||||
}
|
||||
|
||||
private static Sound resolveSound(String name) {
|
||||
|
|
@ -68,13 +92,13 @@ public final class OptionParticles {
|
|||
return Particle.values()[0];
|
||||
}
|
||||
|
||||
private static Option.ParticleShape resolveShape(String name) {
|
||||
private static ParticleShape resolveShape(String name) {
|
||||
if (name != null && !name.isEmpty()) {
|
||||
try {
|
||||
return Option.ParticleShape.valueOf(name.toUpperCase());
|
||||
return ParticleShape.valueOf(name.toUpperCase());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
return Option.ParticleShape.BOX;
|
||||
return ParticleShape.BOX;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,27 +10,61 @@ public final class OptionSQL {
|
|||
private OptionSQL() {
|
||||
}
|
||||
|
||||
public static boolean SQL;
|
||||
public static String SQL_URL;
|
||||
public static int SQL_PORT;
|
||||
public static String SQL_DB;
|
||||
public static String SQL_USERNAME;
|
||||
public static String SQL_PASSWORD;
|
||||
public static String SQL_PREFIX;
|
||||
private static boolean sql;
|
||||
private static String url;
|
||||
private static int port;
|
||||
private static String database;
|
||||
private static String username;
|
||||
private static String password;
|
||||
private static String prefix;
|
||||
|
||||
public static void init() {
|
||||
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");
|
||||
sql = Config.CONFIG.getBoolean("sql.enabled");
|
||||
port = Config.CONFIG.getInt("sql.port");
|
||||
database = Config.CONFIG.getString("sql.database");
|
||||
url = Config.CONFIG.getString("sql.url");
|
||||
username = Config.CONFIG.getString("sql.username");
|
||||
|
||||
// Environment variable override for password
|
||||
final String envPassword = System.getenv("LOPARKOUR_SQL_PASSWORD");
|
||||
SQL_PASSWORD = (envPassword != null && !envPassword.isEmpty())
|
||||
password = (envPassword != null && !envPassword.isEmpty())
|
||||
? envPassword
|
||||
: Config.CONFIG.getString("sql.password");
|
||||
|
||||
SQL_PREFIX = Config.CONFIG.getString("sql.prefix");
|
||||
prefix = Config.CONFIG.getString("sql.prefix");
|
||||
}
|
||||
|
||||
/** Whether SQL storage is active; turned off when the connection cannot be used. */
|
||||
public static boolean enabled() {
|
||||
return sql;
|
||||
}
|
||||
|
||||
/** Turns SQL off for this session (local storage takes over). */
|
||||
public static void disable() {
|
||||
sql = false;
|
||||
}
|
||||
|
||||
public static String url() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public static int port() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public static String database() {
|
||||
return database;
|
||||
}
|
||||
|
||||
public static String username() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public static String password() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public static String prefix() {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package dev.loki.loparkour.duels;
|
|||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.OptionGeneral;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.mode.base.Mode;
|
||||
import dev.loki.loparkour.mode.base.Modes;
|
||||
|
|
@ -78,7 +78,7 @@ public class DuelsGenerator extends ParkourGenerator {
|
|||
gen.owningGenerator = this;
|
||||
gen.setOwnedPlayer(pp);
|
||||
gen.setPlayerIndex(playerGenerators.size());
|
||||
gen.state.heading = Option.HEADING.getDirection();
|
||||
gen.state.heading = OptionGeneral.heading().getDirection();
|
||||
final Location spawnBase = session.getSpawnLocation();
|
||||
if (spawnBase == null || spawnBase.getWorld() == null) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package dev.loki.loparkour.generator.core.model;
|
|||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.OptionGeneral;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import dev.loki.loparkour.util.item.Materials;
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ public final class Island {
|
|||
final int cy = location.getBlockY();
|
||||
final int cz = location.getBlockZ();
|
||||
|
||||
final BlockFace face = Option.HEADING;
|
||||
final BlockFace face = OptionGeneral.heading();
|
||||
final int hx = face.getModX();
|
||||
final int hz = face.getModZ();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package dev.loki.loparkour.generator.jump.calculation;
|
||||
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.section.OptionGeneration;
|
||||
import dev.loki.loparkour.util.misc.MaterialUtil;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.util.Vector;
|
||||
|
|
@ -24,10 +24,10 @@ public class JumpValidator {
|
|||
|
||||
public JumpValidator() {
|
||||
this(
|
||||
Option.JUMP_VALIDATION_ENABLED ? Option.MAX_JUMP_DISTANCE : MAX_JUMP_DISTANCE,
|
||||
Option.JUMP_VALIDATION_ENABLED ? Option.MAX_HORIZONTAL_DISTANCE : MAX_HORIZONTAL_DISTANCE,
|
||||
Option.JUMP_VALIDATION_ENABLED ? Option.MAX_VERTICAL_UP : MAX_VERTICAL_UP,
|
||||
Option.JUMP_VALIDATION_ENABLED ? Option.MAX_VERTICAL_DOWN : MAX_VERTICAL_DOWN
|
||||
OptionGeneration.jumpValidationEnabled() ? OptionGeneration.maxJumpDistance() : MAX_JUMP_DISTANCE,
|
||||
OptionGeneration.jumpValidationEnabled() ? OptionGeneration.maxHorizontalDistance() : MAX_HORIZONTAL_DISTANCE,
|
||||
OptionGeneration.jumpValidationEnabled() ? OptionGeneration.maxVerticalUp() : MAX_VERTICAL_UP,
|
||||
OptionGeneration.jumpValidationEnabled() ? OptionGeneration.maxVerticalDown() : MAX_VERTICAL_DOWN
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package dev.loki.loparkour.generator.lifecycle.loop;
|
||||
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.section.OptionGeneration;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.generator.lifecycle.player.GeneratorCleanup;
|
||||
import org.bukkit.block.Block;
|
||||
|
|
@ -50,7 +50,7 @@ public class LifecycleTickManager {
|
|||
// Check if players fell below threshold
|
||||
checkPlayerFall();
|
||||
|
||||
if (tickCounter % Option.CLEANUP_INTERVAL == 0) {
|
||||
if (tickCounter % OptionGeneration.cleanupInterval() == 0) {
|
||||
cleanup.cleanupDistantBlocks();
|
||||
}
|
||||
if (tickCounter % TIME_UI_INTERVAL == 0) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package dev.loki.loparkour.generator.lifecycle.player;
|
||||
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.OptionGeneral;
|
||||
import dev.loki.loparkour.config.options.section.OptionGeneration;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
import org.bukkit.Location;
|
||||
|
|
@ -41,7 +42,7 @@ public class GeneratorCleanup {
|
|||
return;
|
||||
}
|
||||
|
||||
final int keepBehind = Math.max(0, Option.TRAIL_KEEP_BEHIND);
|
||||
final int keepBehind = Math.max(0, OptionGeneration.trailKeepBehind());
|
||||
final int removeCount = minStandingIndex - keepBehind;
|
||||
if (removeCount <= 0) {
|
||||
return;
|
||||
|
|
@ -164,7 +165,7 @@ public class GeneratorCleanup {
|
|||
|
||||
private static void applyHeading(@NotNull Location blockSpawn) {
|
||||
// Move block forward based on heading (6 blocks forward, same height)
|
||||
switch (Option.HEADING) {
|
||||
switch (OptionGeneral.heading()) {
|
||||
case NORTH -> blockSpawn.add(0, 0, -FIRST_BLOCK_OFFSET);
|
||||
case SOUTH -> blockSpawn.add(0, 0, FIRST_BLOCK_OFFSET);
|
||||
case WEST -> blockSpawn.add(-FIRST_BLOCK_OFFSET, 0, 0);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package dev.loki.loparkour.listener.gameplay;
|
|||
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.OptionGeneral;
|
||||
import dev.loki.loparkour.menu.core.Menus;
|
||||
import dev.loki.loparkour.menu.core.screen.ParkourOption;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
|
|
@ -34,7 +34,7 @@ public class ParkourRestrictionListener implements Listener {
|
|||
|
||||
@EventHandler
|
||||
public void onChat(AsyncPlayerChatEvent event) {
|
||||
if (!Option.OPTIONS_ENABLED.get(ParkourOption.CHAT)) {
|
||||
if (!OptionGeneral.optionsEnabled().getOrDefault(ParkourOption.CHAT, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package dev.loki.loparkour.menu.core;
|
||||
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.OptionGeneral;
|
||||
import dev.loki.loparkour.util.text.ColorUtil;
|
||||
import dev.loki.loparkour.menu.gui.inventory.InventoryGui;
|
||||
import org.bukkit.Material;
|
||||
|
|
@ -26,7 +26,7 @@ public abstract class LPMenu {
|
|||
|
||||
protected String locale(@NotNull Player player) {
|
||||
final var user = dev.loki.loparkour.player.core.ParkourUser.getUser(player);
|
||||
final String loc = user != null ? user.locale : Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
|
||||
final String loc = user != null ? user.locale : OptionGeneral.optionsDefaults().get(ParkourOption.LANG);
|
||||
return (loc != null && !loc.isBlank()) ? loc : "en";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package dev.loki.loparkour.menu.core.screen;
|
||||
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.OptionGeneral;
|
||||
import org.bukkit.permissions.Permissible;
|
||||
|
||||
/**
|
||||
|
|
@ -69,7 +69,7 @@ public enum ParkourOption {
|
|||
* @return True if the player is allowed to view/perform this option, false if not.
|
||||
*/
|
||||
public boolean mayPerform(Permissible permissible) {
|
||||
final boolean value = Option.OPTIONS_ENABLED.getOrDefault(this, true);
|
||||
final boolean value = OptionGeneral.optionsEnabled().getOrDefault(this, true);
|
||||
|
||||
if (value) {
|
||||
if (Config.CONFIG.getBoolean("permissions.enabled")) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package dev.loki.loparkour.player.core;
|
||||
|
||||
import dev.loki.loparkour.config.options.OptionGeneral;
|
||||
import dev.loki.loparkour.player.service.UserRegistry;
|
||||
import dev.loki.loparkour.player.service.ui.ScoreboardManager;
|
||||
|
||||
import dev.loki.loparkour.config.locale.Locales;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.generator.core.coordinator.ParkourGenerator;
|
||||
import dev.loki.loparkour.hook.floodgate.FloodgateHook;
|
||||
import dev.loki.loparkour.menu.core.screen.ParkourOption;
|
||||
|
|
@ -62,7 +62,7 @@ public abstract class ParkourUser {
|
|||
|
||||
// ─── Fields ───────────────────────────────────────────────────────────────
|
||||
|
||||
@NotNull public String locale = Option.OPTIONS_DEFAULTS.get(ParkourOption.LANG);
|
||||
@NotNull public String locale = OptionGeneral.optionsDefaults().get(ParkourOption.LANG);
|
||||
@Nullable public Scoreboard board;
|
||||
@NotNull public PreviousData previousData;
|
||||
public Session.ChatType chatType = Session.ChatType.PUBLIC;
|
||||
|
|
@ -80,7 +80,7 @@ public abstract class ParkourUser {
|
|||
this.joined = Instant.now();
|
||||
this.previousData = previousData == null ? new PreviousData(player) : previousData;
|
||||
|
||||
if (Boolean.parseBoolean(Option.OPTIONS_DEFAULTS.get(ParkourOption.SCOREBOARD))) {
|
||||
if (Boolean.parseBoolean(OptionGeneral.optionsDefaults().get(ParkourOption.SCOREBOARD))) {
|
||||
this.board = Bukkit.getScoreboardManager().getNewScoreboard();
|
||||
this.scoreboardManager = new ScoreboardManager(player, player.getUniqueId(), board, locale);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package dev.loki.loparkour.player.data;
|
|||
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.core.Config;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.OptionGeneral;
|
||||
import dev.loki.loparkour.reward.core.Reward;
|
||||
import io.papermc.lib.PaperLib;
|
||||
import org.bukkit.GameMode;
|
||||
|
|
@ -56,7 +56,7 @@ public class PreviousData {
|
|||
}
|
||||
|
||||
public void apply(Player player, boolean urgent) {
|
||||
final var to = Config.CONFIG.getBoolean("bungeecord.go-back-enabled") ? Option.GO_BACK_LOC : location;
|
||||
final var to = Config.CONFIG.getBoolean("bungeecord.go-back-enabled") ? OptionGeneral.goBackLocation() : location;
|
||||
|
||||
if (!urgent) {
|
||||
PaperLib.teleportAsync(player, to).thenRun(() -> apply(player));
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package dev.loki.loparkour.player.service;
|
||||
|
||||
import dev.loki.loparkour.config.options.OptionGeneral;
|
||||
import dev.loki.loparkour.player.core.ParkourPlayer;
|
||||
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.menu.core.screen.ParkourOption;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -45,7 +45,7 @@ public final class PlayerSettingsManager {
|
|||
// Sanitize legacy Boolean values from old config parsing
|
||||
String sanitized = v;
|
||||
if ("true".equals(v) || "false".equals(v)) {
|
||||
sanitized = Option.OPTIONS_DEFAULTS.getOrDefault(ParkourOption.LANG, "en");
|
||||
sanitized = OptionGeneral.optionsDefaults().getOrDefault(ParkourOption.LANG, "en");
|
||||
}
|
||||
p.locale = sanitized;
|
||||
}));
|
||||
|
|
@ -68,8 +68,8 @@ public final class PlayerSettingsManager {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (value == null || !Option.OPTIONS_ENABLED.getOrDefault(container.option, true)) {
|
||||
final String defaultValue = Option.OPTIONS_DEFAULTS.getOrDefault(container.option, "");
|
||||
if (value == null || !OptionGeneral.optionsEnabled().getOrDefault(container.option, true)) {
|
||||
final String defaultValue = OptionGeneral.optionsDefaults().getOrDefault(container.option, "");
|
||||
container.consumer.accept(player, defaultValue);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -83,15 +83,15 @@ public final class PlayerSettingsManager {
|
|||
}
|
||||
|
||||
private static int defaultBlockLead() {
|
||||
return parseIntSafe(Option.OPTIONS_DEFAULTS.getOrDefault(ParkourOption.LEADS, "1"), 1);
|
||||
return parseIntSafe(OptionGeneral.optionsDefaults().getOrDefault(ParkourOption.LEADS, "1"), 1);
|
||||
}
|
||||
|
||||
private static int defaultSelectedTime() {
|
||||
return parseIntSafe(Option.OPTIONS_DEFAULTS.getOrDefault(ParkourOption.TIME, "6000"), DEFAULT_SELECTED_TIME);
|
||||
return parseIntSafe(OptionGeneral.optionsDefaults().getOrDefault(ParkourOption.TIME, "6000"), DEFAULT_SELECTED_TIME);
|
||||
}
|
||||
|
||||
private static double defaultSchematicDifficulty() {
|
||||
return parseDoubleSafe(Option.OPTIONS_DEFAULTS.getOrDefault(ParkourOption.SCHEMATICS, "0.6"), DEFAULT_SCHEMATIC_DIFFICULTY);
|
||||
return parseDoubleSafe(OptionGeneral.optionsDefaults().getOrDefault(ParkourOption.SCHEMATICS, "0.6"), DEFAULT_SCHEMATIC_DIFFICULTY);
|
||||
}
|
||||
|
||||
private static int parseIntSafe(String v, int fallback) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package dev.loki.loparkour.storage;
|
||||
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.section.OptionSQL;
|
||||
import dev.loki.loparkour.leaderboard.model.Score;
|
||||
import dev.loki.loparkour.storage.disk.StorageDisk;
|
||||
import dev.loki.loparkour.storage.sql.StorageSQL;
|
||||
|
|
@ -22,7 +22,7 @@ public final class Storage {
|
|||
}
|
||||
|
||||
public static void init(String mode) {
|
||||
if (Option.SQL) {
|
||||
if (OptionSQL.enabled()) {
|
||||
StorageSQL.init(mode);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ public final class Storage {
|
|||
* If SQL is disabled (disk), calls the callback immediately.
|
||||
*/
|
||||
public static void runWhenReady(Runnable callback) {
|
||||
if (Option.SQL) {
|
||||
if (OptionSQL.enabled()) {
|
||||
StorageSQL.runWhenConnected(callback);
|
||||
} else {
|
||||
callback.run();
|
||||
|
|
@ -40,7 +40,7 @@ public final class Storage {
|
|||
}
|
||||
|
||||
public static void close() {
|
||||
if (Option.SQL) {
|
||||
if (OptionSQL.enabled()) {
|
||||
StorageSQL.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ public final class Storage {
|
|||
* @return Map with all scores, unsorted.
|
||||
*/
|
||||
public static @NotNull Map<UUID, Score> readScores(@NotNull String mode) {
|
||||
if (Option.SQL) {
|
||||
if (OptionSQL.enabled()) {
|
||||
return StorageSQL.readScores(mode);
|
||||
} else {
|
||||
return StorageDisk.readScores(mode);
|
||||
|
|
@ -66,7 +66,7 @@ public final class Storage {
|
|||
* @param scores The score map.
|
||||
*/
|
||||
public static void writeScores(@NotNull String mode, @NotNull Map<UUID, Score> scores) {
|
||||
if (Option.SQL) {
|
||||
if (OptionSQL.enabled()) {
|
||||
StorageSQL.writeScores(mode, scores);
|
||||
} else {
|
||||
StorageDisk.writeScores(mode, scores);
|
||||
|
|
@ -79,7 +79,7 @@ public final class Storage {
|
|||
* @param player The player.
|
||||
*/
|
||||
public static void readPlayer(@NotNull ParkourPlayer player) {
|
||||
if (Option.SQL) {
|
||||
if (OptionSQL.enabled()) {
|
||||
StorageSQL.readPlayer(player);
|
||||
} else {
|
||||
StorageDisk.readPlayer(player);
|
||||
|
|
@ -92,7 +92,7 @@ public final class Storage {
|
|||
* @param player The player.
|
||||
*/
|
||||
public static void writePlayer(@NotNull ParkourPlayer player) {
|
||||
if (Option.SQL) {
|
||||
if (OptionSQL.enabled()) {
|
||||
StorageSQL.writePlayer(player);
|
||||
} else {
|
||||
StorageDisk.writePlayer(player);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ package dev.loki.loparkour.storage.sql;
|
|||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import dev.loki.loparkour.LoParkour;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.section.OptionSQL;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
|
@ -42,8 +42,8 @@ public class SQLConnectionManager {
|
|||
|
||||
final HikariConfig config = new HikariConfig();
|
||||
config.setJdbcUrl(buildConnectionUrl());
|
||||
config.setUsername(Option.SQL_USERNAME);
|
||||
config.setPassword(Option.SQL_PASSWORD);
|
||||
config.setUsername(OptionSQL.username());
|
||||
config.setPassword(OptionSQL.password());
|
||||
config.setMaximumPoolSize(MAX_POOL_SIZE);
|
||||
config.setMinimumIdle(2);
|
||||
config.setConnectionTimeout(CONNECTION_TIMEOUT_MILLIS);
|
||||
|
|
@ -87,7 +87,7 @@ public class SQLConnectionManager {
|
|||
public void validateConnection() {
|
||||
if (!isConnected()) {
|
||||
LoParkour.getPlugin().getLogger().warning("MySQL pool not available");
|
||||
Option.SQL = false;
|
||||
OptionSQL.disable();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,7 +95,7 @@ public class SQLConnectionManager {
|
|||
return ("jdbc:mysql://%s:%d/%s?allowPublicKeyRetrieval=true"
|
||||
+ "&useSSL=false&useUnicode=true&characterEncoding=utf-8"
|
||||
+ "&autoReconnect=true&maxReconnects=2&connectTimeout=5000&socketTimeout=5000")
|
||||
.formatted(Option.SQL_URL, Option.SQL_PORT, Option.SQL_DB);
|
||||
.formatted(OptionSQL.url(), OptionSQL.port(), OptionSQL.database());
|
||||
}
|
||||
|
||||
private void executeCallbacks() {
|
||||
|
|
@ -113,7 +113,7 @@ public class SQLConnectionManager {
|
|||
LoParkour.getPlugin().getLogger().severe(
|
||||
"Could not connect to MySQL - check your SQL settings - " + ex.getMessage());
|
||||
LoParkour.getPlugin().getLogger().severe("Disabling SQL storage, using local storage instead");
|
||||
Option.SQL = false;
|
||||
OptionSQL.disable();
|
||||
if (dataSource != null && !dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package dev.loki.loparkour.storage.sql.schema;
|
||||
|
||||
import dev.loki.loparkour.config.options.section.OptionSQL;
|
||||
import dev.loki.loparkour.storage.sql.query.SQLQueryExecutor;
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
|
||||
/**
|
||||
* Manages database schema migrations.
|
||||
|
|
@ -23,8 +23,8 @@ public class SQLMigrationManager {
|
|||
}
|
||||
|
||||
private void createDatabase() {
|
||||
queryExecutor.executeStaticUpdate("CREATE DATABASE IF NOT EXISTS `%s`;".formatted(Option.SQL_DB));
|
||||
queryExecutor.executeStaticUpdate("USE `%s`;".formatted(Option.SQL_DB));
|
||||
queryExecutor.executeStaticUpdate("CREATE DATABASE IF NOT EXISTS `%s`;".formatted(OptionSQL.database()));
|
||||
queryExecutor.executeStaticUpdate("USE `%s`;".formatted(OptionSQL.database()));
|
||||
}
|
||||
|
||||
private void createOptionsTable() {
|
||||
|
|
@ -33,11 +33,11 @@ public class SQLMigrationManager {
|
|||
"(`uuid` CHAR(36) NOT NULL, `time` VARCHAR(8), `style` VARCHAR(32), `blockLead` INT, " +
|
||||
"`useParticles` BOOLEAN, `useDifficulty` BOOLEAN, `useSpecial` BOOLEAN, " +
|
||||
"`showFallMsg` BOOLEAN, `showScoreboard` BOOLEAN, PRIMARY KEY (`uuid`)) " +
|
||||
"ENGINE = InnoDB CHARSET = utf8;").formatted(Option.SQL_PREFIX));
|
||||
"ENGINE = InnoDB CHARSET = utf8;").formatted(OptionSQL.prefix()));
|
||||
}
|
||||
|
||||
private void runMigrations() {
|
||||
final String prefix = Option.SQL_PREFIX;
|
||||
final String prefix = OptionSQL.prefix();
|
||||
|
||||
queryExecutor.executeStaticUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `time`;".formatted(prefix));
|
||||
queryExecutor.executeStaticUpdateSuppressed("ALTER TABLE `%soptions` ADD `selectedTime` INT NOT NULL;".formatted(prefix));
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package dev.loki.loparkour.world.core;
|
||||
|
||||
import dev.loki.loparkour.config.options.Option;
|
||||
import dev.loki.loparkour.config.options.OptionGeneral;
|
||||
import dev.loki.loparkour.config.options.section.OptionGeneration;
|
||||
import dev.loki.loparkour.session.core.Session;
|
||||
import org.bukkit.Location;
|
||||
|
||||
|
|
@ -61,9 +62,9 @@ public final class Divider {
|
|||
final int[] xz = spiralAt(SECTIONS.get(session));
|
||||
|
||||
return new Location(World.getWorld(),
|
||||
xz[0] * Option.BORDER_SIZE,
|
||||
(Option.MAX_Y + Option.MIN_Y) / 2.0,
|
||||
xz[1] * Option.BORDER_SIZE);
|
||||
xz[0] * OptionGeneral.borderSize(),
|
||||
(OptionGeneration.maxY() + OptionGeneration.minY()) / 2.0,
|
||||
xz[1] * OptionGeneral.borderSize());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -74,11 +75,11 @@ public final class Divider {
|
|||
final Location center = toLocation(session);
|
||||
|
||||
// get the min and max locations
|
||||
final Location max = center.clone().add(Option.BORDER_SIZE / 2, 0, Option.BORDER_SIZE / 2);
|
||||
final Location min = center.clone().subtract(Option.BORDER_SIZE / 2, 0, Option.BORDER_SIZE / 2);
|
||||
final Location max = center.clone().add(OptionGeneral.borderSize() / 2, 0, OptionGeneral.borderSize() / 2);
|
||||
final Location min = center.clone().subtract(OptionGeneral.borderSize() / 2, 0, OptionGeneral.borderSize() / 2);
|
||||
|
||||
max.setY(Option.MAX_Y);
|
||||
min.setY(Option.MIN_Y);
|
||||
max.setY(OptionGeneration.maxY());
|
||||
min.setY(OptionGeneration.minY());
|
||||
|
||||
return new Location[]{min, max};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue