chore(history): squash 13 commit(s) from 2026-09-08

- fix: replace field-level Minecraft.getInstance() with lazy locals — Phase 5 sweep complete
- docs: plan Phase 7 — new modules inspired by SoupVisuals (re-implementation, no code copied)
- test: Phase 6 — EventBus, config values, profile codec/diff, LegacyTextUtil (54 new tests)
- fix: LegacyTextUtil.isColor via explicit EnumSet instead of ChatFormatting.ordinal()
- fix: last 4 field-level Minecraft instances (ESP, NameTags, TargetHud, ViewModel) — true end of Phase 5
- fix: ClickPearl + ElytraRecastUtil 'client' fields -> lazy access (final mc-field leftovers)
- feat: Phase 7 — Light, Hitboxes, NameProtect, Watermark (re-implemented from SoupVisuals ideas)
- fix: ChinaHat geometry — proper shallow cone per SoupVisuals profile
- refactor: Watermark — static UI element in our style, not a Soup port
- fix: startup crash and trollface texture errors from in-game log
- docs+feat: DEVELOPMENT.md guide, Watermark username field, README refresh
- feat: Reach HUD widget (Settings -> UI) — last hit distance, pure info
- feat: Phase 7 Delta QoL modules + command prefix % with full autocomplete
This commit is contained in:
loki5512344 2026-09-08 23:46:29 +02:00
parent e1d7d19a83
commit 707820e418
167 changed files with 5034 additions and 345 deletions

View file

