chore(history): squash 15 commit(s) from 2026-09-22

- refactor(mixins): разбивка render/entity (crystal/item) и render/level/particles — регресс 8.5.1
- refactor(structure): разбивка 10 папок >4 файлов — регресс 8.5.1 после Фаз 7/10
- refactor(ui): OrderedUiBatcher 944 → 188 + 8 хелперов ≤200 (8.5.2, гейт 9.2)
- refactor(text): MsdfFont 613 → 163 + atlas/ (Atlas,AtlasLoader,Json,Glyph) + GlyphDrawer 179 (8.5.2, гейт 9.2)
- refactor(text): CustomTextRenderer 540 → 235 + CustomFontSet 142 + GradientTexts 95 + HorizontalFade 77 (8.5.2, гейт 9.2)
- refactor(buttons): BetterButtons 1033 → 388 + 7 хелперов ≤200 (8.5.2, гейт 9.2)
- refactor(inventory): удалить InventorySwap 1038 + ShitDropper → ShitESP (Вариант A)
- fix(shitesp): цвет через color() helper как у ESP/Chams (крутой пиккер)
- fix(blockesp): камень→спавнер подсвечивается сразу, docs: DEV_GUIDE + DEVELOPMENT → один гайд
- docs(dev-guide): дополнен §18 современными правилами (web-поиск 2025-2026)
- chore(quality): PMD 7.27.0 + Checkstyle 14.1.0 + SpotBugs 6.5.11 + обновы зависимостей
- refactor(svg): SvgMeshBackend 1020 → 611 + SvgParser 181 + SvgGeometry 173 (8.5.2)
- refactor(svg): SvgMeshBackend 611 → 267 + SvgTexture 223 + SvgRender 35 + SvgCache 25 (8.5.2)
- refactor(theme): Themes 1012 → 562 + 3×163 пресета (8.5.2)
- refactor(particles): WorldParticles 999 → 821 + geometry 93 + sphere 67 (8.5.2)
This commit is contained in:
loki5512344 2026-09-22 20:33:03 +02:00
parent 8181f11c5a
commit ec10395511
140 changed files with 5442 additions and 6457 deletions

View file

@ -1,569 +1,297 @@
# LoVisual Mod — Dev Guide / Справочник по созданию модулей
# LoVisual Mod — Dev Guide (Единый справочник)
Цель: быстро посмотреть структуру и написать новый модуль / HUD-элемент /
команду, не перекапывая весь код. Всё ниже — сводка по реальному коду.
Цель: быстро добавить модуль / HUD-элемент / команду / миксин, не перекапывая весь код. Сводка по реальному коду, объединено из `DEV_GUIDE.md` + `DEVELOPMENT.md`.
- Пакеты: `dev.loki.lovisual.*`
- Minecraft 26.2, Fabric, Java 25, Lombok
- Правила проекта (важно!): файл ≤200 строк, папка ≤3 файлов,
`./gradlew build` после каждого этапа, тесты для новой логики,
никаких `static final Minecraft` в полях — только ленивый `Minecraft.getInstance()`.
- Стек: **Java 25 · Fabric Loader 0.19.3 · Minecraft 26.2** (Fabric Loom), Lombok
- Правила — нарушение = откат (см. §2)
---
## 1. Архитектура (главные пакеты)
## 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`, `Module` ≈1181) — исключение; больше логики → выноси `package-private` хелперы в тот же пакет.
- **≤4 файла на папку** — больше → новая подпапка по смыслу.
- **Никакого статика от Minecraft**: `Minecraft.getInstance()` только локальной переменной в месте использования, никогда в поле. `INSTANCE` — только для сервисов без lifecycle.
- **Lifecycle гейты**: `Lifecycle.isActive()/canRunRender()/canRunHud()` — до `activate()` события/тики не работают; `EventBus.post()` сам гейтит.
- **Никаких this-escape** — конструктор не публикует `this`.
- **Никаких мёртвых миксинов** — миксин только если реально нужен; `cancellable=true` только при реальном `cancel`.
- **`dev.loki.lovisual.mixins.*` — ТОЛЬКО классы с `@Mixin`.** Обычный хелпер там крашит `IllegalClassLoadError` (Knot). Хелперы — в `util/` или пакет фичи (см. `HandDynamics`).
- **Без пустышек** — файл без реализации не создаётся.
- **Каждый этап компилируется**: `./gradlew build` перед коммитом.
- **Тесты обязательны** для новой чистой логики (JUnit 5, без моков Minecraft — выноси математику в `static` и тесть).
- **Референсы (`ref/`)** — чужой код (SoupVisuals SOUP-1.0, Delta и др.): **только идеи**, пере-реализация под наши правила, никакого копирования.
- **`System.currentTimeMillis()` в модулях → `GameClock.millis()`**, `static final Minecraft` в полях — баг.
---
## 3. Карта пакетов
```
dev.loki.lovisual
├── LoVisual.java — ClientModInitializer: весь старт (тики, рендер-фазы)
├── Lifecycle.java — INIT → ACTIVE → SHUTDOWN; гейты isActive/canRunRender/canRunHud
├── LoVisual.java — ClientModInitializer: старт (тики, рендер-фазы)
├── Lifecycle.java — INIT → ACTIVE → SHUTDOWN; гейты
├── events/ — EventBus (@EventHandler, приоритеты, гейт по Module)
│ └── impl/ — GameTickEvent, PacketEvent, AttackEntityEvent, LightmapModifyEvent...
├── config/ — MainConfig, SettingDef, .lvcfg (legacy .cbcfg читается)
│ ├── values/ — ConfigValue: bool/num/mode/color/set/... (есть тесты)
│ └── profile/ — профили (collect/apply/diff/binary codec)
├── features/
│ ├── module/ — СИСТЕМА МОДУЛЕЙ (главное для вас)
│ │ ├── Module.java — базовый класс (1181 стр, фасад)
│ │ ├── ModuleInfo — @annotation для модуля
│ ├── module/ — СИСТЕМА МОДУЛЕЙ (главное)
│ │ ├── Module.java — базовый класс
│ │ ├── ModuleInfo — @annotation
│ │ ├── ModuleManager — реестр + диспетчер фаз
│ │ ├── ModuleAutoLoader — авто-обнаружение по аннотации (ClassGraph)
│ │ ├── Modules — удобный доступ: Modules.get(Класс.class)
│ │ ├── ModuleCategory — COMBAT/MOVEMENT/PLAYER/VISUALS/MISC
│ │ ├── ModuleAutoLoader — ClassGraph по @ModuleInfo (no-args ctor → postInit)
│ │ ├── Modules — Modules.get(Класс.class) / enabled(id)
│ │ ├── ModuleCategory — COMBAT/MOVEMENT/PLAYER/VISUALS/MISC (легит-направление: читы удаляем)
│ │ ├── HudPhase / WorldPhase — фазы рендера
│ │ └── modules/{combat,player,visuals,misc}/
│ ├── command/ — команды: @CommandInfo + ClientCommand
│ ├── command/ — @CommandInfo + ClientCommand (ClassGraph, префикс %)
│ ├── gui/
│ │ ├── clickgui/ — экран настроек (Setting → виджет)
│ │ └── hud/ — HUD-элементы
│ │ ├── draggable/ — перетаскиваемые (Fps, Coords, ModuleList, TargetHud…)
│ │ └── nondraggable/ — статичные (CustomBar, CustomHotbar, BetterChat…)
│ ├── relations/ — друзья/враги/категории (CategoryService)
│ ├── security/ — BackdoorProtection (SSRF/translate-защита)
│ └── theme/ — темы: Theme.theme() → Themes.Theme (цвета)
├── config/
│ ├── values/ — типы настроек: ConfigValue (bool/num/mode/color…)
│ ├── SettingDef.java — GUI-нейтральный дескриптор настройки
│ └── common/CommonSettingSchemas — переиспользуемые схемы (i18n-ключи)
├── events/
│ ├── EventBus (Events.BUS) — шина событий
│ ├── @EventHandler — аннотация хендлера
│ └── impl/ — все события (см. §5)
├── mixins/ — миксины + accessors (mixins/accessors/)
├── addon/ — система аддонов (API v0)
├── api/v0/ — публичный API аддонов (module/client/render/clickgui)
├── render/
│ ├── engine/
│ │ ├── renderer/Renderer2D — весь 2D-рендер (прямоугольники, круги, текст, items)
│ │ ├── renderer/Renderer3D — 3D (линии, меши, quads)
│ │ ├── text/ — Fonts.renderer("Onest", Regular), TextRenderer
│ │ ├── animation/AnimationUtility
│ │ ├── pipeline/LoVisualRenderPipelines — готовые пайплайны (UI_COLORED и т.д.)
│ │ └── uniform/MeshBuilder — построение мешей
│ └── engine/color/RenderColor — ARGB
└── util/ — куча хелперов (см. §8)
│ │ └── nondraggable/ — статичные (CustomBar, BetterChat, BetterButtons, DynamicIsland…)
│ ├── relations/ — друзья/враги (PlayerRelations, CategoryService)
│ ├── security/ — BackdoorProtection (SSRF/translate)
│ └── theme/ — Themes.Theme (windowBg/header/surface/accent...), Theme.theme()
├── render/engine/ — Renderer2D/3D, пайплайны, шрифты, MeshBuilder
│ ├── renderer/Renderer2D — 2D (quad/roundedRect/circle/line/item/svg/effect)
│ ├── renderer/Renderer3D — 3D (line/quad/triangle)
│ ├── text/ — Fonts.renderer("Onest", Regular), TextRenderer
│ ├── animation/AnimationUtility
│ ├── pipeline/LoVisualRenderPipelines — UI_COLORED, WORLD_COLORED, HAND_*, SHADER_ESP_*, POST_FX...
│ └── uniform/MeshBuilder
├── mixins/ — @Mixin-классы (mixins.json, LoVisualMixinPlugin)
│ └── mixins/accessors/ — MinecraftAccessor, PlayerInventoryAccessor...
│ └── mixininterface/ — IGuiGraphics, IEntity...
├── addon/ + api/v0/ — система аддонов (LoVisualAddon, LoVisualModuleExtension, RenderCallback)
└── util/ — хелперы: target, pvp, aiming, time/GameClock, input/KeyManager, screen, text, raycast...
```
---
## 2. Жизненный цикл и старт
## 4. Жизненный цикл и старт
В `LoVisual.onInitializeClient()` по порядку:
1. `LoVisualRenderEngineBootstrap.init()`, `PostProcessManager.register(...)`
2. `MainConfig.get()`, `AccountConfig.get()`, `CommandManager.init()`, `MediaSessionService`
3. `ModuleAutoLoader.load("dev.loki.lovisual.features.module.modules")` — ищет `@ModuleInfo`, `new` + `postInit()`
4. `ModuleManager.loadAllModuleConfigs()` — применяет `.lvcfg`, включает `enabled`
5. `HudElements.init()`, `StaticHudElementBootstrap.init()`
6. `Events.BUS.register(...)` — глобальные сервисы (`RotationManager`, `PvpTracker`...)
7. `Lifecycle.activate()` — после этого тики/события/рендер работают
1. `LoVisualRenderEngineBootstrap.init()`
2. `MainConfig.get()`, `AccountConfig.get()`, `CommandManager.init()`
3. `ModuleAutoLoader.load("dev.loki.lovisual.features.module.modules")` — сканирует пакет,
ищет классы с `@ModuleInfo`, создаёт через no-args конструктор и зовёт `module.postInit()`.
4. `ModuleManager.loadAllModuleConfigs()` — применяет сохранённые конфиги, включает enabled-модули.
5. `HudElements.init()`, `StaticHudElementBootstrap.init()` — HUD-элементы.
6. `Events.BUS.register(...)` — реестрируются глобальные сервисы.
7. `Lifecycle.activate()` — после этого события/тики работают.
**Вывод:** чтобы добавить модуль — просто положи класс с `@ModuleInfo` в пакет
`...modules.<категория>` и всё. Реестрировать руками не нужно.
`Lifecycle` — главный гейт: `Lifecycle.isActive()`, `canRunRender()`, `canRunHud()`.
До `activate()` модули и события не работают.
**Вывод:** новый модуль — просто класс с `@ModuleInfo` в `...modules.<категория>`; регистрировать руками не нужно.
---
## 3. Модуль — быстрый старт
Минимальный модуль (по образцу `FullBright`):
## 5. Модуль — быстрый старт
```java
package dev.loki.lovisual.features.module.modules.misc;
import dev.loki.lovisual.config.values.NumberValue;
import dev.loki.lovisual.events.EventHandler;
import dev.loki.lovisual.events.impl.GameTickEvent;
import dev.loki.lovisual.features.module.Module;
import dev.loki.lovisual.features.module.ModuleCategory;
import dev.loki.lovisual.features.module.ModuleInfo;
import dev.loki.lovisual.config.values.primitive.NumberValue;
import dev.loki.lovisual.events.meta.EventHandler;
import dev.loki.lovisual.events.impl.render.GameTickEvent;
import dev.loki.lovisual.features.module.core.Module;
import dev.loki.lovisual.features.module.core.ModuleCategory;
import dev.loki.lovisual.features.module.core.ModuleInfo;
@ModuleInfo(
id = "my_feature",
displayName = "My Feature",
aliases = {"mf"},
category = ModuleCategory.MISC,
enabledByDefault = false
)
@ModuleInfo(id = "my_feature", displayName = "My Feature", category = ModuleCategory.MISC)
public class MyFeature extends Module {
private final NumberValue<Integer> amount = num("myFeatureAmount", 3, 0, 20);
private final NumberValue<Integer> amount =
num("myFeatureAmount", 3, 0, 20); // имя_конфига, дефолт, мин, макс
@Override
public void onEnable() {
// вызывается при включении
}
@Override
public void onDisable() {
}
@Override
public void onTick() {
// каждый игровой тик, если модуль включён
@Override public void onEnable() {}
@Override public void onDisable() {}
@Override public void onTick() {
if (!isEnabled()) return;
int a = amount.get();
Minecraft mc = Minecraft.getInstance();
if (mc.level == null) return;
}
@EventHandler private void onTick(GameTickEvent e) {}
}
```
### `@ModuleInfo`
```java
id = "имя" // нижний регистр, уникальный, это ключ конфига и команды
displayName = "Имя" // показывается в UI; можно i18n-ключ
aliases = {} // необязательно
category = ModuleCategory.XXX
enabledByDefault = false
description = "" // или i18n-ключ "module.<id>.description"
```
`@ModuleInfo`: `id` (snake, уникальный, ключ конфига/команды), `displayName`, `aliases`, `category`, `enabledByDefault`, `description` (i18n).
### Автогенерируемые настройки модуля
У каждого модуля уже есть (создаются в конструкторе `Module`):
- `enabled` (BooleanValue) — состояние,
- `bind` (KeyBindSetting, значение `KeyBindValue`) — клавиша,
- `activation_source`,
- `show_in_module_list`.
Автогенерируемые поля `Module`: `enabled`/`bind`/`activation_source`/`show_in_module_list` (`postInit` → `SettingFactory.fromDefs` → `ModuleManager`).
`postInit()` вызывается автоматически лоадером: читает поля-настройки, делает
`SettingFactory.fromDefs(...)`, регистрирует модуль в `ModuleManager`.
### Хуки жизненного цикла (переопределяемые)
| Метод | Когда |
|-------|-------|
| `onEnable()` / `onDisable()` | переключение модуля |
| `onTick()` | каждый игровой тик (в мире) |
| `onFrame(float tickDelta)` | каждый кадр |
| `onKey(int key, int action)` | нажатия клавиш |
| `onRender2D(GuiGraphicsExtractor)` | vanilla HUD pass |
| `onRenderHudEngine(Renderer2D, TextRenderer, ...)` | кастомный 2D-рендер (см. §6) |
| `onRenderHudEngineForeground(...)` | 2D поверх всего |
| `onRenderWorld(PoseStack, SubmitNodeCollector)` | 3D vanilla-style |
| `onRenderWorldEngine(Renderer3D, Renderer3D, float)` | 3D через движок (см. §7) |
| `getHudPhase()` / `getWorldPhase()` | фаза, в которой рендерить |
> `getHudPhase()`/`getWorldPhase()` по умолчанию `NONE` — модуль вообще не рендерится
> диспетчером. Верните фазу, чтобы рендер-хуки вызывались (см. §6/§7).
### Управление состоянием
- `isEnabled()`, `setEnabled(boolean)`, `setEnabled(boolean, ModuleActivationSource)`, `toggle()`
- `isAvailable()` / `getAvailabilityReason()` — переопределите `getUnavailableReason()`
- `name()`, `getDisplayName()`, `getAliases()`, `getCategory()`, `getDescription()`
- `saveConfig()`, `getConfigValue(String)`, `getSettings()`
### Доступ к другим модулям
```java
import dev.loki.lovisual.features.module.Modules;
Freecam fc = Modules.get(Freecam.class); // null если не активен/не зарегистрирован
boolean on = Modules.enabled("freecam"); // по id
```
`ModuleManager.get(String id)` / `ModuleManager.get(Class)` / `ModuleManager.require(Class)` тоже доступны.
Хуки: `onEnable/onDisable/onTick/onFrame/onKey/onRender2D/onRenderHudEngine/onRenderWorldEngine`, `getHudPhase()/getWorldPhase()` (по умолч. `NONE` — не рендерится). Управление: `isEnabled()/toggle()/setEnabled()`, `isAvailable()`, `Modules.get(Freecam.class)`.
---
## 4. Настройки (ConfigValue → Setting)
## 6. Настройки (ConfigValue → SettingDef)
Базовый класс: `config/values/ConfigValue<T>` — `get()`, `set(v)`, `getName()`,
`toJson()`, `fromJson(Object)`, `toDisplay()`.
База `ConfigValue<T>`: `get()/set()/getName()/toJson()/fromJson()`.
Внутри модуля настройки создаются **хелперами** в `Module` (все `protected final`).
Имя в конфиге и `settingId` (i18n) могут различаться.
Хелперы в `Module`/`BaseHudElement` (`protected final`):
### Хелперы в Module / BaseHudElement
| Хелпер | Тип | Пример |
|--------|-----|--------|
| `bool(name, def)` | `BooleanValue` | `bool("my_on", true)` |
| `num(name, def, min, max)` | `NumberValue<Integer/Long/Float/Double>` | `num("my_num", 3, 0, 20)` |
| `mode(name, def, options...)` | `ModeValue` (String) | `mode("my_mode", "A", "A", "B", "C")` |
| `enumMode(name, defEnum)` / `enumSetting(name, defEnum, ...)` | `EnumValue<E>` | см. `Reach` |
| `num(name, def, min, max)` | `NumberValue` | `num("my_num", 3, 0, 20)` |
| `mode(name, def, opts...)` | `ModeValue` | `mode("my_mode", "A", "A","B")` |
| `enumMode(name, defEnum)` | `EnumValue` | см. `Reach` |
| `color(name, "#AARRGGBB")` | `RGBAColorValue` | `color("my_bg", "#F7343434")` |
| `colorNoAlpha(name, "#RRGGBB")` | `RGBColorValue` | `colorNoAlpha("my_fg", "#FFFFFF")` |
| `text(name, def)` | `StringValue` | `text("my_text", "")` |
| `textList(name)` / `textList(name, pickerMode)` | `SetValue` (множество строк) | `textList("my_list")` |
| `itemList(name)` | `ItemIdSetValue` (список предметов) | `itemList("my_items")` |
| `group(name, defaultsMap)` | `BooleanMapValue` (группа чекбоксов) | `group("gui_sounds", Map.of(...))` |
| `bind(name, def, mode)` | `KeyBindValue` | `bind("my_bind", "R", BindMode.PRESS)` |
| `action(name, defaultKey, mode)` | `FunctionBindSetting` | см. ниже |
| `setDefaultBind("KEY")` | — | клавиша модуля (см. `ClickGui`) |
| `text/textList/itemList/group/bind/action` | `StringValue/SetValue/ItemIdSetValue/...` | `itemList("my_items")` |
**Варианты с общим i18n** (`*Common`): `boolCommon`, `numCommon`, `modeCommon`,
`enumCommon`, `color` (без common) — берут схему из `CommonSettingSchemas`:
`*Common` варианты (`boolCommon/numCommon/enumCommon`) берут схему из `CommonSettingSchemas`.
```java
distance = numCommon("reachDistance", "range", CommonSettingSchemas.COMBAT_RANGE, 4.5, 3.0, 6.0);
mode = enumCommon("reachMode", "mode", CommonSettingSchemas.ANTICHEAT_MODE, Mode.NORMAL, Mode.values());
```
Условность: `visibleWhen(numCommon(...), () -> cond)`, `appliesWhen(setting, () -> mc.player!=null, "Только в мире")`. Экшены: `action("climb","G", PRESS)` → `isActionPressedOnce("climb")`.
### Условная видимость / доступность
```java
private final NumberValue<Double> wallDistance =
visibleWhen(numCommon("reachWallDistance", "wall_range", schema, 3.0, 0.0, 6.0),
() -> raycast.get() == RaycastMode.THROUGH_WALLS);
// недоступен (показывается серым + причина):
appliesWhen(mySetting, () -> mc.player != null, "Только в мире");
```
### Экшены (вторая кнопка на модуль)
```java
action("climb", "G", BindMode.PRESS); // создаёт FunctionBindSetting
// в тике:
if (isActionPressedOnce("climb")) { ... } // фронт нажатия
if (isActionHeld("climb")) { ... } // удержание
```
### Настройки в HUD-элементах
У `BaseHudElement` те же хелперы (`bool`, `num`, `mode`, `enumSetting`, `color`,
`textList`, `bind`, `group`, `visibleWhen`), но объявление идёт в
`defineSettings(List<SettingDef>)` (см. §9).
### Виджеты в UI (SettingDef)
`SettingDef` — GUI-нейтральный дескриптор. `SettingFactory.fromDef(def)` превращает
его в виджет. Доступные `Kind`: `BOOLEAN, NUMBER, MODE, COLOR, COLOR_NO_ALPHA,
TEXT, TEXT_LIST, COOLDOWN_RULES, PROTOCOL_HEURISTICS, GROUP, BIND`.
Для модулей SettingDef строятся автоматически из хелперов — руками не нужно.
`@DisableSettingI18n(name = false, options = true)` — отключить i18n у настройки.
HUD-элементы объявляют настройки в `defineSettings(List<SettingDef>)`; модули — полями (автоматом). Виджеты `SettingDef.Kind`: `BOOLEAN, NUMBER, MODE, COLOR, TEXT, TEXT_LIST, GROUP, BIND...` (`@DisableSettingI18n`).
---
## 5. События (EventBus)
## 7. HUD-элементы
`Events.BUS.post(event)` — доставка хендлерам. Хендлер — любой метод с одним
аргументом-наследником `Event`, помеченный `@EventHandler(priority = N)`.
Подписчики регистрируются автоматически при `ModuleManager.register()` (модули),
или явно `Events.BUS.register(obj)` (в `LoVisual.onInitializeClient`).
**Draggable** (`hud/draggable/impl/`, `@HudElementInfo(id, displayName, enabledByDefault, order)`, `DraggableHudElement`): `applyDefaultPosition(w,h)`, `renderEngine(Renderer2D, TextRenderer, GuiGraphicsExtractor, tickDelta, w,h)`, `usesEngineRenderer()=true`, `x/y/width/height`.
Модули-подписчики автоматически гейтятся: если модуль выключен — его хендлеры не вызываются.
Поэтому в хендлерах модуля проверка `if (!isEnabled()) return;` не обязательна, но
встречается в коде для ясности.
`Event` — база с `cancelled`/`cancel()`. Наследование событий поддерживается.
Полный список `events/impl/` (по имени файла):
```
AttackEntityEvent BlinkPacketEvent CombatProtocolBossbarEvent
CrosshairTargetUpdateEvent EventBreakBlock EventCollision EventPostSync
EventPushOutOfBlocks EventSync EventTargetChanged FireworkEvent GameTickEvent
I18nPreflightCollectEvent KeybindIsPressedEvent KeyInputEvent LightmapEvent
LightmapModifyEvent MovementInputEvent PacketEvent PlayerJumpEvent PlayerMoveEvent
PlayerSafeWalkEvent PlayerStepEvent PlayerStepSuccessEvent PlayerVelocityStrafe
PostPlayerUpdateEvent PvpChatEvent PvpOverlayEvent PvpTabEvent
RenderPrewarmCollectEvent RotationUpdateEvent SprintControlEvent
```
Типичные события для нового модуля:
- `GameTickEvent` — каждый тик,
- `PacketEvent` — сетевые пакеты,
- `AttackEntityEvent`, `EventTargetChanged` — бой/цели,
- `LightmapModifyEvent` — свет (пример `FullBright`),
- `PlayerMoveEvent`, `MovementInputEvent` — движение,
- `RenderPrewarmCollectEvent` — «прогреть» шрифты/текстуры для вашего рендера.
Пример:
```java
@EventHandler
private void onTick(GameTickEvent event) { ... }
```
**Static** (`hud/nondraggable/impl/`, `@HudElementRegister(order)`, `AbstractHudElement` `super(id,"Title",defaultEnabled)` + `INSTANCE`): для вкладки UI добавить `new StaticHudCard(...)` в `MenuSettingsResolver.STATIC_UI_CARDS`. Тема: `Theme.theme().accent()/windowBg()`, `HudRenderUtil.setAlpha/mixColor`, текст `Fonts.renderer(...)`.
---
## 6. 2D-рендер (HUD)
## 8. Команды
### Фазы
Верните не-`NONE` фазу из `getHudPhase()`:
```java
HudPhase: FIRST, BEFORE_MISC_OVERLAYS, AFTER_MISC_OVERLAYS, AFTER_BOSS_BAR,
BEFORE_DEMO_TIMER, BEFORE_CHAT, AFTER_SUBTITLES, LAST
```
`ModuleManager.renderHudEngine(phase, renderer, textRenderer, ctx, tickDelta)` вызывает
у модуля `onRenderHudEngine(...)`; `renderHudEngineForeground` → `onRenderHudEngineForeground`.
### Renderer2D — главный 2D API
Включается/выключается через `begin()` / `render()` (уже делает диспетчер).
Готовые методы (фасад `Renderer2D.java`, все в `px`):
- **Фигуры:** `quad(x,y,w,h,argb)`, `quad(x,y,w,h,cTL,cTR,cBR,cBL)`,
`roundedRect(x,y,w,h,radius,argb)`, `roundedRectStroke(...)`,
`roundedRectGradient(...)`, `roundedRectGlow/Shadow/SoftShadow(...)`,
`circle(cx,cy,r,argb)`, `circleStroke(...)`, `arcStroke(...)`,
`line(x1,y1,x2,y2,argb)`, `boxLines(...)`, `roundedSmokeFill(...)`.
- **Текст:** см. §6.2.
- **Предметы:** `item(ItemStack, x, y, scale)`, `itemUnscaled(...)`, `itemPivot(...)`.
- **SVG/текстуры:** `svg(Identifier/Path, x, y, w, h)`, `textureQuad(...)`, `msdfTextureQuad(...)`.
- **Effect (стеклянные панели):** `effect(UiEffectSpec)`.
### Текст
```java
TextRenderer tr = Fonts.renderer("Onest", FontInfo.Type.Regular, textRenderer);
// или Fonts.renderer("OnestMedium", Regular), "OnestBold", "Inter", "Iosevka", "Icons"...
tr.begin(scale, true, false);
double w = tr.getWidth("text", false);
double h = tr.getHeight(false);
tr.render("text", x, y, new RenderColor(0xFFFFFFFF), false);
tr.end();
```
Универсальная запись «в одном блоке»:
```java
textRenderer.begin(scale, true, false);
textRenderer.render("FPS", x, y, new RenderColor(argb), false);
textRenderer.end();
```
`TextRenderer.get()` — текущий рендерер (передаётся в хук). `HudScale.scale(screenW, screenH)`
даёт масштаб для адаптивного HUD (см. `Fps`).
### Иконки / текстуры
Идентификаторы: `Identifier.fromNamespaceAndPath("lovisual", "textures/hud/elements/fps.png")`.
SVG лежат в `assets/lovisual/svg/`.
### Анимации
`AnimationUtility`: `approach(value, target, dt, speed)`, `fast(...)`, `lerp(...)`,
`easeOutCubic`, `easeInOutCubic`, `easeOutBack`, `blink(ms)`, `deltaTime()`, `snap(...)`.
### Хелперы
- `HudRenderUtil`: `setAlpha(argb, a)`, `mixColor(a, b, t)`, `glassBackground()`,
`drawLiquidGlass(...)`, `animateVisibility(...)`, `visibilityScale(...)`.
- `HudGlobalConfig.get()`: `getFontSize()`, `getBlurRadius()` и т.д.
- Тема: `Theme.theme()` (статический импорт `dev.loki.lovisual.features.theme.Theme.theme`)
→ `Themes.Theme` c полями `windowBg, windowHeader, windowStroke, surface, surfaceHover,
cardEnabled, cardDisabled, textPrimary, textMuted, accent, accentSoft, strokeSoft`
(всё `int` ARGB). См. `Fps.updatePalette()`.
---
## 7. 3D-рендер (мир)
### Фазы
Верните не-`NONE` фазу из `getWorldPhase()`:
```java
WorldPhase: BEFORE_ENTITIES, AFTER_ENTITIES, BEFORE_TRANSLUCENT, END_MAIN, AFTER_POST_PROCESS
```
### Renderer3D — простой путь
В хуке `onRenderWorldEngine(Renderer3D renderer, Renderer3D depthRenderer, float tickDelta)`:
`ClientCommand` + `@CommandInfo(id, aliases, usage, descriptionKey)`, регистрация в `CommandManager` (ClassGraph). Префикс **`%`** (legacy `@`). `CommandPrefix` — источник истины; автокомплит `suggest()` + `UsageHints`.
```java
renderer.begin();
renderer.line(x1, y1, z1, x2, y2, z2, r, g, b, a); // линия
renderer.quad(x1,y1,z1, x2,y2,z2, x3,y3,z3, x4,y4,z4); // квад
renderer.triangle(...);
renderer.render(new PoseStack()); // в конце
```
Пример: `Tracers.onRenderWorldEngine` (линии от камеры к игрокам).
### MeshBuilder + RenderPipeline — продвинутый путь
```java
MeshBuilder mesh = new MeshBuilder(LoVisualRenderPipelines.UI_TEXTURED_ADDITIVE);
mesh.begin();
mesh.ensureQuadCapacity();
int i1 = mesh.vec2(x, y).vec2(u, v).color(r, g, b, a).next();
int i2 = mesh.vec2(...).vec2(...).color(...).next();
mesh.quad(i1, i2, i3, i4);
mesh.end();
```
Готовые пайплайны в `LoVisualRenderPipelines` (фрагмент):
```
UI_COLORED, UI_COLORED_LINES, UI_TEXTURED, UI_TEXTURED_ADDITIVE, UI_TEXT, UI_TEXT_MSDF,
UI_SVG_MSDF, UI_BLUR, WORLD_COLORED, WORLD_COLORED_LINES, WORLD_COLORED_DEPTH,
WORLD_TEXTURED, WORLD_TEXTURED_DEPTH, WORLD_TEXT, WORLD_TEXT_MSDF, WORLD_DECAL_SDF,
WORLD_COLORED_LINES_DEPTH, WORLD_WIDE_COLORED_LINES, ...HAND_*, SHADER_ESP_*, POST_FX...
```
Отправить на отрисовку (2D-стрелки из `Tracers`):
```java
MeshRenderer.begin()
.attachments(mc.gameRenderer.mainRenderTarget())
.pipeline(LoVisualRenderPipelines.UI_TEXTURED_ADDITIVE)
.mesh(mesh).transform(matrix4f)
.sampler("u_Texture", view, sampler)
.end();
```
`Renderer3D.Cull` — утилиты отсечения: `isInFrustum(...)`, `isSectionVisible(...)`, `isInFront(...)`.
---
## 8. Полезные утилиты (`util/`)
| Пакет / класс | Что даёт |
|---------------|----------|
| `util.target.TargetManager` | текущая цель: `getTarget()`, `getTarget(true/false)`, `onAttack()`, `setPredictionTarget()`, `Source` |
| `util.pvp.PvpTracker`, `PvpTargetTracker` | статистика PvP / отслеживание цели |
| `util.aiming.RotationManager` | плавные/снап-ротации |
| `util.combat.VulcanReachController` | клампинг дистанции (режим античита) |
| `util.time.GameClock` | `millis()` — игровое время (тики×50мс), вне мира — стенное |
| `util.time.TimerController` | таймеры |
| `util.input.KeyManager` | `wasPressed("func")`, `isHeld("func")`, `isComboHeldAllowScreen(...)` |
| `util.screen.ClientScreen` | текущий экран: `ClientScreen.current(mc)` / `.show(mc, screen)` |
| `util.text` | текст-хелперы |
| `util.entity.simulation.PlayerSimulationCache`, `BoatSimulationCache` | предсказание позиций |
| `util.raycast` | рейкасты |
| `util.item.OmniItemUtils` | утилиты предметов |
| `util.combat.protocol.CombatProtocolHeuristics` | эвристики античита |
| `util.logging.DebugLog` | `DebugLog.info/warn/error/config(...)` (printf-style) |
| `util.resources.RenderResourceReadiness` | готовность рендер-ресурсов |
| `features.relations.CategoryService` | `isFriend(Player)`, `getColor(Player)` — категории игроков |
| `util.FastFps` | `getFps()` — быстрый FPS без накладок |
Ленивый доступ к MC:
```java
private final Minecraft mc = Minecraft.getInstance();
```
(поле, но это ок — поле инициализируется при создании модуля, а модули создаются
в рантайме; правило «никакого `static final Minecraft` в полях»).
---
## 9. HUD-элементы
### Draggable (перетаскиваемый) — пример `Fps`
```java
@HudElementInfo(id = "fps", displayName = "FPS", enabledByDefault = true, order = 120)
public final class Fps extends DraggableHudElement {
// настройки — прямо поля:
private final NumberValue<Double> scale = num("fps_scale", 2.37, 0.5, 5.0);
private final RGBColorValue iconColor = visibleWhen(colorNoAlpha("fps_icon_color", "#FFFFFF"), this::isCustomMode);
@Override
public void applyDefaultPosition(int screenW, int screenH) { this.x = 16f; this.y = 20f; }
@Override
public boolean usesEngineRenderer() { return true; }
@Override
public void renderEngine(Renderer2D renderer, TextRenderer textRenderer,
GuiGraphicsExtractor ctx, float tickDelta,
int screenW, int screenH) {
if (!preview && !isEnabled()) { width = 0f; height = 0f; return; }
// считаем width/height, рисуем через renderer + Fonts.renderer(...)
}
}
```
- Настройки объявляются как поля с хелперами (автоматом попадают в SettingDef).
- `defaultLayout(x, y, anchorX, anchorY)` / `defaultLinkedLayout(...)` — стартовое положение.
- `x, y, width, height` — позиция и размер (обновляйте width/height в renderEngine!).
- Регистрация автоматическая (сканирование по `@HudElementInfo`).
- Интерактив: `isMouseOverInteractive(mx, my)`, `onMouseClicked(mx, my, button)`.
### Nondraggable (статичный)
Классы в `features/gui/hud/nondraggable/impl/` (CustomBar, CustomHealthBar, CustomHotbar,
BetterTooltips, BetterButtons, DynamicIsland). Наследуют `BaseHudElement`, рендер через
`renderEngine(...)` + `getRenderSpace()`/`getHudPhase()`.
---
## 10. Команды
Интерфейс `ClientCommand` + аннотация `@CommandInfo`, регистрация — вручную в `CommandManager`.
Префикс команд в чате — **`%`** (старый `@` тоже принимается). Источник истины —
`CommandPrefix` (`CHAR`/`STRING`/`isCommandLike`/`strip`). Автокомплит: имена команд +
аргументы из `suggest()`, а если команда не дала вариантов — подсказка-usage через
`UsageHints` (например `%toggle <module>`).
```java
@CommandInfo(
id = "toggle",
aliases = "t",
usage = "%toggle <module> [on|off|toggle]",
descriptionKey = "command.toggle.description"
)
@CommandInfo(id="toggle", aliases="t", usage="%toggle <module> [on|off]", descriptionKey="command.toggle.description")
public final class ToggleCommand implements ClientCommand {
@Override
public boolean execute(CommandContext ctx) {
// ctx.arg(0), ctx.arg(1), ...
CommandOutput.success("Done");
return true;
}
@Override
public List<String> suggest(CommandContext ctx, int argIndex, String token) {
return List.of();
}
public boolean execute(CommandContext ctx){ CommandOutput.success("Done"); return true; }
public List<String> suggest(CommandContext ctx,int i,String token){ return List.of(); }
}
```
- `CommandContext(mc, raw, name, args)` — `arg(i)` возвращает null если нет.
- `CommandOutput` — `success/warning/error/send(String)` с префиксом `[LoVisual]`.
- `CommandManager.init()` регистрирует команды из `impl/` (через ClassGraph, как модули).
`CommandContext.arg(i)`, `CommandOutput.success/warning/error`.
---
## 11. API для аддонов (`api/v0/`)
## 9. События (EventBus)
- `LoVisualAddon` — entrypoint `"lovisual:addon"`: `onConfigureModules`,
`onInitialize(LoVisualAddonContext)`, `onClientReady`, `onShutdown`.
- `LoVisualModuleExtension` — расширение модуля (`ModuleExtensionContext`),
перехватывает `beforeEnable/afterEnable/beforeTick/...` (см. `ModuleExtensionManager`).
- `LoVisualClientApi.get()` — `modules()`, `module(id)`, `isModuleEnabled(id)`, `setModuleEnabled`.
- `render/` — `LoVisualRenderCallback` + `LoVisualRenderStage`
(HUD_RAW, HUD_SCALED, HUD_LOGICAL, WORLD_*, SCREEN_*), `LoVisualPostProcessCallback`,
`LoVisualUniforms`, `LoVisualRenderPipelineBuilder`.
- `clickgui/` — `LoVisualClickGuiSection`, `LoVisualClickGuiRenderContext` — свои секции в кликгую.
`Events.BUS.post(event)` → `@EventHandler(priority=N)` (один аргумент-`Event`). Модули гейтятся `isEnabled()`. `Event` имеет `cancel()`.
`events/impl/` : `GameTickEvent`, `PacketEvent`, `AttackEntityEvent`, `EventBreakBlock`, `LightmapModifyEvent`, `PlayerMoveEvent`, `RenderPrewarmCollectEvent`...
```java
@EventHandler private void onTick(GameTickEvent e){ }
```
---
## 12. i18n (языковые ключи)
## 10. 2D-рендер (HUD)
Файлы: `src/main/resources/assets/lovisual/lang/en_us.json`, `ru_ru.json`.
Фазы `getHudPhase()`: `FIRST, BEFORE_MISC_OVERLAYS, AFTER_MISC_OVERLAYS, AFTER_BOSS_BAR, BEFORE_DEMO_TIMER, BEFORE_CHAT, AFTER_SUBTITLES, LAST` → `onRenderHudEngine`.
Соглашения по ключам:
- Модуль: `module.<id>`, `module.<id>.name`, `module.<id>.description`,
настройка: `module.<id>.setting.<setting_id>`, опции: `module.<id>.option.<option>`.
- HUD draggable: `hud.draggable.<id>` / `setting.draggable.<id>.<setting>`.
- HUD static: `hud.static.<id>`.
- Команды: `command.<id>.description`.
- Общие схемы: `CommonSettingSchemas` указывают `commonI18nKeys` (например `combat.range`)
— они ищутся в lang-файлах с префиксом `setting.common.<key>` (проверьте по `CommonKey`).
`I18n.get("key")` — получение перевода. Если ключа нет — возвращается сам ключ.
В `Module.getDisplayName()`/`getDescription()` fallback на `displayName`/`description`.
`Renderer2D` (фасад, `begin()/render()` делает диспетчер): `quad/roundedRect/roundedRectGradient/circle/line/item/svg/effect`. Текст: `Fonts.renderer("Onest", Regular, textRenderer).begin(scale,true,false) → getWidth/render → end()` или `textRenderer.begin...`. Анимации `AnimationUtility`, хелперы `HudRenderUtil`/`HudGlobalConfig`/`Theme.theme()`.
---
## 13. Миксины и accessors
## 11. 3D-рендер (мир)
- Все миксины в `mixins/` (`@Mixin(...)`). Пакеты: `accessors`, `iris`, `sodium`, `moreculling`,
`xaero`, `security`.
- Accessors в `mixins/accessors/` — примеры: `MinecraftAccessor`, `LocalPlayerAccessor`,
`PlayerInventoryAccessor`, `LivingEntityAccessor`, `EntityAccessor`, `ItemCooldownManagerAccessor`.
- Реестр: `resources/lovisual.mixins.json`. `LoVisualMixinPlugin` — условная загрузка.
- Правило: «никаких мёртвых миксинов» — добавляйте миксин только если реально нужен.
- `mixininterface/` — интерфейсы-протуберанцы для доступа к данным из миксинов (`IGuiGraphics`, `IEntity`).
Фазы `getWorldPhase()`: `BEFORE_ENTITIES, AFTER_ENTITIES, BEFORE_TRANSLUCENT, END_MAIN, AFTER_POST_PROCESS` → `onRenderWorldEngine(Renderer3D, Renderer3D, tickDelta)`.
Простой путь: `renderer.begin(); renderer.line(...); renderer.quad(...); renderer.render(new PoseStack());` (см. `Tracers`). Продвинутый: `MeshBuilder` + `LoVisualRenderPipelines` (`UI_COLORED, WORLD_COLORED...`). `Renderer3D.Cull.isInFrustum`.
---
## 14. Чек-лист нового модуля
## 12. Миксины и accessors
1. `@ModuleInfo` на классе в `features/module/modules/<категория>/`.
2. Унаследовать `Module`, переопределить `onEnable/onDisable/onTick` и т.д.
3. Настройки — поля с хелперами (`num`, `bool`, `mode`, `color`, `visibleWhen`…).
4. Если нужен рендер — вернуть `getHudPhase()`/`getWorldPhase()` и реализовать
`onRenderHudEngine`/`onRenderWorldEngine`.
5. Если нужны события — методы с `@EventHandler`.
6. `i18n`: добавить `module.<id>.name/description` и ключи настроек в `en_us.json`/`ru_ru.json`.
7. Проверки: `./gradlew build` (сборка), `./gradlew test` (тесты), для чистой логики — JUnit.
8. Файл ≤200 строк — иначе вынести логику в хелпер-классы в том же пакете.
Все в `mixins/` (`@Mixin`), реестр `lovisual.mixins.json` (`LoVisualMixinPlugin`). Accessors в `mixins/accessors/` (`PlayerInventoryAccessor`...), `mixininterface/` (`IGuiGraphics`). Правило: только `@Mixin` в `mixins.*`.
---
## 13. Конфиг, профили и i18n
Модули/HUD → `<game>/lovisual/*.lvcfg` (legacy `.cbcfg` читается). Профили `config/profile/` (ClickGUI → Configs).
i18n `assets/lovisual/lang/{en_us,ru_ru}.json`:
- модуль `module.<id>.description`, `setting.<id>.<setting_id>`, `option.<Option>`
- draggable `hud.draggable.<id>`, static `setting.hud_element.<id>.<key>`, команды `command.<id>.description`, общие `setting.common.<key>`
Пропущенный ключ ловит `MissingI18nReporter` в dev.
---
## 14. Полезные утилиты (`util/`)
`TargetManager`, `PvpTracker`, `RotationManager`, `GameClock.millis()`, `TimerController`, `KeyManager`, `ClientScreen`, `PlayerSimulationCache`, `OmniItemUtils`, `DebugLog`, `RenderResourceReadiness`, `CategoryService`, `FastFps`...
Ленивый `Minecraft.getInstance()` — только локально.
---
## 15. API для аддонов (`api/v0/`)
`LoVisualAddon` (`onConfigureModules/onInitialize/onClientReady/onShutdown`), `LoVisualModuleExtension` (`beforeEnable/afterEnable/beforeTick...`), `LoVisualClientApi.get().modules()/isModuleEnabled()`, `LoVisualRenderCallback` (`HUD_RAW, WORLD_...`), `LoVisualClickGuiSection`.
---
## 16. Тесты
`src/test/java/` зеркалит `main`, **только чистая логика** (без MC рантайма): `util/text/NameProtectorTest`, `config/values/*Test`, `config/profile/ConfigProfileValueCodecTest`, `events/EventBusTest` (нужен `Lifecycle.activate()`).
Выноси `static int tint(...)` / `record Layout(...)` и тесть.
---
## 17. Чек-лист нового модуля / перед коммитом
1. `@ModuleInfo` в `modules/<категория>/`, `extends Module`, `onEnable/onTick` + `@EventHandler` если нужно
2. Настройки хелперами (`num/bool/color/visibleWhen`), `i18n` в оба `lang` файла
3. Рендер: верни `getHudPhase()/getWorldPhase()` + `onRenderHudEngine/onRenderWorldEngine`
4. `./gradlew build` зелёный, `./gradlew test` зелёный, файл ≤200, папка ≤4, нет `static Minecraft` в полях, нет `System.currentTimeMillis()` → `GameClock`, нет пустышек/мёртвых миксинов, `mixin` только `@Mixin`
5. `TODO.md` отмечен, `ref/` не копировался
**Не делай:** `static final Minecraft` в полях, `this`-escape, мёртвый код, копипаст чужого `ref/`.
---
## 18. Современные рекомендации (2025-2026, web-поиск)
### Clean Code — без догматизма
- **YAGNI** «You Aren't Gonna Need It» — не пиши на будущее; **KISS** > OCP в быстро меняющемся коде. Принцип — эвристика, не закон.
- **DRY ловушка**: дублирование дешевле неверной абстракции (Sandi Metz). Выноси только *семантически* одинаковое, синтаксически похожее `for`-циклы — не повод для хелпера → лишняя связность.
- **SOLID критика**: ортодоксальный SOLID плодит `Service → RepoInterface → RepoImpl → QueryBuilder`, низкая связность, но нечитаемо и 15× медленнее (Casey Muratori «Clean Code, Horrible Performance» 2023: замена полиморфного OCP на `switch` дала 15×). Применяй умеренно, только где абстракция реально нужна.
- **Читаемость**: без глубокой вложенности — guard-clauses `if (user==null) return;`, без магических чисел → `static final MIN_DRINKING_AGE=21`, говорящие имена `customerOrderHistoryList` > `list`, комментарии *почему*, а не *что*, консистентное форматирование, DI вместо `new`.
### Java 21/25 (LTS)
- `if (obj instanceof String s)` / `record Point(int x,int y)` + `if (o instanceof Point(int x,int y))` / `switch` с pattern-matching и `case null`, `sealed interface Command permits Get,Put,Delete` — меньше кастов, безопаснее API.
- **Virtual threads** — не «pixie dust»; дают выигрыш на I/O-bound, но не на CPU-bound. **ScopedValue** (финал JDK25) + **Structured Concurrency** (preview JDK25) лучше `ThreadLocal` по памяти/синхронизации, особенно с виртуальными потоками.
- **JDK25 perf**: `String.hash` `@Stable` → константная свёртка `Map.of("constant",...)`, `Stable Value` preview, `ForkJoinPool` теперь `ScheduledExecutorService` (быстрее отмена timeout), ML-KEM/ML-DSA квантостойкая криптография ×2 на AVX-512/ARM, `HKDF` и др. — просто обнови JDK, код ускорится без изменений.
### Secure Coding (OWASP Java Cheat Sheet)
- **Injection**: только параметризация (`PreparedStatement`, JPQL `setParameter`), не конкатенация строк; для `isReachable` вместо `Runtime.exec("ping "+host)`.
- **XSS**: `OWASP Java HTML Sanitizer` + `OWASP Java Encoder` (`Encode.forHtml`) при выводе к пользователю.
- **Крипто**: никогда не пиши своё — используй JCA/JCE, `AES-GCM` + `ECDH`, каждый раз новый `nonce`, секреты — через cloud secret manager, ключи ротируй. Слабые `MD5/DES` запрещены `JEP 565` в Java 21.
### Fabric / Mixin (2025-2026)
- Порядок предпочтения: `@Inject` (callback, стекается) > `MixinExtras @WrapOperation/@ModifyExpressionValue` > `@Redirect/@ModifyConstant` (только один на таргет → конфликт) > `@Overwrite` (почти никогда).
- `@Unique modid$field`, абстрактный mixin-класс (не надо имплементить интерфейсы родителя), `@Shadow` для доступа, `(Target)(Object)this` для `this`, внутренние классы `Outer$Inner`.
- Сеть: пакеты приходят на net-thread — парсь там, а доступ к `Minecraft`/`Level` только через `client.execute` / task-queue на main thread.
- Избегай `java.awt/javax.swing` — вешает игру; не патчи Fabric API напрямую, ищи `FabricBlockSettings` и т.п.
*Источники: Medium 2025 DRY/KISS/YAGNI, Baeldung Clean Code, JavaCodeGeeks Dark Side SOLID, Oracle Inside Java 2025-2026, Fabric Wiki mixin_tips, OWASP Java Security Cheat Sheet.*
**Не делайте:**
- `static final Minecraft` в полях → только ленивый `Minecraft.getInstance()`.
- `System.currentTimeMillis()` в модулях → `GameClock.millis()`.
- this-escape в конструкторе, `#[allow(dead_code)]`-аналогов (мёртвый код удаляется),
мёртвые миксины.