Fix locale bug and parkour spawn position

- Fix locale 'true' bug with sanitization at all levels:
  * ConfigAccessor.getString() - force String.valueOf()
  * Option.java - sanitize LANG defaults from config
  * PlayerSettingsManager - sanitize on settings load
  * SQLDataMapper - sanitize on SQL load
  * StorageDisk - sanitize on JSON load
  * SQLMigrationManager - SQL migration to fix DB

- Fix parkour first block position:
  * Island.java - spawn 6 blocks forward + 1 up from center
  * GeneratorCleanup.java - same position after fall/reset

- Add spawn-location config option at top of config.yml
This commit is contained in:
loki 2026-04-09 13:52:37 +02:00
parent b0c03cd484
commit 3546d7f9e3
10 changed files with 154 additions and 272 deletions

107
TODO.md
View file

@ -1,6 +1,6 @@
# LoParkour — TODO
> Обновлено: 2026-04-01.
> Обновлено: 2026-04-09.
## Легенда
@ -11,9 +11,93 @@
---
## Главный план (стабилизация)
## Текущие задачи (2026-04-09)
Консолидировано после обхода: меню/util, `LoParkour`, `LoParkourCommand`, join/teleport, `SessionStateManager`, `Island.build()`, Locales, LoLib 3.x.
### 🔴 P0 — Критичные баги
| # | Задача | Статус |
|---|--------|--------|
| 1 | Баг с локалью `'true'` в БД/конфиге | ✅ Исправлено (санитизация на всех уровнях) |
| 2 | Первый блок паркура генерится на платформе | ✅ Исправлено (6 блоков вперёд + 1 вверх) |
| 3 | ElytraMode полностью не работает | 🔴 **КРИТИЧНО** - отсутствуют классы |
### 🟡 P1 — Важные недоработки
| # | Задача | Статус |
|---|--------|--------|
| 4 | GravityShiftMode не реализован | 🟡 Только конфиг |
| 5 | HardcoreMode не реализован | 🟡 Только конфиг |
### 🟢 P2 — Проверка после исправлений
| # | Задача | Статус |
|---|--------|--------|
| 6 | DefaultMode после изменений Island.java | 🟢 Требует проверки |
| 7 | SpeedrunMode таймеры блоков | 🟢 Требует проверки |
| 8 | RaceMode прогресс-бар и финиш | 🟢 Требует проверки |
| 9 | CoopMode мультиплеер | 🟢 Требует проверки |
---
## Детальный план исправления режимов
### ElytraMode (4-6 часов)
**Проблема:** Отсутствуют классы, на которые ссылается `ElytraGenerator`:
- `ElytraConfig` - загрузка настроек из config.yml
- `ElytraRing` - модель кольца (центр, радиус, направление)
- `ElytraRingGenerator` - генерация колец по траектории
- `ElytraPhysics` - проверка пролёта, падения, буста
- `ElytraRenderer` - отрисовка частицами
**План:**
1. Создать `mode/elytra/ElytraConfig.java`
2. Создать `mode/elytra/ElytraRing.java`
3. Создать `mode/elytra/ElytraRingGenerator.java`
4. Создать `mode/elytra/ElytraPhysics.java`
5. Создать `mode/elytra/ElytraRenderer.java`
6. Исправить `generateFirst()` в `ElytraGenerator`
7. Добавить выдачу элитры и фейерверков
### GravityShiftMode (2-3 часа)
**План:**
1. Создать `GravityShiftMode.java`
2. Создать `GravityShiftGenerator extends ParkourGenerator`
3. Счётчик прыжков, каждые N прыжков - случайный эффект
4. Эффекты: jump-boost, speed, slowness, levitation
5. Зарегистрировать в `Modes.java`
### HardcoreMode (1-2 часа)
**План:**
1. Создать `HardcoreMode.java`
2. Создать `HardcoreGenerator extends ParkourGenerator`
3. Переопределить `fall()` - сбросить `collectedRewards`
4. Зарегистрировать в `Modes.java`
---
## Исправления от 2026-04-09
### ✅ Баг с локалью
- `ConfigAccessor.getString()` - форсирует String через `String.valueOf()`
- `Option.java:267-272` - санитизация при загрузке дефолтов из конфига
- `PlayerSettingsManager.java:37-44` - санитизация при загрузке настроек
- `SQLDataMapper.java:87-95` - санитизация при загрузке из SQL
- `StorageDisk.java:90-106` - санитизация при загрузке из JSON
- `SQLMigrationManager.java:51-53` - SQL миграция для исправления БД
### ✅ Первый блок паркура
- `Island.java:75-90` - первый блок на 6 блоков вперёд + 1 вверх от центра
- `GeneratorCleanup.java:110-127` - после падения тоже 6 вперёд + 1 вверх
### ✅ Spawn location в конфиге
- Добавлены `spawn-location` и `spawn-axes` в начало config.yml
---
## Главный план (стабилизация)
### P0 — блокер загрузки или геймплей
@ -37,17 +121,6 @@
| 6 | `LifecycleTickManager` / cleanup / time UI | ✅ |
| 7 | Scoring в воздухе | ✅ документировано (стояние на блоке) |
### Контекст (не в «критичном» списке)
- Порядок `generateFirst` / телепорт из `Island.build()` — ок при неизменённом порядке вызовов.
- `history.contains(Block)` — Bukkit сравнивает по миру и координатам.
- `InventoryType.CHEST` в GUI — при странных кейсах проверить тип инвентаря.
### Порядок работ (регрессия)
1. `./gradlew build`, старт без `LinkageError`.
2. Ручной прогон паркура: счёт, меню, scoreboard, режимы.
---
## Follow-up (сделано в коде)
@ -62,7 +135,6 @@
- **RandomStyle** / **ElytraGenerator**: защита от пустых списков.
- **InventoryData**: новые сохранения в **YAML**; legacy binary читается один раз при миграции.
---
## Чек-лист перед релизом
@ -71,7 +143,4 @@
- [ ] Старт сервера, `/parkour`, меню, один полный забег
- [ ] SQL: при проде выставить `LOPARKOUR_SQL_PASSWORD` на хосте
- [ ] Проверить миграцию старых `.dat` инвентарей при первом заходе игрока
---
- [ ] Протестировать все режимы: default, speedrun, race, coop, elytra, gravity-shift, hardcore

