refactor(player): encapsulate game state behind PlayerSettings, clear VisibilityModifier and HiddenField

- ParkourUser/ParkourPlayer/GeneratorState public mutable fields -> private + short accessors
- per-player settings extracted to player/data/PlayerSettings (ParkourPlayer 188 -> 96 lines,
  PMD-counted methods 20 -> 9); this is what fixed the TooManyMethods violation that the
  encapsulation itself introduced — by decomposition, not by lowering the threshold
- disk storage reads and writes the same canonical 12-key map as the SQL JSON column;
  legacy key names normalized on read, @Expose dropped from domain classes
- 39 settings call sites rewritten across 7 files
- HiddenField backlog cleared (Config, ObstacleGenerator, GhostService, Leaderboard,
  PagedMenu, Reward, ParticleData, World)
- checkstyle ratchet: VisibilityModifier, HiddenField, EmptyLineSeparator and
  MissingDeprecated flipped to error once their counters hit zero; suppressions.xml empty
- javac -Xmaxwarns raised so NullAway/Error Prone counts are honest
  (baseline 165/289, now 164/265)

checkstyle 0 errors / 135 warnings, PMD 0 violations, tests 94/0 fail, ./gradlew check green.
TODO.md: items 30 and 107 updated, new items 167-170, ПРОГРЕСС-6/7 recorded.
This commit is contained in:
loki5512344 2026-10-06 18:57:39 +02:00
parent c120309663
commit 02e2947f47
Signed by: boba
GPG key ID: 253067914055423B
90 changed files with 1103 additions and 660 deletions

99
TODO.md
View file

@ -55,7 +55,7 @@
- **27. SQL: утечка Hikari-пулов.** `StorageSQL.init(mode)` вызывается на каждый режим (6 лидербордов), каждый раз пересоздаёт `connectionManager` (StorageSQL.java:66-77, LeaderboardStorage.java:35) → до ~10 соединений висят навсегда. → Один пул на плагин, инициализация один раз.
- **28. SQL: блокирующий connect на main thread** (SQLConnectionManager.java:34-56) — при лежащей БД включение висит до 5с×режимы. → `setInitializationFailTimeout(-1)` + асинхронный connect.
- **29. SQL: адаптив не стартует.** `AdaptiveServices.init` вызывается до создания лидербордов, а `StorageSQL.runWhenConnected` выбрасывает колбэк при не-подключении (StorageSQL.java:45-51). → Очередь колбэков в SQLConnectionManager.onConnectCallbacks; порядок init: лидерборды → storage → adaptive.
- **30. SQL: настройки игрока не сохраняются.** `SQLDataMapper.serializeSettings` всегда `"{}"`, десериализация no-op (SQLDataMapper.java:105-115) → при входе `schematicDifficulty`/`blockLead` и др. = null → NPE в `ParkourPlayer.updateGeneratorSettings:157-164` и `PAPIHook.java:140`. → Реализовать сериализацию/десериализацию всех настроек (JSON-колонка) + fallback на defaults.
- **30. SQL: настройки игрока не сохраняются.** ✅ **СДЕЛАНО**: `SQLDataMapper.serializeSettings` пишет `PlayerSettingsManager.toSettingsMap(player)` (12 ключей) в JSON-колонку, `deserializeSettings` читает её тем же словарём (`normalizeNumericSettings` + `applySettings`) и падает на `applyDefaults` при пустоте/битости → `schematicDifficulty`/`blockLead` больше не null при входе (NPE в `updateGeneratorSettings`/`PAPIHook` не стало). Диск и SQL теперь используют **один и тот же** набор ключей.
- **31. Схематики: креш на main thread от битых файлов.** `LPSchematicLegacy`/`SchemLoader` парсят без валидации; NPE/AIOOBE мимо `catch(IOException)` (SchematicManager.java:109-133, LpschemConverter.java:44-54). Битый файл = отказ включения плагина. → Валидация dimensions/palette/frameCount + `catch(RuntimeException)` + лимиты размера файла/блоков.
- **32. JumpCalculator: возврат невалидного прыжка.** После 10 неудач валидация сдаётся и возвращает заведомо невалидный блок (JumpCalculator.java:125-136). → Пере-роллить offset/направление, а не только уменьшать distance/height; при исчерпании — safety-fallback (простой допустимый шаг).
- **33. JumpCalculator: диагональный heading залипает.** `JumpDirector.getRecommendedHeading` в безопасной зоне возвращает current без изменений (JumpDirector.java:105-107), а `calculateJumpOffset` при `offset.getX()!=0` перезаписывает Z (JumpCalculator.java:110-115) → курс зигзагом и не выпрямляется. → При диагонали аппендикс не перезаписывать случайным; корректный поворот-домножитель.
@ -86,12 +86,14 @@
- **47. `UserRegistry.getUsers()` строит полный Set на каждый BlockBreak/Place/Drop/Damage** (UserRegistry.java:94-98) → O(n) аллокаций на горячем пути. → Кэш `Map<UUID, ParkourUser>` + привязка через PersistentDataContainer или entity.
- **48. `sendTranslated(...).formatted()` бросает IllegalFormatException при `%` в нике/сообщении** (ParkourUser.java:102, Locales.java:76-82). → Обернуть в try или использовать replace-based формат.
- **49. `getJoinCount` — int между main и async (bStats)** (UserRegistry.java:100-104). → AtomicInteger.
- **168. `ParkourUser.chatType` — состояние без записи.** Поле инициализировано `PUBLIC`, читается в `ParkourRestrictionListener.onChat` (switch `chatType()`), но сеттера нет нигде → ветки PRIVATE/GUILD недостижимы, режим группового чата не работает. → Либо подключить переключатель (команда/меню), либо убрать switch и мёртвый enum.
### Modes
- **50. `GravityShiftMode` не снимает эффекты при выходе игрока** (только в reset для присутствующих, GravityShiftMode.java:149-163). → Снимать в unregister.
- **51. `RaceMode.finishRace` — `reset(false)` оставляет игрока в воздухе** (RaceMode.java:142-147; GeneratorCleanup.reset чистит курс без регенерации/телепорта). → Телепорт в лобби + регенерация старта.
- **52. `SpeedrunMode` оставляет удалённые блоки в `history`** → ломает координатные поиски. → Удалять координату из history при снятии блока.
- **167. Elytra-под-режимы не отличаются от базового.** `ElytraGenerator.getMode()` возвращает `Modes.elytra()`, а 5 под-генераторов (`Close/MinSpeed/TimeTrial/SpeedDemon/ObstacleGenerator`) его не переопределяют → guard `getMode() instanceof CloseMode` в `create()` (строка 47 во всех 5 режимах) **никогда не истинен**: повторная команда в том же режиме не делает early-return, а пересоздаёт сессию. Геттеры `Modes.speedDemon()/minSpeed()/timeTrial()/close()/obstacle()` не вызываются нигде (5 статиков только пишутся). → Переопределить `getMode()` в под-генераторах (или сравнивать класс генератора) и использовать/удалить геттеры.
### Схематики (жизнь в игре)
@ -181,7 +183,7 @@
- **104. `ProfileValue.asDouble/asInt` бросают NFE на нечисловых строках** (Profile.java:54,64) — только empty-guard.
- **105. Дубликат логики `create()` во всех режимах** с несогласованными проверками дублей (instanceof DefaultMode vs instanceof SpeedrunGenerator).
- **106. Мёртвый код**: `EffectManager` целиком, `SessionUserManager`, `SQLMigrationManager`, `SQLQueryExecutor`, `SkullSetter`, `Locations.max/min`, `ParkourBlockGenerateEvent`, `onPlayerJoin` (CoopMode.java:169), `PLAYER_COLUMNS`/`OptionContainer` (ParkourPlayer.java:38,205), `acceptingPlayers`-механизм, `Muted` в двух местах.
- **107. Публичные mutable поля-государство**: `GeneratorState` — 15 публичных полей, `ParkourGenerator.session/island/profile/state/generatorOptions` публичные final, `Score` времени в строке. → Инкапсуляция.
- **107. Публичные mutable поля-государство**: ✅ **ЧАСТИЧНО** — `GeneratorState` (15 полей) и весь кластер `player/*` (`ParkourUser`, `ParkourPlayer`, `PlayerSettings`) инкапсулированы в ПРОГРЕСС-6, `VisibilityModifier` переведён в `error`. Остались `ParkourGenerator.session/island/profile/state/generatorOptions` (публичные final — правило их разрешает, но состояние чужое мутируется) и `Score` времени в строке. → Инкапсуляция.
## 🧪 Тесты
@ -306,7 +308,19 @@ Checkstyle отработал `0 errors` (error-уровневые legacy-нар
- **135. [M] Починить `NPathComplexity` (legacy, 29 / 25 файлов).** Решается декомпозицией методов (файлы и так в списке на рефакторинг).
- **136. [M] Починить `IllegalCatch` (legacy, 22 / 19 файлов).** catch(Exception/Throwable/RuntimeException)→сужение до конкретных исключений либо переименовать в `expected|ignore`.
- **137. [M] Починить все нарушения PMD (45):** `CognitiveComplexity(17)` + `CyclomaticComplexity(7)` — декомпозиция; `GodClass(4)` + `TooManyMethods(8)` — декомпозиция; `AvoidReassigningParameters(7)` — переименование параметров; `EmptyCatchBlock(2)` — комментарий/конкретный тип.
- **138. [S] Перевести PMD из report-only в жёсткий гейт.** После зачистки убрать `isIgnoreFailures=true` в `build.gradle.kts` (pmd-блок).
- **138. [S] Перевести PMD из report-only в жёсткий гейт.** После зачистки убрать `isIgnoreFailures=true` в `build.gradle.kts` (pmd-блок). ✅ сделано в `b771cc1`.
- **169. [M] Починить `ReturnCount` (69 warning / 52 файла) — ПЕРЕСТРУКТУРИРОВАНИЕМ, не порогом.** Все 69 — это половина
правила про void (`maxForVoid` по умолчанию = 1 ⇒ запрещена любая guard-клауза `if (x) { return; }`); для value-методов
при `max=5` нарушений 0. Владелец запретил поднимать порог → в каждом из 69 void-методов убираем ранний `return`
(условие становится обычным `if`-блоком) либо выносим тело в helper-метод. Следить за `NestedIfDepth` (error, max=2)
и `NPathComplexity`. После обнуления — перевести `ReturnCount` в `error`.
- **170. [M] Починить `IllegalCatch` (23 warning / 18 файлов).** 19× `catch (Exception)`, 3× `catch (Throwable)`,
1× `catch (RuntimeException)`. Там, где конкретика реальна, — сужать до `IOException`/`NumberFormatException`/
`SQLException`/`YAMLException` и т. п. На истинных границах (диспетчер команд `LoParkourCommand:36`, `session-state:84`,
`LoParkour:128`) `Throwable` **обязан** стать `RuntimeException`: `Throwable` глотает `Error` (OOM, StackOverflow) —
это баг, а не стиль. Пограничные `catch (Exception)` в `onEnable/onDisable`, загрузчиках конфигов/схематчей и
`SchematicManager` — оставляем широкими осознанно (релоад плагина не должен ронять сервер), но каждый такой site
требует комментария «почему именно Exception»; после разбора правило остаётся в warning на незачищенном остатке.
После зачистки п.132–137 — удалить соответствующие записи из `config/checkstyle/suppressions.xml` (супрессии остаются только для ещё незачищенных legacy-файлов).
@ -398,8 +412,9 @@ Checkstyle отработал `0 errors` (error-уровневые legacy-нар
> `ClassFanOutComplexity` 30→12, `MagicNumber` и `FinalLocalVariable` **warning→error** (оба чистые!).
> Добавлены: `ReturnCount` (max 5), `FinalClass`, `HiddenField`, `VisibilityModifier`, `NPathComplexity` (секция вернулась в явном виде).
> - `severity="warning"` (build зелёный, но каждое нарушение видно в отчёте) — **бэклог дизайна**, правило:
> **счётчик обнулился → сразу переводить в error**. Сейчас: `VisibilityModifier` 163, `ReturnCount` 69, `HiddenField` 30,
> `IllegalCatch` 23, `ClassFanOutComplexity` 17, `NPathComplexity` 12, `ParameterNumber` 8, `MethodLength` 5, `CDAC` 2.
> **счётчик обнулился → сразу переводить в error**. Актуальный список — в ПРОГРЕСС-6 ниже
> (на шаге 0.5: `VisibilityModifier` 163, `ReturnCount` 69, `HiddenField` 30, `IllegalCatch` 23,
> `ClassFanOutComplexity` 17, `NPathComplexity` 12, `ParameterNumber` 8, `MethodLength` 5, `CDAC` 2).
> - **`suppressions.xml` ОБНУЛЁН** (45 записей удалено, включая FileLength/NPath/WhitespaceAround/IllegalCatch).
>
> **Красный baseline (честный, до фиксов): 431 нарушение в 129 файлах.** Замеры порогов перед выбором:
@ -460,8 +475,80 @@ Checkstyle отработал `0 errors` (error-уровневые legacy-нар
> в `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).
> `./gradlew check` зелёный: PMD 0, тесты 94/0 fail, JaCoCo/ArchUnit/Spotless ок, NullAway 48 (как в baseline;
> ⚠️ как выяснилось на шаге 6 — это было артефактом обрезки javac на 100 предупреждениях, честные числа там же).
> Пороги правил не снижались, `suppressions.xml` пустая. Реальных прогонов на сервере после правок не было (только `check`).
>
> ## ✅ ПРОГРЕСС-6 (Задача 2, шаг (c): инкапсуляция игрового состояния — `player/*` + `GeneratorState`, 2026-10-06)
> **Checkstyle 329 → 165 warning; `VisibilityModifier` 75 → 0 и переведён в `error`.**
> - **Кластер `player` закрыт:** `ParkourUser` (5 полей) и `ParkourPlayer` (12) → `private` + аксессоры без `get` (та же идиома,
> что в ПРОГРЕСС-5 для `config/options`). ~46 обращений в 19 файлах переписаны.
> - **Выключен баг тенимого `locale`:** `@Expose String locale` был объявлен и в `ParkourUser`, и в `ParkourPlayer` —
> запись в поле и в аксессор расходилась. Осталось одно поле в родителе; на gson-2.11.0 проверено, что `@Expose`
> наследуемого поля сериализуется, поэтому набор ключей на диске не изменился.
> - **П.107 для `GeneratorState`:** 15 публичных полей → `private` + аксессоры/сеттеры; инкременты и сбросы оформлены
> методами (`addGeneratedBlocks(int)`, `resetScore()`, `setScore(int)`), ~90 обращений в 25 файлах.
> Публичные `ParkourGenerator.session/island/profile/state` — как были (остаток п.107).
> - **SRP: настройки игрока вынесены в `player/data/PlayerSettings`** (11 полей + аксессоры). `ParkourPlayer` —
> 194 → 93 строки, засчитываемых методов 20 → 9. Это и починило свежесломанное нарушение PMD `TooManyMethods` (гейт 0 нарушений
> был испорчен моим же рефакторингом) — декомпозицией, а НЕ ослаблением порога и НЕ переименованием под name-based
> освобождение правила (`getX()` в проекте не используется: 0 объявлений в `src/main`).
> - **DRY по настройкам:** троекратное повторение санитизации «legacy-locale» → `PlayerSettingsManager.sanitizeLocale()`
> (фолбэк берётся из конфига, а не захардкожен `"en"`); байт-в-байт одинаковая сборка 12-ключевого map в
> `StorageDisk.readPlayer` и `SQLDataMapper.serializeSettings` → `toSettingsMap()`; удалены мёртвый `getColumnMappings()`,
> `PLAYER_COLUMNS` и дубль `OptionContainer` (п.106); реестр-фасады `ParkourPlayer.isPlayer/getPlayer/getPlayers`
> переехали в `UserRegistry` (22 файла);
> дубли `parseIntSafe`/`parseDoubleSafe` схлопнуты в `numericOrFallback`.
> - **Закрыт п.30** (SQL не сохранял настройки) и **формат disk-файла игрока пересобран сознательно:** раньше Gson писал
> `ParkourPlayer` по `@Expose`-полям, а читал его через Unsafe-аллокацию «полупустого» объекта, чей map потом переливали
> в живого игрока. Теперь `StorageDisk` пишет и читает тот же канонический `Map<String,Object>`, что и JSON-колонка SQL —
> один словарь на оба бэкенда, `@Expose` из доменных классов убран совсем. Старые файлы остаются читаемыми: 3 ключа,
> которые совпадали с именами полей (`particles`/`useSpecialBlocks`/`showFallMessage`), нормализует
> `StorageDisk.normalizeLegacyKeys()`. ⚠️ Живых файлов игроков в `run/plugins/LoParkour/players/` нет — миграция
> проверена кодом и тестами, не реальным апгрейдом.
> - **Артефакт замера предупреждений:** в `build.gradle.kts` (JavaCompile) добавлен `-Xmaxwarns 100000`, потому что javac по умолчанию
> обрывает список предупреждений на 100 — отсюда «NullAway 48» в ПРОГРЕСС-5. Честные замеры через `git stash`:
> baseline HEAD **NullAway 165 / Error Prone 289**, сейчас **NullAway 164 / EP 265** — регресса нет.
> - **Счётчики:** checkstyle **0 error / 165 warning** (бэклог: `ReturnCount` 69, `HiddenField` 30, `IllegalCatch` 23,
> `ClassFanOutComplexity` 17, `NPathComplexity` 12, `ParameterNumber` 8, `MethodLength` 4, `CDAC` 2).
> В ноль обнулены и переведены в `error`: `VisibilityModifier`, `EmptyLineSeparator`, `MissingDeprecated`.
> PMD **0** (жёсткий гейт), `./gradlew check` зелёный, тесты **94 / 0 fail (1 skipped)**. Пороги правил не снижались,
> `suppressions.xml` пустая. Реальных прогонов на сервере не было (только `check`).
> - **Найдено по пути и оформлено отдельными пунктами (молча не чинилось):** **167** (elytra-под-режимы недостижимы —
> `getMode()` не переопределён) и **168** (`ParkourUser.chatType` не записывается нигде).
>
> ## ✅ ПРОГРЕСС-7 (Задача 2, шаг (c): `HiddenField` обнулён + директива владельца про линтеры, 2026-10-06)
> **Checkstyle 165 → 135 warning; `HiddenField` 30 → 0 и переведён в `error`.**
> - **Директива владельца (зафиксирована, действует дальше):** нарушения линтеров **чинить**, а не **глушить** —
> пороги правил не поднимать, `suppressions.xml` не наполнять, под name-based освобождения не подстраиваться.
> В `error` правило переводится только после того, как счётчик реально обнулён правками.
> - **`HiddenField` 30 → 0** — тенящие параметры/локалы переименованы (поля не трогали): `Config` (12 методов-делегата,
> параметр `path` → `key`), `ObstacleGenerator` (внешнее поле `radius` → `obstacleRadius` — одним переименованием сняли
> 7 теней в enum-реализациях `getPoints(center, radius)`), `GhostService.setReplaying(uuid, active)`,
> `Leaderboard.sort(order)`, `PagedMenu.addToDisplay(menuItems)`, `Reward` (локал `string` → `command`),
> `ParticleData` (fluent-сеттеры принимают `value`), `World.create()` (локал `world` → `existingWorld`, а
> `World.world = world` превратился в обычное присваивание поля).
> - **`ReturnCount` 69 — решено оформить отдельным пунктом 169:** все 69 нарушений приходятся на половину правила
> про void (`maxForVoid` по умолчанию = 1, то есть запрещена любая guard-клауза `if (x) { return; }`);
> для value-методов при `max=5` нарушений **0**. Поднимать `maxForVoid` я предлагал, владелец отказал — значит
> 69 void-методов переструктурируются по-настоящему (early-return → `if`-блок или выделение helper'а),
> с оглядкой на `NestedIfDepth` (error, max=2) и `NPathComplexity`.
> - **`IllegalCatch` 23** — сузить типы там, где конкретика реальна (IO/NumberFormat/SQL/ClassNotFound),
> а на истинных границах заменить `catch (Throwable)` → `catch (RuntimeException)` (это не «приглушение», а починка:
> `Throwable` глотает `Error`-ы — OOM, StackOverflow). Диспетчер команд, `onEnable/onDisable`, загрузчики конфигов
> и схематчей, session-state — пограничные точки, их фиксируем отдельно (п.170).
> - **Авария и восстановление:** после частичного отката правок в IDE (отклонённые diffs) рабочий код был сломан:
> `player/data/PlayerSettings.java` удалился целиком, `ParkourPlayer` вернулся к 11 `@Expose`-полям при уже
> переписанных вызовах (`settings()` ссылался на несуществующее поле, `updateGeneratorSettings` пропал),
> в `GeneratorCleanup` потерялся импорт `@Nullable` (2 ошибки компиляции), `checkstyle.xml` откатился к
> HEAD-версии и **потерял все четыре флипа** (`VisibilityModifier`, `EmptyLineSeparator`, `MissingDeprecated`,
> `HiddenField`), `PagedMenu`/`GhostService` вернулись к тенящим параметрам, а с `ParkourUser` слез лишний `@Expose`
> (после перехода диска на map он мёртвый). Всё восстановлено и перепроверено:
> `./gradlew check` — **BUILD SUCCESSFUL**, checkstyle **0 error / 135 warning** (test-отчёт пуст), PMD **0**,
> тесты **94 / 0 fail (1 skipped)**, `suppressions.xml` пустая. Реальных прогонов на сервере по-прежнему не было.
> Урок на будущее: после любой отменённой пачки правок сверять `git status` + полный `check`, а не только счётчики.
> - **Остаток бэклога (135):** `ReturnCount` 69, `IllegalCatch` 23, `ClassFanOutComplexity` 17, `NPathComplexity` 12,
> `ParameterNumber` 8, `MethodLength` 4, `ClassDataAbstractionCoupling` 2.
---