feat(mod): scripted HoldMyItems on LuaJ (hand/item/model scripts, addons, vanilla overrides, particles, camera)

Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ
This commit is contained in:
loki5512344 2026-10-02 09:47:30 +02:00
parent 44353e893c
commit 0504be5b57
Signed by: boba
GPG key ID: 253067914055423B
21 changed files with 750 additions and 139 deletions

View file

@ -4,7 +4,7 @@
## Правила (ОБЯЗАТЕЛЬНЫЕ, нарушение = откат) ## Правила (ОБЯЗАТЕЛЬНЫЕ, нарушение = откат)
- **KISS / DRY / SOLID** — простая структура, без оверинжиниринга - **KISS / DRY / SOLID** — простая структура, без оверинжиниринга
- **Макс. 200 строк на файл** (было >1500) — превышение = разбить на модули - **Макс. 250 строк на файл** (было >1500) — превышение = разбить на модули
- **Макс. 4 файла на папку** — больше = новая подпапка по смыслу - **Макс. 4 файла на папку** — больше = новая подпапка по смыслу
- **Публичный API можно менять свободно** (решение владельца 2026-09-05): - **Публичный API можно менять свободно** (решение владельца 2026-09-05):
KISS важнее обратной совместимости; при рефакторинге разрешено менять KISS важнее обратной совместимости; при рефакторинге разрешено менять
@ -84,9 +84,9 @@
### Проверка: `./gradlew build` ✓, тесты ✓ (13), `rg runtime-подсистема` → 0 ### Проверка: `./gradlew build` ✓, тесты ✓ (13), `rg runtime-подсистема` → 0
## Фаза 3: Разбивка файлов-гигантов (пункт 3) — ЗАВЕРШЕНА ## Фаза 3: Разбивка файлов-гигантов (пункт 3) — ЗАВЕРШЕНА
> Стратегия: публичный фасад исключён из лимита ≤200 (делегирует ~все публичные методы), > Стратегия: публичный фасад исключён из лимита ≤250 (делегирует ~все публичные методы),
> вся логика вынесена в package-private final-хелперы в том же пакете (плоско), > вся логика вынесена в package-private final-хелперы в том же пакете (плоско),
> ≤200 строк каждый. 0 вызывающих файлов изменено. AttributeSwap/Scaffold/ElytraTarget/ > ≤250 строк каждый. 0 вызывающих файлов изменено. AttributeSwap/Scaffold/ElytraTarget/
> AutoCrystal удалены в Фазе 3.5 — их разбивать не нужно. > AutoCrystal удалены в Фазе 3.5 — их разбивать не нужно.
- [x] Renderer2D (3379) → фасад 2150 + 14 хелперов (Blur, Chamfered, Circles, Connectors, - [x] Renderer2D (3379) → фасад 2150 + 14 хелперов (Blur, Chamfered, Circles, Connectors,
Items, Lines, LiquidGlass, Orbiz, Polygons, Rects, RectsExtra, RectsGlow, SmokeFill, Textures) Items, Lines, LiquidGlass, Orbiz, Polygons, Rects, RectsExtra, RectsGlow, SmokeFill, Textures)
@ -105,15 +105,15 @@
SearchOverlay, Outline, MeshBuilder, ColorUtil + Model) SearchOverlay, Outline, MeshBuilder, ColorUtil + Model)
- [x] CustomBar (1495) → фасад 426 + 11 хелперов (XpBar, JumpBar, Locator*×7, Resolver + Model) - [x] CustomBar (1495) → фасад 426 + 11 хелперов (XpBar, JumpBar, Locator*×7, Resolver + Model)
### Проверка: после каждого файла `./gradlew build` ✓; хелперы ≤200, фасады исключены; ### Проверка: после каждого файла `./gradlew build` ✓; хелперы ≤250, фасады исключены;
### 0 вызывающих файлов изменено во всей Фазе 3 ### 0 вызывающих файлов изменено во всей Фазе 3
### Долг (файлы >200 вне списка гигантов, в т.ч. новые фасады): ### Долг (файлы >250 вне списка гигантов, в т.ч. новые фасады):
### Renderer3D 433, Module 1181, LoVisualRenderPipelines 1329, DraggableHudElementRegistry 1449, ### Renderer3D 433, Module 1181, LoVisualRenderPipelines 1329, DraggableHudElementRegistry 1449,
### Predictions 1363, DynamicIsland 1255, CustomHealthBar 412, CustomHotbar 547, HudNotifier 1116, ### Predictions 1363, DynamicIsland 1255, CustomHealthBar 412, CustomHotbar 547, HudNotifier 1116,
### Triangulator 1102, BetterButtons 1033, Themes 1012, SvgMeshBackend 1014, UiPrimitiveRenderer 1042, ### Triangulator 1102, BetterButtons 1033, Themes 1012, SvgMeshBackend 1014, UiPrimitiveRenderer 1042,
### BetterTooltips 948, CustomTabList 829, InventorySwap 1009, ModuleManager 521, LoVisual 441, ### BetterTooltips 948, CustomTabList 829, InventorySwap 1009, ModuleManager 521, LoVisual 441,
### а также многие HUD/module/rendering-файлы 200–800 строк. Разбирать по желанию. ### а также многие HUD/module/rendering-файлы 250–800 строк. Разбирать по желанию.
## Фаза 3.5: Удаление читерских модулей (легит-направление) ## Фаза 3.5: Удаление читерских модулей (легит-направление)
> Пользователь решил: LoVisual — легитное приложение. Все читерские модули удаляются. > Пользователь решил: LoVisual — легитное приложение. Все читерские модули удаляются.
@ -230,29 +230,29 @@
### CommandDiscoveryTest обновлён) ### CommandDiscoveryTest обновлён)
## Фаза 8: Структура «как у Delta» (маленькие файлы) — ПЛАН ## Фаза 8: Структура «как у Delta» (маленькие файлы) — ПЛАН
> Правило ≤200 уже есть (Фаза 1–3), но долг: ~240 файлов >200 строк. > Правило ≤250 уже есть (Фаза 1–3), но долг: ~280 файлов >250 строк.
> У Delta файлы компактные и слои разделены: api / client(features, ui, utils, processors) / > У Delta файлы компактные и слои разделены: api / client(features, ui, utils, processors) /
> inject(accessors, invokers, mixin). Наша пакетная структура сопоставима — проблема в размере файлов. > inject(accessors, invokers, mixin). Наша пакетная структура сопоставима — проблема в размере файлов.
- [ ] Топ-гигантов к разбивке (после Фазы 3): LoVisualRenderPipelines 1527, - [ ] Топ-гигантов к разбивке (после Фазы 3): LoVisualRenderPipelines 1527,
DraggableHudElementRegistry 1449, Predictions 1363, DynamicIsland 1254, Module 1181, DraggableHudElementRegistry 1449, Predictions 1363, DynamicIsland 1254, Module 1181,
HudNotifier 1116, AltManager 1115, Triangulator 1102, TargetESP 1061, UiPrimitiveRenderer 1042 HudNotifier 1116, AltManager 1115, Triangulator 1102, TargetESP 1061, UiPrimitiveRenderer 1042
- [ ] Стратегия: как в Фазе 3 — фасад + package-private хелперы ≤200, 0 изменений вызывающих, - [ ] Стратегия: как в Фазе 3 — фасад + package-private хелперы ≤250, 0 изменений вызывающих,
`./gradlew build` после каждого; по мере касания файлов в Фазе 7 — разбивать на ходу `./gradlew build` после каждого; по мере касания файлов в Фазе 7 — разбивать на ходу
- [ ] Facades >200 (Renderer2D 2150 и т.п.) — оставить как есть (исключение по правилам) - [ ] Facades >250 (Renderer2D 2150 и т.п.) — оставить как есть (исключение по правилам)
- [ ] Не смешивать с Фазой 7: разбивка структуры — отдельными проходами, иначе diff-каша - [ ] Не смешивать с Фазой 7: разбивка структуры — отдельными проходами, иначе diff-каша
### Проверка: `./gradlew test` зелёный ### Проверка: `./gradlew test` зелёный
## Фаза 8.5: Аудит структуры (правила ≤200 строк / ≤4 файла) — ПОЛНЫЙ СПИСОК НА РЕФАКТОРИНГ ## Фаза 8.5: Аудит структуры (правила ≤250 строк / ≤4 файла) — ПОЛНЫЙ СПИСОК НА РЕФАКТОРИНГ
> Снимок 2026-09-09 (`src/main/java`, 1459 файлов / 197k строк). > Снимок 2026-09-09 (`src/main/java`, 1459 файлов / 197k строк).
> **Гейт перед Фазой 9.2 (Optimize)**: движковые файлы, которые Optimize будет трогать, > **Гейт перед Фазой 9.2 (Optimize)**: движковые файлы, которые Optimize будет трогать,
> должны быть разбиты заранее. 9.1 (мёртвые грузы) от этого НЕ зависит — идёт своим порядком. > должны быть разбиты заранее. 9.1 (мёртвые грузы) от этого НЕ зависит — идёт своим порядком.
> Стратегия та же, что Фаза 3/8: фасад + package-private хелперы ≤200, 0 правок вызывающих, > Стратегия та же, что Фаза 3/8: фасад + package-private хелперы ≤250, 0 правок вызывающих,
> билд после каждого файла. Фасад-исключение (Renderer2D) — см. оговорку ниже. > билд после каждого файла. Фасад-исключение (Renderer2D) — см. оговорку ниже.
### Итоговая картина ### Итоговая картина
- **Файлы >200 строк: 244 шт.** → >800 строк: 26 | 400–800: 78 | 201–400: 140 - **Файлы >250 строк: 244 шт.** → >800 строк: 26 | 400–800: 78 | 201–400: 140
- **Папки >4 файлов: 86 шт.** - **Папки >4 файлов: 86 шт.**
- Разбивка больших файлов по зонам: features/gui — 73 | render/engine — 54 | - Разбивка больших файлов по зонам: features/gui — 73 | render/engine — 54 |
features/module — 35 | util — ~35 | mixins — 3 | config — ~6 | прочее — остальное features/module — 35 | util — ~35 | mixins — 3 | config — ~6 | прочее — остальное
@ -296,7 +296,7 @@
> Проверено 2026-09-28: `find src/main/java -name "*.java" | xargs wc -l` → 0 файлов >600 строк > Проверено 2026-09-28: `find src/main/java -name "*.java" | xargs wc -l` → 0 файлов >600 строк
> (самый крупный — Renderer2D.java 474, осознанный фасад-делегат, исключение Фазы 8). Все 53 > (самый крупный — Renderer2D.java 474, осознанный фасад-делегат, исключение Фазы 8). Все 53
> пункта очереди ниже закрыты. Остаток структурного долга — 8.5.3 (201–400 строк), не марафон, > пункта очереди ниже закрыты. Остаток структурного долга — 8.5.3 (201–400 строк), не марафон,
> а правило «трогаешь файл по другой задаче — первым коммитом режешь до ≤200». > а правило «трогаешь файл по другой задаче — первым коммитом режешь до ≤250».
> Формат: файл (строк) → идея разбивки. Порядок сверху вниз. Facade-оговорка: > Формат: файл (строк) → идея разбивки. Порядок сверху вниз. Facade-оговорка:
> Renderer2D остаётся фасадом-делегатом (правило Фазы 8), но его вспомогательные > Renderer2D остаётся фасадом-делегатом (правило Фазы 8), но его вспомогательные
> классы (kawase/blur/glass) — вынести из OrderedUiBatcher (935) в `ui/blur/`. > классы (kawase/blur/glass) — вынести из OrderedUiBatcher (935) в `ui/blur/`.
@ -304,15 +304,15 @@
далее 1597→475 наследованием (волна 2026-09-25 ниже) далее 1597→475 наследованием (волна 2026-09-25 ниже)
(исключение по правилу Фазы 8) — делегирование оставлено; −234 мёртвые шимы (исключение по правилу Фазы 8) — делегирование оставлено; −234 мёртвые шимы
совместимости, −271 недостижимые перегрузки liquidGlass/item/Corners, совместимости, −271 недостижимые перегрузки liquidGlass/item/Corners,
−115 прочие мёртвые перегрузки+импорты; макс. тело метода 35 строк (блоков >200 нет), −115 прочие мёртвые перегрузки+импорты; макс. тело метода 35 строк (блоков >250 нет),
хелперы в `renderer/renderer2d/` (Rects2D/Strokes2D/…), build+test ✓ хелперы в `renderer/renderer2d/` (Rects2D/Strokes2D/…), build+test ✓
- [x] LoVisualAltManagerScreen 1113 → ГОТОВО (2026-09-10): 11 файлов `mainmenu/alt/` - [x] LoVisualAltManagerScreen 1113 → ГОТОВО (2026-09-10): 11 файлов `mainmenu/alt/`
(Screen 132, Controller 200, Ops 59, Anims 108 + auth/ 171 + render/ 157/156/139/100 (Screen 132, Controller 250, Ops 59, Anims 108 + auth/ 171 + render/ 157/156/139/100
+ render/cards/ 178/166), все ≤200, build+test ✓ + render/cards/ 178/166), все ≤250, build+test ✓
- [x] UiPrimitiveRenderer 1053 → ГОТОВО (2026-09-10): фасад 188 (коннекторы) + - [x] UiPrimitiveRenderer 1053 → ГОТОВО (2026-09-10): фасад 188 (коннекторы) +
render/primitive/ 4 (Context 153, Props 158, Painter 145, GlowPainter 147) + render/primitive/ 4 (Context 153, Props 158, Painter 145, GlowPainter 147) +
render/rect/ 2 (RectPainter 150, CornerPainter 150), build+test ✓ render/rect/ 2 (RectPainter 150, CornerPainter 150), build+test ✓
- [x] BetterButtons 1032 → ГОТОВО 7e86f8d: 388 + 7×≤200 (state/hover/parallax/queue/render) — extraction+tooltip+render → `hud/nondraggable/impl/buttons/` - [x] BetterButtons 1032 → ГОТОВО 7e86f8d: 388 + 7×≤250 (state/hover/parallax/queue/render) — extraction+tooltip+render → `hud/nondraggable/impl/buttons/`
- [x] InventorySwap 1031 → УДАЛЁН 5acafc8: чит-инфра, заменён ShitEsp + хотбар-only — по стратегиям свапа; чистая логика отдельными классами с тестами - [x] InventorySwap 1031 → УДАЛЁН 5acafc8: чит-инфра, заменён ShitEsp + хотбар-only — по стратегиям свапа; чистая логика отдельными классами с тестами
- [x] SvgMeshBackend 1014 → ГОТОВО c11ebe3/aafe7de: 1020→267 + Parser181/Geometry173/Texture223/Render35/Cache25 — parser/triangulate/cache разнести - [x] SvgMeshBackend 1014 → ГОТОВО c11ebe3/aafe7de: 1020→267 + Parser181/Geometry173/Texture223/Render35/Cache25 — parser/triangulate/cache разнести
- [x] Themes 1012 → ГОТОВО e51a610: 1012→562 + 3×163 пресета — реестр тем в `theme/presets/` (по 4 темы/файл), дефолты отдельно - [x] Themes 1012 → ГОТОВО e51a610: 1012→562 + 3×163 пресета — реестр тем в `theme/presets/` (по 4 темы/файл), дефолты отдельно
@ -322,7 +322,7 @@
- [x] Module 989 → ГОТОВО c8da336: 1031→139 + 4×≤134 (ext/) — базовый класс: settings-механику вынести в `module/ext/` (осторожно, общий предок!) - [x] Module 989 → ГОТОВО c8da336: 1031→139 + 4×≤134 (ext/) — базовый класс: settings-механику вынести в `module/ext/` (осторожно, общий предок!)
- [x] LoVisualAddonManagerScreen 989 → ГОТОВО de56b4b9: 989→112 + 10 файлов mainmenu/addon/ - [x] LoVisualAddonManagerScreen 989 → ГОТОВО de56b4b9: 989→112 + 10 файлов mainmenu/addon/
(Controller 164, Ops 112, PanelAnims 56, auth/AddonScanState 52 + render/ 115/162/183 (Controller 164, Ops 112, PanelAnims 56, auth/AddonScanState 52 + render/ 115/162/183
+ render/cards/ 79/88), все ≤200 — по образцу AltManager + render/cards/ 79/88), все ≤250 — по образцу AltManager
- [x] Chams 976 → ГОТОВО 78b5335: 985→129 + 4×≤195 — режимы рендера в подклассы-политики (фасад модуля тонкий) - [x] Chams 976 → ГОТОВО 78b5335: 985→129 + 4×≤195 — режимы рендера в подклассы-политики (фасад модуля тонкий)
- [x] ClickGuiPickerState 962 → ГОТОВО 234f0d9: 970→166 + 4×≤140; CombatProtocolHeuristicsEditorState 932→79 +4×≤192 03ae4a6 — state/actions хелперы - [x] ClickGuiPickerState 962 → ГОТОВО 234f0d9: 970→166 + 4×≤140; CombatProtocolHeuristicsEditorState 932→79 +4×≤192 03ae4a6 — state/actions хелперы
- [x] ModulesMenuScreen 960 → ГОТОВО 234f0d9: 966→145 + 4×≤178 — рендер категорий отдельно от состояния - [x] ModulesMenuScreen 960 → ГОТОВО 234f0d9: 966→145 + 4×≤178 — рендер категорий отдельно от состояния
@ -332,8 +332,8 @@
Cosmetics built-in (2026-09-11, см. Фазу 7), Cosmetics built-in (2026-09-11, см. Фазу 7),
ShaderPatchCompiler 843 → ГОТОВО 2b55fbc: 843→66+4×≤174; OnlineRelationPlayerPickerComponent 837→74+4×≤192 17fc665; CustomTabList 830 → ГОТОВО 9e9b71d: 834→136+4×≤166, ShaderPatchCompiler 843 → ГОТОВО 2b55fbc: 843→66+4×≤174; OnlineRelationPlayerPickerComponent 837→74+4×≤192 17fc665; CustomTabList 830 → ГОТОВО 9e9b71d: 834→136+4×≤166,
GameRendererMixin 830 → ГОТОВО f45f46e: 830→344 (только хуки+Host/BobInvoker) + GameRendererMixin 830 → ГОТОВО f45f46e: 830→344 (только хуки+Host/BobInvoker) +
mixins/render/main/handlers/ 4×≤200 (ProjectionHelper/WorldEngine/WorldPost/WorldDebug), Predictions 805 → ГОТОВО (см. хвост) mixins/render/main/handlers/ 4×≤250 (ProjectionHelper/WorldEngine/WorldPost/WorldDebug), Predictions 805 → ГОТОВО (см. хвост)
- [x] Хвост 400–800: ЧАСТИЧНО — ConfigProfilesComponent 799→125 d95bfec, HitEffect 770→120 5e1ce05, PlayerMovementSimulation 757→157 4b253d1, ProjectilePuncher 747→104 fba4f7d, MediaPlayer 739→174 8ce3927, Crosshair 737→143 4cf0970, Itemizer 699→193/Cooldowns 707→151 11245ac, Potions 617→176 2e4912a, Admins 702→118 f18cb96, Keybinds 611→157 cc18feb, ModuleList 499→181 52327dc, Radar 392→161 f580373, Fps 309→168 5df2cc0, Memory 304→151 de7cc2b, Tps 339→154 e167f0c, Ping 309→145 15d04cb, SpeedBps 334→156 b78a638, GameTime 314→150 922d3bc, SystemTime 300→136 3cb6e40, Coordinates 337→174 9412982, Armor 324→162 932639d, Inventory 476→153 f4124f4, BlockHighlight 692→271+4×≤200 9f76e5a5 (scene/highlight/), см. /tmp-снимок в истории команд: ConfigProfilesComponent 791, - [x] Хвост 400–800: ЧАСТИЧНО — ConfigProfilesComponent 799→125 d95bfec, HitEffect 770→120 5e1ce05, PlayerMovementSimulation 757→157 4b253d1, ProjectilePuncher 747→104 fba4f7d, MediaPlayer 739→174 8ce3927, Crosshair 737→143 4cf0970, Itemizer 699→193/Cooldowns 707→151 11245ac, Potions 617→176 2e4912a, Admins 702→118 f18cb96, Keybinds 611→157 cc18feb, ModuleList 499→181 52327dc, Radar 392→161 f580373, Fps 309→168 5df2cc0, Memory 304→151 de7cc2b, Tps 339→154 e167f0c, Ping 309→145 15d04cb, SpeedBps 334→156 b78a638, GameTime 314→150 922d3bc, SystemTime 300→136 3cb6e40, Coordinates 337→174 9412982, Armor 324→162 932639d, Inventory 476→153 f4124f4, BlockHighlight 692→271+4×≤250 9f76e5a5 (scene/highlight/), см. /tmp-снимок в истории команд: ConfigProfilesComponent 791,
HitEffect 770, PlayerMovementSimulation 755, ProjectilePuncher 746, MediaPlayer 739, HitEffect 770, PlayerMovementSimulation 755, ProjectilePuncher 746, MediaPlayer 739,
Crosshair 737, MeshBuilder 729, SettingsPanelComponent 703, Admins 702, Cooldowns 701, Crosshair 737, MeshBuilder 729, SettingsPanelComponent 703, Admins 702, Cooldowns 701,
Itemizer 699, LocalPlayerMixin 687, Trails 681, ItemBatchRenderer 673, Itemizer 699, LocalPlayerMixin 687, Trails 681, ItemBatchRenderer 673,
@ -345,7 +345,7 @@
CustomTextRenderer 229→195 (GradientTexts), CustomBar 438→185 (+CustomBarModel константы CustomTextRenderer 229→195 (GradientTexts), CustomBar 438→185 (+CustomBarModel константы
+bars/impl/BarSettings, forwardеры удалены, mixins правлены на реальные рендереры) 4e442fb7, +bars/impl/BarSettings, forwardеры удалены, mixins правлены на реальные рендереры) 4e442fb7,
CustomHealthBar 406→146 (+bars/impl/HealthBarPainter+HealthScriptRenderer, dead-code удалён) a3415435. CustomHealthBar 406→146 (+bars/impl/HealthBarPainter+HealthScriptRenderer, dead-code удалён) a3415435.
bars/ закрыт: все файлы ≤200, impl/ = 3 файла. bars/ закрыт: все файлы ≤250, impl/ = 3 файла.
- [x] Волна 2026-09-25 (реестры): Renderer2D 2179→1597 (см. строку 8.5.2 выше, - [x] Волна 2026-09-25 (реестры): Renderer2D 2179→1597 (см. строку 8.5.2 выше,
7b848ddd+464f7868+098c5515), LoVisualRenderPipelines 432→311 9577ed3d 7b848ddd+464f7868+098c5515), LoVisualRenderPipelines 432→311 9577ed3d
(SHADER_* 105 констант → pipeline/ShaderIds 130, мёртвые импорты удалены; (SHADER_* 105 констант → pipeline/ShaderIds 130, мёртвые импорты удалены;
@ -355,7 +355,7 @@
Path(245) extends Rounded(315) extends Base(23); API вызывающих не менялся Path(245) extends Rounded(315) extends Base(23); API вызывающих не менялся
(0 правок callers, static-import ITEM_OVERLAY_* и Renderer2D.LiquidGlassPreset (0 правок callers, static-import ITEM_OVERLAY_* и Renderer2D.LiquidGlassPreset
работают по наследству); renderer/facade/ = 4 файла, facade/shape/ = 2 ✓ лимит; работают по наследству); renderer/facade/ = 4 файла, facade/shape/ = 2 ✓ лимит;
Base.self() для this→Renderer2D-делегатов; семейства >200 — фасад-части Base.self() для this→Renderer2D-делегатов; семейства >250 — фасад-части
(исключение TODO:Facades); 37c7c410+64e23ef8+9ae72eeb+2b8c40a3+6204017b+ (исключение TODO:Facades); 37c7c410+64e23ef8+9ae72eeb+2b8c40a3+6204017b+
9fffc58d, build+test+checkstyle ✓. 9fffc58d, build+test+checkstyle ✓.
@ -373,9 +373,9 @@
76bd8e95; BetterButtons 381→197 (настройки → ButtonStyleSettings по образцу 76bd8e95; BetterButtons 381→197 (настройки → ButtonStyleSettings по образцу
BarSettings, иконки → ButtonIconRenderer, −3 мёртвых метода без вызовов) BarSettings, иконки → ButtonIconRenderer, −3 мёртвых метода без вызовов)
71f779c8; `./gradlew build` ✓ test 438 ✓ checkFolderLimit ✓ на каждом шаге; 71f779c8; `./gradlew build` ✓ test 438 ✓ checkFolderLimit ✓ на каждом шаге;
файлов >200: 155 → 151 файлов >250: 155 → 151
- [ ] Не делать отдельным марафоном: при правке файла по другой фазе — первым коммитом - [ ] Не делать отдельным марафоном: при правке файла по другой фазе — первым коммитом
разбивка на ≤200, вторым — целевое изменение (иначе diff-каша, правило Фазы 8) разбивка на ≤250, вторым — целевое изменение (иначе diff-каша, правило Фазы 8)
### 8.5.4 Порядок и критерии готовности ### 8.5.4 Порядок и критерии готовности
- [x] Очередь: 8.5.1 крупные папки (mixins, visuals, clickgui/settings, hud impl) → - [x] Очередь: 8.5.1 крупные папки (mixins, visuals, clickgui/settings, hud impl) →
@ -384,12 +384,12 @@
- [ ] После каждого шага: `./gradlew build` ✓, `./gradlew test` ✓; конфиг-id модулей и - [ ] После каждого шага: `./gradlew build` ✓, `./gradlew test` ✓; конфиг-id модулей и
ключи сеттингов НЕ ломаем (совместимость .lvcfg); скрипты-переименования пакетов ключи сеттингов НЕ ломаем (совместимость .lvcfg); скрипты-переименования пакетов
не трогают string-literals id — проверять grep'ом после moves. не трогают string-literals id — проверять grep'ом после moves.
- [x] Критерий закрытия для старта 9.2: ноль файлов >200 в путях, которые трогает Optimize - [x] Критерий закрытия для старта 9.2: ноль файлов >250 в путях, которые трогает Optimize
(Renderer2D-секции blur/glass, OrderedUiBatcher, HUD-имплементации bgEffect, (Renderer2D-секции blur/glass, OrderedUiBatcher, HUD-имплементации bgEffect,
MsdfFont/CustomTextRenderer), и ноль папок >4 в этих же пакетах. Полный ноль по всем MsdfFont/CustomTextRenderer), и ноль папок >4 в этих же пакетах. Полный ноль по всем
244/86 — долгий хвост, 9.2 им не блокируется. 244/86 — долгий хвост, 9.2 им не блокируется.
→ 2026-09-24: MsdfFont 155 ✓, OrderedUiBatcher 180 ✓, CustomTextRenderer 229→195 ✓ → 2026-09-24: MsdfFont 155 ✓, OrderedUiBatcher 180 ✓, CustomTextRenderer 229→195 ✓
(glyph-pass вынесен в GradientTexts). Остаток гейта — bgEffect-HUD >200: (glyph-pass вынесен в GradientTexts). Остаток гейта — bgEffect-HUD >250:
TargetHud 333, Triangulator 304, Scoreboard 233 — ВСЕ фасады Фазы 3 с helper-пакетами TargetHud 333, Triangulator 304, Scoreboard 233 — ВСЕ фасады Фазы 3 с helper-пакетами
(targethud/render/, triangulator/{render,Solver,Tracker,View}, panels/scoreboard/) → (targethud/render/, triangulator/{render,Solver,Tracker,View}, panels/scoreboard/) →
исключение по правилу фасадов. ГЕЙТ 9.2 ЗАКРЫТ. исключение по правилу фасадов. ГЕЙТ 9.2 ЗАКРЫТ.
@ -429,7 +429,7 @@
### 9.2 Модуль **Optimize** (misc) — ручной рубильник экономии ### 9.2 Модуль **Optimize** (misc) — ручной рубильник экономии
> ⛔ ГЕЙТ: не начинать, пока не закрыта Фаза 8.5 (рефакторинг-аудитStructure ниже) — > ⛔ ГЕЙТ: не начинать, пока не закрыта Фаза 8.5 (рефакторинг-аудитStructure ниже) —
> Optimize трогает Renderer2D/OrderedUiBatcher/HUD-имплементации, все они >200 строк. > Optimize трогает Renderer2D/OrderedUiBatcher/HUD-имплементации, все они >250 строк.
> Идея владельца: один тумблер «хочу максимум FPS». НИКАКОГО автовключения по FPS. > Идея владельца: один тумблер «хочу максимум FPS». НИКАКОГО автовключения по FPS.
> Пока активен — движок и HUD работают в лёгком режиме. > Пока активен — движок и HUD работают в лёгком режиме.
- [x] Каркас — ГОТОВО ade82f06: `modules/misc/optimize/Optimize.java` (в подпапке — лимит - [x] Каркас — ГОТОВО ade82f06: `modules/misc/optimize/Optimize.java` (в подпапке — лимит
@ -705,8 +705,16 @@
hand_pose в `submitArmWithItem`, item_pose в `renderItem` (first person). 7 тестов `LuaSandboxTest`. hand_pose в `submitArmWithItem`, item_pose в `renderItem` (first person). 7 тестов `LuaSandboxTest`.
Не сделано из 1-8: `hand_relative_pose`, аддоны (`*_addon.lua`), подмена ванильного equip/swing скриптом, Не сделано из 1-8: `hand_relative_pose`, аддоны (`*_addon.lua`), подмена ванильного equip/swing скриптом,
обе руки всегда, ItemStack-поля. Не проверено вживую: направление осей M:rotate на реальной руке. обе руки всегда, ItemStack-поля. Не проверено вживую: направление осей M:rotate на реальной руке.
- [ ] 1b. HMI хвост (пункты 9-16): счётчики свинга и события, обе руки, ItemStack-поля, use_duration/using_item, - [x] 1b. HMI хвост (сделано): 4 вида скриптов `hand_pose`, `hand_relative_pose`, `item_pose`, `item_model` + аддоны
аниматор частей модели (`item_model.lua`), камера из Lua, Lua-частицы, клавиша осмотра. `hand_addon`, `hand_relative_addon`, `item_addon`, `item_model_addon` (только из `config/lovisual/holdmyitems/`,
выполняются сразу после основного). `V:cancelEquip/cancelSwing(bool)` отключают ваниль (флаги читаются на следующем
кадре); настройка «Всегда рисовать обе руки» (`always_hands`); `animator` (`ext/ModelAnimator` + миксин
`ItemModelAnimatorMixin` подменяет список квадов слоя, операции rotate/move/scale по диапазону индексов, pivot
в пикселях); `particleManager:addParticle{...}` с Lua-колбэком `tick` (`ext/HandParticles`, аддитивный BLOOM в
мире); `C:setCamRot/setCamPos` (`CameraScriptMixin`, действует 150 мс после вызова). Встроенные примеры для всех
скриптов. Тесты: `QuadOpsTest`. Не сделано: события `swingMHand/interact/blockBreaking`, `use_duration`/
`using_item` для моделей, ItemStack-поля (`setRenderAsBlock`), крен камеры (roll), загрузка из ресурс-паков.
Не проверено вживую: оси и знаки M:rotate, видимость Lua-частиц, анимация квадов на реальной модели.
- [ ] 2. Wings: режим «Модель» (Bedrock geo.json + animation.json, 14 видов, общий `BedrockGeoParser` + бейк + сэмплер). - [ ] 2. Wings: режим «Модель» (Bedrock geo.json + animation.json, 14 видов, общий `BedrockGeoParser` + бейк + сэмплер).
- [ ] 3. Модуль Knife (Melee Skins): 33 скина, клипы draw/inspect/melee_1..3/run, first/third person. - [ ] 3. Модуль Knife (Melee Skins): 33 скина, клипы draw/inspect/melee_1..3/run, first/third person.
- [ ] 4. Быстрые модули: TNTTimer, AutoSprint, TotemCounter, PlaceAnimation, EnchantGlint, ArmorColor, GodRays, ProjectileTrails. - [ ] 4. Быстрые модули: TNTTimer, AutoSprint, TotemCounter, PlaceAnimation, EnchantGlint, ArmorColor, GodRays, ProjectileTrails.
@ -725,7 +733,7 @@
(`ClickGuiScreenManager`, три места `new ClickGuiScreen()`), `classic` по умолчанию, второй вид позже; (`ClickGuiScreenManager`, три места `new ClickGuiScreen()`), `classic` по умолчанию, второй вид позже;
бинд и звуки остаются в модуле `ClickGui`. Ждёт решения, какой второй вид делать первым. бинд и звуки остаются в модуле `ClickGui`. Ждёт решения, какой второй вид делать первым.
- [ ] `.gitignore` для `server-plugin/.classpath`, `.project`, `.settings/`. - [ ] `.gitignore` для `server-plugin/.classpath`, `.project`, `.settings/`.
- [ ] Фаза 8.5.3 по-прежнему в очереди (около 150 файлов >200 строк), по мере касания. - [ ] Фаза 8.5.3 по-прежнему в очереди (около 150 файлов >250 строк), по мере касания.
- [ ] Приёмка серверного плагина (Фаза 11 в корневом TODO.md): Velocity + `%policy` на стенде. - [ ] Приёмка серверного плагина (Фаза 11 в корневом TODO.md): Velocity + `%policy` на стенде.
### Заметки для следующих сессий ### Заметки для следующих сессий
@ -735,5 +743,5 @@
`flock /tmp/claude-1000/gradle.lock ./gradlew build --offline -q`. `flock /tmp/claude-1000/gradle.lock ./gradlew build --offline -q`.
- Новые классы-помощники для миксинов НЕЛЬЗЯ класть в пакет `dev.loki.lovisual.mixins.*` - Новые классы-помощники для миксинов НЕЛЬЗЯ класть в пакет `dev.loki.lovisual.mixins.*`
(Mixin: `IllegalClassLoadError`); им место в `mixinhandlers/` или `render/`. (Mixin: `IllegalClassLoadError`); им место в `mixinhandlers/` или `render/`.
- Лимиты проекта: ≤200 строк на файл, ≤4 файла на папку (`checkFolderLimit` падает при нарушении), - Лимиты проекта: ≤250 строк на файл, ≤4 файла на папку (`checkFolderLimit` падает при нарушении),
без длинного тире в текстах сайта и ответах. без длинного тире в текстах сайта и ответах.

View file

@ -5,6 +5,7 @@ import dev.loki.lovisual.config.values.primitive.BooleanValue;
import dev.loki.lovisual.config.values.primitive.NumberValue; import dev.loki.lovisual.config.values.primitive.NumberValue;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.HandScripts; import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.HandScripts;
import dev.loki.lovisual.features.module.core.Module; import dev.loki.lovisual.features.module.core.Module;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.features.module.core.ModuleCategory; import dev.loki.lovisual.features.module.core.ModuleCategory;
import dev.loki.lovisual.features.module.core.ModuleInfo; import dev.loki.lovisual.features.module.core.ModuleInfo;
import dev.loki.lovisual.util.animation.EaseCurves; import dev.loki.lovisual.util.animation.EaseCurves;
@ -55,6 +56,28 @@ public final class HoldMyItems extends Module {
public final NumberValue<Float> gripTurn = visibleWhen(num("grip_turn", 0.0f, -90.0f, 90.0f), gripEnabled::get); public final NumberValue<Float> gripTurn = visibleWhen(num("grip_turn", 0.0f, -90.0f, 90.0f), gripEnabled::get);
public final NumberValue<Float> gripRoll = visibleWhen(num("grip_roll", 0.0f, -180.0f, 180.0f), gripEnabled::get); public final NumberValue<Float> gripRoll = visibleWhen(num("grip_roll", 0.0f, -180.0f, 180.0f), gripEnabled::get);
/** Принудительно рисовать обе руки (как AlwaysVisibleHand), если включён режим со скриптами. */
public final BooleanValue alwaysHands = visibleWhen(bool("always_hands", false), this::scriptsActive);
public boolean forcesBothHands() {
return isEnabled() && scriptsActive() && alwaysHands.get();
}
/** Скрипты, если они уже создавались (для миксинов камеры и свинга), без ленивой загрузки LuaJ. */
public HandScripts scriptsOrNull() {
return isEnabled() && scriptsActive() ? scripts : null;
}
@Override
public void onTick() {
if (scripts != null && scriptsActive()) scripts.particles().tick();
}
@Override
public void onRenderWorldEngine(Renderer3D renderer, Renderer3D depth, float partialTicks) {
if (scripts != null && isEnabled() && scriptsActive()) scripts.particles().render(renderer, partialTicks);
}
public boolean dynamicsActive() { public boolean dynamicsActive() {
return !"Scripts".equals(handMode.get()); return !"Scripts".equals(handMode.get());
} }

View file

@ -1,14 +1,17 @@
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua; package dev.loki.lovisual.features.module.modules.visuals.items.held.lua;
import com.mojang.blaze3d.vertex.PoseStack; import com.mojang.blaze3d.vertex.PoseStack;
import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.Easings; import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.Easings;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.EntityApi; import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.EntityApi;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.LuaTables; import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.LuaTables;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.PoseApi; import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.PoseApi;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext.HandParticles;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext.ModelAnimator;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext.VanillaApi;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.runtime.LuaSandbox; import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.runtime.LuaSandbox;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.runtime.ScriptFiles;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.runtime.ScriptSlot; import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.runtime.ScriptSlot;
import net.fabricmc.loader.api.FabricLoader;
import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems;
import net.minecraft.client.player.AbstractClientPlayer; import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.HumanoidArm; import net.minecraft.world.entity.HumanoidArm;
@ -22,30 +25,24 @@ import org.luaj.vm2.LuaValue;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
/** /**
* Скрипты руки: {@code hand_pose.lua} (поза всей руки) и {@code item_pose.lua} (поза предмета). * Скрипты руки (см. {@link ScriptKind}): hand_pose, hand_relative_pose, item_pose, item_model и их аддоны.
* Источник: {@code config/lovisual/holdmyitems/<name>.lua} (горячая перезагрузка по mtime раз в * Порядок за кадр руки: hand_pose, hand_relative_pose, (ваниль), item_pose, item_model. Источник и горячая
* секунду), иначе встроенный пример из ресурсов мода. Ошибка скрипта пишется в лог один раз и * перезагрузка по mtime раз в секунду: {@link ScriptFiles}. Ошибка скрипта пишется в лог один раз и отключает
* отключает только этот скрипт до правки файла. * только этот скрипт до правки файла.
*/ */
public final class HandScripts { public final class HandScripts {
public static final String HAND_POSE = "hand_pose";
public static final String ITEM_POSE = "item_pose";
private static final Logger LOG = LoggerFactory.getLogger(HandScripts.class); private static final Logger LOG = LoggerFactory.getLogger(HandScripts.class);
private static final long RECHECK_MS = 1000L; private static final long RECHECK_MS = 1000L;
private final LuaSandbox sandbox = new LuaSandbox(); private final LuaSandbox sandbox = new LuaSandbox();
private final PoseApi pose = new PoseApi(); private final PoseApi pose = new PoseApi();
private final long[] stamps = {Long.MIN_VALUE, Long.MIN_VALUE}; private final VanillaApi vanilla = new VanillaApi();
private final ScriptSlot[] slots = new ScriptSlot[2]; private final HandParticles particles = new HandParticles(sandbox, pose);
private final ScriptKind[] kinds = ScriptKind.values();
private final ScriptSlot[][] slots = new ScriptSlot[kinds.length][2];
private final long[][] stamps = new long[kinds.length][2];
private final float[][] lastState = new float[2][2]; private final float[][] lastState = new float[2][2];
private long lastCheck; private long lastCheck;
private Player player; private Player player;
@ -56,66 +53,82 @@ public final class HandScripts {
g.set("M", pose.table()); g.set("M", pose.table());
g.set("P", EntityApi.player(() -> player)); g.set("P", EntityApi.player(() -> player));
g.set("I", EntityApi.item(() -> stack)); g.set("I", EntityApi.item(() -> stack));
g.set("V", vanilla.vanillaTable());
g.set("C", vanilla.cameraTable());
g.set("animator", ModelAnimator.INSTANCE.table());
g.set("particleManager", particles.table());
LuaTable easings = new LuaTable(); LuaTable easings = new LuaTable();
Easings.all().forEach((name, f) -> LuaTables.put(easings, name, a -> LuaValue.valueOf(f.applyAsDouble(LuaTables.num(a, 2, 0))))); Easings.all().forEach((name, f) -> LuaTables.put(easings, name, a -> LuaValue.valueOf(f.applyAsDouble(LuaTables.num(a, 2, 0)))));
g.set("Easings", easings); g.set("Easings", easings);
reload();
} }
/** Применяет hand_pose к матрице руки; true, если был pushPose и нужен popPose. */ public VanillaApi vanilla() {
return vanilla;
}
public HandParticles particles() {
return particles;
}
/** Принудительная перезагрузка всех скриптов на ближайшем кадре. */
public final void reload() {
for (long[] row : stamps) java.util.Arrays.fill(row, Long.MIN_VALUE);
lastCheck = 0L;
particles.clear();
}
/** Применяет hand_pose и hand_relative_pose к матрице руки; true, если был pushPose и нужен popPose. */
public static boolean applyHandPose(HoldMyItems module, AbstractClientPlayer player, InteractionHand hand, public static boolean applyHandPose(HoldMyItems module, AbstractClientPlayer player, InteractionHand hand,
float swing, float equip, float tickDelta, ItemStack item, PoseStack ms) { float swing, float equip, float tickDelta, ItemStack item, PoseStack ms) {
if (!module.scriptsActive()) return false; if (!module.scriptsActive()) return false;
boolean mainHand = hand == InteractionHand.MAIN_HAND; boolean mainHand = hand == InteractionHand.MAIN_HAND;
boolean rightArm = (player.getMainArm() == HumanoidArm.RIGHT) == mainHand; boolean rightArm = (player.getMainArm() == HumanoidArm.RIGHT) == mainHand;
ms.pushPose(); ms.pushPose();
module.scripts().runHand(new HandFrame(player, item, mainHand, rightArm, swing, equip, tickDelta, ms)); HandScripts s = module.scripts();
HandFrame f = new HandFrame(player, item, mainHand, rightArm, swing, equip, tickDelta, ms);
s.vanilla.begin(mainHand);
s.run(ScriptKind.HAND, f);
s.run(ScriptKind.RELATIVE, f);
s.lastState[mainHand ? 0 : 1][0] = swing;
s.lastState[mainHand ? 0 : 1][1] = equip;
return true; return true;
} }
/** Применяет item_pose к матрице предмета; true, если был pushPose и нужен popPose. */ /** Применяет item_pose и item_model; true, если был pushPose и нужен popPose. */
public static boolean applyItemPose(HoldMyItems module, LivingEntity entity, ItemStack stack, public static boolean applyItemPose(HoldMyItems module, LivingEntity entity, ItemStack stack,
ItemDisplayContext ctx, PoseStack ms) { ItemDisplayContext ctx, PoseStack ms) {
if (!ctx.firstPerson() || !(entity instanceof AbstractClientPlayer player)) return false; if (!ctx.firstPerson() || !(entity instanceof AbstractClientPlayer player)) return false;
if (!module.isEnabled() || !module.scriptsActive()) return false; if (!module.isEnabled() || !module.scriptsActive()) return false;
boolean rightArm = ctx == ItemDisplayContext.FIRST_PERSON_RIGHT_HAND; boolean rightArm = ctx == ItemDisplayContext.FIRST_PERSON_RIGHT_HAND;
boolean mainHand = (player.getMainArm() == HumanoidArm.RIGHT) == rightArm; boolean mainHand = (player.getMainArm() == HumanoidArm.RIGHT) == rightArm;
HandScripts scripts = module.scripts(); HandScripts s = module.scripts();
float[] last = scripts.lastState(mainHand); float[] last = s.lastState[mainHand ? 0 : 1];
HandFrame f = new HandFrame(player, stack, mainHand, rightArm, last[0], last[1], 0.0f, ms);
ms.pushPose(); ms.pushPose();
scripts.runItem(new HandFrame(player, stack, mainHand, rightArm, last[0], last[1], 0.0f, ms)); s.run(ScriptKind.ITEM, f);
ModelAnimator.INSTANCE.clear();
s.sandbox.globals().set("quadCount", LuaValue.valueOf(ModelAnimator.INSTANCE.lastQuadCount()));
s.run(ScriptKind.MODEL, f);
return true; return true;
} }
public static Path scriptDir() { private void run(ScriptKind kind, HandFrame f) {
return FabricLoader.getInstance().getConfigDir().resolve("lovisual").resolve("holdmyitems");
}
/** Принудительная перезагрузка на следующем кадре. */
public void reload() {
stamps[0] = Long.MIN_VALUE;
stamps[1] = Long.MIN_VALUE;
lastCheck = 0L;
}
public void runHand(HandFrame f) {
run(0, HAND_POSE, f);
lastState[f.mainHand() ? 0 : 1][0] = f.swingProgress();
lastState[f.mainHand() ? 0 : 1][1] = f.equipProgress();
}
public void runItem(HandFrame f) {
run(1, ITEM_POSE, f);
}
/** Последний известный swing/equip руки: item_pose рендерится вне submitArmWithItem. */
public float[] lastState(boolean mainHand) {
return lastState[mainHand ? 0 : 1];
}
private void run(int index, String name, HandFrame f) {
refresh(); refresh();
ScriptSlot slot = slots[index]; for (int part = 0; part < 2; part++) {
if (slot == null || !slot.canRun()) return; ScriptSlot slot = slots[kind.ordinal()][part];
if (slot == null || !slot.canRun()) continue;
bind(slot, f);
try {
slot.run();
} finally {
pose.finish();
}
if (!slot.canRun()) LOG.warn("HoldMyItems script {} disabled: {}", kind.file(part), slot.error());
}
}
private void bind(ScriptSlot slot, HandFrame f) {
this.player = f.player(); this.player = f.player();
this.stack = f.stack(); this.stack = f.stack();
Globals g = sandbox.globals(); Globals g = sandbox.globals();
@ -129,47 +142,23 @@ public final class HandScripts {
g.set("useDuration", LuaValue.valueOf(f.player().getTicksUsingItem())); g.set("useDuration", LuaValue.valueOf(f.player().getTicksUsingItem()));
g.set("time", LuaValue.valueOf(System.nanoTime() / 1.0e9)); g.set("time", LuaValue.valueOf(System.nanoTime() / 1.0e9));
pose.bind(f.pose()); pose.bind(f.pose());
try {
slot.run();
} finally {
pose.finish();
}
if (!slot.canRun()) LOG.warn("HoldMyItems script {} disabled: {}", name, slot.error());
} }
private void refresh() { private void refresh() {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
if (now - lastCheck < RECHECK_MS) return; if (now - lastCheck < RECHECK_MS) return;
lastCheck = now; lastCheck = now;
String[] names = {HAND_POSE, ITEM_POSE}; for (ScriptKind kind : kinds) {
for (int i = 0; i < names.length; i++) { for (int part = 0; part < 2; part++) {
Path file = scriptDir().resolve(names[i] + ".lua"); String name = kind.file(part);
long stamp = stampOf(file); long stamp = ScriptFiles.stamp(name, part == 0);
if (stamp == stamps[i]) continue; if (stamp == stamps[kind.ordinal()][part]) continue;
stamps[i] = stamp; stamps[kind.ordinal()][part] = stamp;
String source = read(file, names[i]); String source = stamp < 0 ? null : ScriptFiles.read(name);
slots[i] = source == null ? null : new ScriptSlot(names[i], source, sandbox); ScriptSlot slot = source == null ? null : new ScriptSlot(name, source, sandbox);
if (slots[i] != null && !slots[i].canRun()) LOG.warn("HoldMyItems script {}: {}", names[i], slots[i].error()); slots[kind.ordinal()][part] = slot;
} if (slot != null && !slot.canRun()) LOG.warn("HoldMyItems script {}: {}", name, slot.error());
}
private static long stampOf(Path file) {
try {
return Files.isRegularFile(file) ? Files.getLastModifiedTime(file).toMillis() : -1L;
} catch (IOException e) {
return -1L;
}
}
private static String read(Path file, String name) {
try {
if (Files.isRegularFile(file)) return Files.readString(file, StandardCharsets.UTF_8);
try (InputStream in = HandScripts.class.getResourceAsStream("/assets/lovisual/holdmyitems/" + name + ".lua")) {
return in == null ? null : new String(in.readAllBytes(), StandardCharsets.UTF_8);
} }
} catch (IOException e) {
LOG.warn("Cannot read HoldMyItems script {}: {}", name, e.getMessage());
return null;
} }
} }
} }

View file

@ -0,0 +1,21 @@
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua;
/** Виды скриптов HoldMyItems: основной файл и необязательный аддон, который выполняется сразу после него. */
public enum ScriptKind {
HAND("hand_pose", "hand_addon"),
RELATIVE("hand_relative_pose", "hand_relative_addon"),
ITEM("item_pose", "item_addon"),
MODEL("item_model", "item_model_addon");
public final String main;
public final String addon;
ScriptKind(String main, String addon) {
this.main = main;
this.addon = addon;
}
public String file(int part) {
return part == 0 ? main : addon;
}
}

View file

@ -21,6 +21,11 @@ public final class PoseApi {
return this; return this;
} }
/** Текущая матрица позы (копия) либо null, если скрипт не выполняется. */
public org.joml.Matrix4f matrix() {
return stack == null ? null : new org.joml.Matrix4f(stack.last().pose());
}
/** Закрывает push, оставленные скриптом незакрытыми. */ /** Закрывает push, оставленные скриптом незакрытыми. */
public void finish() { public void finish() {
while (stack != null && depth > 0) { while (stack != null && depth > 0) {

View file

@ -0,0 +1,149 @@
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.LuaTables;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.PoseApi;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.runtime.LuaSandbox;
import dev.loki.lovisual.render.engine.RenderState;
import dev.loki.lovisual.render.engine.pipeline.LoVisualRenderPipelines;
import dev.loki.lovisual.render.engine.renderer.Renderer3D;
import dev.loki.lovisual.render.engine.texture.TextureStorage;
import dev.loki.lovisual.render.engine.uniform.MeshBuilder;
import net.minecraft.client.Minecraft;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.luaj.vm2.LuaError;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.addBillboardVertices;
import static dev.loki.lovisual.features.module.modules.combat.targetesp.TargetEspRender.applyOpacity;
/**
* Lua-глобал {@code particleManager}: {@code particleManager:addParticle{ x,y,z, vx,vy,vz, lifetime, size, color,
* alpha, gravity, drag, tick = function(p) ... end }}. Координаты x,y,z заданы в системе текущей позы руки
* (после M:), частица живёт в мире. Колбэк tick получает таблицу p и может менять x,y,z,vx,vy,vz,size,alpha,color.
*/
public final class HandParticles {
private static final Logger LOG = LoggerFactory.getLogger(HandParticles.class);
private static final int MAX = 256;
private static final class P {
float x, y, z, px, py, pz, vx, vy, vz, size, alpha, gravity, drag;
int age, life, color;
LuaValue tick;
}
private final List<P> list = new ArrayList<>();
private final LuaSandbox sandbox;
private final PoseApi pose;
public HandParticles(LuaSandbox sandbox, PoseApi pose) {
this.sandbox = sandbox;
this.pose = pose;
}
public void clear() {
list.clear();
}
public LuaTable table() {
LuaTable t = new LuaTable();
LuaTables.put(t, "addParticle", a -> {
if (list.size() < MAX && a.arg(2).istable()) spawn(a.arg(2));
return LuaValue.NIL;
});
return t;
}
private void spawn(LuaValue o) {
Matrix4f m = pose.matrix();
Minecraft mc = Minecraft.getInstance();
if (m == null || mc.gameRenderer == null || mc.gameRenderer.mainCamera() == null) return;
Vector3f local = m.transformPosition(num(o, "x", 0), num(o, "y", 0), num(o, "z", 0), new Vector3f());
Vector3f world = new Quaternionf(RenderState.cameraRotation).transform(local);
Vec3 cam = mc.gameRenderer.mainCamera().position();
P p = new P();
p.x = p.px = (float) cam.x + world.x;
p.y = p.py = (float) cam.y + world.y;
p.z = p.pz = (float) cam.z + world.z;
Vector3f v = new Quaternionf(RenderState.cameraRotation).transform(new Vector3f(num(o, "vx", 0), num(o, "vy", 0), num(o, "vz", 0)));
p.vx = v.x;
p.vy = v.y;
p.vz = v.z;
p.life = Math.max(1, Math.min(200, (int) num(o, "lifetime", 20)));
p.size = num(o, "size", 0.05f);
p.alpha = num(o, "alpha", 1f);
p.gravity = num(o, "gravity", 0f);
p.drag = num(o, "drag", 0.98f);
LuaValue c = o.get("color");
p.color = c.isnil() ? 0xFFFFFFFF : (int) (long) c.todouble();
LuaValue tick = o.get("tick");
p.tick = tick.isfunction() ? tick : null;
list.add(p);
}
private static float num(LuaValue o, String key, float def) {
LuaValue v = o.get(key);
return v.isnumber() ? (float) v.todouble() : def;
}
public void tick() {
for (int i = list.size() - 1; i >= 0; i--) {
P p = list.get(i);
p.px = p.x;
p.py = p.y;
p.pz = p.z;
p.vy -= p.gravity;
p.vx *= p.drag;
p.vy *= p.drag;
p.vz *= p.drag;
p.x += p.vx;
p.y += p.vy;
p.z += p.vz;
if (p.tick != null) callTick(p);
if (++p.age >= p.life) list.remove(i);
}
}
private void callTick(P p) {
LuaTable t = new LuaTable();
t.set("x", p.x); t.set("y", p.y); t.set("z", p.z);
t.set("vx", p.vx); t.set("vy", p.vy); t.set("vz", p.vz);
t.set("size", p.size); t.set("alpha", p.alpha); t.set("color", (double) (p.color & 0xFFFFFFFFL));
t.set("age", p.age); t.set("life", p.life);
sandbox.beginRun();
try {
p.tick.call(t);
p.x = num(t, "x", p.x); p.y = num(t, "y", p.y); p.z = num(t, "z", p.z);
p.vx = num(t, "vx", p.vx); p.vy = num(t, "vy", p.vy); p.vz = num(t, "vz", p.vz);
p.size = num(t, "size", p.size); p.alpha = num(t, "alpha", p.alpha);
p.color = (int) (long) t.get("color").todouble();
} catch (LuaError e) {
LOG.warn("HoldMyItems particle tick failed: {}", e.getMessage());
p.tick = null;
}
}
public void render(Renderer3D renderer, float partialTicks) {
if (renderer == null || list.isEmpty()) return;
MeshBuilder mesh = renderer.batchTextured(LoVisualRenderPipelines.WORLD_TEXTURED_ADDITIVE,
TextureStorage.BLOOM, Renderer3D.DepthMode.NONE);
if (mesh == null) return;
for (P p : list) {
float fade = 1.0f - (p.age + partialTicks) / p.life;
int color = applyOpacity(p.color, p.alpha * Math.max(0f, fade));
Vector3f right = new Vector3f(1, 0, 0).rotate(RenderState.cameraRotation).mul(p.size);
Vector3f up = new Vector3f(0, 1, 0).rotate(RenderState.cameraRotation).mul(p.size);
addBillboardVertices(mesh, p.px + (p.x - p.px) * partialTicks, p.py + (p.y - p.py) * partialTicks,
p.pz + (p.z - p.pz) * partialTicks, right, up, color, color, color, color);
}
}
}

View file

@ -0,0 +1,96 @@
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.LuaTables;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext.QuadOps.Op;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext.QuadOps.Type;
import net.minecraft.client.resources.model.geometry.BakedQuad;
import org.joml.Vector3f;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;
import java.util.ArrayList;
import java.util.List;
/**
* Lua-глобал {@code animator} для item_model.lua: анимация частей (квадов) модели предмета.
* {@code animator:rotateX(from, to, deg, px, py, pz)}, {@code moveX/Y/Z(from, to, px)}, {@code scale(from, to, k)}
* по диапазону индексов квадов; pivot и сдвиги в пикселях. Операции действуют на один рендер предмета.
*/
public final class ModelAnimator {
public static final ModelAnimator INSTANCE = new ModelAnimator();
private static final float PX = 1.0f / 16.0f;
private final List<Op> ops = new ArrayList<>();
private int lastQuadCount;
private ModelAnimator() {
}
public void clear() {
ops.clear();
}
/** Число квадов последнего отрендеренного слоя (скрипту отдаётся глобалом quadCount). */
public int lastQuadCount() {
return lastQuadCount;
}
public LuaTable table() {
LuaTable t = new LuaTable();
LuaTables.put(t, "rotateX", a -> rotate(Type.ROT_X, a));
LuaTables.put(t, "rotateY", a -> rotate(Type.ROT_Y, a));
LuaTables.put(t, "rotateZ", a -> rotate(Type.ROT_Z, a));
LuaTables.put(t, "moveX", a -> move(Type.MOVE_X, a));
LuaTables.put(t, "moveY", a -> move(Type.MOVE_Y, a));
LuaTables.put(t, "moveZ", a -> move(Type.MOVE_Z, a));
LuaTables.put(t, "scale", a -> {
ops.add(new Op(Type.SCALE, (int) LuaTables.num(a, 2, 0), (int) LuaTables.num(a, 3, 0),
(float) LuaTables.num(a, 4, 1), (float) LuaTables.num(a, 5, 8) * PX,
(float) LuaTables.num(a, 6, 8) * PX, (float) LuaTables.num(a, 7, 8) * PX));
return LuaValue.NIL;
});
return t;
}
private LuaValue rotate(Type type, Varargs a) {
ops.add(new Op(type, (int) LuaTables.num(a, 2, 0), (int) LuaTables.num(a, 3, 0), (float) LuaTables.num(a, 4, 0),
(float) LuaTables.num(a, 5, 8) * PX, (float) LuaTables.num(a, 6, 8) * PX, (float) LuaTables.num(a, 7, 8) * PX));
return LuaValue.NIL;
}
private LuaValue move(Type type, Varargs a) {
ops.add(new Op(type, (int) LuaTables.num(a, 2, 0), (int) LuaTables.num(a, 3, 0),
(float) LuaTables.num(a, 4, 0) * PX, 0, 0, 0));
return LuaValue.NIL;
}
/** Вызывается миксином перед отправкой квадов на рендер: возвращает изменённую копию либо исходный список. */
public List<BakedQuad> apply(List<BakedQuad> quads) {
lastQuadCount = quads.size();
if (ops.isEmpty()) return quads;
List<BakedQuad> out = new ArrayList<>(quads.size());
for (int i = 0; i < quads.size(); i++) {
BakedQuad q = quads.get(i);
out.add(covered(i) ? transform(q, i) : q);
}
return out;
}
private boolean covered(int index) {
for (Op op : ops) if (op.covers(index)) return true;
return false;
}
private BakedQuad transform(BakedQuad q, int index) {
Vector3f[] p = new Vector3f[4];
for (int v = 0; v < 4; v++) {
float[] pos = {q.position(v).x(), q.position(v).y(), q.position(v).z()};
for (Op op : ops) if (op.covers(index)) QuadOps.apply(op, pos);
p[v] = new Vector3f(pos[0], pos[1], pos[2]);
}
return new BakedQuad(p[0], p[1], p[2], p[3], q.packedUV0(), q.packedUV1(), q.packedUV2(), q.packedUV3(),
q.direction(), q.materialInfo());
}
}

View file

@ -0,0 +1,53 @@
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext;
/**
* Чистая математика аниматора частей модели: операции над вершиной (x,y,z в блоках, 0..1).
* Углы в градусах, сдвиги и pivot приходят из Lua в пикселях (1/16 блока) и уже переведены вызывающим.
*/
public final class QuadOps {
public enum Type { ROT_X, ROT_Y, ROT_Z, MOVE_X, MOVE_Y, MOVE_Z, SCALE }
/** Операция над квадами с индексами [from, to]; a: градусы/блоки/множитель, p: pivot в блоках. */
public record Op(Type type, int from, int to, float a, float px, float py, float pz) {
public boolean covers(int quadIndex) {
return quadIndex >= from && quadIndex <= to;
}
}
private QuadOps() {
}
/** Применяет операцию к вершине in-place. */
public static void apply(Op op, float[] v) {
switch (op.type()) {
case MOVE_X -> v[0] += op.a();
case MOVE_Y -> v[1] += op.a();
case MOVE_Z -> v[2] += op.a();
case SCALE -> {
v[0] = op.px() + (v[0] - op.px()) * op.a();
v[1] = op.py() + (v[1] - op.py()) * op.a();
v[2] = op.pz() + (v[2] - op.pz()) * op.a();
}
default -> rotate(op, v);
}
}
private static void rotate(Op op, float[] v) {
double rad = Math.toRadians(op.a());
float c = (float) Math.cos(rad);
float s = (float) Math.sin(rad);
float x = v[0] - op.px();
float y = v[1] - op.py();
float z = v[2] - op.pz();
float nx = x, ny = y, nz = z;
switch (op.type()) {
case ROT_X -> { ny = y * c - z * s; nz = y * s + z * c; }
case ROT_Y -> { nx = x * c + z * s; nz = -x * s + z * c; }
default -> { nx = x * c - y * s; ny = x * s + y * c; }
}
v[0] = nx + op.px();
v[1] = ny + op.py();
v[2] = nz + op.pz();
}
}

View file

@ -0,0 +1,73 @@
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.api.LuaTables;
import org.luaj.vm2.LuaTable;
import org.luaj.vm2.LuaValue;
/**
* Lua-глобалы {@code V} и {@code C}.
* V: {@code V:cancelEquip(true)} отключает ванильный подъём предмета, {@code V:cancelSwing(true)} ванильный свинг
* (скрипт сам рисует их через hand_pose); флаги читаются миксином на следующем кадре.
* C: {@code C:setCamRot(yaw, pitch)} добавка к повороту камеры в градусах, {@code C:setCamPos(surge, heave, sway)}
* сдвиг камеры в блоках; действует, пока скрипт обновляет значения (до 150 мс без вызова).
*/
public final class VanillaApi {
private static final long CAM_TTL_MS = 150L;
private final boolean[] cancelEquip = new boolean[2];
private final boolean[] cancelSwing = new boolean[2];
private final float[] cam = new float[5];
private long camStamp;
private int current;
/** Начало кадра руки: флаги этой руки сбрасываются и выставляются скриптом заново. */
public void begin(boolean mainHand) {
current = mainHand ? 0 : 1;
cancelEquip[current] = false;
cancelSwing[current] = false;
}
public boolean cancelEquip(boolean mainHand) {
return cancelEquip[mainHand ? 0 : 1];
}
public boolean cancelSwing(boolean mainHand) {
return cancelSwing[mainHand ? 0 : 1];
}
/** Добавка камеры {yaw, pitch, surge, heave, sway}, либо null, если скрипт её не задаёт. */
public float[] cameraOffset() {
return System.currentTimeMillis() - camStamp > CAM_TTL_MS ? null : cam;
}
public LuaTable vanillaTable() {
LuaTable t = new LuaTable();
LuaTables.put(t, "cancelEquip", a -> flag(cancelEquip, a.arg(2).toboolean()));
LuaTables.put(t, "cancelSwing", a -> flag(cancelSwing, a.arg(2).toboolean()));
return t;
}
public LuaTable cameraTable() {
LuaTable t = new LuaTable();
LuaTables.put(t, "setCamRot", a -> cam(0, a.arg(2).optdouble(0), a.arg(3).optdouble(0)));
LuaTables.put(t, "setCamPos", a -> {
cam(2, a.arg(2).optdouble(0), a.arg(3).optdouble(0));
cam[4] = (float) a.arg(4).optdouble(0);
return LuaValue.NIL;
});
return t;
}
private LuaValue flag(boolean[] array, boolean value) {
array[current] = value;
return LuaValue.NIL;
}
private LuaValue cam(int at, double first, double second) {
cam[at] = (float) first;
cam[at + 1] = (float) second;
camStamp = System.currentTimeMillis();
return LuaValue.NIL;
}
}

View file

@ -0,0 +1,55 @@
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua.runtime;
import net.fabricmc.loader.api.FabricLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Поиск исходников скриптов: {@code config/lovisual/holdmyitems/<name>.lua}, иначе встроенный пример
* из ресурсов мода (только для основных скриптов; аддоны бывают лишь в конфиге).
*/
public final class ScriptFiles {
private static final Logger LOG = LoggerFactory.getLogger(ScriptFiles.class);
private ScriptFiles() {
}
public static Path dir() {
return FabricLoader.getInstance().getConfigDir().resolve("lovisual").resolve("holdmyitems");
}
/** Метка изменения: mtime файла, 0 для встроенного примера, -1 если источника нет. */
public static long stamp(String name, boolean builtinAllowed) {
Path file = dir().resolve(name + ".lua");
try {
if (Files.isRegularFile(file)) return Files.getLastModifiedTime(file).toMillis();
} catch (IOException e) {
return -1L;
}
return builtinAllowed && ScriptFiles.class.getResource(resource(name)) != null ? 0L : -1L;
}
public static String read(String name) {
Path file = dir().resolve(name + ".lua");
try {
if (Files.isRegularFile(file)) return Files.readString(file, StandardCharsets.UTF_8);
try (InputStream in = ScriptFiles.class.getResourceAsStream(resource(name))) {
return in == null ? null : new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
} catch (IOException e) {
LOG.warn("Cannot read HoldMyItems script {}: {}", name, e.getMessage());
return null;
}
}
private static String resource(String name) {
return "/assets/lovisual/holdmyitems/" + name + ".lua";
}
}

View file

@ -3,6 +3,7 @@ package dev.loki.lovisual.mixins.render.item;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import dev.loki.lovisual.features.module.lifecycle.Modules; import dev.loki.lovisual.features.module.lifecycle.Modules;
import dev.loki.lovisual.features.module.modules.visuals.items.held.AlwaysVisibleHand; import dev.loki.lovisual.features.module.modules.visuals.items.held.AlwaysVisibleHand;
import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems;
import net.minecraft.client.renderer.ItemInHandRenderer; import net.minecraft.client.renderer.ItemInHandRenderer;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
@ -25,6 +26,7 @@ public abstract class ItemInHandAlwaysVisibleMixin {
) )
) )
private boolean lovisual$alwaysRenderMainHand(boolean vanillaDecision) { private boolean lovisual$alwaysRenderMainHand(boolean vanillaDecision) {
if (lovisual$forceBoth()) return true;
AlwaysVisibleHand module = Modules.get(AlwaysVisibleHand.class); AlwaysVisibleHand module = Modules.get(AlwaysVisibleHand.class);
return module == null ? vanillaDecision : module.renderMainHand(vanillaDecision); return module == null ? vanillaDecision : module.renderMainHand(vanillaDecision);
} }
@ -37,7 +39,14 @@ public abstract class ItemInHandAlwaysVisibleMixin {
) )
) )
private boolean lovisual$alwaysRenderOffHand(boolean vanillaDecision) { private boolean lovisual$alwaysRenderOffHand(boolean vanillaDecision) {
if (lovisual$forceBoth()) return true;
AlwaysVisibleHand module = Modules.get(AlwaysVisibleHand.class); AlwaysVisibleHand module = Modules.get(AlwaysVisibleHand.class);
return module == null ? vanillaDecision : module.renderOffHand(vanillaDecision); return module == null ? vanillaDecision : module.renderOffHand(vanillaDecision);
} }
@org.spongepowered.asm.mixin.Unique
private static boolean lovisual$forceBoth() {
HoldMyItems held = Modules.get(HoldMyItems.class);
return held != null && held.forcesBothHands();
}
} }

View file

@ -21,6 +21,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import dev.loki.lovisual.features.module.lifecycle.Modules; import dev.loki.lovisual.features.module.lifecycle.Modules;
import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems; import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.HandScripts; import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.HandScripts;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext.ModelAnimator;
import dev.loki.lovisual.util.animation.hand.HandDynamics; import dev.loki.lovisual.util.animation.hand.HandDynamics;
import dev.loki.lovisual.features.module.modules.visuals.camera.ViewModel; import dev.loki.lovisual.features.module.modules.visuals.camera.ViewModel;
import dev.loki.lovisual.util.animation.EquipProgressComposer; import dev.loki.lovisual.util.animation.EquipProgressComposer;
@ -114,6 +115,8 @@ public abstract class ItemInHandRendererMixin {
boolean lowerEnabled = viewModel != null && viewModel.isEnabled(); boolean lowerEnabled = viewModel != null && viewModel.isEnabled();
boolean curveEnabled = holdMyItems != null && holdMyItems.isEnabled() && holdMyItems.dynamicsActive() boolean curveEnabled = holdMyItems != null && holdMyItems.isEnabled() && holdMyItems.dynamicsActive()
&& !(hand == InteractionHand.OFF_HAND && !holdMyItems.offhandToo.get()); && !(hand == InteractionHand.OFF_HAND && !holdMyItems.offhandToo.get());
HandScripts scripts = holdMyItems == null ? null : holdMyItems.scriptsOrNull();
if (scripts != null && scripts.vanilla().cancelEquip(hand == InteractionHand.MAIN_HAND)) return 0.0f;
if (!lowerEnabled && !curveEnabled) return equipProgress; if (!lowerEnabled && !curveEnabled) return equipProgress;
return EquipProgressComposer.compose( return EquipProgressComposer.compose(
equipProgress, equipProgress,
@ -137,15 +140,8 @@ public abstract class ItemInHandRendererMixin {
target = "Lnet/minecraft/client/renderer/item/ItemStackRenderState;submit(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;III)V" target = "Lnet/minecraft/client/renderer/item/ItemStackRenderState;submit(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/SubmitNodeCollector;III)V"
) )
) )
private void lovisual$scaleMiniItems( private void lovisual$scaleMiniItems(LivingEntity entity, ItemStack stack, ItemDisplayContext ctx,
LivingEntity entity, PoseStack ms, SubmitNodeCollector queue, int light, CallbackInfo ci) {
ItemStack stack,
ItemDisplayContext ctx,
PoseStack ms,
SubmitNodeCollector queue,
int light,
CallbackInfo ci
) {
HoldMyItems held = Modules.get(HoldMyItems.class); HoldMyItems held = Modules.get(HoldMyItems.class);
if (held != null) lovisual$itemPosePushed = HandScripts.applyItemPose(held, entity, stack, ctx, ms); if (held != null) lovisual$itemPosePushed = HandScripts.applyItemPose(held, entity, stack, ctx, ms);
ViewModel viewModel = Modules.get(ViewModel.class); ViewModel viewModel = Modules.get(ViewModel.class);
@ -162,26 +158,23 @@ public abstract class ItemInHandRendererMixin {
private void lovisual$restoreItemPose( private void lovisual$restoreItemPose(
LivingEntity entity, ItemStack stack, ItemDisplayContext ctx, PoseStack ms, LivingEntity entity, ItemStack stack, ItemDisplayContext ctx, PoseStack ms,
SubmitNodeCollector queue, int light, CallbackInfo ci) { SubmitNodeCollector queue, int light, CallbackInfo ci) {
ModelAnimator.INSTANCE.clear();
if (!lovisual$itemPosePushed) return; if (!lovisual$itemPosePushed) return;
lovisual$itemPosePushed = false; lovisual$itemPosePushed = false;
ms.popPose(); ms.popPose();
} }
@Inject( @Inject(method = "swingArm", at = @At("HEAD"), cancellable = true)
method = "swingArm", private void lovisual$customSwing(float swingProgress, PoseStack ms, int direction, HumanoidArm arm, CallbackInfo ci) {
at = @At("HEAD"), Minecraft mc = Minecraft.getInstance();
cancellable = true HoldMyItems held = Modules.get(HoldMyItems.class);
) HandScripts scripts = held == null ? null : held.scriptsOrNull();
private void lovisual$customSwing( if (scripts != null && scripts.vanilla().cancelSwing(arm == mc.options.mainHand().get())) {
float swingProgress, ci.cancel();
PoseStack ms, return;
int direction, }
HumanoidArm arm,
CallbackInfo ci
) {
ViewModel viewModel = Modules.get(ViewModel.class); ViewModel viewModel = Modules.get(ViewModel.class);
if (viewModel == null || !viewModel.isEnabled()) return; if (viewModel == null || !viewModel.isEnabled()) return;
Minecraft mc = Minecraft.getInstance();
if (mc.player == null) return; if (mc.player == null) return;
ItemStack stack = mc.player.getMainHandItem(); ItemStack stack = mc.player.getMainHandItem();

View file

@ -0,0 +1,26 @@
package dev.loki.lovisual.mixins.render.item.model;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext.ModelAnimator;
import net.minecraft.client.resources.model.geometry.BakedQuad;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import java.util.List;
/** Подмена списка квадов слоя предмета на анимированную копию (item_model.lua, {@code animator}). */
@Mixin(targets = "net.minecraft.client.renderer.item.ItemStackRenderState$LayerRenderState")
public abstract class ItemModelAnimatorMixin {
@ModifyArg(
method = "submit",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/SubmitNodeCollector;submitItem(Lcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/world/item/ItemDisplayContext;III[ILjava/util/List;Lnet/minecraft/client/renderer/item/ItemStackRenderState$FoilType;)V"
),
index = 6
)
private List<BakedQuad> lovisual$animateQuads(List<BakedQuad> quads) {
return ModelAnimator.INSTANCE.apply(quads);
}
}

View file

@ -0,0 +1,39 @@
package dev.loki.lovisual.mixins.render.level;
import dev.loki.lovisual.features.module.lifecycle.Modules;
import dev.loki.lovisual.features.module.modules.visuals.items.held.HoldMyItems;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.HandScripts;
import net.minecraft.client.Camera;
import net.minecraft.client.DeltaTracker;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** Смещение камеры из Lua-скриптов HoldMyItems ({@code C:setCamRot}, {@code C:setCamPos}). */
@Mixin(Camera.class)
public abstract class CameraScriptMixin {
@Shadow
protected abstract void setRotation(float yaw, float pitch);
@Shadow
protected abstract void move(float surge, float heave, float sway);
@Shadow
public abstract float yRot();
@Shadow
public abstract float xRot();
@Inject(method = "update", at = @At("TAIL"))
private void lovisual$applyScriptCamera(DeltaTracker tickCounter, CallbackInfo ci) {
HoldMyItems held = Modules.get(HoldMyItems.class);
HandScripts scripts = held == null ? null : held.scriptsOrNull();
float[] c = scripts == null ? null : scripts.vanilla().cameraOffset();
if (c == null) return;
if (c[0] != 0.0f || c[1] != 0.0f) setRotation(yRot() + c[0], Math.max(-90.0f, Math.min(90.0f, xRot() + c[1])));
if (c[2] != 0.0f || c[3] != 0.0f || c[4] != 0.0f) move(c[2], c[3], c[4]);
}
}

View file

@ -0,0 +1,9 @@
-- LoVisual HoldMyItems: поза руки относительно сцены (после hand_pose).
-- Пример: при смене предмета рука чуть "отъезжает" в сторону. Свои флаги ванили:
-- V:cancelEquip(true) отключить ванильный подъём предмета (его заменяет скрипт)
-- V:cancelSwing(true) отключить ванильный свинг
local side = rightArm and 1 or -1
if equipProgress > 0 then
M:moveX(0.12 * equipProgress * side)
M:rotateZ(-10 * equipProgress * side)
end

View file

@ -0,0 +1,7 @@
-- LoVisual HoldMyItems: анимация частей (квадов) модели предмета.
-- animator:rotateX/Y/Z(from, to, deg, px, py, pz) вращение квадов с индексами from..to вокруг pivot (в пикселях)
-- animator:moveX/Y/Z(from, to, pixels), animator:scale(from, to, k, px, py, pz); quadCount = число квадов.
-- Пример: мечи слегка "дышат" клинком.
if I:isSword() and quadCount > 0 then
animator:rotateZ(0, quadCount, M:sin(time * 2.0) * 1.5, 8, 0, 8)
end

View file

@ -16,3 +16,13 @@ end
if usingItem and (I:getUseAction() == 'eat' or I:getUseAction() == 'drink') then if usingItem and (I:getUseAction() == 'eat' or I:getUseAction() == 'drink') then
M:rotateZ(M:sin(useDuration * 1.1) * 3) M:rotateZ(M:sin(useDuration * 1.1) * 3)
end end
-- искры у зачарованных предметов: Lua-частицы (координаты в системе руки, колбэк tick на каждый тик)
if I:isEnchanted() and M:sin(time * 9) > 0.96 then
particleManager:addParticle({
x = 0.0, y = 0.2, z = -0.3,
vx = 0.0, vy = 0.01, vz = 0.0,
lifetime = 14, size = 0.02, color = 0xFFB66CFF, gravity = -0.001,
tick = function(p) p.size = p.size * 0.96 end
})
end

View file

@ -3341,6 +3341,7 @@
"setting.autorespawn.delay": "Delay", "setting.autorespawn.delay": "Delay",
"module.deathcoords.description": "Sends the coordinates of your latest death to the chat.", "module.deathcoords.description": "Sends the coordinates of your latest death to the chat.",
"setting.deathcoords.show_dimension": "Show dimension", "setting.deathcoords.show_dimension": "Show dimension",
"setting.holdmyitems.always_hands": "Always render both hands",
"setting.holdmyitems.mode": "Mode", "setting.holdmyitems.mode": "Mode",
"setting.holdmyitems.mode.option.Dynamics": "Dynamics (Java)", "setting.holdmyitems.mode.option.Dynamics": "Dynamics (Java)",
"setting.holdmyitems.mode.option.Scripts": "Scripts (Lua)", "setting.holdmyitems.mode.option.Scripts": "Scripts (Lua)",

View file

@ -3136,6 +3136,7 @@
"setting.autorespawn.delay": "Задержка", "setting.autorespawn.delay": "Задержка",
"module.deathcoords.description": "Отправляет в чат координаты вашей последней смерти.", "module.deathcoords.description": "Отправляет в чат координаты вашей последней смерти.",
"setting.deathcoords.show_dimension": "Показывать измерение", "setting.deathcoords.show_dimension": "Показывать измерение",
"setting.holdmyitems.always_hands": "Всегда рисовать обе руки",
"setting.holdmyitems.mode": "Режим", "setting.holdmyitems.mode": "Режим",
"setting.holdmyitems.mode.option.Dynamics": "Динамика (Java)", "setting.holdmyitems.mode.option.Dynamics": "Динамика (Java)",
"setting.holdmyitems.mode.option.Scripts": "Скрипты (Lua)", "setting.holdmyitems.mode.option.Scripts": "Скрипты (Lua)",

View file

@ -156,8 +156,10 @@
"render.item.EquipmentLayerRendererMixin", "render.item.EquipmentLayerRendererMixin",
"render.item.ItemFeatureRendererMixin", "render.item.ItemFeatureRendererMixin",
"render.item.ItemInHandAlwaysVisibleMixin", "render.item.ItemInHandAlwaysVisibleMixin",
"render.item.model.ItemModelAnimatorMixin",
"render.item.ItemInHandRendererMixin", "render.item.ItemInHandRendererMixin",
"render.level.CameraMixin", "render.level.CameraMixin",
"render.level.CameraScriptMixin",
"render.level.particles.FireworkParticlesStarterMixin", "render.level.particles.FireworkParticlesStarterMixin",
"render.level.LevelRendererMixin", "render.level.LevelRendererMixin",
"render.level.particles.ParticleManagerMixin", "render.level.particles.ParticleManagerMixin",

View file

@ -0,0 +1,42 @@
package dev.loki.lovisual.features.module.modules.visuals.items.held.lua;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext.QuadOps;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext.QuadOps.Op;
import dev.loki.lovisual.features.module.modules.visuals.items.held.lua.ext.QuadOps.Type;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class QuadOpsTest {
@Test
void rotateZ90AroundPivotMovesVertex() {
float[] v = {1f, 0.5f, 0.3f};
QuadOps.apply(new Op(Type.ROT_Z, 0, 0, 90f, 0.5f, 0.5f, 0f), v);
assertEquals(0.5f, v[0], 1e-5f);
assertEquals(1.0f, v[1], 1e-5f);
assertEquals(0.3f, v[2], 1e-5f);
}
@Test
void scaleAroundPivotKeepsPivotFixed() {
float[] v = {0.5f, 0.5f, 0.5f};
QuadOps.apply(new Op(Type.SCALE, 0, 3, 2f, 0.5f, 0.5f, 0.5f), v);
assertArrayEquals(new float[]{0.5f, 0.5f, 0.5f}, v, 1e-6f);
float[] w = {1f, 0.5f, 0.5f};
QuadOps.apply(new Op(Type.SCALE, 0, 3, 2f, 0.5f, 0.5f, 0.5f), w);
assertEquals(1.5f, w[0], 1e-6f);
}
@Test
void moveAndRangeCoverage() {
Op op = new Op(Type.MOVE_Y, 2, 4, 0.25f, 0, 0, 0);
assertFalse(op.covers(1));
assertTrue(op.covers(2));
assertTrue(op.covers(4));
assertFalse(op.covers(5));
float[] v = {0f, 0f, 0f};
QuadOps.apply(op, v);
assertEquals(0.25f, v[1], 1e-6f);
}
}