View file

@ -85,11 +85,17 @@ public class ConfigAccessor {
/**
* Get string value at path.
* Forces conversion to String even if YAML parsed as Boolean/Number.
*/
@NotNull
public String getString(@NotNull String path) {
warnIfMissing(path);
return config.getString(path, "");
Object value = config.get(path);
if (value == null) {
return "";
}
// Force toString() to handle Boolean/Number values
return String.valueOf(value);
}
/**

View file

@ -266,6 +266,12 @@ public class Option {
// Always read as String to avoid Boolean parsing issues
String value = Config.CONFIG.getString(defaultPath);
// Sanitize legacy Boolean values for LANG option
if (option == ParkourOption.LANG && ("true".equals(value) || "false".equals(value) || "1".equals(value) || "0".equals(value))) {
value = "en";
LoParkour.getPlugin().getLogger().warning("LANG option had invalid Boolean value, reset to 'en'");
}
// Debug logging
if (option == ParkourOption.LANG) {
LoParkour.getPlugin().getLogger().info("LANG option: path=" + defaultPath + ", value=" + value);

View file

@ -72,11 +72,12 @@ public final class Island {
}
}
// Player stand 2 blocks "behind" center along heading; course line continues +4 then first jump +4 more (old island logic)
int px = cx - 2 * hx;
int pz = cz - 2 * hz;
int qx = px + 4 * hx;
int qz = pz + 4 * hz;
// Player spawns at center, first parkour block 6 blocks forward and 1 block up
int px = cx;
int pz = cz;
int qx = cx + 6 * hx;
int qz = cz + 6 * hz;
int qy = cy + 1;
Block playerFoot = world.getBlockAt(px, cy, pz);
@ -84,10 +85,7 @@ public final class Island {
ps.setYaw((float) Config.GENERATION.getInt("advanced.island.spawn.yaw"));
ps.setPitch((float) Config.GENERATION.getInt("advanced.island.spawn.pitch"));
Location parkourStart = world.getBlockAt(qx, cy, qz).getLocation().add(
h.getX() * 4,
0,
h.getZ() * 4);
Location parkourStart = world.getBlockAt(qx, qy, qz).getLocation();
session.generator.generateFirst(ps, parkourStart);
session.generator.startTick();

View file

@ -111,15 +111,15 @@ public class GeneratorCleanup {
// Regenerate first block
Location spawn = generator.state.playerSpawn;
if (spawn != null) {
// Place first block ahead of spawn based on heading direction
Location blockSpawn = spawn.clone().add(0, -1, 0);
// Place first block 6 blocks forward and 1 block up from spawn
Location blockSpawn = spawn.clone();
// Move block forward based on heading
// Move block forward based on heading (6 blocks forward, 1 up)
switch (dev.loki.loparkour.config.Option.HEADING) {
case NORTH -> blockSpawn.add(0, 0, -3);
case SOUTH -> blockSpawn.add(0, 0, 3);
case WEST -> blockSpawn.add(-3, 0, 0);
case EAST -> blockSpawn.add(3, 0, 0);
case NORTH -> blockSpawn.add(0, 1, -6);
case SOUTH -> blockSpawn.add(0, 1, 6);
case WEST -> blockSpawn.add(-6, 1, 0);
case EAST -> blockSpawn.add(6, 1, 0);
}
generator.generateFirst(spawn, blockSpawn);

View file

@ -35,8 +35,13 @@ public class PlayerSettingsManager {
p.selectedTime = parseIntSafe(v, defaultSelectedTime())));
COLUMN_MAPPINGS.put("collectedRewards", new OptionContainer(null, PlayerSettingsManager::applyCollectedRewards));
COLUMN_MAPPINGS.put("locale", new OptionContainer(ParkourOption.LANG, (p, v) -> {
p._locale = v;
p.locale = v;
// 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");
}
p._locale = sanitized;
p.locale = sanitized;
}));
COLUMN_MAPPINGS.put("schematicDifficulty", new OptionContainer(ParkourOption.SCHEMATICS, (p, v) ->
p.schematicDifficulty = parseDoubleSafe(v, defaultSchematicDifficulty())));

View file

@ -85,9 +85,14 @@ public class SQLDataMapper {
* Apply player data to ParkourPlayer object.
*/
public static void applyPlayerData(@NotNull ParkourPlayer player, @NotNull PlayerData data) {
player.locale = data.locale();
// Sanitize legacy Boolean locale values
String locale = data.locale();
if (locale == null || "true".equals(locale) || "false".equals(locale) || "1".equals(locale) || "0".equals(locale)) {
locale = "en";
}
player.locale = locale;
player.style = data.styleName();
// Apply settings if available
if (data.settings() != null && !data.settings().isEmpty()) {
deserializeSettings(player, data.settings());

View file

@ -46,5 +46,9 @@ class SQLMigrationManager {
queryExecutor.executeStaticUpdateSuppressed("ALTER TABLE `%soptions` ADD `sound` BOOLEAN;".formatted(prefix));
queryExecutor.executeStaticUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useDifficulty`;".formatted(prefix));
queryExecutor.executeStaticUpdateSuppressed("ALTER TABLE `%soptions` DROP COLUMN `useStructure`;".formatted(prefix));
// Fix legacy Boolean locale values (true/false) to proper locale codes
queryExecutor.executeStaticUpdateSuppressed(
"UPDATE `%soptions` SET `locale` = 'en' WHERE `locale` IN ('true', 'false', '1', '0');".formatted(prefix));
}
}

View file

@ -97,7 +97,13 @@ class StorageDisk {
settings.put("showScoreboard", from.showScoreboard);
settings.put("selectedTime", from.selectedTime);
settings.put("collectedRewards", from.collectedRewards);
settings.put("locale", from._locale);
// Sanitize legacy Boolean locale values
String locale = from._locale;
if (locale == null || "true".equals(locale) || "false".equals(locale) || "1".equals(locale) || "0".equals(locale)) {
locale = "en";
}
settings.put("locale", locale);
settings.put("schematicDifficulty", from.schematicDifficulty);
settings.put("sound", from.sound);

View file

@ -17,107 +17,53 @@
# #
#===============================================================================#
# -= Spawn location =-
# Location where players spawn when joining parkour
# Syntax: '(x,y,z,worldName)'
spawn-location: '(0,100,0,loparkour)'
# -= Spawn axes =-
# Sets the pitch and yaw of the spawn location
# First argument is pitch, second is yaw
spawn-axes: '0,0'
# -= Debugging =-
debug: false
# -= Enable joining =-
# Whether players can join the parkour.
# When joining is disabled, the IP instance on this server will only read data from leaderboard databases.
# Set joining to false if you want to create a cross-server leaderboard on this server.
# true = players can join, false = players can't join.
joining: true
# -= Proxy options =-
# Change the proxy settings. Applies to Velocity as well.
# This also applies to servers who only want to run this plugin.
bungeecord:
# -= Proxy enabled =-
# Enabling this will make players who join the server automatically join the parkour.
# You can enable this even without BungeeCord to make this a parkour-only server.
# True = players will automatically join the parkour on join, false = players have to use the menu/parkour command.
enabled: false
# -= Proxy return server =-
# The server the players will be returned to after they leave.
return_server: 'Lobby'
# -= Go back enabled =-
# Whether players should be teleported to a specific location after leaving.
# True = players get teleported to a specific location when they leave, false = they go to their location when they joined.
go-back-enabled: false
# -= Go back location =-
# The place players will go back to when they leave
# Only applies if proxy mode is disabled and the go-back-enabled option is true.
# Syntax: '(x,y,z,worldName)'
go-back: '(0,100,0,world)'
# -= Go back axes =-
# Sets the pitch and yaw of the go-back location.
# The first argument is the pitch, second yaw.
go-back-axes: '0,0'
# -= Storage update interval =-
# The amount of seconds between every disk/database storage update.
# In this update, the in-memory scores will be written to storage.
# A lower number might reduce the chance of losing data on server crashes,
# but will increase the amount of requests to the disk/database.
# (requires a restart, default = 300)
storage-update-interval: 300
# -= MySQL =-
# The options for using MySQL as storage.
sql:
# -= MySQL enabled =-
# Using databases makes you able to sync player data across multiple servers.
# True = data will be stored in a database, false = data will be stored locally.
enabled: false
# -= MySQL settings =-
url: 'localhost'
port: 3306
username: 'efnilite'
# If env LOPARKOUR_SQL_PASSWORD is set (non-empty), it overrides this value (recommended for production).
password: 'super secret password'
database: 'ip'
# -= MySQL prefix =-
# The prefix given to any table names
# The default is ''. Example: 'ip-' -> this would make the table 'ip-players'
prefix: ''
# -= World options =-
world:
# -= World name =-
# The world in which parkour will take place
name: 'loparkour'
# -= Island schematic =-
# Name of the spawn island schematic (schematics-new folder). Falls back to generation.yml if empty.
island-schematic: island
# -= Delete world on reload =-
# Deletes the world on reload/restart of the server.
# This makes sure there are no ghost blocks left when a server crash occurs.
# Disable this if you want to have a custom map.
island-schematic: 'island'
delete-on-reload: true
# -= World fall back =-
# The world players will be teleported to if they accidentally join the Parkour world without joining parkour
fall-back: 'world'
# -= General options =-
options:
# -= Leads options =-
leads:
# -= Leads amounts =-
# The lead amounts a player can select in their options menu.
# Values also need to be above 1 and below 64.
amount:
- 1
- 2
@ -130,46 +76,15 @@ options:
- 9
- 10
# -= Time options =-
time:
# -= Time format =-
# Choose between:
# 24 - 24 hour clock. (00:00, 12:00, 23:59)
# 12 - 12 hour clock. (12:00 AM, 12:00 PM, 11:59 PM)
format: 24
# -= Score time format =-
# H = hours, m = minutes, s = seconds, S = milliseconds
score-format: "mm:ss:SSS"
# -= Inventory handling =-
# Toggles the inventory handling system.
# True = players' inventories get saved and cleared when they join the parkour and when they leave they get their items back
# False = players keep their inventories when they join the parkour
inventory-handling: true
# -= Inventory saving =-
# Logs inventories of the players that join to the inventories/ folder.
# It is highly recommended to keep this enabled. Server crashes or errors while leaving might cause
# inventories of players to be lost if this is set to false!
# Use /ip recoverinventory to recover inventories of players who joined the parkour and who lost their inventories in a crash, etc.
inventory-saving: true
# -= Alternate inventory saving command =-
# If you don't trust this plugin to save inventories, you can select another plugin to execute a command
# to save players' inventories.
# Example: /inventory save %player%
# This command will be executed by console.
alt-inventory-saving-command: ''
# -= Disables inventory blocks =-
# Disables the ability for a player to open blocks with an inventory.
disable-inventory-blocks: false
# -= Hotbar slots =-
# Configure which hotbar slots (0-8) to use for parkour items
# Items will be placed in these slots in order: play, community, settings, lobby, quit
hotbar-slots:
play: 1
community: 3
@ -178,33 +93,13 @@ options:
quit: 7
# -= Permissions options =-
# These permissions are used to determine which things the player can change.
# Find them at https://efnilite.dev/projects/ip/wiki
permissions:
# -= Permissions enabled =-
# If permission handling is enabled.
# True = players need a permission to join, view the leaderboard, etc., false = players can do and view anything
# from this plugin besides admin commands.
enabled: false
# -= Per style permissions =-
# Gives every style a permission. The permission is the name of the style.
# Syntax: ip.settings.styles.<style> - example: ip.settings.styles.red
# Useful for servers with ranks.
per-style: false
# -= Focus mode options =-
focus-mode:
# -= Focus mode enabled =-
# If focus mode is enabled players can't use commands while in parkour.
# Note: this might not work on all commands because Minecraft.
enabled: false
# -=
# A whitelist for commands which ARE allowed during focus mode (if focus mode is enabled).
# If you want to disable the default whitelisted commands you can remove every item in the list below.
whitelist:
- 'ip'
- 'parkour'
@ -217,10 +112,6 @@ focus-mode:
# -= Styles options =-
styles:
# -= Styles list =-
# The list of all available styles, which you can add to if you want.
# For changes, you have to restart or reload your server.
list:
red: [red_wool, red_stained_glass, red_concrete, red_terracotta, red_concrete_slab, red_terracotta_slab]
blue: [blue_wool, blue_stained_glass, blue_concrete, blue_terracotta, blue_concrete_slab, blue_terracotta_slab]
@ -244,37 +135,15 @@ styles:
# -= Jump validation options =-
jump-validation:
# -= Jump validation enabled =-
# Validates generated jumps using Minecraft physics.
# Ensures all jumps are physically possible.
enabled: true
# -= Maximum jump distance =-
# Maximum 3D distance for a jump (Pythagorean theorem).
max-distance: 4.5
# -= Maximum horizontal distance =-
# Maximum horizontal distance (X and Z axes only).
max-horizontal: 4.1
# -= Maximum vertical up =-
# Maximum height a player can jump up.
max-vertical-up: 1.25
# -= Maximum vertical down =-
# Maximum height a player can jump down.
max-vertical-down: 3.0
# -= Jump types options =-
jump-types:
# -= Jump types enabled =-
# Enables special jump types (neo-jumps, fence jumps, etc.).
enabled: false
# -= Jump types list =-
# Configure which jump types are enabled and their spawn chances.
types:
neo-jump:
enabled: true
@ -294,50 +163,24 @@ jump-types:
# -= Memory optimization =-
memory:
# -= Block cleanup distance =-
# Span (in history indices) for schematic trail removal and similar bulk clears.
block-cleanup-distance: 20
# -= Trail keep-behind =-
# How many course blocks to keep behind the rearmost standing player before removing older trail (index-based; not world distance).
trail-keep-behind: 10
# -= Cleanup interval =-
# How often (in ticks) to run trail cleanup behind players.
# 20 ticks = 1 second
cleanup-interval: 100
# -= Ghost mode options =-
ghost-mode:
# -= Ghost mode enabled =-
# Shows ghosts of top players running alongside you.
enabled: false
# -= Show top =-
# How many top ghosts to show (1-3).
show-top: 3
# -= Ghost transparency =-
# Transparency of ghost entities (0.0 = invisible, 1.0 = fully visible).
transparency: 0.5
# -= Game modes options =-
modes:
# -= Speedrun mode =-
speedrun:
# Block lifetime in seconds before disappearing
block-lifetime: 1.5
# Warning time in seconds before block disappears
warning-time: 0.5
# -= Gravity shift mode =-
gravity-shift:
# Interval in jumps before effect changes
interval: 10
effects:
jump-boost:
enabled: true
@ -356,97 +199,45 @@ modes:
amplifier: 1
duration: 5
# -= Hardcore mode =-
hardcore:
# Reset all rewards on fall
reset-rewards: true
# -= Elytra mode =-
elytra:
# Enable elytra mode
enabled: true
# --- Ring course settings ---
# Minimum distance (blocks) between consecutive rings
ring-distance-min: 20
# Maximum distance (blocks) between consecutive rings
ring-distance-max: 40
# Ring radius in blocks. Player must fly through the center hole.
ring-size: 5
# How many rings to generate ahead of the player
ring-lead: 8
# Maximum horizontal heading change per ring in degrees (course curvature)
max-heading-change-horizontal: 30
# Maximum vertical heading change per ring in degrees
max-heading-change-vertical: 15
# Maximum Y blocks above spawn point that the course can reach
max-height-above-spawn: 60
# How far from the course (blocks) before a fall is triggered
max-deviation: 25
# --- Firework / boost settings ---
# Firework velocity boost multiplier
boost-power: 1.5
# Cooldown between firework boosts in milliseconds
firework-cooldown-ms: 3000
# Number of fireworks given at session start
starting-fireworks: 64
# -= Race mode =-
race:
# Enable race mode
enabled: true
# Target score to win
target-score: 100
# -= Co-op mode =-
coop:
# Enable co-op mode
enabled: true
# Maximum players in co-op session
max-players: 4
# -= Scoring options =-
scoring:
# -= All points =-
# This will give the player all possible points.
# If a player skips a block jump, it will still give the points for that block.
all-points: false
# -= Rewards use total score =-
# Whether interval rewards should use the total score of a session or not.
# The total score doesn't reset when a player falls.
rewards-use-total-score: false
# -= Particle options =-
particles:
# -= Particle shape =-
# The shape of the particle that will appear when a new block generates.
# Possible options: dot, box, circle
particle-shape: 'box'
# -= Particle type =-
# The type of particle that will appear when a new block generates.
# You can find all types at https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Particle.html
particle-type: 'SPELL_INSTANT'
# -= Sound type =-
# The sound that will play when a new block generates.
# You can find all sounds at https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html
sound-type: 'BLOCK_NOTE_BLOCK_GUITAR'
# -= Sound pitch =-
# The pitch of the sound that will play when a new block generates.
sound-pitch: 3
# -= Sound volume =-
sound-volume: 2
# -= Default values =-
# This is a list of all options and defaults.
default-values:
main:
enabled: true
@ -456,39 +247,33 @@ default-values:
play:
enabled: true
single:
enabled: true
spectator:
enabled: true
community:
enabled: true
leaderboards:
enabled: true
settings:
enabled: true
lang:
enabled: true
default: en
default: 'en'
chat:
enabled: true
parkour_settings:
item:
enabled: true
items:
styles:
enabled: true
default: "red"
default: 'red'
leads:
enabled: true
@ -524,9 +309,7 @@ default-values:
lobby:
enabled: true
visibility:
enabled: true
player_management:
enabled: true
enabled: true