@ -0,0 +1,200 @@
# LoVisual — руководство разработчика
Как устроен проект, как добавлять фичи и какие правила обязательны.
Версии: **Java 25 · Fabric Loader 0.19.3 · Minecraft 26.2** (Loom).
---
## 1. Сборка и запуск
```bash
cd combatant-client-26.2
./gradlew build # jar: build/libs/lovisual-<ver>.jar
./gradlew test # юнит-тесты (сейчас ~320)
./gradlew runClient # быстрый dev-запуск Minecraft с модом
./gradlew runClient --rerun-tasks # если кэш врёт
```
Готовый jar для теста в реальной сборке — скопировать в инстанс:
```bash
cp build/libs/lovisual-*.jar /storage/prismlauncher/instances/26.2/minecraft/mods/
```
Ресурсы (текстуры) — **только настоящие PNG** (сигнатура `89 50 4E 47`).
JPEG, переименованный в .png, уронит `Bad PNG Signature` при загрузке ресурспака.
---
## 2. Правила проекта (нарушение = откат)
- **KISS / DRY / SOLID** — простая структура, без оверинжиниринга.
- **≤ 200 строк на файл.** Фасад-делегат (например `Renderer2D`) — исключение.
Больше логики — выноси package-private хелперы в тот же пакет.
- **≤ 4 файла на папку** — больше, дели папку по смыслу (подпапка).
- **Никакого статики от Minecraft**: `Minecraft.getInstance()` — только локальной
переменной в месте использования, НИКОГДА в поле (`private final Minecraft mc = ...` — баг).
Функциональный синглтон (`INSTANCE`) — только для сервисов без состояния жизненного цикла.
- **`Lifecycle.isActive()/canRunHud()` — гейты** для рендера/событий; `EventBus.post()`
сам игнорирует события до activation.
- **Никаких this-escape** — конструктор не публикует `this`.
- **Никаких мёртвых миксинов** — миксин только если реально нужен;
`cancellable=true` только при реальном cancel.
- **`dev.loki.lovisual.mixins.*` — ТОЛЬКО классы с `@Mixin`.** Обычный helper-класс
внутри mixin-пакета крашит игру: Knot не даёт transformed-классу обратиться к нему
(`IllegalClassLoadError`). Хелперы — в `util/` или пакет фичи (см. `HandDynamics`).
- **Без пустышек** — файл без реализации не создаётся.
- **Каждый этап компилируется**: `./gradlew build` перед коммитом.
- **Тесты обязательны** для новой чистой логики (JUnit 5, без моков Minecraft —
выноси математику/парсинг в чистые статические методы и тесть их).
- **Референсы (`ref/`)** — чужой код (SoupVisuals SOUP-1.0, Delta и др.):
**только идеи**, пере-реализация под наши правила, никакого копирования.
---
## 3. Карта пакетов
```
dev.loki.lovisual
├── LoVisual.java — инициализация ClientModInitializer
├── Lifecycle.java — INIT → ACTIVE → SHUTDOWN, гейты canRunHud/Render
├── events/ — EventBus (@EventHandler, приоритеты, гейт по Module)
│ └── impl/ — конкретные события (Tick, RenderHud, Packet, Lightmap...)
├── config/ — MainConfig, SettingDef, сериализация (.lvcfg, legacy .cbcfg)
│ ├── values/ — ConfigValue: bool/num/mode/color/set/... (есть тесты)
│ └── profile/ — профили конфигов (collect/apply/diff/binary codec)
├── features/
│ ├── module/ — Module, ModuleCategory, ModuleInfo, ModuleManager,
│ │ └── modules/... — сами модули по категориям
│ ├── gui/ — ClickGUI, HUD (draggable + nondraggable), BetterChat
│ ├── command/ — чат-команды (@CommandInfo, ClassGraph-поиск)
│ ├── relations/ — друзья/враги/стаф (PlayerRelations, CategoryService)
│ └── security/ — AntiBot-фильтр, StaffTracker
├── render/engine/ — Renderer2D/3D, пайплайны, шрифты, MeshBuilder
├── mixins/ — @Mixin-классы (см. правило выше)
└── util/ — чистые утилиты (text/animation/time/...) — тут живут тесты
```
---
## 4. Как добавить МОДУЛЬ
Модули находятся в `features/module/modules/<категория>/`. Категории:
`COMBAT · MOVEMENT · PLAYER · VISUALS · MISC` (см. `ModuleCategory`).
Легит-направление: читерские модули (автоудары, полёты, speed...) **удаляем, не добавляем**.
Регистрация автоматическая: **ClassGraph сканирует `@ModuleInfo`** — ничего никуда
дописывать не надо. Пустой конструктор обязателен.
Шаблон:
```java
@ModuleInfo(id = "mymodule", displayName = "My Module", category = ModuleCategory.VISUALS)
public class MyModule extends Module {
private static final String SETTING_X = "x_amount";
private final NumberValue<Integer> amount = num("myModuleAmount", SETTING_X, 5, 1, 10);
@Override
public void onTick() {
Minecraft mc = Minecraft.getInstance(); // локально, не в поле!
if (mc.level == null) return;
// ...
}
@EventHandler
private void onRenderHud(RenderHudEvent event) { /* авто-подписка через EventBus */ }
}
```
Ключевые хуки `Module`: `onEnable/onDisable/onTick/onFrame`,
`onRenderHudEngine(...)`, `onRenderWorldEngine(...)`, `getWorldPhase()/getHudPhase()`.
Хелперы настроек сами объявляют `SettingDef`: `bool/num/mode/color/colorNoAlpha/
enumSetting/text/group/bind` + `visibleWhen(value, () -> cond)`.
Модуль-лист по умолчанию недоступен цели — смотри также `boolCommon`/`modeCommon`
для общих схем (`CommonSettingSchemas`).
Доступ к чужому модулю: `Modules.get(X.class)`, проверка состояния — `isEnabled()`.
Мировой рендер (боксы/линии) — через `Renderer3D.batch(WORLD_COLORED_LINES, DepthMode...)`
и MeshBuilder; готовые куски геометрии — `EspMeshBuilder.addOutlineBox` и т.п.
---
## 5. Как добавить HUD-элемент
Два вида, оба ищутся ClassGraph-ом автоматически:
**A. Draggable (перетаскиваемый, вкладка Settings → HUD):**
`features/gui/hud/draggable/impl/`, аннотация `@HudElementInfo(id, displayName,
enabledByDefault, order)`, базовый класс `DraggableHudElement`.
Обязательно: `applyDefaultPosition(screenW, screenH)`, рендер в `renderEngine(...)`,
`usesEngineRenderer() = true`. Позиция сохраняется через `layoutValue`.
**B. Static (фиксированный, вкладка Settings → UI):**
`features/gui/hud/nondraggable/impl/`, аннотация `@HudElementRegister(order=N)`,
базовый класс `AbstractHudElement`, конструктор `super(id, "Title", defaultEnabled)`,
синглтон `public static final X INSTANCE` (лоадер забирает его из статического поля).
Чтобы карточка появилась во вкладке **UI**, добавь `new StaticHudCard("<id>", "<Title>")`
в `STATIC_UI_CARDS` в `MenuSettingsResolver.java` (draggable-виджеты там не нужны —
они собираются из реестра сами).
Цвет из темы: `Theme.theme().accent()/windowBg()/textMuted()`; альфа —
`HudRenderUtil.setAlpha(argb, a)`; текст — `Fonts.renderer(name, type, fallback)`
(`begin(scale,...)` → `getWidth`/`render` → `end()`).
---
## 6. Как добавить СОБЫТИЕ / МИКСИН
1. Событие: наследуй `Event` (есть `cancel()`), положи в `events/impl/`.
2. Публикация: `Events.BUS.post(new MyEvent(...))` из миксина.
3. Миксин: класс с `@Mixin(Target.class)` в `mixins/` **и имя в `lovisual.mixins.json`**
(секции `mixins`/`client`). Миксин, которого нет в json, не применится.
4. Если миксин опциональный (зависит от чужого мода, напр. Sodium) — цель может
отсутствовать: Mixin напечатает WARN и пройдёт (см. `SodiumChunkMeshFormatsMixin`).
Такой WARN — норма, краш — нет.
5. Чистая логика из миксина — вобы в обычный класс вне `mixins/`-пакета (см. правило про `HandDynamics`).
---
## 7. Настройки, конфиг и i18n
- Конфиги модулей/HUD пишутся в `<game>/lovisual/` в формате **`.lvcfg`**
(legacy `.cbcfg` читается). Профили — `config/profile/` (кнопка в ClickGUI → Configs).
- Каждой настройке нужен i18n-ключ в **обоих** файлах
`src/main/resources/assets/lovisual/lang/{en_us,ru_ru}.json`:
- модуль: `module.<id>.description`, настройки `setting.<id>.<setting_id>`,
опции режима `setting.<id>.<setting_id>.option.<Option>`;
- draggable HUD: `hud.draggable.<id>` / `setting.draggable.<id>.<key>`;
- static HUD: `setting.hud_element.<id>.<key>` (см. Watermark, Swap Tooltip).
- `setting_id` — snake_case (`"show_ping"`); configName —camelCase с префиксом мода
(`"watermark_show_ping"`) — из него авто-выводится id.
- Пропущенный ключ ловит `MissingI18nReporter` в дев-окружении — проверяй консоль.
---
## 8. Тесты
`src/test/java/` зеркалит main. Пишутся **только на чистую логику** (без Minecraft-
рантайма): парсеры, математика, codec'ы, матчинги. Примеры:
- `util/text/NameProtectorTest`, `LegacyTextUtilTest` — парсинг/подмена,
- `config/values/*Test` — значения конфига,
- `config/profile/ConfigProfileValueCodecTest` — бинарный round-trip,
- `events/EventBusTest` — события (нужен `Lifecycle.activate()` в `@BeforeAll`).
Если фича завязана на Minecraft — выделяй статический pure-метод
(`static int tint(...)` / `static record Layout(...)`) и тесть его (см. `Light.tintAmbient`).
---
## 9. Чеклист перед коммитом
- [ ] `./gradlew build` зелёный, `./gradlew test` зелёный
- [ ] новых файлов >200 строк нет; папка не разрослась >4 файлов
- [ ] нет полей `Minecraft` / `getInstance()` в полях
- [ ] lang-ключи en_us + ru_ru добавлены для всех новых настроек
- [ ] тест на новую чистую логику есть
- [ ] в mixin-пакете только `@Mixin`-классы
- [ ] TODO.md обновлён (галка/заметка), если это этап плана
- [ ] в чужой код (`ref/`) не заглядывался копипаст