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:
parent
44353e893c
commit
0504be5b57
21 changed files with 750 additions and 139 deletions
70
mod/TODO.md
70
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` падает при нарушении),
|
||||
без длинного тире в текстах сайта и ответах.
|
||||
|
|
|
|||
|
|
@ -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<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);
|
||||
|
||||
/** Принудительно рисовать обе руки (как 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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/<name>.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue