From 0504be5b574956a10c58b02019a6208477831ffb Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Fri, 2 Oct 2026 09:47:30 +0200 Subject: [PATCH] feat(mod): scripted HoldMyItems on LuaJ (hand/item/model scripts, addons, vanilla overrides, particles, camera) Claude-Session: https://claude.ai/code/session_01F1M1Jic1wTSn4igUENynmZ --- mod/TODO.md | 70 ++++---- .../visuals/items/held/HoldMyItems.java | 23 +++ .../visuals/items/held/lua/HandScripts.java | 161 ++++++++---------- .../visuals/items/held/lua/ScriptKind.java | 21 +++ .../visuals/items/held/lua/api/PoseApi.java | 5 + .../items/held/lua/ext/HandParticles.java | 149 ++++++++++++++++ .../items/held/lua/ext/ModelAnimator.java | 96 +++++++++++ .../visuals/items/held/lua/ext/QuadOps.java | 53 ++++++ .../items/held/lua/ext/VanillaApi.java | 73 ++++++++ .../items/held/lua/runtime/ScriptFiles.java | 55 ++++++ .../item/ItemInHandAlwaysVisibleMixin.java | 9 + .../render/item/ItemInHandRendererMixin.java | 37 ++-- .../item/model/ItemModelAnimatorMixin.java | 26 +++ .../render/level/CameraScriptMixin.java | 39 +++++ .../holdmyitems/hand_relative_pose.lua | 9 + .../lovisual/holdmyitems/item_model.lua | 7 + .../assets/lovisual/holdmyitems/item_pose.lua | 10 ++ .../resources/assets/lovisual/lang/en_us.json | 1 + .../resources/assets/lovisual/lang/ru_ru.json | 1 + mod/src/main/resources/lovisual.mixins.json | 2 + .../visuals/items/held/lua/QuadOpsTest.java | 42 +++++ 21 files changed, 750 insertions(+), 139 deletions(-) create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ScriptKind.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/HandParticles.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/ModelAnimator.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/QuadOps.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/VanillaApi.java create mode 100644 mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/runtime/ScriptFiles.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/render/item/model/ItemModelAnimatorMixin.java create mode 100644 mod/src/main/java/dev/loki/lovisual/mixins/render/level/CameraScriptMixin.java create mode 100644 mod/src/main/resources/assets/lovisual/holdmyitems/hand_relative_pose.lua create mode 100644 mod/src/main/resources/assets/lovisual/holdmyitems/item_model.lua create mode 100644 mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/QuadOpsTest.java diff --git a/mod/TODO.md b/mod/TODO.md index b11d876..eb783ee 100644 --- a/mod/TODO.md +++ b/mod/TODO.md @@ -4,7 +4,7 @@ ## Правила (ОБЯЗАТЕЛЬНЫЕ, нарушение = откат) - **KISS / DRY / SOLID** — простая структура, без оверинжиниринга -- **Макс. 200 строк на файл** (было >1500) — превышение = разбить на модули +- **Макс. 250 строк на файл** (было >1500) — превышение = разбить на модули - **Макс. 4 файла на папку** — больше = новая подпапка по смыслу - **Публичный API можно менять свободно** (решение владельца 2026-09-05): KISS важнее обратной совместимости; при рефакторинге разрешено менять @@ -84,9 +84,9 @@ ### Проверка: `./gradlew build` ✓, тесты ✓ (13), `rg runtime-подсистема` → 0 ## Фаза 3: Разбивка файлов-гигантов (пункт 3) — ЗАВЕРШЕНА -> Стратегия: публичный фасад исключён из лимита ≤200 (делегирует ~все публичные методы), +> Стратегия: публичный фасад исключён из лимита ≤250 (делегирует ~все публичные методы), > вся логика вынесена в package-private final-хелперы в том же пакете (плоско), -> ≤200 строк каждый. 0 вызывающих файлов изменено. AttributeSwap/Scaffold/ElytraTarget/ +> ≤250 строк каждый. 0 вызывающих файлов изменено. AttributeSwap/Scaffold/ElytraTarget/ > AutoCrystal удалены в Фазе 3.5 — их разбивать не нужно. - [x] Renderer2D (3379) → фасад 2150 + 14 хелперов (Blur, Chamfered, Circles, Connectors, Items, Lines, LiquidGlass, Orbiz, Polygons, Rects, RectsExtra, RectsGlow, SmokeFill, Textures) @@ -105,15 +105,15 @@ SearchOverlay, Outline, MeshBuilder, ColorUtil + Model) - [x] CustomBar (1495) → фасад 426 + 11 хелперов (XpBar, JumpBar, Locator*×7, Resolver + Model) -### Проверка: после каждого файла `./gradlew build` ✓; хелперы ≤200, фасады исключены; +### Проверка: после каждого файла `./gradlew build` ✓; хелперы ≤250, фасады исключены; ### 0 вызывающих файлов изменено во всей Фазе 3 -### Долг (файлы >200 вне списка гигантов, в т.ч. новые фасады): +### Долг (файлы >250 вне списка гигантов, в т.ч. новые фасады): ### Renderer3D 433, Module 1181, LoVisualRenderPipelines 1329, DraggableHudElementRegistry 1449, ### Predictions 1363, DynamicIsland 1255, CustomHealthBar 412, CustomHotbar 547, HudNotifier 1116, ### Triangulator 1102, BetterButtons 1033, Themes 1012, SvgMeshBackend 1014, UiPrimitiveRenderer 1042, ### BetterTooltips 948, CustomTabList 829, InventorySwap 1009, ModuleManager 521, LoVisual 441, -### а также многие HUD/module/rendering-файлы 200–800 строк. Разбирать по желанию. +### а также многие HUD/module/rendering-файлы 250–800 строк. Разбирать по желанию. ## Фаза 3.5: Удаление читерских модулей (легит-направление) > Пользователь решил: LoVisual — легитное приложение. Все читерские модули удаляются. @@ -230,29 +230,29 @@ ### CommandDiscoveryTest обновлён) ## Фаза 8: Структура «как у Delta» (маленькие файлы) — ПЛАН -> Правило ≤200 уже есть (Фаза 1–3), но долг: ~240 файлов >200 строк. +> Правило ≤250 уже есть (Фаза 1–3), но долг: ~280 файлов >250 строк. > У Delta файлы компактные и слои разделены: api / client(features, ui, utils, processors) / > inject(accessors, invokers, mixin). Наша пакетная структура сопоставима — проблема в размере файлов. - [ ] Топ-гигантов к разбивке (после Фазы 3): LoVisualRenderPipelines 1527, DraggableHudElementRegistry 1449, Predictions 1363, DynamicIsland 1254, Module 1181, HudNotifier 1116, AltManager 1115, Triangulator 1102, TargetESP 1061, UiPrimitiveRenderer 1042 -- [ ] Стратегия: как в Фазе 3 — фасад + package-private хелперы ≤200, 0 изменений вызывающих, +- [ ] Стратегия: как в Фазе 3 — фасад + package-private хелперы ≤250, 0 изменений вызывающих, `./gradlew build` после каждого; по мере касания файлов в Фазе 7 — разбивать на ходу -- [ ] Facades >200 (Renderer2D 2150 и т.п.) — оставить как есть (исключение по правилам) +- [ ] Facades >250 (Renderer2D 2150 и т.п.) — оставить как есть (исключение по правилам) - [ ] Не смешивать с Фазой 7: разбивка структуры — отдельными проходами, иначе diff-каша ### Проверка: `./gradlew test` зелёный -## Фаза 8.5: Аудит структуры (правила ≤200 строк / ≤4 файла) — ПОЛНЫЙ СПИСОК НА РЕФАКТОРИНГ +## Фаза 8.5: Аудит структуры (правила ≤250 строк / ≤4 файла) — ПОЛНЫЙ СПИСОК НА РЕФАКТОРИНГ > Снимок 2026-09-09 (`src/main/java`, 1459 файлов / 197k строк). > **Гейт перед Фазой 9.2 (Optimize)**: движковые файлы, которые Optimize будет трогать, > должны быть разбиты заранее. 9.1 (мёртвые грузы) от этого НЕ зависит — идёт своим порядком. -> Стратегия та же, что Фаза 3/8: фасад + package-private хелперы ≤200, 0 правок вызывающих, +> Стратегия та же, что Фаза 3/8: фасад + package-private хелперы ≤250, 0 правок вызывающих, > билд после каждого файла. Фасад-исключение (Renderer2D) — см. оговорку ниже. ### Итоговая картина -- **Файлы >200 строк: 244 шт.** → >800 строк: 26 | 400–800: 78 | 201–400: 140 +- **Файлы >250 строк: 244 шт.** → >800 строк: 26 | 400–800: 78 | 201–400: 140 - **Папки >4 файлов: 86 шт.** - Разбивка больших файлов по зонам: features/gui — 73 | render/engine — 54 | 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 строк > (самый крупный — Renderer2D.java 474, осознанный фасад-делегат, исключение Фазы 8). Все 53 > пункта очереди ниже закрыты. Остаток структурного долга — 8.5.3 (201–400 строк), не марафон, -> а правило «трогаешь файл по другой задаче — первым коммитом режешь до ≤200». +> а правило «трогаешь файл по другой задаче — первым коммитом режешь до ≤250». > Формат: файл (строк) → идея разбивки. Порядок сверху вниз. Facade-оговорка: > Renderer2D остаётся фасадом-делегатом (правило Фазы 8), но его вспомогательные > классы (kawase/blur/glass) — вынести из OrderedUiBatcher (935) в `ui/blur/`. @@ -304,15 +304,15 @@ далее 1597→475 наследованием (волна 2026-09-25 ниже) (исключение по правилу Фазы 8) — делегирование оставлено; −234 мёртвые шимы совместимости, −271 недостижимые перегрузки liquidGlass/item/Corners, - −115 прочие мёртвые перегрузки+импорты; макс. тело метода 35 строк (блоков >200 нет), + −115 прочие мёртвые перегрузки+импорты; макс. тело метода 35 строк (блоков >250 нет), хелперы в `renderer/renderer2d/` (Rects2D/Strokes2D/…), build+test ✓ - [x] LoVisualAltManagerScreen 1113 → ГОТОВО (2026-09-10): 11 файлов `mainmenu/alt/` - (Screen 132, Controller 200, Ops 59, Anims 108 + auth/ 171 + render/ 157/156/139/100 - + render/cards/ 178/166), все ≤200, build+test ✓ + (Screen 132, Controller 250, Ops 59, Anims 108 + auth/ 171 + render/ 157/156/139/100 + + render/cards/ 178/166), все ≤250, build+test ✓ - [x] UiPrimitiveRenderer 1053 → ГОТОВО (2026-09-10): фасад 188 (коннекторы) + render/primitive/ 4 (Context 153, Props 158, Painter 145, GlowPainter 147) + 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] 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 темы/файл), дефолты отдельно @@ -322,7 +322,7 @@ - [x] Module 989 → ГОТОВО c8da336: 1031→139 + 4×≤134 (ext/) — базовый класс: settings-механику вынести в `module/ext/` (осторожно, общий предок!) - [x] LoVisualAddonManagerScreen 989 → ГОТОВО de56b4b9: 989→112 + 10 файлов mainmenu/addon/ (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] ClickGuiPickerState 962 → ГОТОВО 234f0d9: 970→166 + 4×≤140; CombatProtocolHeuristicsEditorState 932→79 +4×≤192 03ae4a6 — state/actions хелперы - [x] ModulesMenuScreen 960 → ГОТОВО 234f0d9: 966→145 + 4×≤178 — рендер категорий отдельно от состояния @@ -332,8 +332,8 @@ 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, GameRendererMixin 830 → ГОТОВО f45f46e: 830→344 (только хуки+Host/BobInvoker) + - mixins/render/main/handlers/ 4×≤200 (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, + 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×≤250 9f76e5a5 (scene/highlight/), см. /tmp-снимок в истории команд: ConfigProfilesComponent 791, HitEffect 770, PlayerMovementSimulation 755, ProjectilePuncher 746, MediaPlayer 739, Crosshair 737, MeshBuilder 729, SettingsPanelComponent 703, Admins 702, Cooldowns 701, Itemizer 699, LocalPlayerMixin 687, Trails 681, ItemBatchRenderer 673, @@ -345,7 +345,7 @@ CustomTextRenderer 229→195 (GradientTexts), CustomBar 438→185 (+CustomBarModel константы +bars/impl/BarSettings, forwardеры удалены, mixins правлены на реальные рендереры) 4e442fb7, 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 выше, 7b848ddd+464f7868+098c5515), LoVisualRenderPipelines 432→311 9577ed3d (SHADER_* 105 констант → pipeline/ShaderIds 130, мёртвые импорты удалены; @@ -355,7 +355,7 @@ Path(245) extends Rounded(315) extends Base(23); API вызывающих не менялся (0 правок callers, static-import ITEM_OVERLAY_* и Renderer2D.LiquidGlassPreset работают по наследству); renderer/facade/ = 4 файла, facade/shape/ = 2 ✓ лимит; - Base.self() для this→Renderer2D-делегатов; семейства >200 — фасад-части + Base.self() для this→Renderer2D-делегатов; семейства >250 — фасад-части (исключение TODO:Facades); 37c7c410+64e23ef8+9ae72eeb+2b8c40a3+6204017b+ 9fffc58d, build+test+checkstyle ✓. @@ -373,9 +373,9 @@ 76bd8e95; BetterButtons 381→197 (настройки → ButtonStyleSettings по образцу BarSettings, иконки → ButtonIconRenderer, −3 мёртвых метода без вызовов) 71f779c8; `./gradlew build` ✓ test 438 ✓ checkFolderLimit ✓ на каждом шаге; - файлов >200: 155 → 151 + файлов >250: 155 → 151 - [ ] Не делать отдельным марафоном: при правке файла по другой фазе — первым коммитом - разбивка на ≤200, вторым — целевое изменение (иначе diff-каша, правило Фазы 8) + разбивка на ≤250, вторым — целевое изменение (иначе diff-каша, правило Фазы 8) ### 8.5.4 Порядок и критерии готовности - [x] Очередь: 8.5.1 крупные папки (mixins, visuals, clickgui/settings, hud impl) → @@ -384,12 +384,12 @@ - [ ] После каждого шага: `./gradlew build` ✓, `./gradlew test` ✓; конфиг-id модулей и ключи сеттингов НЕ ломаем (совместимость .lvcfg); скрипты-переименования пакетов не трогают string-literals id — проверять grep'ом после moves. -- [x] Критерий закрытия для старта 9.2: ноль файлов >200 в путях, которые трогает Optimize +- [x] Критерий закрытия для старта 9.2: ноль файлов >250 в путях, которые трогает Optimize (Renderer2D-секции blur/glass, OrderedUiBatcher, HUD-имплементации bgEffect, MsdfFont/CustomTextRenderer), и ноль папок >4 в этих же пакетах. Полный ноль по всем 244/86 — долгий хвост, 9.2 им не блокируется. → 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/render/, triangulator/{render,Solver,Tracker,View}, panels/scoreboard/) → исключение по правилу фасадов. ГЕЙТ 9.2 ЗАКРЫТ. @@ -429,7 +429,7 @@ ### 9.2 Модуль **Optimize** (misc) — ручной рубильник экономии > ⛔ ГЕЙТ: не начинать, пока не закрыта Фаза 8.5 (рефакторинг-аудитStructure ниже) — -> Optimize трогает Renderer2D/OrderedUiBatcher/HUD-имплементации, все они >200 строк. +> Optimize трогает Renderer2D/OrderedUiBatcher/HUD-имплементации, все они >250 строк. > Идея владельца: один тумблер «хочу максимум FPS». НИКАКОГО автовключения по FPS. > Пока активен — движок и HUD работают в лёгком режиме. - [x] Каркас — ГОТОВО ade82f06: `modules/misc/optimize/Optimize.java` (в подпапке — лимит @@ -705,8 +705,16 @@ hand_pose в `submitArmWithItem`, item_pose в `renderItem` (first person). 7 тестов `LuaSandboxTest`. Не сделано из 1-8: `hand_relative_pose`, аддоны (`*_addon.lua`), подмена ванильного equip/swing скриптом, обе руки всегда, ItemStack-поля. Не проверено вживую: направление осей M:rotate на реальной руке. -- [ ] 1b. HMI хвост (пункты 9-16): счётчики свинга и события, обе руки, ItemStack-поля, use_duration/using_item, - аниматор частей модели (`item_model.lua`), камера из Lua, Lua-частицы, клавиша осмотра. +- [x] 1b. HMI хвост (сделано): 4 вида скриптов `hand_pose`, `hand_relative_pose`, `item_pose`, `item_model` + аддоны + `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` + бейк + сэмплер). - [ ] 3. Модуль Knife (Melee Skins): 33 скина, клипы draw/inspect/melee_1..3/run, first/third person. - [ ] 4. Быстрые модули: TNTTimer, AutoSprint, TotemCounter, PlaceAnimation, EnchantGlint, ArmorColor, GodRays, ProjectileTrails. @@ -725,7 +733,7 @@ (`ClickGuiScreenManager`, три места `new ClickGuiScreen()`), `classic` по умолчанию, второй вид позже; бинд и звуки остаются в модуле `ClickGui`. Ждёт решения, какой второй вид делать первым. - [ ] `.gitignore` для `server-plugin/.classpath`, `.project`, `.settings/`. -- [ ] Фаза 8.5.3 по-прежнему в очереди (около 150 файлов >200 строк), по мере касания. +- [ ] Фаза 8.5.3 по-прежнему в очереди (около 150 файлов >250 строк), по мере касания. - [ ] Приёмка серверного плагина (Фаза 11 в корневом TODO.md): Velocity + `%policy` на стенде. ### Заметки для следующих сессий @@ -735,5 +743,5 @@ `flock /tmp/claude-1000/gradle.lock ./gradlew build --offline -q`. - Новые классы-помощники для миксинов НЕЛЬЗЯ класть в пакет `dev.loki.lovisual.mixins.*` (Mixin: `IllegalClassLoadError`); им место в `mixinhandlers/` или `render/`. -- Лимиты проекта: ≤200 строк на файл, ≤4 файла на папку (`checkFolderLimit` падает при нарушении), +- Лимиты проекта: ≤250 строк на файл, ≤4 файла на папку (`checkFolderLimit` падает при нарушении), без длинного тире в текстах сайта и ответах. diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java index c8d078f..869e011 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/HoldMyItems.java @@ -5,6 +5,7 @@ import dev.loki.lovisual.config.values.primitive.BooleanValue; 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.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.ModuleInfo; import dev.loki.lovisual.util.animation.EaseCurves; @@ -55,6 +56,28 @@ public final class HoldMyItems extends Module { public final NumberValue gripTurn = visibleWhen(num("grip_turn", 0.0f, -90.0f, 90.0f), gripEnabled::get); public final NumberValue 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() { return !"Scripts".equals(handMode.get()); } diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/HandScripts.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/HandScripts.java index b3abf17..eddcc5d 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/HandScripts.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/HandScripts.java @@ -1,14 +1,17 @@ package dev.loki.lovisual.features.module.modules.visuals.items.held.lua; 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.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.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.ScriptFiles; 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.world.InteractionHand; import net.minecraft.world.entity.HumanoidArm; @@ -22,30 +25,24 @@ import org.luaj.vm2.LuaValue; 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 hand_pose.lua} (поза всей руки) и {@code item_pose.lua} (поза предмета). - * Источник: {@code config/lovisual/holdmyitems/.lua} (горячая перезагрузка по mtime раз в - * секунду), иначе встроенный пример из ресурсов мода. Ошибка скрипта пишется в лог один раз и - * отключает только этот скрипт до правки файла. + * Скрипты руки (см. {@link ScriptKind}): hand_pose, hand_relative_pose, item_pose, item_model и их аддоны. + * Порядок за кадр руки: hand_pose, hand_relative_pose, (ваниль), item_pose, item_model. Источник и горячая + * перезагрузка по mtime раз в секунду: {@link ScriptFiles}. Ошибка скрипта пишется в лог один раз и отключает + * только этот скрипт до правки файла. */ 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 long RECHECK_MS = 1000L; private final LuaSandbox sandbox = new LuaSandbox(); private final PoseApi pose = new PoseApi(); - private final long[] stamps = {Long.MIN_VALUE, Long.MIN_VALUE}; - private final ScriptSlot[] slots = new ScriptSlot[2]; + private final VanillaApi vanilla = new VanillaApi(); + 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 long lastCheck; private Player player; @@ -56,66 +53,82 @@ public final class HandScripts { g.set("M", pose.table()); g.set("P", EntityApi.player(() -> player)); 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(); Easings.all().forEach((name, f) -> LuaTables.put(easings, name, a -> LuaValue.valueOf(f.applyAsDouble(LuaTables.num(a, 2, 0))))); 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, float swing, float equip, float tickDelta, ItemStack item, PoseStack ms) { if (!module.scriptsActive()) return false; boolean mainHand = hand == InteractionHand.MAIN_HAND; boolean rightArm = (player.getMainArm() == HumanoidArm.RIGHT) == mainHand; 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; } - /** Применяет item_pose к матрице предмета; true, если был pushPose и нужен popPose. */ + /** Применяет item_pose и item_model; true, если был pushPose и нужен popPose. */ public static boolean applyItemPose(HoldMyItems module, LivingEntity entity, ItemStack stack, ItemDisplayContext ctx, PoseStack ms) { if (!ctx.firstPerson() || !(entity instanceof AbstractClientPlayer player)) return false; if (!module.isEnabled() || !module.scriptsActive()) return false; boolean rightArm = ctx == ItemDisplayContext.FIRST_PERSON_RIGHT_HAND; boolean mainHand = (player.getMainArm() == HumanoidArm.RIGHT) == rightArm; - HandScripts scripts = module.scripts(); - float[] last = scripts.lastState(mainHand); + HandScripts s = module.scripts(); + float[] last = s.lastState[mainHand ? 0 : 1]; + HandFrame f = new HandFrame(player, stack, mainHand, rightArm, last[0], last[1], 0.0f, ms); 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; } - public static Path scriptDir() { - 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) { + private void run(ScriptKind kind, HandFrame f) { refresh(); - ScriptSlot slot = slots[index]; - if (slot == null || !slot.canRun()) return; + for (int part = 0; part < 2; part++) { + 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.stack = f.stack(); Globals g = sandbox.globals(); @@ -129,47 +142,23 @@ public final class HandScripts { g.set("useDuration", LuaValue.valueOf(f.player().getTicksUsingItem())); g.set("time", LuaValue.valueOf(System.nanoTime() / 1.0e9)); pose.bind(f.pose()); - try { - slot.run(); - } finally { - pose.finish(); - } - if (!slot.canRun()) LOG.warn("HoldMyItems script {} disabled: {}", name, slot.error()); } private void refresh() { long now = System.currentTimeMillis(); if (now - lastCheck < RECHECK_MS) return; lastCheck = now; - String[] names = {HAND_POSE, ITEM_POSE}; - for (int i = 0; i < names.length; i++) { - Path file = scriptDir().resolve(names[i] + ".lua"); - long stamp = stampOf(file); - if (stamp == stamps[i]) continue; - stamps[i] = stamp; - String source = read(file, names[i]); - slots[i] = source == null ? null : new ScriptSlot(names[i], source, sandbox); - if (slots[i] != null && !slots[i].canRun()) LOG.warn("HoldMyItems script {}: {}", names[i], slots[i].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); + for (ScriptKind kind : kinds) { + for (int part = 0; part < 2; part++) { + String name = kind.file(part); + long stamp = ScriptFiles.stamp(name, part == 0); + if (stamp == stamps[kind.ordinal()][part]) continue; + stamps[kind.ordinal()][part] = stamp; + String source = stamp < 0 ? null : ScriptFiles.read(name); + ScriptSlot slot = source == null ? null : new ScriptSlot(name, source, sandbox); + slots[kind.ordinal()][part] = slot; + if (slot != null && !slot.canRun()) LOG.warn("HoldMyItems script {}: {}", name, slot.error()); } - } catch (IOException e) { - LOG.warn("Cannot read HoldMyItems script {}: {}", name, e.getMessage()); - return null; } } } diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ScriptKind.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ScriptKind.java new file mode 100644 index 0000000..e0bf9b2 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ScriptKind.java @@ -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; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/api/PoseApi.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/api/PoseApi.java index c353222..fba7340 100644 --- a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/api/PoseApi.java +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/api/PoseApi.java @@ -21,6 +21,11 @@ public final class PoseApi { return this; } + /** Текущая матрица позы (копия) либо null, если скрипт не выполняется. */ + public org.joml.Matrix4f matrix() { + return stack == null ? null : new org.joml.Matrix4f(stack.last().pose()); + } + /** Закрывает push, оставленные скриптом незакрытыми. */ public void finish() { while (stack != null && depth > 0) { diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/HandParticles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/HandParticles.java new file mode 100644 index 0000000..ee1b9ae --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/HandParticles.java @@ -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

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); + } + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/ModelAnimator.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/ModelAnimator.java new file mode 100644 index 0000000..4dedd57 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/ModelAnimator.java @@ -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 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 apply(List quads) { + lastQuadCount = quads.size(); + if (ops.isEmpty()) return quads; + List 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()); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/QuadOps.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/QuadOps.java new file mode 100644 index 0000000..4e49306 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/QuadOps.java @@ -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(); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/VanillaApi.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/VanillaApi.java new file mode 100644 index 0000000..e909526 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/ext/VanillaApi.java @@ -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; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/runtime/ScriptFiles.java b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/runtime/ScriptFiles.java new file mode 100644 index 0000000..9e7b89b --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/runtime/ScriptFiles.java @@ -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/.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"; + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java index 6ebfe58..9063d4b 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandAlwaysVisibleMixin.java @@ -3,6 +3,7 @@ package dev.loki.lovisual.mixins.render.item; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; 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.HoldMyItems; import net.minecraft.client.renderer.ItemInHandRenderer; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -25,6 +26,7 @@ public abstract class ItemInHandAlwaysVisibleMixin { ) ) private boolean lovisual$alwaysRenderMainHand(boolean vanillaDecision) { + if (lovisual$forceBoth()) return true; AlwaysVisibleHand module = Modules.get(AlwaysVisibleHand.class); return module == null ? vanillaDecision : module.renderMainHand(vanillaDecision); } @@ -37,7 +39,14 @@ public abstract class ItemInHandAlwaysVisibleMixin { ) ) private boolean lovisual$alwaysRenderOffHand(boolean vanillaDecision) { + if (lovisual$forceBoth()) return true; AlwaysVisibleHand module = Modules.get(AlwaysVisibleHand.class); 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(); + } } diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java index 1a202fb..46be5a9 100644 --- a/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/ItemInHandRendererMixin.java @@ -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.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.ext.ModelAnimator; import dev.loki.lovisual.util.animation.hand.HandDynamics; import dev.loki.lovisual.features.module.modules.visuals.camera.ViewModel; import dev.loki.lovisual.util.animation.EquipProgressComposer; @@ -114,6 +115,8 @@ public abstract class ItemInHandRendererMixin { boolean lowerEnabled = viewModel != null && viewModel.isEnabled(); boolean curveEnabled = holdMyItems != null && holdMyItems.isEnabled() && holdMyItems.dynamicsActive() && !(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; return EquipProgressComposer.compose( 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" ) ) - private void lovisual$scaleMiniItems( - LivingEntity entity, - ItemStack stack, - ItemDisplayContext ctx, - PoseStack ms, - SubmitNodeCollector queue, - int light, - CallbackInfo ci - ) { + private void lovisual$scaleMiniItems(LivingEntity entity, ItemStack stack, ItemDisplayContext ctx, + PoseStack ms, SubmitNodeCollector queue, int light, CallbackInfo ci) { HoldMyItems held = Modules.get(HoldMyItems.class); if (held != null) lovisual$itemPosePushed = HandScripts.applyItemPose(held, entity, stack, ctx, ms); ViewModel viewModel = Modules.get(ViewModel.class); @@ -162,26 +158,23 @@ public abstract class ItemInHandRendererMixin { private void lovisual$restoreItemPose( LivingEntity entity, ItemStack stack, ItemDisplayContext ctx, PoseStack ms, SubmitNodeCollector queue, int light, CallbackInfo ci) { + ModelAnimator.INSTANCE.clear(); if (!lovisual$itemPosePushed) return; lovisual$itemPosePushed = false; ms.popPose(); } - @Inject( - method = "swingArm", - at = @At("HEAD"), - cancellable = true - ) - private void lovisual$customSwing( - float swingProgress, - PoseStack ms, - int direction, - HumanoidArm arm, - CallbackInfo ci - ) { + @Inject(method = "swingArm", at = @At("HEAD"), cancellable = true) + private void lovisual$customSwing(float swingProgress, PoseStack ms, int direction, HumanoidArm arm, CallbackInfo ci) { + Minecraft mc = Minecraft.getInstance(); + HoldMyItems held = Modules.get(HoldMyItems.class); + HandScripts scripts = held == null ? null : held.scriptsOrNull(); + if (scripts != null && scripts.vanilla().cancelSwing(arm == mc.options.mainHand().get())) { + ci.cancel(); + return; + } ViewModel viewModel = Modules.get(ViewModel.class); if (viewModel == null || !viewModel.isEnabled()) return; - Minecraft mc = Minecraft.getInstance(); if (mc.player == null) return; ItemStack stack = mc.player.getMainHandItem(); diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/item/model/ItemModelAnimatorMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/model/ItemModelAnimatorMixin.java new file mode 100644 index 0000000..a1d3ad4 --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/item/model/ItemModelAnimatorMixin.java @@ -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 lovisual$animateQuads(List quads) { + return ModelAnimator.INSTANCE.apply(quads); + } +} diff --git a/mod/src/main/java/dev/loki/lovisual/mixins/render/level/CameraScriptMixin.java b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/CameraScriptMixin.java new file mode 100644 index 0000000..8e46fdb --- /dev/null +++ b/mod/src/main/java/dev/loki/lovisual/mixins/render/level/CameraScriptMixin.java @@ -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]); + } +} diff --git a/mod/src/main/resources/assets/lovisual/holdmyitems/hand_relative_pose.lua b/mod/src/main/resources/assets/lovisual/holdmyitems/hand_relative_pose.lua new file mode 100644 index 0000000..a96242c --- /dev/null +++ b/mod/src/main/resources/assets/lovisual/holdmyitems/hand_relative_pose.lua @@ -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 diff --git a/mod/src/main/resources/assets/lovisual/holdmyitems/item_model.lua b/mod/src/main/resources/assets/lovisual/holdmyitems/item_model.lua new file mode 100644 index 0000000..709a387 --- /dev/null +++ b/mod/src/main/resources/assets/lovisual/holdmyitems/item_model.lua @@ -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 diff --git a/mod/src/main/resources/assets/lovisual/holdmyitems/item_pose.lua b/mod/src/main/resources/assets/lovisual/holdmyitems/item_pose.lua index 0034b69..164af9b 100644 --- a/mod/src/main/resources/assets/lovisual/holdmyitems/item_pose.lua +++ b/mod/src/main/resources/assets/lovisual/holdmyitems/item_pose.lua @@ -16,3 +16,13 @@ end if usingItem and (I:getUseAction() == 'eat' or I:getUseAction() == 'drink') then M:rotateZ(M:sin(useDuration * 1.1) * 3) 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 diff --git a/mod/src/main/resources/assets/lovisual/lang/en_us.json b/mod/src/main/resources/assets/lovisual/lang/en_us.json index d4d1c8c..1e8d1be 100644 --- a/mod/src/main/resources/assets/lovisual/lang/en_us.json +++ b/mod/src/main/resources/assets/lovisual/lang/en_us.json @@ -3341,6 +3341,7 @@ "setting.autorespawn.delay": "Delay", "module.deathcoords.description": "Sends the coordinates of your latest death to the chat.", "setting.deathcoords.show_dimension": "Show dimension", + "setting.holdmyitems.always_hands": "Always render both hands", "setting.holdmyitems.mode": "Mode", "setting.holdmyitems.mode.option.Dynamics": "Dynamics (Java)", "setting.holdmyitems.mode.option.Scripts": "Scripts (Lua)", diff --git a/mod/src/main/resources/assets/lovisual/lang/ru_ru.json b/mod/src/main/resources/assets/lovisual/lang/ru_ru.json index 2fb12e7..ab6e1e7 100644 --- a/mod/src/main/resources/assets/lovisual/lang/ru_ru.json +++ b/mod/src/main/resources/assets/lovisual/lang/ru_ru.json @@ -3136,6 +3136,7 @@ "setting.autorespawn.delay": "Задержка", "module.deathcoords.description": "Отправляет в чат координаты вашей последней смерти.", "setting.deathcoords.show_dimension": "Показывать измерение", + "setting.holdmyitems.always_hands": "Всегда рисовать обе руки", "setting.holdmyitems.mode": "Режим", "setting.holdmyitems.mode.option.Dynamics": "Динамика (Java)", "setting.holdmyitems.mode.option.Scripts": "Скрипты (Lua)", diff --git a/mod/src/main/resources/lovisual.mixins.json b/mod/src/main/resources/lovisual.mixins.json index bdfcaa8..0893327 100644 --- a/mod/src/main/resources/lovisual.mixins.json +++ b/mod/src/main/resources/lovisual.mixins.json @@ -156,8 +156,10 @@ "render.item.EquipmentLayerRendererMixin", "render.item.ItemFeatureRendererMixin", "render.item.ItemInHandAlwaysVisibleMixin", + "render.item.model.ItemModelAnimatorMixin", "render.item.ItemInHandRendererMixin", "render.level.CameraMixin", + "render.level.CameraScriptMixin", "render.level.particles.FireworkParticlesStarterMixin", "render.level.LevelRendererMixin", "render.level.particles.ParticleManagerMixin", diff --git a/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/QuadOpsTest.java b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/QuadOpsTest.java new file mode 100644 index 0000000..7c21f13 --- /dev/null +++ b/mod/src/test/java/dev/loki/lovisual/features/module/modules/visuals/items/held/lua/QuadOpsTest.java @@ -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); + } +}