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:
loki5512344 2026-10-06 15:54:31 +02:00
parent 4263df50a2
commit c120309663
Signed by: boba
GPG key ID: 253067914055423B
27 changed files with 386 additions and 359 deletions

47
TODO.md
View file

@ -112,10 +112,10 @@
- **60. `SQLDataMapper` UUID.fromString кидает unchecked IllegalArgumentException** (SQLDataMapper.java:65,31) мимо catch(SQLException) → битые строки роняют чтение. → Ловить и логировать/скипать строку.
- **61. `SQLQueryExecutor.prepareStatement`** (без callers) никогда не закрывает connection → утечка пула при будущем использовании. → try-with-resources / borrow-tracking.
- **62. `SQLMigrationManager` мёртв; `migration_adaptive.sql` ссылается на несуществующую `loparkour_scores` и нигде не исполняется**; та же DDL в 3 копиях; префикс `SQL_PREFIX` применён непоследовательно. → Единый мигратор с версионированием; починить/удалить SQL-файл.
- **62. `SQLMigrationManager` мёртв; `migration_adaptive.sql` ссылается на несуществующую `loparkour_scores` и нигде не исполняется**; та же DDL в 3 копиях; префикс `OptionSQL.prefix()` применён непоследовательно. → Единый мигратор с версионированием; починить/удалить SQL-файл.
- **63. `writeScores` гоняет всю таблицу туда-сюда** без дельт (StorageSQL.java:105-128). → Инкрементальные upsert'ы только изменённых записей.
- **64. `Score.fromString` — CSV по запятой: ник с `,` ломает все поля** (Score.java:19-31). → Экранирование или JSON.
- **65. `validateConnection` мутирует `Option.SQL=false` при транзиентном сбое** (SQLConnectionManager.java:81-86) — SQL отключается навсегда. → Отдельный флаг/ретрай.
- **65. `validateConnection` мутирует `OptionSQL.disable()` при транзиентном сбое** (SQLConnectionManager.java:87-92) — SQL отключается навсегда. → Отдельный флаг/ретрай.
### Adaptive
@ -126,11 +126,14 @@
### Config / reload
- **70. `/lp reload` не атомарен**: дублирует `Registry.STYLES` (никогда не чистится), оставляет старые `Rewards` при `enabled:false`, `BORDER_SIZE` устаревший, миры/генераторы не обновляются (ConfigUpdater, OptionStyles.java:23-43, Rewards.java:41-52, OptionGeneral.java:38-40). → Полный reset registry + пересоздание зависимых подсистем.
- **70. `/lp reload` не атомарен**: дублирует `Registry.STYLES` (никогда не чистится), оставляет старые `Rewards` при `enabled:false`, `OptionGeneral.borderSize()` устаревший (перечитывается только при `firstLoad`), миры/генераторы не обновляются (ConfigUpdater, OptionStyles.java:23-43, Rewards.java:41-52, OptionGeneral.java:44-50). → Полный reset registry + пересоздание зависимых подсистем.
- **71. `LocaleLoader` при ошибке подменяет кэш на emptyMap** → все сообщения пустые (LocaleCache.java:22-25). → Сохранять старый кэш при сбое.
- **72. Нет fallback на en** для пропущенных ключей/неизвестных локалей (Locales.java:69-71). → Fallback-цепочка locale → en.
- **73. `Rewards.init()` при `enabled:false` не чистит статические мапы** → старые награды продолжают выдаваться после reload. → Очистка.
- **74. `ConfigAccessor` типо-опечатка тихо даёт false/0** (ConfigAccessor.java:45-48). → Строгая валидация ключей при старте.
- **164. Секция `generation.*` в generation.yml игнорируется.** Живые веса берутся из `advanced.distance|height|type|special` (`GeneratorProfileManager`, `GenerationProfileLoader`), а `generation.type|normal|special` читал только мёртвый `OptionGeneration.initGenerationProbabilities()` — удалён на шаге инкапсуляции `config/options`. В shipped `generation.yml` обе секции дублируют друг друга (значения разные: 85/10/5 против 80/15/5) → пользователь правит то, что не работает. → Либо удалить `generation.type|normal|special` из generation.yml, либо сделать их источником для профилей; `generation.settings.min-y/max-y` остаются живыми. `ConfigLoader.validateConfigs` уже проверяет живые пути `advanced.*`.
- **165. Мёртвый конфиг частиц/звуков.** `particles.sound-type|sound-pitch|sound-volume|particle-type|particle-shape` читаются в `OptionParticles`, все 6 геттеров (`shape()`, `soundType()`, `particleData()`…) не вызываются нигде → настройки не влияют на игру (используются per-player `ParkourPlayer.particles/sound` из меню). → Подключить в рендере частиц/звуков (`util/particle/`, `HookSound`) или убрать секцию из config.yml.
- **166. Ещё два читаемых, но не потребляемых ключа.** `options.leads.amount` → `OptionGeneral.possibleLeads()` (валидация 1–128 и лог работают, список не читает никто — lead берётся из `default-values.leads.default`), `memory.block-cleanup-distance` → `OptionGeneration.blockCleanupDistance()` (дистанция чистки захардкожена в `GeneratorCleanup`). → Подключить или удалить.
### Session
@ -169,7 +172,7 @@
- **95. `SchematicIdGenerator` хеширует только byte-координаты → коллизии id** (SchematicIdGenerator.java:23-28).
- **96. `parseDifficulty` принимает NaN/Infinity** (SchematicCreator.java:64-70) → схема навсегда невыбираема.
- **97. `stem()` регистрирует файл `.nbt` под пустым id**; `.NBT` игнорируется, `.schematic` — case-insensitive (SchematicManager.java:147-150,88).
- **98. `Locales.getPlayerLocale`/`ParkourUser.locale` deref `OPTIONS_DEFAULTS.get(LANG)` без null-check** (Locales.java:170-184, ParkourUser.java:64).
- **98. `Locales.getPlayerLocale`/`ParkourUser.locale` deref `OptionGeneral.optionsDefaults().get(LANG)` без null-check** (Locales.java:170-184, ParkourUser.java:64).
- **99. Магические числа**: fall-threshold -30 (LifecycleTickManager.java:72), render 24.0/0.1/%4 (InvisibleBarrierMode.java:67-68,98), 10/30 (GravityShiftMode.java:82,108), 100 тиков (RaceMode.java:147), hotbar delay 5 (ParkourPlayer.java:192), `TICKS_PER_SECOND=20` переопределён (SpeedrunMode.java:82). → В конфиг/константы.
- **100. Deprecated API**: `Material.STONE_SLAB` (MaterialUtil.java:20), `registerNewObjective(String,String,String)` (ScoreboardManager.java:75), `getOfflinePlayer(name)` (AdminCommandHandler.java:186-199), legacy ACTION_BAR (ParkourSpectator.java:73, RaceMode.java:103).
- **101. `ParkourSpectator.java:96` — бессмысленный double-set spectator target** (no-op).
@ -427,14 +430,38 @@ Checkstyle отработал `0 errors` (error-уровневые legacy-нар
> Изменено 45 файлов (TODO.md, 2 конфига линтеров, 46 исходников).
>
> **Осталось (порядок):**
> 1. Тест-гейт «**в папке ≤ 4 файлов**» (JUnit, без новых зависимостей) + разбивка 6 переполненных пакетов:
> `elytra/generator` 7, `elytra/mode` 6, `command/schematic` 6, `player/core` 5, `generator/jump/placement` 5, `generator/jump/calculation` 5 —
> 10 файлов переносятся в подпапки (механически: package + импорты).
> 2. Пакет за пакетом обнулять бэклог, начиная с `config/options` (91 нарушение в 2 файлах: `Option` — 45 public-полей,
> `OptionGeneration` — 25; это и есть п.77/107 про public mutable state) → после обнуления перевести правило в error.
> 1. ✅ Тест-гейт «**в папке ≤ 4 файлов**» (JUnit, без новых зависимостей) + разбивка 6 переполненных пакетов —
> сделано в `4263df5`, `FOLDER_DEBT = Set.of()`, ни одна папка `src/main/java` не содержит >4 файлов.
> 2. ✅ Бэклог `config/options` обнулён (см. ПРОГРЕСС-5 ниже) → **`VisibilityModifier` ещё НЕ в error**: в пакете остались
> только 2 `IllegalCatch` (общий бэклог п.3), а правило висит на 75 нарушениях в других пакетах.
> 3. `IllegalCatch` 23: 3 сайта намеренные (команда/конфиг с откатом/скетчи — boundary-catch), остальные 20 — выловить конкретные типы;
> **нужно решение владельца** по трём.
> 4. Дальше по плану (a): PMD-категории целиком, CPD, SpotBugs, ArchUnit-слои, JaCoCo, Spotless, quality-job в CI.
> 4. Дальше по плану (a): PMD-категории целиком, CPD, SpotBugs, ArchUnit-слои, JaCoCo, Spotless, quality-job в CI — ✅ сделано в `b771cc1`.
>
> ## ✅ ПРОГРЕСС-5 (Задача 2, шаг (c): `config/options` инкапсулирован, 2026-10-06)
> Был анти-паттерн: `Option` — фасад-зеркало на **45 `public static` полей**, которые `syncFields()` копировал
> из 4 секций (`OptionSQL`, `OptionParticles`, `OptionGeneration`, `OptionGeneral`), и в самих секциях поля тоже
> были `public static` (мутабельное состояние чужих пакетов = п.77/107 в миниатюре).
> - **`Option` сокращён до `init(boolean)`** — единственная точка перезагрузки конфигов (её зовёт `ConfigLoader`);
> зеркало, `syncFields()` и мёртвый `@Deprecated initStyles` (0 вызовов) удалены. Класс больше не дублирует секции.
> - **Секции: `private static` поля + fluent-аксессоры** (`OptionSQL.enabled()`, `OptionGeneration.cleanupInterval()`,
> `OptionGeneral.goBackLocation()`…), без префикса `get` — как у соседних статик-холдеров (`Materials.parse`, `Probs.random`).
> Две точки записи `Option.SQL = false` → `OptionSQL.disable()`.
> - **Защита от протечки состояния:** `possibleLeads()/optionsEnabled()/optionsDefaults()` отдают unmodifiable-представления
> (null сохраняется как раньше), `goBackLocation()` — копию (`Location` мутируется при reload через `setPitch/setYaw`).
> - `ParticleShape` вынесен из вложенного enum'а в `config/options/ParticleShape.java` (в `section/` леж бы 5-й файл — гейт «≤4»).
> - **Удалены мёртвые данные:** 17 полей вероятностей генерации + `initGenerationProbabilities()` — не читались нигде,
> живые веса берутся из `advanced.*` (`GeneratorProfileManager`/`GenerationProfileLoader`). Границы `min-y/max-y`
> с проверкой `minY >= maxY` перенесены 1-в-1 в `initBounds()`. Найденное оформлено пунктами **164–166**.
> - **63 обращения `Option.<ПОЛЕ>`** (не 127 — счёт предыдущей сессии был завышен из-за подстроки в `ParkourOption.*`)
> переписаны в **19 файлах**; `Option`-импорт остался только в `ConfigLoader`.
> - Попутно (предложения `CodeReview`): `ConfigLoader.validateConfigs()` проверяет **живые** пути
> `advanced.distance.chances|height.chances|height.min|max|type` (раньше проверялась мёртвая секция `generation.normal.height.*`);
> в `ParkourRestrictionListener.onChat` unboxing `Boolean` → `getOrDefault(CHAT, true)` как в `ParkourOption.mayPerform`.
> - **Счётчики:** checkstyle **0 error / 329 → 240 warning**; `VisibilityModifier` **163 → 75**, `MethodLength` 5 → 4.
> В `config/options` — **0 нарушений VisibilityModifier** (было 88), остаются 2 `IllegalCatch`.
> `./gradlew check` зелёный: PMD 0, тесты 94/0 fail, JaCoCo/ArchUnit/Spotless ок, NullAway 48 (как в baseline).
> Пороги правил не снижались, `suppressions.xml` пустая. Реальных прогонов на сервере после правок не было (только `check`).
---

View file

@ -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();

View file

@ -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", () ->

View file

@ -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";
}

View file

@ -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));
}
}
}

View file

@ -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
}

View file

@ -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 }
}

View file

@ -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;
}
};

View file

@ -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
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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();

View file

@ -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
);
}

View file

@ -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) {

View file

@ -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);

View file

@ -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;
}

View file

@ -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";
}

View file

@ -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")) {

View file

@ -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);

View file

@ -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));

View file

@ -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) {

View file

@ -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);

View file

@ -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();
}

View file

@ -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));

View file

@ -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};
}