Compare commits
73 Commits
develop
...
feature/ri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23b249dcd2 | ||
|
|
25a9251959 | ||
|
|
d90ff14668 | ||
|
|
7c994a9ffa | ||
|
|
9171ff0def | ||
|
|
fdf7330523 | ||
|
|
227cf5f346 | ||
|
|
86d35c4b5d | ||
|
|
a7a1c774f7 | ||
|
|
c9d5271102 | ||
|
|
76587c0393 | ||
|
|
ed0fb49792 | ||
|
|
a0b6ac5b04 | ||
|
|
efba00d005 | ||
|
|
dd30a8d4f9 | ||
|
|
4648107ebe | ||
|
|
e969131ad2 | ||
|
|
d39a9d5ebc | ||
|
|
7281548a6a | ||
|
|
5428f13f98 | ||
|
|
e15ab7b831 | ||
|
|
cae3572488 | ||
|
|
de691e24ec | ||
|
|
a2bcfe2dda | ||
|
|
13b0f8f590 | ||
|
|
aebc7f3868 | ||
|
|
e37dad18aa | ||
|
|
9a31f21b55 | ||
|
|
921a028a53 | ||
|
|
744aef63b5 | ||
|
|
c1ecfd75c7 | ||
|
|
639e9e94f7 | ||
|
|
ab93dc80be | ||
|
|
2a4ec170ef | ||
|
|
5d108f51b4 | ||
|
|
ddaa25b971 | ||
|
|
cef589aac1 | ||
|
|
15e405f5b0 | ||
|
|
1fa291563c | ||
|
|
05cc07a97d | ||
|
|
d3a3b400aa | ||
|
|
687b810a0e | ||
|
|
06ec7c7c5f | ||
|
|
8530671a49 | ||
|
|
03c28e3332 | ||
|
|
4152f9fc71 | ||
|
|
647894508d | ||
|
|
f4aae9adb7 | ||
|
|
f80dc68c0b | ||
|
|
5a39fb0c1c | ||
|
|
b494b60d60 | ||
|
|
08808dbcc1 | ||
|
|
73264db3c6 | ||
|
|
987efde86b | ||
|
|
d129983eb7 | ||
|
|
8dff4c0e03 | ||
|
|
39f6177595 | ||
|
|
79fc470aa0 | ||
|
|
ccec6bd87e | ||
|
|
faad0ced0f | ||
|
|
3aec681436 | ||
|
|
4a587b7478 | ||
|
|
4d90a87b48 | ||
|
|
29c4fddb90 | ||
|
|
94fcece05a | ||
|
|
4a615368df | ||
|
|
1cef57a472 | ||
|
|
bdbd429bdf | ||
|
|
f0d8408384 | ||
|
|
324e7fb984 | ||
|
|
cbf61906e0 | ||
|
|
b141e137e7 | ||
|
|
b0b719b3dd |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -40,9 +40,11 @@ package-lock.json
|
|||||||
|
|
||||||
# Build logs
|
# Build logs
|
||||||
build_output.log
|
build_output.log
|
||||||
|
logs/
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
desktop.ini
|
desktop.ini
|
||||||
docs/
|
docs/
|
||||||
|
docs.tar.gz
|
||||||
|
|||||||
102
LICENSE
102
LICENSE
@@ -1,66 +1,21 @@
|
|||||||
# TiedUp! Remake - License
|
# TiedUp! Remake - License
|
||||||
|
|
||||||
**Effective Date:** January 2025
|
**Effective Date:** April 2026 (license change from GPL-3.0 + Commons-Clause to GPL-3.0 pure)
|
||||||
**Applies to:** All versions of TiedUp! Remake (past, present, and future)
|
**Applies to:** All versions from 0.6.0-ALPHA onwards. Prior versions (0.1.0 through 0.5.x) were distributed under GPL-3.0 WITH Commons-Clause.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
This software is licensed under **GPL-3.0 with Commons Clause** and additional restrictions.
|
This software is licensed under **GPL-3.0-or-later** (GNU General Public License, version 3 or any later version).
|
||||||
|
|
||||||
**You CAN:**
|
The Commons-Clause restriction and additional monetization restrictions present in prior versions are **removed** effective this release, to enable incorporating third-party GPLv3 code (notably the Epic Fight animation/skeleton/mesh subsystem — see `docs/plans/rig/`).
|
||||||
- Use the mod for free
|
|
||||||
- Modify the source code
|
|
||||||
- Distribute the mod (with source code)
|
|
||||||
- Create derivative works (must be open source under the same license)
|
|
||||||
|
|
||||||
**You CANNOT:**
|
|
||||||
- Sell this software
|
|
||||||
- Put this software behind a paywall, subscription, or any form of monetization
|
|
||||||
- Distribute without providing source code
|
|
||||||
- Use a more restrictive license for derivative works
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Full License Terms
|
## GNU General Public License v3.0-or-later
|
||||||
|
|
||||||
### Part 1: Commons Clause Restriction
|
Copyright (C) 2024-2026 TiedUp! Remake Contributors
|
||||||
|
|
||||||
"Commons Clause" License Condition v1.0
|
|
||||||
|
|
||||||
The Software is provided to you by the Licensor under the License, as defined
|
|
||||||
below, subject to the following condition.
|
|
||||||
|
|
||||||
Without limiting other conditions in the License, the grant of rights under the
|
|
||||||
License will not include, and the License does not grant to you, the right to
|
|
||||||
Sell the Software.
|
|
||||||
|
|
||||||
For purposes of the foregoing, "Sell" means practicing any or all of the rights
|
|
||||||
granted to you under the License to provide to third parties, for a fee or other
|
|
||||||
consideration (including without limitation fees for hosting or consulting/
|
|
||||||
support services related to the Software), a product or service whose value
|
|
||||||
derives, entirely or substantially, from the functionality of the Software.
|
|
||||||
|
|
||||||
**Additional Monetization Restrictions:**
|
|
||||||
|
|
||||||
The following are explicitly prohibited:
|
|
||||||
1. Selling the Software or any derivative work
|
|
||||||
2. Requiring payment, subscription, or donation to access or download the Software
|
|
||||||
3. Placing the Software behind a paywall of any kind (Patreon, Ko-fi, etc.)
|
|
||||||
4. Bundling the Software with paid products or services
|
|
||||||
5. Using the Software as an incentive for paid memberships or subscriptions
|
|
||||||
6. Early access monetization (charging for early access to updates)
|
|
||||||
|
|
||||||
**Permitted:**
|
|
||||||
- Accepting voluntary donations (as long as the Software remains freely accessible)
|
|
||||||
- Using the Software on monetized content platforms (YouTube, Twitch, etc.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Part 2: GNU General Public License v3.0
|
|
||||||
|
|
||||||
Copyright (C) 2024-2025 TiedUp! Remake Contributors
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
it under the terms of the GNU General Public License as published by
|
it under the terms of the GNU General Public License as published by
|
||||||
@@ -80,14 +35,24 @@ https://www.gnu.org/licenses/gpl-3.0.txt
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Part 3: Asset Exclusions
|
## Derived Work — Epic Fight
|
||||||
|
|
||||||
The following assets are NOT covered by this license and remain property of
|
Portions of this project (everything under `com.tiedup.remake.v3.*`, starting with 0.6.0-ALPHA) are derived from **Epic Fight** by the Epic Fight Team, licensed under GPLv3. See:
|
||||||
their respective owners:
|
|
||||||
|
- Upstream repository: https://github.com/Epic-Fight/epicfight
|
||||||
|
- Upstream license: GPLv3 (identical to this project)
|
||||||
|
|
||||||
|
Each derived file in `com.tiedup.remake.rig.*` carries a header attribution to Epic Fight. No Epic Fight assets (textures, 3D models, animations) are reused — only Java source code for the animation/skeleton/mesh infrastructure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Asset Exclusions
|
||||||
|
|
||||||
|
The following assets are NOT covered by this license and remain property of their respective owners:
|
||||||
|
|
||||||
1. **Original kdnp mod Assets** (textures, models, sounds from the 1.12.2 version)
|
1. **Original kdnp mod Assets** (textures, models, sounds from the 1.12.2 version)
|
||||||
- Original creators: Yuti & Marl Velius
|
- Original creators: Yuti & Marl Velius
|
||||||
- These assets are used under fair use for preservation/educational purposes
|
- Used under fair use for preservation/educational purposes
|
||||||
- Contact original authors for commercial use
|
- Contact original authors for commercial use
|
||||||
|
|
||||||
2. **Minecraft Assets**
|
2. **Minecraft Assets**
|
||||||
@@ -95,28 +60,15 @@ their respective owners:
|
|||||||
- Subject to Minecraft EULA: https://www.minecraft.net/en-us/eula
|
- Subject to Minecraft EULA: https://www.minecraft.net/en-us/eula
|
||||||
|
|
||||||
3. **Third-Party Libraries**
|
3. **Third-Party Libraries**
|
||||||
- PlayerAnimator: Subject to its own license (dev.kosmx.player-anim)
|
- Forge: subject to Forge license (MinecraftForge)
|
||||||
- Forge: Subject to Forge license (MinecraftForge)
|
- Other dependencies: subject to their respective licenses
|
||||||
- Other dependencies: Subject to their respective licenses
|
- (Prior to 0.6.0, PlayerAnimator and bendy-lib were used; removed in the RIG system.)
|
||||||
|
|
||||||
**Code written for this remake** (files in `src/main/java/com/tiedup/remake/`)
|
Code written for this project (files in `src/main/java/com/tiedup/remake/`) is fully covered by GPL-3.0-or-later.
|
||||||
is fully covered by this GPL-3.0 + Commons Clause license.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Part 4: Derivative Works
|
## Disclaimer
|
||||||
|
|
||||||
Any derivative work based on this Software MUST:
|
|
||||||
|
|
||||||
1. Be distributed under this same license (GPL-3.0 + Commons Clause)
|
|
||||||
2. Provide complete source code
|
|
||||||
3. Maintain all copyright notices
|
|
||||||
4. Not be sold or monetized in any way
|
|
||||||
5. Credit the original TiedUp! Remake project
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Part 5: Disclaimer
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
@@ -131,9 +83,9 @@ SOFTWARE.
|
|||||||
## SPDX Identifier
|
## SPDX Identifier
|
||||||
|
|
||||||
```
|
```
|
||||||
SPDX-License-Identifier: GPL-3.0-only WITH Commons-Clause-1.0
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
```
|
```
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
For licensing questions or permission requests, open an issue on the project repository.
|
For licensing questions, open an issue on the project repository.
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ minecraft {
|
|||||||
// However, it must be at "META-INF/accesstransformer.cfg" in the final mod jar to be loaded by Forge.
|
// However, it must be at "META-INF/accesstransformer.cfg" in the final mod jar to be loaded by Forge.
|
||||||
// This default location is a best practice to automatically put the file in the right place in the final jar.
|
// This default location is a best practice to automatically put the file in the right place in the final jar.
|
||||||
// See https://docs.minecraftforge.net/en/latest/advanced/accesstransformers/ for more information.
|
// See https://docs.minecraftforge.net/en/latest/advanced/accesstransformers/ for more information.
|
||||||
// accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg')
|
accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg')
|
||||||
|
|
||||||
// Default run configurations.
|
// Default run configurations.
|
||||||
// These can be tweaked, removed, or duplicated as needed.
|
// These can be tweaked, removed, or duplicated as needed.
|
||||||
@@ -105,6 +105,7 @@ minecraft {
|
|||||||
// Mixin config arg
|
// Mixin config arg
|
||||||
args '-mixin.config=tiedup.mixins.json'
|
args '-mixin.config=tiedup.mixins.json'
|
||||||
args '-mixin.config=tiedup-compat.mixins.json'
|
args '-mixin.config=tiedup-compat.mixins.json'
|
||||||
|
args '-mixin.config=tiedup-rig.mixins.json'
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
@@ -118,6 +119,7 @@ minecraft {
|
|||||||
// Mixin config arg
|
// Mixin config arg
|
||||||
args '-mixin.config=tiedup.mixins.json'
|
args '-mixin.config=tiedup.mixins.json'
|
||||||
args '-mixin.config=tiedup-compat.mixins.json'
|
args '-mixin.config=tiedup-compat.mixins.json'
|
||||||
|
args '-mixin.config=tiedup-rig.mixins.json'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional client instances for multiplayer testing
|
// Additional client instances for multiplayer testing
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ mod_id=tiedup
|
|||||||
# The human-readable display name for the mod.
|
# The human-readable display name for the mod.
|
||||||
mod_name=TiedUp
|
mod_name=TiedUp
|
||||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||||
mod_license=GPL-3.0 WITH Commons-Clause (No Sale/Paywall)
|
mod_license=GPL-3.0-or-later
|
||||||
# The mod version. See https://semver.org/
|
# The mod version. See https://semver.org/
|
||||||
mod_version=0.5.6-ALPHA
|
mod_version=0.5.6-ALPHA
|
||||||
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
|
||||||
|
|||||||
137
scripts/rig-extract-phase0.sh
Executable file
137
scripts/rig-extract-phase0.sh
Executable file
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# rig-extract-phase0.sh
|
||||||
|
# Extrait le core Epic Fight nécessaire pour le RIG system TiedUp.
|
||||||
|
# Voir docs/plans/rig/EXTRACTION.md §9 pour détails.
|
||||||
|
#
|
||||||
|
# NE PAS utiliser "set -e" — certains cp peuvent échouer (fichiers non
|
||||||
|
# critiques, 2>/dev/null || true) et on veut continuer.
|
||||||
|
#
|
||||||
|
# Lancer depuis la racine du projet.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
SRC="docs/ModSources/epicfight-1.20.1/src/main/java/yesman/epicfight"
|
||||||
|
DST="src/main/java/com/tiedup/remake/rig"
|
||||||
|
|
||||||
|
if [ ! -d "$SRC" ]; then
|
||||||
|
echo "ERROR: Epic Fight source not found at $SRC"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== rig-extract-phase0.sh ==="
|
||||||
|
echo "Source : $SRC"
|
||||||
|
echo "Cible : $DST"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "=== 0. Structure des packages ==="
|
||||||
|
mkdir -p "$DST"/{math,armature,armature/types,anim,anim/types,anim/property,anim/client,anim/client/property,mesh,mesh/transformer,cloth,asset,event,patch,render,render/compute,registry,bridge,tick,mixin,util,util/datastruct,exception}
|
||||||
|
|
||||||
|
echo "=== 1. Math utils ==="
|
||||||
|
cp -v "$SRC"/api/utils/math/*.java "$DST/math/"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 2. Armature (Armature + Joint + JointTransform) ==="
|
||||||
|
cp -v "$SRC"/api/model/Armature.java "$DST/armature/"
|
||||||
|
cp -v "$SRC"/api/animation/Joint.java "$DST/armature/"
|
||||||
|
cp -v "$SRC"/api/animation/JointTransform.java "$DST/armature/"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 3. Animation core + LivingMotion + ServerAnimator ==="
|
||||||
|
for f in Animator AnimationPlayer AnimationClip AnimationManager AnimationVariables \
|
||||||
|
SynchedAnimationVariableKey SynchedAnimationVariableKeys Keyframe Pose TransformSheet \
|
||||||
|
LivingMotion LivingMotions ServerAnimator; do
|
||||||
|
cp -v "$SRC/api/animation/$f.java" "$DST/anim/" 2>/dev/null || echo " (skip : $f.java non trouvé)"
|
||||||
|
done
|
||||||
|
cp -v "$SRC"/api/animation/property/*.java "$DST/anim/property/" 2>/dev/null
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 4. Animation types (filtrés + stubs combat pour JsonAssetLoader) ==="
|
||||||
|
for f in DynamicAnimation StaticAnimation LinkAnimation \
|
||||||
|
ConcurrentLinkAnimation LayerOffAnimation EntityState \
|
||||||
|
ActionAnimation AttackAnimation MainFrameAnimation; do
|
||||||
|
# ActionAnimation/AttackAnimation/MainFrameAnimation seront simplifiés
|
||||||
|
# manuellement en stubs (retirer le combat, garder signatures).
|
||||||
|
cp -v "$SRC/api/animation/types/$f.java" "$DST/anim/types/" 2>/dev/null || echo " (skip : $f.java non trouvé)"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 5. Animation client ==="
|
||||||
|
cp -v "$SRC"/api/client/animation/*.java "$DST/anim/client/" 2>/dev/null
|
||||||
|
cp -v "$SRC"/api/client/animation/property/*.java "$DST/anim/client/property/" 2>/dev/null
|
||||||
|
# TrailInfo hors scope
|
||||||
|
rm -f "$DST/anim/client/property/TrailInfo.java"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 6. Mesh ==="
|
||||||
|
cp -v "$SRC"/api/client/model/*.java "$DST/mesh/" 2>/dev/null
|
||||||
|
# Retirer ItemSkinsReloadListener (cosmetics combat)
|
||||||
|
rm -f "$DST/mesh/ItemSkinsReloadListener.java"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 7. Cloth (absorbé Phase 0 — StaticMesh en dépend) ==="
|
||||||
|
cp -v "$SRC"/api/client/physics/AbstractSimulator.java "$DST/cloth/" 2>/dev/null
|
||||||
|
cp -v "$SRC"/api/client/physics/cloth/*.java "$DST/cloth/" 2>/dev/null
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 8. Asset loader ==="
|
||||||
|
cp -v "$SRC"/api/asset/*.java "$DST/asset/" 2>/dev/null
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 9. Forge events ==="
|
||||||
|
for f in PatchedRenderersEvent PrepareModelEvent RegisterResourceLayersEvent; do
|
||||||
|
cp -v "$SRC/api/client/forgeevent/$f.java" "$DST/event/" 2>/dev/null || echo " (skip : $f.java non trouvé)"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 10. RenderTypes ==="
|
||||||
|
cp -v "$SRC"/client/renderer/EpicFightRenderTypes.java "$DST/render/TiedUpRenderTypes.java" 2>/dev/null
|
||||||
|
echo " NOTE: RenderEngine.Events à extraire manuellement dans render/TiedUpRenderEngine.java"
|
||||||
|
echo " NOTE: TiedUpRigConstants.java à créer manuellement (factory ANIMATOR_PROVIDER + isPhysicalClient)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 11. ComputeShader stubs (à créer manuellement, no-op) ==="
|
||||||
|
echo " NOTE: créer render/compute/ComputeShaderSetup.java et ComputeShaderProvider.java (stubs vides)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 12. Transitives oubliées (découvertes par review) ==="
|
||||||
|
cp -v "$SRC"/api/utils/ParseUtil.java "$DST/util/" 2>/dev/null
|
||||||
|
cp -v "$SRC"/api/utils/datastruct/*.java "$DST/util/datastruct/" 2>/dev/null
|
||||||
|
cp -v "$SRC"/api/exception/*.java "$DST/exception/" 2>/dev/null
|
||||||
|
cp -v "$SRC"/model/armature/HumanoidArmature.java "$DST/armature/" 2>/dev/null
|
||||||
|
cp -v "$SRC"/model/armature/types/HumanLikeArmature.java "$DST/armature/types/" 2>/dev/null
|
||||||
|
cp -v "$SRC"/client/mesh/HumanoidMesh.java "$DST/mesh/" 2>/dev/null
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 13. ClientPlayerPatch + LocalPlayerPatch (typo upstream 'capabilites') ==="
|
||||||
|
cp -v "$SRC"/client/world/capabilites/entitypatch/player/AbstractClientPlayerPatch.java "$DST/patch/ClientPlayerPatch.java" 2>/dev/null
|
||||||
|
cp -v "$SRC"/client/world/capabilites/entitypatch/player/LocalPlayerPatch.java "$DST/patch/" 2>/dev/null
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 14. Rewrite imports ==="
|
||||||
|
bash scripts/rig-rewrite-imports.sh "$DST"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== 15. License headers ==="
|
||||||
|
bash scripts/rig-headers.sh "$DST"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "--- Phase 0 extraction done ---"
|
||||||
|
echo ""
|
||||||
|
echo "Fichiers copiés :"
|
||||||
|
find "$DST" -type f -name "*.java" | wc -l
|
||||||
|
echo "LOC totales :"
|
||||||
|
find "$DST" -type f -name "*.java" -exec cat {} + | wc -l
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Next steps (manuel, cf. EXTRACTION.md §9) :"
|
||||||
|
echo " 1. Fixer compile errors (strip combat from types, stub refs EpicFightMod/ClientConfig/ClientEngine/SkillManager)"
|
||||||
|
echo " 2. Strip valeurs combat de LivingMotion/LivingMotions enum, ajouter valeurs TiedUp"
|
||||||
|
echo " 3. Extraire RenderEngine.Events → render/TiedUpRenderEngine.java"
|
||||||
|
echo " 4. Créer ComputeShader stubs (render/compute/ComputeShaderSetup.java + ComputeShaderProvider.java no-op)"
|
||||||
|
echo " 5. Adapter TiedUpCapabilities.java depuis EpicFightCapabilities.java (retirer combat caps)"
|
||||||
|
echo " 6. Écrire TiedUpCapabilityEvents.java from scratch (~50L : RegisterCapabilitiesEvent + AttachCapabilitiesEvent)"
|
||||||
|
echo " 7. Fork mixins (MixinEntity, MixinLivingEntity, MixinLivingEntityRenderer — @Invoker only pour renderer)"
|
||||||
|
echo " 8. Simplifier LivingEntityPatch.java (1213L → ~400L, strip combat)"
|
||||||
|
echo " 9. Créer TiedUpRigConstants.java (factory ANIMATOR_PROVIDER)"
|
||||||
|
echo ""
|
||||||
|
echo "Budget total post-script : 2 à 3 semaines pour vert-compile."
|
||||||
38
scripts/rig-headers.sh
Executable file
38
scripts/rig-headers.sh
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# rig-headers.sh
|
||||||
|
# Ajoute un header attribution Epic Fight + GPLv3 à chaque fichier .java
|
||||||
|
# forké dans v3/rig qui n'en a pas déjà un.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
TARGET="${1:-src/main/java/com/tiedup/remake/rig}"
|
||||||
|
|
||||||
|
if [ ! -d "$TARGET" ]; then
|
||||||
|
echo "ERROR: target dir not found: $TARGET"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
HEADER='/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'
|
||||||
|
|
||||||
|
count=0
|
||||||
|
skipped=0
|
||||||
|
find "$TARGET" -type f -name "*.java" | while read f; do
|
||||||
|
if head -5 "$f" | grep -q "Derived from Epic Fight"; then
|
||||||
|
skipped=$((skipped + 1))
|
||||||
|
else
|
||||||
|
tmp=$(mktemp)
|
||||||
|
printf '%s' "$HEADER" > "$tmp"
|
||||||
|
cat "$f" >> "$tmp"
|
||||||
|
mv "$tmp" "$f"
|
||||||
|
count=$((count + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Headers injected in files (check count via grep)."
|
||||||
|
echo "Run: grep -l 'Derived from Epic Fight' $TARGET -r | wc -l"
|
||||||
75
scripts/rig-rewrite-imports.sh
Executable file
75
scripts/rig-rewrite-imports.sh
Executable file
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# rig-rewrite-imports.sh
|
||||||
|
# Renomme les imports yesman.epicfight.* vers com.tiedup.remake.rig.*
|
||||||
|
# dans tous les fichiers Java fraîchement copiés dans v3/rig.
|
||||||
|
#
|
||||||
|
# IMPORTANT : ordre du plus spécifique au plus général.
|
||||||
|
# Si on fait api.animation avant api.client.animation, la première rule
|
||||||
|
# mange la seconde. Chaque règle utilise un pattern qui matche exactement
|
||||||
|
# le chemin complet jusqu'au séparateur suivant.
|
||||||
|
#
|
||||||
|
# Portabilité : sed -i non-portable BSD (macOS) — utiliser "sed -i.bak"
|
||||||
|
# si besoin support Mac.
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
TARGET="${1:-src/main/java/com/tiedup/remake/rig}"
|
||||||
|
|
||||||
|
if [ ! -d "$TARGET" ]; then
|
||||||
|
echo "ERROR: target dir not found: $TARGET"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Rewriting imports in $TARGET..."
|
||||||
|
|
||||||
|
find "$TARGET" -type f -name "*.java" -exec sed -i \
|
||||||
|
-e 's|yesman\.epicfight\.api\.utils\.math|com.tiedup.remake.rig.math|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.utils\.datastruct|com.tiedup.remake.rig.util.datastruct|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.utils\.ParseUtil|com.tiedup.remake.rig.util.ParseUtil|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.utils|com.tiedup.remake.rig.util|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.exception|com.tiedup.remake.rig.exception|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.forgeevent|com.tiedup.remake.rig.event|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.model\.Armature|com.tiedup.remake.rig.armature.Armature|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.animation\.Joint|com.tiedup.remake.rig.armature.Joint|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.animation\.JointTransform|com.tiedup.remake.rig.armature.JointTransform|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.animation\.types\.datapack|com.tiedup.remake.rig.anim.types.datapack|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.animation\.types\.grappling|com.tiedup.remake.rig.anim.types.grappling|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.animation\.types\.procedural|com.tiedup.remake.rig.anim.types.procedural|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.animation\.types|com.tiedup.remake.rig.anim.types|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.animation\.property|com.tiedup.remake.rig.anim.property|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.animation|com.tiedup.remake.rig.anim|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.client\.animation\.property|com.tiedup.remake.rig.anim.client.property|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.client\.animation|com.tiedup.remake.rig.anim.client|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.client\.model\.transformer|com.tiedup.remake.rig.mesh.transformer|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.client\.model|com.tiedup.remake.rig.mesh|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.client\.physics\.cloth|com.tiedup.remake.rig.cloth|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.client\.physics|com.tiedup.remake.rig.cloth|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.client\.forgeevent|com.tiedup.remake.rig.event|g' \
|
||||||
|
-e 's|yesman\.epicfight\.api\.asset|com.tiedup.remake.rig.asset|g' \
|
||||||
|
-e 's|yesman\.epicfight\.model\.armature\.types|com.tiedup.remake.rig.armature.types|g' \
|
||||||
|
-e 's|yesman\.epicfight\.model\.armature|com.tiedup.remake.rig.armature|g' \
|
||||||
|
-e 's|yesman\.epicfight\.world\.capabilities\.provider|com.tiedup.remake.rig.patch|g' \
|
||||||
|
-e 's|yesman\.epicfight\.world\.capabilities\.entitypatch\.player|com.tiedup.remake.rig.patch|g' \
|
||||||
|
-e 's|yesman\.epicfight\.world\.capabilities\.entitypatch|com.tiedup.remake.rig.patch|g' \
|
||||||
|
-e 's|yesman\.epicfight\.world\.capabilities\.EpicFightCapabilities|com.tiedup.remake.rig.patch.TiedUpCapabilities|g' \
|
||||||
|
-e 's|yesman\.epicfight\.world\.capabilities|com.tiedup.remake.rig.patch|g' \
|
||||||
|
-e 's|yesman\.epicfight\.client\.world\.capabilites\.entitypatch\.player|com.tiedup.remake.rig.patch|g' \
|
||||||
|
-e 's|yesman\.epicfight\.client\.world\.capabilites\.entitypatch|com.tiedup.remake.rig.patch|g' \
|
||||||
|
-e 's|yesman\.epicfight\.client\.world\.capabilites|com.tiedup.remake.rig.patch|g' \
|
||||||
|
-e 's|yesman\.epicfight\.client\.renderer\.patched\.entity|com.tiedup.remake.rig.render|g' \
|
||||||
|
-e 's|yesman\.epicfight\.client\.renderer\.patched|com.tiedup.remake.rig.render|g' \
|
||||||
|
-e 's|yesman\.epicfight\.client\.renderer\.EpicFightRenderTypes|com.tiedup.remake.rig.render.TiedUpRenderTypes|g' \
|
||||||
|
-e 's|yesman\.epicfight\.client\.mesh|com.tiedup.remake.rig.mesh|g' \
|
||||||
|
{} +
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Verifying no yesman.epicfight references remain..."
|
||||||
|
remaining=$(grep -r "yesman\.epicfight" "$TARGET" 2>/dev/null | wc -l)
|
||||||
|
if [ "$remaining" -eq 0 ]; then
|
||||||
|
echo "OK - all imports rewritten."
|
||||||
|
else
|
||||||
|
echo "WARN - $remaining residual refs found:"
|
||||||
|
grep -rn "yesman\.epicfight" "$TARGET" | head -20
|
||||||
|
echo "..."
|
||||||
|
echo "(affichage limité aux 20 premiers)"
|
||||||
|
fi
|
||||||
@@ -125,6 +125,18 @@ public class ModKeybindings {
|
|||||||
CATEGORY
|
CATEGORY
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RIG debug overlay keybinding (P3-19) - Toggle the F3+B-style debug
|
||||||
|
* overlay that displays rig state in real-time.
|
||||||
|
* Default: F6 (unused by vanilla Minecraft).
|
||||||
|
*/
|
||||||
|
public static final KeyMapping RIG_DEBUG_KEY = new KeyMapping(
|
||||||
|
"key.tiedup.rig_debug",
|
||||||
|
InputConstants.Type.KEYSYM,
|
||||||
|
org.lwjgl.glfw.GLFW.GLFW_KEY_F6, // Default key: F6
|
||||||
|
CATEGORY
|
||||||
|
);
|
||||||
|
|
||||||
/** Track last sent state to avoid spamming packets */
|
/** Track last sent state to avoid spamming packets */
|
||||||
private static boolean lastForceSeatState = false;
|
private static boolean lastForceSeatState = false;
|
||||||
|
|
||||||
@@ -149,7 +161,8 @@ public class ModKeybindings {
|
|||||||
event.register(BOUNTY_KEY);
|
event.register(BOUNTY_KEY);
|
||||||
event.register(FORCE_SEAT_KEY);
|
event.register(FORCE_SEAT_KEY);
|
||||||
event.register(TIGHTEN_KEY);
|
event.register(TIGHTEN_KEY);
|
||||||
TiedUpMod.LOGGER.info("Registered {} keybindings", 7);
|
event.register(RIG_DEBUG_KEY);
|
||||||
|
TiedUpMod.LOGGER.info("Registered {} keybindings", 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== STRUGGLE MINI-GAME (uses vanilla movement keys) ====================
|
// ==================== STRUGGLE MINI-GAME (uses vanilla movement keys) ====================
|
||||||
@@ -212,6 +225,19 @@ public class ModKeybindings {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RIG debug overlay toggle (P3-19) — F6 by default.
|
||||||
|
// Consumed BEFORE the player/level guard: it's a pure client-side
|
||||||
|
// boolean flip, needs no world context. Otherwise clicks queued on
|
||||||
|
// main menu / loading screen would flush on world-join → phantom
|
||||||
|
// toggle (reviewer SMELL-02).
|
||||||
|
while (RIG_DEBUG_KEY.consumeClick()) {
|
||||||
|
boolean nowOn = com.tiedup.remake.rig.debug.RigDebugOverlay.toggle();
|
||||||
|
TiedUpMod.LOGGER.debug(
|
||||||
|
"[CLIENT] RIG debug overlay: {}",
|
||||||
|
nowOn ? "ENABLED" : "DISABLED"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Minecraft mc = Minecraft.getInstance();
|
Minecraft mc = Minecraft.getInstance();
|
||||||
if (mc.player == null || mc.level == null) {
|
if (mc.player == null || mc.level == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package com.tiedup.remake.client.animation;
|
package com.tiedup.remake.client.animation;
|
||||||
|
|
||||||
import com.mojang.logging.LogUtils;
|
import com.mojang.logging.LogUtils;
|
||||||
import com.tiedup.remake.v2.furniture.ISeatProvider;
|
|
||||||
import dev.kosmx.playerAnim.api.layered.IAnimation;
|
import dev.kosmx.playerAnim.api.layered.IAnimation;
|
||||||
import dev.kosmx.playerAnim.api.layered.KeyframeAnimationPlayer;
|
import dev.kosmx.playerAnim.api.layered.KeyframeAnimationPlayer;
|
||||||
import dev.kosmx.playerAnim.api.layered.ModifierLayer;
|
import dev.kosmx.playerAnim.api.layered.ModifierLayer;
|
||||||
import dev.kosmx.playerAnim.core.data.KeyframeAnimation;
|
import dev.kosmx.playerAnim.core.data.KeyframeAnimation;
|
||||||
import dev.kosmx.playerAnim.impl.IAnimatedPlayer;
|
import dev.kosmx.playerAnim.impl.IAnimatedPlayer;
|
||||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
|
|
||||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationFactory;
|
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationFactory;
|
||||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
|
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -15,7 +13,6 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import net.minecraft.client.player.AbstractClientPlayer;
|
import net.minecraft.client.player.AbstractClientPlayer;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.minecraft.world.entity.Entity;
|
|
||||||
import net.minecraft.world.entity.LivingEntity;
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraftforge.api.distmarker.Dist;
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
@@ -39,92 +36,42 @@ public class BondageAnimationManager {
|
|||||||
|
|
||||||
private static final Logger LOGGER = LogUtils.getLogger();
|
private static final Logger LOGGER = LogUtils.getLogger();
|
||||||
|
|
||||||
/** Cache of ModifierLayers for NPC entities (players use PlayerAnimationAccess) */
|
/** Cache of item-layer ModifierLayers for NPC entities. */
|
||||||
private static final Map<UUID, ModifierLayer<IAnimation>> npcLayers =
|
private static final Map<UUID, ModifierLayer<IAnimation>> npcLayers =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/** Cache of context ModifierLayers for NPC entities */
|
/** Cache of context-layer ModifierLayers for NPC entities. */
|
||||||
private static final Map<UUID, ModifierLayer<IAnimation>> npcContextLayers =
|
private static final Map<UUID, ModifierLayer<IAnimation>> npcContextLayers =
|
||||||
new ConcurrentHashMap<>();
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/** Cache of furniture ModifierLayers for NPC entities */
|
|
||||||
private static final Map<
|
|
||||||
UUID,
|
|
||||||
ModifierLayer<IAnimation>
|
|
||||||
> npcFurnitureLayers = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
/** Factory ID for PlayerAnimator item layer (players only) */
|
|
||||||
private static final ResourceLocation FACTORY_ID =
|
|
||||||
ResourceLocation.fromNamespaceAndPath("tiedup", "bondage");
|
|
||||||
|
|
||||||
/** Factory ID for PlayerAnimator context layer (players only) */
|
|
||||||
private static final ResourceLocation CONTEXT_FACTORY_ID =
|
|
||||||
ResourceLocation.fromNamespaceAndPath("tiedup", "bondage_context");
|
|
||||||
|
|
||||||
/** Factory ID for PlayerAnimator furniture layer (players only) */
|
|
||||||
private static final ResourceLocation FURNITURE_FACTORY_ID =
|
|
||||||
ResourceLocation.fromNamespaceAndPath("tiedup", "bondage_furniture");
|
|
||||||
|
|
||||||
/** Priority for context animation layer (lower = overridable by item layer) */
|
/** Priority for context animation layer (lower = overridable by item layer) */
|
||||||
private static final int CONTEXT_LAYER_PRIORITY = 40;
|
private static final int CONTEXT_LAYER_PRIORITY = 40;
|
||||||
/** Priority for item animation layer (higher = overrides context layer) */
|
/** Priority for item animation layer (higher = overrides context layer) */
|
||||||
private static final int ITEM_LAYER_PRIORITY = 42;
|
private static final int ITEM_LAYER_PRIORITY = 42;
|
||||||
/**
|
|
||||||
* Priority for furniture animation layer (highest = overrides item layer on blocked bones).
|
|
||||||
* Non-blocked bones are disabled so items can still animate them via the item layer.
|
|
||||||
*/
|
|
||||||
private static final int FURNITURE_LAYER_PRIORITY = 43;
|
|
||||||
|
|
||||||
/** Number of ticks to wait before removing a stale furniture animation. */
|
|
||||||
private static final int FURNITURE_GRACE_TICKS = 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks ticks since a player with an active furniture animation stopped riding
|
|
||||||
* an ISeatProvider. After {@link #FURNITURE_GRACE_TICKS}, the animation is removed
|
|
||||||
* to prevent stuck poses from entity death or network issues.
|
|
||||||
*
|
|
||||||
* <p>Uses ConcurrentHashMap for safe access from both client tick and render thread.</p>
|
|
||||||
*/
|
|
||||||
private static final Map<UUID, Integer> furnitureGraceTicks =
|
|
||||||
new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the animation system.
|
* Initialize the animation system.
|
||||||
* Must be called during client setup to register the player animation factory.
|
*
|
||||||
|
* <p><b>Pipeline NPC-only</b> — depuis Phase 2.7, les joueurs sont tickés par
|
||||||
|
* {@code RigAnimationTickHandler} via le renderer RIG patched. Aucune
|
||||||
|
* {@link PlayerAnimationFactory} n'est enregistrée pour le joueur et tous
|
||||||
|
* les chemins joueur dans cette classe sont de short-circuits logués.</p>
|
||||||
|
*
|
||||||
|
* <p>Cette classe reste active <b>uniquement pour les NPCs</b>
|
||||||
|
* (entités implémentant {@link IAnimatedPlayer} qui ne sont pas un
|
||||||
|
* {@link Player}) : {@link #getOrCreateLayer} leur crée un {@link ModifierLayer}
|
||||||
|
* via accès direct au stack d'animation
|
||||||
|
* ({@code animated.getAnimationStack().addAnimLayer(...)}) — ce path ne dépend
|
||||||
|
* d'aucune factory. Consumer principal : {@code NpcAnimationTickHandler}.</p>
|
||||||
|
*
|
||||||
|
* <p>Conservé comme méthode publique pour ne pas casser les call sites
|
||||||
|
* externes. Rework V3 (player anim natives RIG) : voir V3-REW-01 dans
|
||||||
|
* {@code docs/plans/rig/V3_REWORK_BACKLOG.md}.</p>
|
||||||
*/
|
*/
|
||||||
public static void init() {
|
public static void init() {
|
||||||
LOGGER.info("BondageAnimationManager initializing...");
|
|
||||||
|
|
||||||
// Context layer: lower priority = evaluated first, overridable by item layer.
|
|
||||||
// In AnimationStack, layers are sorted ascending by priority and evaluated in order.
|
|
||||||
// Higher priority layers override lower ones.
|
|
||||||
PlayerAnimationFactory.ANIMATION_DATA_FACTORY.registerFactory(
|
|
||||||
CONTEXT_FACTORY_ID,
|
|
||||||
CONTEXT_LAYER_PRIORITY,
|
|
||||||
player -> new ModifierLayer<>()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Item layer: higher priority = evaluated last, overrides context layer
|
|
||||||
PlayerAnimationFactory.ANIMATION_DATA_FACTORY.registerFactory(
|
|
||||||
FACTORY_ID,
|
|
||||||
ITEM_LAYER_PRIORITY,
|
|
||||||
player -> new ModifierLayer<>()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Furniture layer: highest priority = overrides item layer on blocked bones.
|
|
||||||
// Non-blocked bones are disabled via FurnitureAnimationContext so items
|
|
||||||
// can still animate free regions (gag, blindfold, etc.).
|
|
||||||
PlayerAnimationFactory.ANIMATION_DATA_FACTORY.registerFactory(
|
|
||||||
FURNITURE_FACTORY_ID,
|
|
||||||
FURNITURE_LAYER_PRIORITY,
|
|
||||||
player -> new ModifierLayer<>()
|
|
||||||
);
|
|
||||||
|
|
||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
"BondageAnimationManager: Factories registered — context (pri {}), item (pri {}), furniture (pri {})",
|
"BondageAnimationManager: NPC-only pipeline (Phase 2.8 RIG cleanup). " +
|
||||||
CONTEXT_LAYER_PRIORITY,
|
"Players handled by RigAnimationTickHandler; all player call sites no-op."
|
||||||
ITEM_LAYER_PRIORITY,
|
|
||||||
FURNITURE_LAYER_PRIORITY
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +98,11 @@ public class BondageAnimationManager {
|
|||||||
* <p>If the animation layer is not available (e.g., remote player not fully
|
* <p>If the animation layer is not available (e.g., remote player not fully
|
||||||
* initialized), the animation will be queued for retry via PendingAnimationManager.
|
* initialized), the animation will be queued for retry via PendingAnimationManager.
|
||||||
*
|
*
|
||||||
|
* <p><b>Phase 2.8</b> — les appels sur un {@link Player} sont no-op : le pipeline
|
||||||
|
* joueur est désormais RIG-native (voir {@link #init} Javadoc). Un WARN est logué
|
||||||
|
* une fois par UUID pour signaler les call sites stale qui devraient être purgés
|
||||||
|
* lors du rework V3.</p>
|
||||||
|
*
|
||||||
* @param entity The entity to animate
|
* @param entity The entity to animate
|
||||||
* @param animId Full ResourceLocation of the animation
|
* @param animId Full ResourceLocation of the animation
|
||||||
* @return true if animation started successfully, false if layer not available
|
* @return true if animation started successfully, false if layer not available
|
||||||
@@ -163,6 +115,12 @@ public class BondageAnimationManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2.8 : player path is dead. Log once per UUID and no-op.
|
||||||
|
if (entity instanceof Player player) {
|
||||||
|
logPlayerCallOnce(player, "playAnimation(" + animId + ")");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
KeyframeAnimation anim = PlayerAnimationRegistry.getAnimation(animId);
|
KeyframeAnimation anim = PlayerAnimationRegistry.getAnimation(animId);
|
||||||
if (anim == null) {
|
if (anim == null) {
|
||||||
// Try fallback: remove _sneak_ suffix if present
|
// Try fallback: remove _sneak_ suffix if present
|
||||||
@@ -199,7 +157,7 @@ public class BondageAnimationManager {
|
|||||||
}
|
}
|
||||||
layer.setAnimation(new KeyframeAnimationPlayer(anim));
|
layer.setAnimation(new KeyframeAnimationPlayer(anim));
|
||||||
|
|
||||||
// Remove from pending queue if it was waiting
|
// Remove from pending queue if it was waiting (legacy, may still hold NPC entries)
|
||||||
PendingAnimationManager.remove(entity.getUUID());
|
PendingAnimationManager.remove(entity.getUUID());
|
||||||
|
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
@@ -209,23 +167,11 @@ public class BondageAnimationManager {
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// Layer not available - queue for retry if it's a player
|
LOGGER.warn(
|
||||||
if (entity instanceof AbstractClientPlayer) {
|
"Animation layer is NULL for NPC: {} (type: {})",
|
||||||
PendingAnimationManager.queueForRetry(
|
entity.getName().getString(),
|
||||||
entity.getUUID(),
|
entity.getClass().getSimpleName()
|
||||||
animId.getPath()
|
);
|
||||||
);
|
|
||||||
LOGGER.debug(
|
|
||||||
"Animation layer not ready for {}, queued for retry",
|
|
||||||
entity.getName().getString()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
LOGGER.warn(
|
|
||||||
"Animation layer is NULL for NPC: {} (type: {})",
|
|
||||||
entity.getName().getString(),
|
|
||||||
entity.getClass().getSimpleName()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,6 +192,12 @@ public class BondageAnimationManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2.8 : player path is dead.
|
||||||
|
if (entity instanceof Player player) {
|
||||||
|
logPlayerCallOnce(player, "playDirect");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
ModifierLayer<IAnimation> layer = getOrCreateLayer(entity);
|
ModifierLayer<IAnimation> layer = getOrCreateLayer(entity);
|
||||||
if (layer != null) {
|
if (layer != null) {
|
||||||
IAnimation current = layer.getAnimation();
|
IAnimation current = layer.getAnimation();
|
||||||
@@ -273,6 +225,11 @@ public class BondageAnimationManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 2.8 : player path is dead — no layer to clear.
|
||||||
|
if (entity instanceof Player) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ModifierLayer<IAnimation> layer = getLayer(entity);
|
ModifierLayer<IAnimation> layer = getLayer(entity);
|
||||||
if (layer != null) {
|
if (layer != null) {
|
||||||
layer.setAnimation(null);
|
layer.setAnimation(null);
|
||||||
@@ -284,56 +241,36 @@ public class BondageAnimationManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the ModifierLayer for an entity (without creating).
|
* Get the ModifierLayer for an entity (without creating).
|
||||||
|
*
|
||||||
|
* <p>Phase 2.8 : returns {@code null} directly for any {@link Player} — the
|
||||||
|
* player animation pipeline is RIG-native, this manager only tracks NPCs.</p>
|
||||||
*/
|
*/
|
||||||
private static ModifierLayer<IAnimation> getLayer(LivingEntity entity) {
|
private static ModifierLayer<IAnimation> getLayer(LivingEntity entity) {
|
||||||
// Players: try PlayerAnimationAccess first, then cache
|
if (entity instanceof Player) {
|
||||||
if (entity instanceof AbstractClientPlayer player) {
|
return null;
|
||||||
ModifierLayer<IAnimation> factoryLayer = getPlayerLayer(player);
|
|
||||||
if (factoryLayer != null) {
|
|
||||||
return factoryLayer;
|
|
||||||
}
|
|
||||||
// Check cache (for remote players using fallback)
|
|
||||||
return npcLayers.get(entity.getUUID());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NPCs: use cache
|
|
||||||
return npcLayers.get(entity.getUUID());
|
return npcLayers.get(entity.getUUID());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or create the ModifierLayer for an entity.
|
* Get or create the ModifierLayer for an entity.
|
||||||
|
*
|
||||||
|
* <p>Phase 2.8 : returns {@code null} directly for any {@link Player} — the
|
||||||
|
* player fallback via {@code IAnimatedPlayer.getAnimationStack()} has been
|
||||||
|
* retired because it was partially alive (FP vanilla render consumed it,
|
||||||
|
* TP RIG override bypassed it), producing a confusing behavior split. All
|
||||||
|
* player anim needs are now handled by {@code RigAnimationTickHandler}.</p>
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static ModifierLayer<IAnimation> getOrCreateLayer(
|
private static ModifierLayer<IAnimation> getOrCreateLayer(
|
||||||
LivingEntity entity
|
LivingEntity entity
|
||||||
) {
|
) {
|
||||||
UUID uuid = entity.getUUID();
|
// Phase 2.8 : strip player path entirely (no partially-alive fallback).
|
||||||
|
if (entity instanceof Player) {
|
||||||
// Players: try factory-based access first, fallback to direct stack access
|
return null;
|
||||||
if (entity instanceof AbstractClientPlayer player) {
|
|
||||||
// Try the registered factory first (works for local player)
|
|
||||||
ModifierLayer<IAnimation> factoryLayer = getPlayerLayer(player);
|
|
||||||
if (factoryLayer != null) {
|
|
||||||
return factoryLayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback for remote players: use direct stack access like NPCs
|
|
||||||
// This handles cases where the factory data isn't available
|
|
||||||
if (player instanceof IAnimatedPlayer animated) {
|
|
||||||
return npcLayers.computeIfAbsent(uuid, k -> {
|
|
||||||
ModifierLayer<IAnimation> newLayer = new ModifierLayer<>();
|
|
||||||
animated
|
|
||||||
.getAnimationStack()
|
|
||||||
.addAnimLayer(ITEM_LAYER_PRIORITY, newLayer);
|
|
||||||
LOGGER.info(
|
|
||||||
"Created animation layer for remote player via stack: {}",
|
|
||||||
player.getName().getString()
|
|
||||||
);
|
|
||||||
return newLayer;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UUID uuid = entity.getUUID();
|
||||||
|
|
||||||
// NPCs implementing IAnimatedPlayer: create/cache layer
|
// NPCs implementing IAnimatedPlayer: create/cache layer
|
||||||
if (entity instanceof IAnimatedPlayer animated) {
|
if (entity instanceof IAnimatedPlayer animated) {
|
||||||
return npcLayers.computeIfAbsent(uuid, k -> {
|
return npcLayers.computeIfAbsent(uuid, k -> {
|
||||||
@@ -353,87 +290,49 @@ public class BondageAnimationManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Per-player dedup set so we log the factory-access failure at most once per UUID. */
|
/** Per-player-UUID dedup so stale call sites log at most once per session. */
|
||||||
private static final java.util.Set<UUID> layerFailureLogged =
|
private static final java.util.Set<UUID> playerCallLogged =
|
||||||
java.util.concurrent.ConcurrentHashMap.newKeySet();
|
java.util.concurrent.ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the animation layer for a player from PlayerAnimationAccess.
|
* Log once per player UUID that a stale call site is invoking this manager.
|
||||||
*
|
* Used by the player no-op short-circuits ({@link #playAnimation},
|
||||||
* <p>Throws during the factory-race window for remote players (the factory
|
* {@link #playDirect}) to surface call sites that should be migrated to the
|
||||||
* hasn't yet initialized their associated data). This is the expected path
|
* RIG pipeline (tracked in V3_REWORK_BACKLOG).
|
||||||
* for the {@link PendingAnimationManager} retry loop, so we log at DEBUG
|
|
||||||
* and at most once per UUID — a per-tick log would flood during busy
|
|
||||||
* multiplayer.</p>
|
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
private static void logPlayerCallOnce(Player player, String op) {
|
||||||
private static ModifierLayer<IAnimation> getPlayerLayer(
|
if (playerCallLogged.add(player.getUUID())) {
|
||||||
AbstractClientPlayer player
|
LOGGER.warn(
|
||||||
) {
|
"BondageAnimationManager.{} called on player {} — no-op " +
|
||||||
try {
|
"(RIG owns player anims since Phase 2.7). " +
|
||||||
return (ModifierLayer<
|
"Migrate call site to RigAnimationTickHandler (V3 rework).",
|
||||||
IAnimation
|
op,
|
||||||
>) PlayerAnimationAccess.getPlayerAssociatedData(player).get(
|
player.getName().getString()
|
||||||
FACTORY_ID
|
|
||||||
);
|
);
|
||||||
} catch (Exception e) {
|
|
||||||
if (layerFailureLogged.add(player.getUUID())) {
|
|
||||||
LOGGER.debug(
|
|
||||||
"Animation layer not yet available for player {} (will retry): {}",
|
|
||||||
player.getName().getString(),
|
|
||||||
e.toString()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely get the animation layer for a player.
|
* Safely get the animation layer for a player.
|
||||||
* Returns null if the layer is not yet initialized.
|
|
||||||
*
|
*
|
||||||
* <p>Public method for PendingAnimationManager to access.
|
* <p>Phase 2.8 : always returns {@code null}. The player pipeline is
|
||||||
* Checks both the factory-based layer and the NPC cache fallback.
|
* RIG-native; the {@link PendingAnimationManager} retry loop is no
|
||||||
|
* longer fed (player calls to {@link #playAnimation} short-circuit
|
||||||
|
* before queueing), so this getter is maintained only to preserve the
|
||||||
|
* public signature for external call sites.</p>
|
||||||
*
|
*
|
||||||
* @param player The player
|
* @param player The player (unused)
|
||||||
* @return The animation layer, or null if not available
|
* @return always null in Phase 2.8+
|
||||||
*/
|
*/
|
||||||
@javax.annotation.Nullable
|
@javax.annotation.Nullable
|
||||||
public static ModifierLayer<IAnimation> getPlayerLayerSafe(
|
public static ModifierLayer<IAnimation> getPlayerLayerSafe(
|
||||||
AbstractClientPlayer player
|
AbstractClientPlayer player
|
||||||
) {
|
) {
|
||||||
// Try factory first
|
return null;
|
||||||
ModifierLayer<IAnimation> factoryLayer = getPlayerLayer(player);
|
|
||||||
if (factoryLayer != null) {
|
|
||||||
return factoryLayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check NPC cache (for remote players using fallback path)
|
|
||||||
return npcLayers.get(player.getUUID());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// CONTEXT LAYER (lower priority, for sit/kneel/sneak)
|
// CONTEXT LAYER (lower priority, for sit/kneel/sneak)
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the context animation layer for a player from PlayerAnimationAccess.
|
|
||||||
* Returns null if the layer is not yet initialized.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@javax.annotation.Nullable
|
|
||||||
private static ModifierLayer<IAnimation> getPlayerContextLayer(
|
|
||||||
AbstractClientPlayer player
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
return (ModifierLayer<
|
|
||||||
IAnimation
|
|
||||||
>) PlayerAnimationAccess.getPlayerAssociatedData(player).get(
|
|
||||||
CONTEXT_FACTORY_ID
|
|
||||||
);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get or create the context animation layer for an NPC entity.
|
* Get or create the context animation layer for an NPC entity.
|
||||||
* Uses CONTEXT_LAYER_PRIORITY, below the item layer at ITEM_LAYER_PRIORITY.
|
* Uses CONTEXT_LAYER_PRIORITY, below the item layer at ITEM_LAYER_PRIORITY.
|
||||||
@@ -471,13 +370,14 @@ public class BondageAnimationManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModifierLayer<IAnimation> layer;
|
// Phase 2.8 : player context layer is dead (sit/kneel/sneak visuals
|
||||||
if (entity instanceof AbstractClientPlayer player) {
|
// will be re-expressed as RIG StaticAnimations — cf. V3-REW-14).
|
||||||
layer = getPlayerContextLayer(player);
|
if (entity instanceof Player player) {
|
||||||
} else {
|
logPlayerCallOnce(player, "playContext");
|
||||||
layer = getOrCreateNpcContextLayer(entity);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ModifierLayer<IAnimation> layer = getOrCreateNpcContextLayer(entity);
|
||||||
if (layer != null) {
|
if (layer != null) {
|
||||||
layer.setAnimation(new KeyframeAnimationPlayer(anim));
|
layer.setAnimation(new KeyframeAnimationPlayer(anim));
|
||||||
return true;
|
return true;
|
||||||
@@ -495,13 +395,12 @@ public class BondageAnimationManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModifierLayer<IAnimation> layer;
|
// Phase 2.8 : player path is dead — no layer to clear.
|
||||||
if (entity instanceof AbstractClientPlayer player) {
|
if (entity instanceof Player) {
|
||||||
layer = getPlayerContextLayer(player);
|
return;
|
||||||
} else {
|
|
||||||
layer = npcContextLayers.get(entity.getUUID());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ModifierLayer<IAnimation> layer = npcContextLayers.get(entity.getUUID());
|
||||||
if (layer != null) {
|
if (layer != null) {
|
||||||
layer.setAnimation(null);
|
layer.setAnimation(null);
|
||||||
}
|
}
|
||||||
@@ -533,194 +432,46 @@ public class BondageAnimationManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModifierLayer<IAnimation> layer = getOrCreateFurnitureLayer(player);
|
// Phase 2.8 : player furniture seat pose is dead (will be ported to
|
||||||
if (layer != null) {
|
// RIG StaticAnimations — cf. V3_REWORK_BACKLOG furniture seat entry).
|
||||||
layer.setAnimation(new KeyframeAnimationPlayer(animation));
|
logPlayerCallOnce(player, "playFurniture");
|
||||||
// Reset grace ticks since we just started/refreshed the animation
|
|
||||||
furnitureGraceTicks.remove(player.getUUID());
|
|
||||||
LOGGER.debug(
|
|
||||||
"Playing furniture animation on player: {}",
|
|
||||||
player.getName().getString()
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGER.warn(
|
|
||||||
"Furniture layer not available for player: {}",
|
|
||||||
player.getName().getString()
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the furniture layer animation for a player.
|
* Stop the furniture layer animation for a player.
|
||||||
*
|
*
|
||||||
|
* <p>Phase 2.8 : no-op — the player furniture layer is dead. Kept for
|
||||||
|
* signature compatibility with {@code EntityFurniture} cleanup call site.</p>
|
||||||
|
*
|
||||||
* @param player the player whose furniture animation should stop
|
* @param player the player whose furniture animation should stop
|
||||||
*/
|
*/
|
||||||
public static void stopFurniture(Player player) {
|
public static void stopFurniture(Player player) {
|
||||||
if (player == null || !player.level().isClientSide()) {
|
// Phase 2.8 : dead path. Retained signature for backward-compat.
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModifierLayer<IAnimation> layer = getFurnitureLayer(player);
|
|
||||||
if (layer != null) {
|
|
||||||
layer.setAnimation(null);
|
|
||||||
}
|
|
||||||
furnitureGraceTicks.remove(player.getUUID());
|
|
||||||
LOGGER.debug(
|
|
||||||
"Stopped furniture animation on player: {}",
|
|
||||||
player.getName().getString()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a player currently has an active furniture animation.
|
* Check whether a player currently has an active furniture animation.
|
||||||
*
|
*
|
||||||
|
* <p>Phase 2.8 : always returns {@code false} — player furniture layer is dead.</p>
|
||||||
|
*
|
||||||
* @param player the player to check
|
* @param player the player to check
|
||||||
* @return true if the furniture layer has an active animation
|
* @return always false in Phase 2.8+
|
||||||
*/
|
*/
|
||||||
public static boolean hasFurnitureAnimation(Player player) {
|
public static boolean hasFurnitureAnimation(Player player) {
|
||||||
if (player == null || !player.level().isClientSide()) {
|
return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ModifierLayer<IAnimation> layer = getFurnitureLayer(player);
|
|
||||||
return layer != null && layer.getAnimation() != null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the furniture ModifierLayer for a player (READ-ONLY).
|
* Safety tick for furniture animations.
|
||||||
* Uses PlayerAnimationAccess for local/factory-registered players,
|
|
||||||
* falls back to NPC cache for remote players. Returns null if no layer
|
|
||||||
* has been created yet — callers that need to guarantee a layer should use
|
|
||||||
* {@link #getOrCreateFurnitureLayer}.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@javax.annotation.Nullable
|
|
||||||
private static ModifierLayer<IAnimation> getFurnitureLayer(Player player) {
|
|
||||||
if (player instanceof AbstractClientPlayer clientPlayer) {
|
|
||||||
try {
|
|
||||||
ModifierLayer<IAnimation> layer = (ModifierLayer<
|
|
||||||
IAnimation
|
|
||||||
>) PlayerAnimationAccess.getPlayerAssociatedData(
|
|
||||||
clientPlayer
|
|
||||||
).get(FURNITURE_FACTORY_ID);
|
|
||||||
if (layer != null) {
|
|
||||||
return layer;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Fall through to NPC cache
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback for remote players: check NPC furniture cache
|
|
||||||
return npcFurnitureLayers.get(player.getUUID());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-player entities: use NPC cache
|
|
||||||
return npcFurnitureLayers.get(player.getUUID());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get or create the furniture ModifierLayer for a player. Mirrors
|
|
||||||
* {@link #getOrCreateLayer} but for the FURNITURE layer priority.
|
|
||||||
*
|
*
|
||||||
* <p>For the local player (factory-registered), returns the factory layer.
|
* <p>Phase 2.8 : no-op — the player furniture layer is dead, nothing to
|
||||||
* For remote players, creates a new layer on first call and caches it in
|
* guard. Kept as an empty stub in case older call sites remain.</p>
|
||||||
* {@link #npcFurnitureLayers} — remote players don't own a factory layer,
|
|
||||||
* so without a fallback they can't receive any furniture seat pose.</p>
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
@javax.annotation.Nullable
|
|
||||||
private static ModifierLayer<IAnimation> getOrCreateFurnitureLayer(
|
|
||||||
Player player
|
|
||||||
) {
|
|
||||||
if (player instanceof AbstractClientPlayer clientPlayer) {
|
|
||||||
try {
|
|
||||||
ModifierLayer<IAnimation> layer = (ModifierLayer<
|
|
||||||
IAnimation
|
|
||||||
>) PlayerAnimationAccess.getPlayerAssociatedData(
|
|
||||||
clientPlayer
|
|
||||||
).get(FURNITURE_FACTORY_ID);
|
|
||||||
if (layer != null) {
|
|
||||||
return layer;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Fall through to fallback-create below.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remote players: fallback-create via the animation stack.
|
|
||||||
if (clientPlayer instanceof IAnimatedPlayer animated) {
|
|
||||||
return npcFurnitureLayers.computeIfAbsent(
|
|
||||||
clientPlayer.getUUID(),
|
|
||||||
k -> {
|
|
||||||
ModifierLayer<IAnimation> newLayer =
|
|
||||||
new ModifierLayer<>();
|
|
||||||
animated
|
|
||||||
.getAnimationStack()
|
|
||||||
.addAnimLayer(FURNITURE_LAYER_PRIORITY, newLayer);
|
|
||||||
LOGGER.debug(
|
|
||||||
"Created furniture animation layer for remote player via stack: {}",
|
|
||||||
clientPlayer.getName().getString()
|
|
||||||
);
|
|
||||||
return newLayer;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return npcFurnitureLayers.get(clientPlayer.getUUID());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-player entities: use NPC cache (read-only; NPC furniture animation
|
|
||||||
// is not currently produced by this codebase).
|
|
||||||
return npcFurnitureLayers.get(player.getUUID());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safety tick for furniture animations. Call once per client tick per player.
|
|
||||||
*
|
*
|
||||||
* <p>If a player has an active furniture animation but is NOT riding an
|
* @param player the player to check (unused)
|
||||||
* {@link ISeatProvider}, increment a grace counter. After
|
|
||||||
* {@link #FURNITURE_GRACE_TICKS} consecutive ticks without a seat, the
|
|
||||||
* animation is removed to prevent stuck poses from entity death, network
|
|
||||||
* desync, or teleportation.</p>
|
|
||||||
*
|
|
||||||
* <p>If the player IS riding an ISeatProvider, the counter is reset.</p>
|
|
||||||
*
|
|
||||||
* @param player the player to check
|
|
||||||
*/
|
*/
|
||||||
public static void tickFurnitureSafety(Player player) {
|
public static void tickFurnitureSafety(Player player) {
|
||||||
if (player == null || !player.level().isClientSide()) {
|
// Phase 2.8 : dead path. Retained signature for backward-compat.
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasFurnitureAnimation(player)) {
|
|
||||||
// No furniture animation active, nothing to guard
|
|
||||||
furnitureGraceTicks.remove(player.getUUID());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UUID uuid = player.getUUID();
|
|
||||||
|
|
||||||
// Check if the player is riding an ISeatProvider
|
|
||||||
Entity vehicle = player.getVehicle();
|
|
||||||
boolean ridingSeat = vehicle instanceof ISeatProvider;
|
|
||||||
|
|
||||||
if (ridingSeat) {
|
|
||||||
// Player is properly seated, reset grace counter
|
|
||||||
furnitureGraceTicks.remove(uuid);
|
|
||||||
} else {
|
|
||||||
// Player has furniture anim but no seat -- increment grace
|
|
||||||
int ticks = furnitureGraceTicks.merge(uuid, 1, Integer::sum);
|
|
||||||
if (ticks >= FURNITURE_GRACE_TICKS) {
|
|
||||||
LOGGER.info(
|
|
||||||
"Removing stale furniture animation for player {} " +
|
|
||||||
"(not riding ISeatProvider for {} ticks)",
|
|
||||||
player.getName().getString(),
|
|
||||||
ticks
|
|
||||||
);
|
|
||||||
stopFurniture(player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FALLBACK ANIMATION HANDLING
|
// FALLBACK ANIMATION HANDLING
|
||||||
@@ -789,8 +540,9 @@ public class BondageAnimationManager {
|
|||||||
* @param entityId UUID of the removed entity
|
* @param entityId UUID of the removed entity
|
||||||
*/
|
*/
|
||||||
/** All NPC layer caches, for bulk cleanup operations. */
|
/** All NPC layer caches, for bulk cleanup operations. */
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
private static final Map<UUID, ModifierLayer<IAnimation>>[] ALL_NPC_CACHES =
|
private static final Map<UUID, ModifierLayer<IAnimation>>[] ALL_NPC_CACHES =
|
||||||
new Map[] { npcLayers, npcContextLayers, npcFurnitureLayers };
|
new Map[] { npcLayers, npcContextLayers };
|
||||||
|
|
||||||
public static void cleanup(UUID entityId) {
|
public static void cleanup(UUID entityId) {
|
||||||
for (Map<UUID, ModifierLayer<IAnimation>> cache : ALL_NPC_CACHES) {
|
for (Map<UUID, ModifierLayer<IAnimation>> cache : ALL_NPC_CACHES) {
|
||||||
@@ -799,8 +551,7 @@ public class BondageAnimationManager {
|
|||||||
layer.setAnimation(null);
|
layer.setAnimation(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
furnitureGraceTicks.remove(entityId);
|
playerCallLogged.remove(entityId);
|
||||||
layerFailureLogged.remove(entityId);
|
|
||||||
LOGGER.debug("Cleaned up animation layers for entity: {}", entityId);
|
LOGGER.debug("Cleaned up animation layers for entity: {}", entityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -813,8 +564,7 @@ public class BondageAnimationManager {
|
|||||||
cache.values().forEach(layer -> layer.setAnimation(null));
|
cache.values().forEach(layer -> layer.setAnimation(null));
|
||||||
cache.clear();
|
cache.clear();
|
||||||
}
|
}
|
||||||
furnitureGraceTicks.clear();
|
playerCallLogged.clear();
|
||||||
layerFailureLogged.clear();
|
|
||||||
LOGGER.info("Cleared all NPC animation layers");
|
LOGGER.info("Cleared all NPC animation layers");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,8 +52,15 @@ public class DogPoseRenderHandler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the rotation delta applied to a player's render for DOG pose.
|
* Get the rotation delta applied to a player's render for DOG pose.
|
||||||
* Used by MixinPlayerModel to compensate head rotation.
|
*
|
||||||
|
* @deprecated since Phase 2.8 — this getter fed {@code MixinPlayerModel}
|
||||||
|
* (removed Phase 2.8 RIG cleanup) so head rotation could be compensated
|
||||||
|
* against the body's -90° pitch. No remaining reader. To be deleted
|
||||||
|
* when V3-REW-07 re-expresses dog pose head compensation as a RIG
|
||||||
|
* {@code StaticAnimation pose_dog.json}. See
|
||||||
|
* {@code docs/plans/rig/V3_REWORK_BACKLOG.md#V3-REW-07}.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(since = "2.8")
|
||||||
public static float getAppliedRotationDelta(UUID playerUuid) {
|
public static float getAppliedRotationDelta(UUID playerUuid) {
|
||||||
float[] state = dogPoseState.get(playerUuid);
|
float[] state = dogPoseState.get(playerUuid);
|
||||||
return state != null ? state[IDX_DELTA] : 0f;
|
return state != null ? state[IDX_DELTA] : 0f;
|
||||||
@@ -61,7 +68,14 @@ public class DogPoseRenderHandler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a player is currently moving in DOG pose.
|
* Check if a player is currently moving in DOG pose.
|
||||||
|
*
|
||||||
|
* @deprecated since Phase 2.8 — same cause as {@link #getAppliedRotationDelta}
|
||||||
|
* (fed {@code MixinPlayerModel}, now removed). To be deleted alongside
|
||||||
|
* V3-REW-07 when dog pose head compensation is re-expressed as a RIG
|
||||||
|
* {@code StaticAnimation pose_dog.json}. See
|
||||||
|
* {@code docs/plans/rig/V3_REWORK_BACKLOG.md#V3-REW-07}.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(since = "2.8")
|
||||||
public static boolean isDogPoseMoving(UUID playerUuid) {
|
public static boolean isDogPoseMoving(UUID playerUuid) {
|
||||||
float[] state = dogPoseState.get(playerUuid);
|
float[] state = dogPoseState.get(playerUuid);
|
||||||
return state != null && state[IDX_MOVING] > 0.5f;
|
return state != null && state[IDX_MOVING] > 0.5f;
|
||||||
|
|||||||
@@ -3,29 +3,17 @@ package com.tiedup.remake.client.animation.tick;
|
|||||||
import com.mojang.logging.LogUtils;
|
import com.mojang.logging.LogUtils;
|
||||||
import com.tiedup.remake.client.animation.AnimationStateRegistry;
|
import com.tiedup.remake.client.animation.AnimationStateRegistry;
|
||||||
import com.tiedup.remake.client.animation.BondageAnimationManager;
|
import com.tiedup.remake.client.animation.BondageAnimationManager;
|
||||||
import com.tiedup.remake.client.animation.PendingAnimationManager;
|
|
||||||
import com.tiedup.remake.client.animation.context.AnimationContext;
|
|
||||||
import com.tiedup.remake.client.animation.context.AnimationContextResolver;
|
|
||||||
import com.tiedup.remake.client.animation.context.RegionBoneMapper;
|
|
||||||
import com.tiedup.remake.client.events.CellHighlightHandler;
|
import com.tiedup.remake.client.events.CellHighlightHandler;
|
||||||
import com.tiedup.remake.client.events.LeashProxyClientHandler;
|
import com.tiedup.remake.client.events.LeashProxyClientHandler;
|
||||||
import com.tiedup.remake.client.gltf.GltfAnimationApplier;
|
import com.tiedup.remake.client.gltf.GltfAnimationApplier;
|
||||||
import com.tiedup.remake.client.state.ClothesClientCache;
|
import com.tiedup.remake.client.state.ClothesClientCache;
|
||||||
import com.tiedup.remake.client.state.MovementStyleClientState;
|
import com.tiedup.remake.client.state.MovementStyleClientState;
|
||||||
import com.tiedup.remake.client.state.PetBedClientState;
|
import com.tiedup.remake.client.state.PetBedClientState;
|
||||||
import com.tiedup.remake.util.HumanChairHelper;
|
|
||||||
import com.tiedup.remake.state.PlayerBindState;
|
import com.tiedup.remake.state.PlayerBindState;
|
||||||
import com.tiedup.remake.v2.BodyRegionV2;
|
|
||||||
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
|
|
||||||
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
|
||||||
import com.tiedup.remake.v2.bondage.movement.MovementStyle;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
import net.minecraft.client.player.AbstractClientPlayer;
|
|
||||||
import net.minecraft.world.entity.player.Player;
|
|
||||||
import net.minecraft.world.item.ItemStack;
|
|
||||||
import net.minecraftforge.api.distmarker.Dist;
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
import net.minecraftforge.event.TickEvent;
|
import net.minecraftforge.event.TickEvent;
|
||||||
@@ -35,16 +23,29 @@ import net.minecraftforge.fml.common.Mod;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event handler for player animation tick updates.
|
* Event handler for animation tick updates.
|
||||||
*
|
*
|
||||||
* <p>Simplified handler that:
|
* <p><b>Phase 2.8 RIG cleanup</b> : le ticking <i>player</i> V2 (boucle
|
||||||
|
* {@code mc.level.players()} + {@code updatePlayerAnimation}) est entièrement
|
||||||
|
* désactivé. Les joueurs sont désormais pilotés par
|
||||||
|
* {@link com.tiedup.remake.rig.tick.RigAnimationTickHandler} via le pipeline
|
||||||
|
* RIG (capability {@code LivingEntityPatch} + {@code Animator} natif EF).
|
||||||
|
* Les features V2 qui dépendaient du tick player sont trackées dans
|
||||||
|
* {@code docs/plans/rig/V3_REWORK_BACKLOG.md} (V3-REW-01/02/03/07).</p>
|
||||||
|
*
|
||||||
|
* <p>Restent actifs ici :
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Tracks tied/struggling/sneaking state for players</li>
|
* <li>Nettoyage périodique de {@code ClothesClientCache} (cache remote
|
||||||
* <li>Plays animations via BondageAnimationManager when state changes</li>
|
* players, hygiène mémoire indépendante du pipeline de rendu)</li>
|
||||||
* <li>Handles cleanup on logout/world unload</li>
|
* <li>Cleanup logout / world unload (caches V2 encore utilisés par les
|
||||||
|
* NPCs ticked par {@link NpcAnimationTickHandler})</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <p>Registered on the FORGE event bus (not MOD bus).
|
* <p>Le ticking NPC est assuré par {@link NpcAnimationTickHandler}. Ce
|
||||||
|
* handler ne tick plus les NPCs directement — il ne gère que les hooks
|
||||||
|
* lifecycle globaux (logout + world unload).</p>
|
||||||
|
*
|
||||||
|
* <p>Registered on the FORGE event bus (not MOD bus).</p>
|
||||||
*/
|
*/
|
||||||
@Mod.EventBusSubscriber(
|
@Mod.EventBusSubscriber(
|
||||||
modid = "tiedup",
|
modid = "tiedup",
|
||||||
@@ -83,8 +84,20 @@ public class AnimationTickHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client tick event - called every tick on the client.
|
* Client tick event — called every tick on the client.
|
||||||
* Updates animations for all players when their bondage state changes.
|
*
|
||||||
|
* <p>Phase 2.8 : la boucle {@code mc.level.players()} qui appelait
|
||||||
|
* {@code updatePlayerAnimation}, {@code tickFurnitureSafety} et le
|
||||||
|
* cold-cache retry furniture a été entièrement supprimée. Les joueurs
|
||||||
|
* sont désormais ticked par {@link com.tiedup.remake.rig.tick.RigAnimationTickHandler}
|
||||||
|
* via le pipeline RIG (capability {@code LivingEntityPatch} +
|
||||||
|
* {@code Animator}). Les régressions visuelles (V2 bondage layer cassé,
|
||||||
|
* furniture seat pose sur joueur cassée, pet bed pose cassée) sont
|
||||||
|
* listées dans {@code docs/plans/rig/V3_REWORK_BACKLOG.md}.</p>
|
||||||
|
*
|
||||||
|
* <p>Seul le nettoyage périodique de {@link ClothesClientCache} reste
|
||||||
|
* — c'est de l'hygiène mémoire sur un cache indexé UUID joueur,
|
||||||
|
* indépendant du pipeline de rendu.</p>
|
||||||
*/
|
*/
|
||||||
@SubscribeEvent
|
@SubscribeEvent
|
||||||
public static void onClientTick(TickEvent.ClientTickEvent event) {
|
public static void onClientTick(TickEvent.ClientTickEvent event) {
|
||||||
@@ -97,193 +110,17 @@ public class AnimationTickHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process pending animations first (retry failed animations for remote players)
|
// Periodic cleanup of stale clothes cache entries (every 60 seconds = 1200 ticks).
|
||||||
PendingAnimationManager.processPending(mc.level);
|
// Indépendant du rendu V2/RIG — c'est juste un cache UUID→ClothesData qui
|
||||||
|
// doit libérer la mémoire des joueurs déconnectés depuis >5min.
|
||||||
// Periodic cleanup of stale cache entries (every 60 seconds = 1200 ticks)
|
|
||||||
if (++cleanupTickCounter >= 1200) {
|
if (++cleanupTickCounter >= 1200) {
|
||||||
cleanupTickCounter = 0;
|
cleanupTickCounter = 0;
|
||||||
ClothesClientCache.cleanupStale();
|
ClothesClientCache.cleanupStale();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then update all player animations
|
// Le tick per-player V2 (updatePlayerAnimation, tickFurnitureSafety,
|
||||||
for (Player player : mc.level.players()) {
|
// cold-cache furniture retry) est délégué à RigAnimationTickHandler
|
||||||
if (player instanceof AbstractClientPlayer clientPlayer) {
|
// Phase 2.7+. Rien à faire ici.
|
||||||
updatePlayerAnimation(clientPlayer);
|
|
||||||
}
|
|
||||||
// Safety: remove stale furniture animations for players no longer on seats
|
|
||||||
BondageAnimationManager.tickFurnitureSafety(player);
|
|
||||||
// Cold-cache retry: if the player is seated on furniture but has no
|
|
||||||
// active pose (GLB was not yet loaded at mount time, or the GLB cache
|
|
||||||
// entry was a transient failure), retry until the cache warms.
|
|
||||||
// FurnitureGltfCache memoizes failures via Optional.empty(), so
|
|
||||||
// retries after a genuine parse failure return instantly with no
|
|
||||||
// reparse. Bounded at MAX_FURNITURE_RETRIES so a legacy V1-only
|
|
||||||
// GLB (no Player_* armature → seatSkeleton==null → no animation
|
|
||||||
// ever possible) doesn't spam retries at 20 Hz forever.
|
|
||||||
// Single read of getVehicle() — avoids a re-read where the
|
|
||||||
// vehicle could change between instanceof and cast.
|
|
||||||
com.tiedup.remake.v2.furniture.EntityFurniture furniture =
|
|
||||||
player.getVehicle() instanceof
|
|
||||||
com.tiedup.remake.v2.furniture.EntityFurniture f ? f : null;
|
|
||||||
boolean hasAnim = BondageAnimationManager.hasFurnitureAnimation(
|
|
||||||
player
|
|
||||||
);
|
|
||||||
UUID playerUuid = player.getUUID();
|
|
||||||
if (furniture != null && !hasAnim) {
|
|
||||||
int retries = furnitureRetryCounters.getOrDefault(
|
|
||||||
playerUuid,
|
|
||||||
0
|
|
||||||
);
|
|
||||||
if (retries < MAX_FURNITURE_RETRIES) {
|
|
||||||
furnitureRetryCounters.put(playerUuid, retries + 1);
|
|
||||||
com.tiedup.remake.v2.furniture.client.FurnitureClientAnimator
|
|
||||||
.start(furniture, player);
|
|
||||||
if (retries + 1 == MAX_FURNITURE_RETRIES) {
|
|
||||||
LOGGER.debug(
|
|
||||||
"[FurnitureAnim] Giving up on furniture animation retry for {} after {} attempts — GLB likely has no Player_* armature.",
|
|
||||||
player.getName().getString(),
|
|
||||||
MAX_FURNITURE_RETRIES
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Dismounted or successfully applied — drop the counter so a
|
|
||||||
// later re-mount starts fresh.
|
|
||||||
furnitureRetryCounters.remove(playerUuid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update animation for a single player.
|
|
||||||
*/
|
|
||||||
private static void updatePlayerAnimation(AbstractClientPlayer player) {
|
|
||||||
// Safety check: skip for removed/dead players
|
|
||||||
if (player.isRemoved() || !player.isAlive()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
|
||||||
UUID uuid = player.getUUID();
|
|
||||||
|
|
||||||
// Check if player has ANY V2 bondage item equipped (not just ARMS).
|
|
||||||
// isTiedUp() only checks ARMS, but items on LEGS, HEAD, etc. also need animation.
|
|
||||||
boolean isTied =
|
|
||||||
state != null &&
|
|
||||||
(state.isTiedUp() || V2EquipmentHelper.hasAnyEquipment(player));
|
|
||||||
boolean wasTied =
|
|
||||||
AnimationStateRegistry.getLastTiedState().getOrDefault(uuid, false);
|
|
||||||
|
|
||||||
// Pet bed animations take priority over bondage animations
|
|
||||||
if (PetBedClientState.get(uuid) != 0) {
|
|
||||||
// Lock body rotation to bed facing (prevents camera from rotating the model)
|
|
||||||
float lockedRot = PetBedClientState.getFacing(uuid);
|
|
||||||
player.yBodyRot = lockedRot;
|
|
||||||
player.yBodyRotO = lockedRot;
|
|
||||||
|
|
||||||
// Clamp head rotation to ±50° from body (like vehicle)
|
|
||||||
float headRot = player.getYHeadRot();
|
|
||||||
float clamped =
|
|
||||||
lockedRot +
|
|
||||||
net.minecraft.util.Mth.clamp(
|
|
||||||
net.minecraft.util.Mth.wrapDegrees(headRot - lockedRot),
|
|
||||||
-50f,
|
|
||||||
50f
|
|
||||||
);
|
|
||||||
player.setYHeadRot(clamped);
|
|
||||||
player.yHeadRotO = clamped;
|
|
||||||
|
|
||||||
AnimationStateRegistry.getLastTiedState().put(uuid, isTied);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Human chair: clamp 1st-person camera only (body lock handled by MixinLivingEntityBodyRot)
|
|
||||||
// NO return — animation HUMAN_CHAIR must continue playing below
|
|
||||||
if (isTied && state != null) {
|
|
||||||
ItemStack chairBind = state.getEquipment(BodyRegionV2.ARMS);
|
|
||||||
if (HumanChairHelper.isActive(chairBind)) {
|
|
||||||
// 1st person only: clamp yRot so player can't look behind
|
|
||||||
// 3rd person: yRot untouched → camera orbits freely 360°
|
|
||||||
if (
|
|
||||||
player == Minecraft.getInstance().player &&
|
|
||||||
Minecraft.getInstance().options.getCameraType() ==
|
|
||||||
net.minecraft.client.CameraType.FIRST_PERSON
|
|
||||||
) {
|
|
||||||
float lockedRot = HumanChairHelper.getFacing(chairBind);
|
|
||||||
float camClamped =
|
|
||||||
lockedRot +
|
|
||||||
net.minecraft.util.Mth.clamp(
|
|
||||||
net.minecraft.util.Mth.wrapDegrees(
|
|
||||||
player.getYRot() - lockedRot
|
|
||||||
),
|
|
||||||
-90f,
|
|
||||||
90f
|
|
||||||
);
|
|
||||||
player.setYRot(camClamped);
|
|
||||||
player.yRotO =
|
|
||||||
lockedRot +
|
|
||||||
net.minecraft.util.Mth.clamp(
|
|
||||||
net.minecraft.util.Mth.wrapDegrees(
|
|
||||||
player.yRotO - lockedRot
|
|
||||||
),
|
|
||||||
-90f,
|
|
||||||
90f
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isTied) {
|
|
||||||
// Resolve V2 equipped items
|
|
||||||
IV2BondageEquipment equipment = V2EquipmentHelper.getEquipment(
|
|
||||||
player
|
|
||||||
);
|
|
||||||
Map<BodyRegionV2, ItemStack> equipped =
|
|
||||||
equipment != null ? equipment.getAllEquipped() : Map.of();
|
|
||||||
|
|
||||||
// Resolve ALL V2 items with GLB models and per-item bone ownership
|
|
||||||
java.util.List<RegionBoneMapper.V2ItemAnimInfo> v2Items =
|
|
||||||
RegionBoneMapper.resolveAllV2Items(equipped);
|
|
||||||
|
|
||||||
if (!v2Items.isEmpty()) {
|
|
||||||
// V2 path: multi-item composite animation
|
|
||||||
java.util.Set<String> allOwnedParts =
|
|
||||||
RegionBoneMapper.computeAllOwnedParts(v2Items);
|
|
||||||
MovementStyle activeStyle = MovementStyleClientState.get(
|
|
||||||
player.getUUID()
|
|
||||||
);
|
|
||||||
AnimationContext context = AnimationContextResolver.resolve(
|
|
||||||
player,
|
|
||||||
state,
|
|
||||||
activeStyle
|
|
||||||
);
|
|
||||||
GltfAnimationApplier.applyMultiItemV2Animation(
|
|
||||||
player,
|
|
||||||
v2Items,
|
|
||||||
context,
|
|
||||||
allOwnedParts
|
|
||||||
);
|
|
||||||
} else if (GltfAnimationApplier.hasActiveState(player)) {
|
|
||||||
// Clear any residual V2 composite animation when the player
|
|
||||||
// is still isTiedUp() but has no GLB-bearing items — e.g.
|
|
||||||
// a non-GLB item keeps the tied state, or a GLB item was
|
|
||||||
// removed while another V2 item remains on a non-animated
|
|
||||||
// region. Leaving the composite in place locks the arms in
|
|
||||||
// the pose of an item the player no longer wears.
|
|
||||||
GltfAnimationApplier.clearV2Animation(player);
|
|
||||||
}
|
|
||||||
} else if (wasTied) {
|
|
||||||
// Was tied, now free - stop all animations
|
|
||||||
if (GltfAnimationApplier.hasActiveState(player)) {
|
|
||||||
GltfAnimationApplier.clearV2Animation(player);
|
|
||||||
} else {
|
|
||||||
BondageAnimationManager.stopAnimation(player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
AnimationStateRegistry.getLastTiedState().put(uuid, isTied);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,22 +15,14 @@ import net.minecraftforge.api.distmarker.OnlyIn;
|
|||||||
* <li>Head yaw: convert to zRot (roll) since yRot axis is sideways</li>
|
* <li>Head yaw: convert to zRot (roll) since yRot axis is sideways</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* <h2>Architecture: Players vs NPCs</h2>
|
* <h2>Architecture — NPCs only (Phase 2.8 RIG cleanup)</h2>
|
||||||
* <pre>
|
* <p>Le path PLAYER (ex-{@code MixinPlayerModel.setupAnim @TAIL}) a été retiré
|
||||||
* ┌─────────────────────────────────────────────────────────────────┐
|
* Phase 2.8 : le renderer RIG patched ne passe plus par {@code PlayerModel.setupAnim},
|
||||||
* │ PLAYERS │
|
* donc le mixin devenait dead code. La compensation head dog pose sera ré-exprimée
|
||||||
* ├─────────────────────────────────────────────────────────────────┤
|
* nativement en StaticAnimation {@code pose_dog.json} (cf. V3-REW-07 dans
|
||||||
* │ 1. PlayerArmHideEventHandler.onRenderPlayerPre() │
|
* {@code docs/plans/rig/V3_REWORK_BACKLOG.md}).</p>
|
||||||
* │ - Offset vertical (-6 model units) │
|
|
||||||
* │ - Rotation Y lissée (dogPoseState tracking) │
|
|
||||||
* │ │
|
|
||||||
* │ 2. Animation (PlayerAnimator) │
|
|
||||||
* │ - body.pitch = -90° → appliqué au PoseStack automatiquement │
|
|
||||||
* │ │
|
|
||||||
* │ 3. MixinPlayerModel.setupAnim() @TAIL │
|
|
||||||
* │ - Uses DogPoseHelper.applyHeadCompensationClamped() │
|
|
||||||
* └─────────────────────────────────────────────────────────────────┘
|
|
||||||
*
|
*
|
||||||
|
* <pre>
|
||||||
* ┌─────────────────────────────────────────────────────────────────┐
|
* ┌─────────────────────────────────────────────────────────────────┐
|
||||||
* │ NPCs │
|
* │ NPCs │
|
||||||
* ├─────────────────────────────────────────────────────────────────┤
|
* ├─────────────────────────────────────────────────────────────────┤
|
||||||
@@ -48,25 +40,13 @@ import net.minecraftforge.api.distmarker.OnlyIn;
|
|||||||
* └─────────────────────────────────────────────────────────────────┘
|
* └─────────────────────────────────────────────────────────────────┘
|
||||||
* </pre>
|
* </pre>
|
||||||
*
|
*
|
||||||
* <h2>Key Differences</h2>
|
|
||||||
* <table>
|
|
||||||
* <tr><th>Aspect</th><th>Players</th><th>NPCs</th></tr>
|
|
||||||
* <tr><td>Rotation X application</td><td>Auto by PlayerAnimator</td><td>Manual in setupRotations()</td></tr>
|
|
||||||
* <tr><td>Rotation Y smoothing</td><td>PlayerArmHideEventHandler</td><td>EntityDamsel.tick() via RotationSmoother</td></tr>
|
|
||||||
* <tr><td>Head compensation</td><td>MixinPlayerModel</td><td>DamselModel.setupAnim()</td></tr>
|
|
||||||
* <tr><td>Reset body.xRot</td><td>Not needed</td><td>Yes (prevents double rotation)</td></tr>
|
|
||||||
* <tr><td>Vertical offset</td><td>-6 model units</td><td>-7 model units</td></tr>
|
|
||||||
* </table>
|
|
||||||
*
|
|
||||||
* <h2>Usage</h2>
|
* <h2>Usage</h2>
|
||||||
* <p>Used by:
|
* <p>Used by:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>MixinPlayerModel - for player head compensation</li>
|
|
||||||
* <li>DamselModel - for NPC head compensation</li>
|
* <li>DamselModel - for NPC head compensation</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @see RotationSmoother for Y rotation smoothing
|
* @see RotationSmoother for Y rotation smoothing
|
||||||
* @see com.tiedup.remake.mixin.client.MixinPlayerModel
|
|
||||||
* @see com.tiedup.remake.client.model.DamselModel
|
* @see com.tiedup.remake.client.model.DamselModel
|
||||||
*/
|
*/
|
||||||
@OnlyIn(Dist.CLIENT)
|
@OnlyIn(Dist.CLIENT)
|
||||||
@@ -130,7 +110,14 @@ public final class DogPoseHelper {
|
|||||||
* @param headPitch Player's up/down look angle in degrees
|
* @param headPitch Player's up/down look angle in degrees
|
||||||
* @param headYaw Head yaw relative to body in degrees
|
* @param headYaw Head yaw relative to body in degrees
|
||||||
* @param maxYaw Maximum allowed yaw angle in degrees
|
* @param maxYaw Maximum allowed yaw angle in degrees
|
||||||
|
* @deprecated since Phase 2.8 — player dog pose head compensation was
|
||||||
|
* previously applied via {@code MixinPlayerModel.setupAnim @TAIL}
|
||||||
|
* (removed Phase 2.8 RIG cleanup). No remaining call site; retained
|
||||||
|
* only to preserve the API until V3-REW-07 re-expresses the behavior
|
||||||
|
* as a RIG {@code StaticAnimation pose_dog.json}. See
|
||||||
|
* {@code docs/plans/rig/V3_REWORK_BACKLOG.md#V3-REW-07}.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(since = "2.8")
|
||||||
public static void applyHeadCompensationClamped(
|
public static void applyHeadCompensationClamped(
|
||||||
ModelPart head,
|
ModelPart head,
|
||||||
ModelPart hat,
|
ModelPart hat,
|
||||||
|
|||||||
@@ -137,6 +137,27 @@ public class TiedUpMod {
|
|||||||
|
|
||||||
// Register dispenser behaviors (must be on main thread)
|
// Register dispenser behaviors (must be on main thread)
|
||||||
event.enqueueWork(DispenserBehaviors::register);
|
event.enqueueWork(DispenserBehaviors::register);
|
||||||
|
|
||||||
|
// RIG Phase 3 — force class-load des motions pour assigner les
|
||||||
|
// universalOrdinal() via ExtendableEnumManager (init lazy JLS sinon).
|
||||||
|
//
|
||||||
|
// ORDER MATTERS (review RISK-001 on P3-01 commit 15e405f) : LivingMotions
|
||||||
|
// vanilla EF doit etre class-loaded AVANT TiedUpLivingMotions, sinon nos
|
||||||
|
// ordinals prennent 0..N, puis les vanilla arriveraient plus tard avec
|
||||||
|
// les memes ordinals -> collision silencieuse. Les patches EF ne
|
||||||
|
// garantissent pas que LivingMotions soit touche avant ce hook, donc on
|
||||||
|
// le force explicitement ici.
|
||||||
|
com.tiedup.remake.rig.anim.LivingMotions.values();
|
||||||
|
com.tiedup.remake.rig.anim.TiedUpLivingMotions.values();
|
||||||
|
|
||||||
|
// RIG Phase 2 — dispatcher EntityType → EntityPatch (PLAYER Phase 2, NPCs Phase 5)
|
||||||
|
event.enqueueWork(com.tiedup.remake.rig.patch.EntityPatchProvider::registerEntityPatches);
|
||||||
|
|
||||||
|
// RIG — zero Java-side init pour les StaticAnimation. Toutes les anims
|
||||||
|
// (y compris CONTEXT_STAND_IDLE) sont auto-registered via le bloc
|
||||||
|
// "constructor" de leur JSON respectif, parsé par
|
||||||
|
// AnimationManager.readResourcepackAnimation au datapack/resource-pack
|
||||||
|
// reload. Voir TiedUpAnimationRegistry Javadoc.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -185,9 +206,14 @@ public class TiedUpMod {
|
|||||||
|
|
||||||
// Initialize animation system
|
// Initialize animation system
|
||||||
event.enqueueWork(() -> {
|
event.enqueueWork(() -> {
|
||||||
// Initialize unified BondageAnimationManager
|
// RIG Phase 2 — override client dispatch PLAYER → Local/Client/ServerPlayerPatch
|
||||||
com.tiedup.remake.client.animation.BondageAnimationManager.init();
|
com.tiedup.remake.rig.patch.EntityPatchProvider.registerEntityPatchesClient();
|
||||||
LOGGER.info("BondageAnimationManager initialized");
|
|
||||||
|
// Phase 2.8 RIG cleanup : BondageAnimationManager.init() (factory
|
||||||
|
// registrations PlayerAnimator côté joueur) a été supprimé — le RIG
|
||||||
|
// prend le relai pour les joueurs via RigAnimationTickHandler.
|
||||||
|
// Les NPCs continuent d'être animés via BondageAnimationManager en
|
||||||
|
// accès direct animation stack (cf. NpcAnimationTickHandler).
|
||||||
|
|
||||||
// Initialize OBJ model registry for 3D bondage items
|
// Initialize OBJ model registry for 3D bondage items
|
||||||
com.tiedup.remake.client.renderer.obj.ObjModelRegistry.init();
|
com.tiedup.remake.client.renderer.obj.ObjModelRegistry.init();
|
||||||
@@ -589,6 +615,39 @@ public class TiedUpMod {
|
|||||||
LOGGER.info(
|
LOGGER.info(
|
||||||
"Registered RoomThemeReloadListener for data-driven room themes"
|
"Registered RoomThemeReloadListener for data-driven room themes"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Data-driven LivingMotion additions (server-side, from data/<namespace>/tiedup/living_motions/)
|
||||||
|
// Enables modders to add new LivingMotion values via datapack JSON,
|
||||||
|
// without writing Java enum extensions. Ordinals remain stable for
|
||||||
|
// the lifetime of the JVM (see LivingMotionReloadListener javadoc).
|
||||||
|
event.addListener(
|
||||||
|
new com.tiedup.remake.rig.anim.LivingMotionReloadListener()
|
||||||
|
);
|
||||||
|
LOGGER.info(
|
||||||
|
"Registered LivingMotionReloadListener for data-driven motion additions"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Data-driven custom armatures (server-side, from data/<namespace>/tiedup/armatures/)
|
||||||
|
// Enables modders to ship custom armatures (quadruped, centaur, neko, ...)
|
||||||
|
// via JSON — resolved by TiedUpArmatures.get() for any ID that is not
|
||||||
|
// the builtin tiedup:biped. See ArmatureReloadListener javadoc (D6).
|
||||||
|
event.addListener(
|
||||||
|
new com.tiedup.remake.rig.armature.datapack.ArmatureReloadListener()
|
||||||
|
);
|
||||||
|
LOGGER.info(
|
||||||
|
"Registered ArmatureReloadListener for data-driven custom armatures"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Data-driven PoseType additions (server-side, from data/<namespace>/tiedup/pose_types/)
|
||||||
|
// Additive registry — the 6 builtin PoseType enum values remain the
|
||||||
|
// only poses consumable by legacy V1 call-sites. Datapack types are
|
||||||
|
// visible only to Phase 3 consumers (DataDrivenItemParser, etc.).
|
||||||
|
event.addListener(
|
||||||
|
new com.tiedup.remake.v2.bondage.PoseTypeReloadListener()
|
||||||
|
);
|
||||||
|
LOGGER.info(
|
||||||
|
"Registered PoseTypeReloadListener for data-driven pose_type additions"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
package com.tiedup.remake.mixin.client;
|
|
||||||
|
|
||||||
import com.tiedup.remake.client.animation.render.DogPoseRenderHandler;
|
|
||||||
import com.tiedup.remake.client.animation.util.DogPoseHelper;
|
|
||||||
import com.tiedup.remake.items.base.PoseType;
|
|
||||||
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
|
|
||||||
import com.tiedup.remake.state.PlayerBindState;
|
|
||||||
import com.tiedup.remake.v2.BodyRegionV2;
|
|
||||||
import net.minecraft.client.model.PlayerModel;
|
|
||||||
import net.minecraft.client.player.AbstractClientPlayer;
|
|
||||||
import net.minecraft.world.entity.LivingEntity;
|
|
||||||
import net.minecraft.world.item.ItemStack;
|
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
|
||||||
import org.spongepowered.asm.mixin.injection.At;
|
|
||||||
import org.spongepowered.asm.mixin.injection.Inject;
|
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mixin for PlayerModel to handle DOG pose head adjustments.
|
|
||||||
*
|
|
||||||
* When in DOG pose (body horizontal):
|
|
||||||
* - Head pitch offset so player looks forward
|
|
||||||
* - Head yaw converted to zRot (roll) since yRot axis is sideways when body is horizontal
|
|
||||||
*/
|
|
||||||
@Mixin(PlayerModel.class)
|
|
||||||
public class MixinPlayerModel {
|
|
||||||
|
|
||||||
@Inject(method = "setupAnim", at = @At("TAIL"))
|
|
||||||
private void tiedup$adjustDogPose(
|
|
||||||
LivingEntity entity,
|
|
||||||
float limbSwing,
|
|
||||||
float limbSwingAmount,
|
|
||||||
float ageInTicks,
|
|
||||||
float netHeadYaw,
|
|
||||||
float headPitch,
|
|
||||||
CallbackInfo ci
|
|
||||||
) {
|
|
||||||
if (!(entity instanceof AbstractClientPlayer player)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerBindState state = PlayerBindState.getInstance(player);
|
|
||||||
if (state == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
|
|
||||||
if (bind.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PoseTypeHelper.getPoseType(bind) != PoseType.DOG) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
PlayerModel<?> model = (PlayerModel<?>) (Object) this;
|
|
||||||
|
|
||||||
// === HEAD ROTATION FOR HORIZONTAL BODY ===
|
|
||||||
// Body is at -90° pitch (horizontal, face down)
|
|
||||||
// We apply a rotation delta to the poseStack in PlayerArmHideEventHandler
|
|
||||||
// The head needs to compensate for this transformation
|
|
||||||
|
|
||||||
float rotationDelta = DogPoseRenderHandler.getAppliedRotationDelta(
|
|
||||||
player.getUUID()
|
|
||||||
);
|
|
||||||
boolean moving = DogPoseRenderHandler.isDogPoseMoving(player.getUUID());
|
|
||||||
|
|
||||||
// netHeadYaw is head relative to vanilla body (yHeadRot - yBodyRot)
|
|
||||||
// We rotated the model by rotationDelta, so compensate:
|
|
||||||
// effectiveHeadYaw = netHeadYaw + rotationDelta
|
|
||||||
float headYaw = netHeadYaw + rotationDelta;
|
|
||||||
|
|
||||||
// Clamp based on movement state and apply head compensation
|
|
||||||
float maxYaw = moving ? 60f : 90f;
|
|
||||||
DogPoseHelper.applyHeadCompensationClamped(
|
|
||||||
model.head,
|
|
||||||
model.hat,
|
|
||||||
headPitch,
|
|
||||||
headYaw,
|
|
||||||
maxYaw
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -64,6 +64,7 @@ import com.tiedup.remake.network.sync.PacketSyncPetBedState;
|
|||||||
import com.tiedup.remake.network.sync.PacketSyncStruggleState;
|
import com.tiedup.remake.network.sync.PacketSyncStruggleState;
|
||||||
import com.tiedup.remake.network.trader.PacketBuyCaptive;
|
import com.tiedup.remake.network.trader.PacketBuyCaptive;
|
||||||
import com.tiedup.remake.network.trader.PacketOpenTraderScreen;
|
import com.tiedup.remake.network.trader.PacketOpenTraderScreen;
|
||||||
|
import com.tiedup.remake.rig.network.PacketPlayRigAnim;
|
||||||
import com.tiedup.remake.v2.bondage.network.PacketSyncV2Equipment;
|
import com.tiedup.remake.v2.bondage.network.PacketSyncV2Equipment;
|
||||||
import com.tiedup.remake.v2.bondage.network.PacketV2LockToggle;
|
import com.tiedup.remake.v2.bondage.network.PacketV2LockToggle;
|
||||||
import com.tiedup.remake.v2.bondage.network.PacketV2SelfEquip;
|
import com.tiedup.remake.v2.bondage.network.PacketV2SelfEquip;
|
||||||
@@ -592,6 +593,14 @@ public class ModNetwork {
|
|||||||
PacketSyncMovementStyle::handle
|
PacketSyncMovementStyle::handle
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// RIG animation system (S2C cinematic one-shot)
|
||||||
|
reg(
|
||||||
|
PacketPlayRigAnim.class,
|
||||||
|
PacketPlayRigAnim::encode,
|
||||||
|
PacketPlayRigAnim::decode,
|
||||||
|
PacketPlayRigAnim::handleOnClient
|
||||||
|
);
|
||||||
|
|
||||||
TiedUpMod.LOGGER.info("Registered {} network packets", packetId);
|
TiedUpMod.LOGGER.info("Registered {} network packets", packetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remplace {@code yesman.epicfight.config.ClientConfig} du fork upstream.
|
||||||
|
* Expose uniquement les flags pertinents au pipeline animation/rendu RIG.
|
||||||
|
*
|
||||||
|
* <p><b>Note Phase 0</b> : les flags sont des {@code static final} avec
|
||||||
|
* valeurs par défaut hardcodées. À convertir en {@code ForgeConfigSpec} réel
|
||||||
|
* (TOML config file) Phase 2 ou plus tard si on veut permettre la
|
||||||
|
* configuration utilisateur.</p>
|
||||||
|
*/
|
||||||
|
public final class TiedUpAnimationConfig {
|
||||||
|
|
||||||
|
private TiedUpAnimationConfig() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle pour le chemin "compute shader" de {@code SkinnedMesh.draw} —
|
||||||
|
* quand true et qu'un {@code ComputeShaderSetup} est disponible, la mesh
|
||||||
|
* est skinnée côté GPU (plus rapide sur modèles lourds). False (défaut)
|
||||||
|
* = skin CPU comme vanilla.
|
||||||
|
*/
|
||||||
|
public static final boolean activateComputeShader = false;
|
||||||
|
}
|
||||||
172
src/main/java/com/tiedup/remake/rig/TiedUpAnimationRegistry.java
Normal file
172
src/main/java/com/tiedup/remake/rig/TiedUpAnimationRegistry.java
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationManager;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry helper (lookup + fallback) pour les {@link StaticAnimation} TiedUp.
|
||||||
|
*
|
||||||
|
* <h2>Data-driven — zéro Java hardcoding</h2>
|
||||||
|
* <p>Depuis la migration {@code CONTEXT_STAND_IDLE} full data-driven, ce
|
||||||
|
* registry n'instancie plus aucune {@code StaticAnimation} côté Java. Toutes
|
||||||
|
* les anims TiedUp (y compris l'idle par défaut) sont enregistrées via le bloc
|
||||||
|
* {@code "constructor"} de leur JSON respectif dans
|
||||||
|
* {@code assets/tiedup/animmodels/animations/*.json}, parsé par
|
||||||
|
* {@link AnimationManager#readResourcepackAnimation} à chaque reload de resource
|
||||||
|
* pack / datapack. Voir {@code armbinder_idle.json} ou
|
||||||
|
* {@code context_stand_idle.json} pour la forme attendue.</p>
|
||||||
|
*
|
||||||
|
* <p>Goal : un modder peut ajouter une anim TiedUp compatible sans écrire UNE
|
||||||
|
* ligne de Java — il drop un JSON dans
|
||||||
|
* {@code assets/<modid>/animmodels/animations/}, le référence depuis un item
|
||||||
|
* JSON binding, {@code /reload}, l'anim fire.</p>
|
||||||
|
*
|
||||||
|
* <h2>Ce que le registry expose</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #CONTEXT_STAND_IDLE_ID} — ID canonique de l'anim idle par
|
||||||
|
* défaut (résolue en
|
||||||
|
* {@code assets/tiedup/animmodels/animations/context_stand_idle.json}).
|
||||||
|
* Consommée par {@code PlayerPatch.initAnimator},
|
||||||
|
* {@code RigAnimationTickHandler.maybePlayIdle}, etc.</li>
|
||||||
|
* <li>{@link #resolveWithFallback(ResourceLocation)} — lookup
|
||||||
|
* {@link AnimationManager#byKey} avec fallback safe sur
|
||||||
|
* {@link TiedUpRigRegistry#EMPTY_ANIMATION} si l'ID est inconnu (datapack
|
||||||
|
* pas encore rechargé, typo modder, etc.). Jamais null.</li>
|
||||||
|
* <li>{@link #resetWarnedMissing()} — hook tests / hot-reload pour purger le
|
||||||
|
* set dedup des WARN de miss.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Placeholder assets</h2>
|
||||||
|
* <p>Les JSON actuels sont des <b>placeholders procéduraux</b> (2 keyframes
|
||||||
|
* identity) à remplacer par des assets Blender-authored. Voir
|
||||||
|
* {@code docs/plans/rig/ASSETS_NEEDED.md} section 2 pour la spec de l'anim
|
||||||
|
* idle définitive (swing respiration subtle 3 keyframes, 2s boucle).</p>
|
||||||
|
*/
|
||||||
|
public final class TiedUpAnimationRegistry {
|
||||||
|
|
||||||
|
private TiedUpAnimationRegistry() {}
|
||||||
|
|
||||||
|
/** Registry name de l'anim idle par défaut (résolue en
|
||||||
|
* {@code assets/tiedup/animmodels/animations/context_stand_idle.json}).
|
||||||
|
* L'anim elle-même est auto-registered au datapack reload via le bloc
|
||||||
|
* {@code "constructor"} du JSON — pas d'init Java. */
|
||||||
|
public static final ResourceLocation CONTEXT_STAND_IDLE_ID =
|
||||||
|
ResourceLocation.fromNamespaceAndPath(TiedUpRigConstants.MODID, "context_stand_idle");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set (thread-safe) des IDs pour lesquels un WARN de fallback a déjà été
|
||||||
|
* émis. Évite le spam log si un consumer appelle
|
||||||
|
* {@link #resolveWithFallback(ResourceLocation)} tick après tick avec un ID
|
||||||
|
* invalide (ex. un item bondage data-driven qui référence un anim ID cassé,
|
||||||
|
* appelé dans la boucle de rendu). Un seul WARN par ID unique sur toute la
|
||||||
|
* durée de vie du process (jusqu'à {@link #resetWarnedMissing()}).
|
||||||
|
*
|
||||||
|
* <p>Pattern inspiré de
|
||||||
|
* {@code RigAnimationTickHandler.LOGGED_ERRORS} — {@code ConcurrentHashMap.newKeySet()}
|
||||||
|
* pour être safe en cas d'appels concurrents (client tick thread + network
|
||||||
|
* handler thread pour {@code PacketPlayRigAnim.handleOnClient}).</p>
|
||||||
|
*/
|
||||||
|
private static final Set<ResourceLocation> WARNED_MISSING_ANIMS = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Résout une animation par {@link ResourceLocation} avec fallback safe
|
||||||
|
* si le registry ne la connaît pas.
|
||||||
|
*
|
||||||
|
* <p>Utilisé par le pipeline d'équipement
|
||||||
|
* ({@code ClientRigEquipmentHandler.rebuildBondageAnimations}, P3-05), le
|
||||||
|
* packet cinematic ({@code PacketPlayRigAnim.handleOnClient}, P3-12), et
|
||||||
|
* désormais le path idle ({@code PlayerPatch.initAnimator} +
|
||||||
|
* {@code RigAnimationTickHandler.maybePlayIdle}). Un miss dans le registry
|
||||||
|
* peut survenir dans plusieurs scénarios :</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Typo modder dans un JSON data-driven bondage item</li>
|
||||||
|
* <li>Datapack/resource-pack pas encore rechargé (pré-{@code apply} au
|
||||||
|
* bootstrap, entre deux {@code /reload})</li>
|
||||||
|
* <li>Animation supprimée entre deux versions du mod</li>
|
||||||
|
* <li>Race entre packet réception et
|
||||||
|
* {@code AnimationManager.apply()} en début de session</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Dans tous ces cas, on retourne {@link TiedUpRigRegistry#EMPTY_ANIMATION}
|
||||||
|
* — un singleton qui ne joue rien visuellement (pas de keyframe, pose
|
||||||
|
* identity). Mieux qu'un NPE pour la robustesse du pipeline : l'anim
|
||||||
|
* équipement continue de tourner avec les anim connues, et l'anim inconnue
|
||||||
|
* est juste silencieusement un no-op.</p>
|
||||||
|
*
|
||||||
|
* <p><b>Dedup WARN</b> : un miss donné ne log qu'une fois par session
|
||||||
|
* ({@link #WARNED_MISSING_ANIMS}). Ça évite le spam dans la console quand
|
||||||
|
* l'ID invalide est consommé tick après tick (ex. équipement resté en place).
|
||||||
|
* Le set peut être reset via {@link #resetWarnedMissing()} (hot-reload
|
||||||
|
* F3+T, runtime datapack reload).</p>
|
||||||
|
*
|
||||||
|
* <p><b>Pourquoi réutiliser {@link TiedUpRigRegistry#EMPTY_ANIMATION}</b> vs
|
||||||
|
* créer un {@code empty_fallback} séparé : plusieurs sites du runtime
|
||||||
|
* ({@code Layer#off}, {@code AnimationPlayer#isEmpty}, {@code LayerOffAnimation#getNextAnimation})
|
||||||
|
* testent l'identité via {@code == EMPTY_ANIMATION}. Retourner une autre
|
||||||
|
* instance d'empty provoquerait des false-negatives sur ces checks — le
|
||||||
|
* runtime penserait qu'une anim réelle joue alors qu'en fait c'est un
|
||||||
|
* empty différent. Le singleton canonique évite ce piège.</p>
|
||||||
|
*
|
||||||
|
* @param id l'ID registry à résoudre (ex. {@code tiedup:context_stand_idle})
|
||||||
|
* @return l'{@link AnimationAccessor} enregistré, ou
|
||||||
|
* {@link TiedUpRigRegistry#EMPTY_ANIMATION} si l'ID est inconnu.
|
||||||
|
* Jamais null.
|
||||||
|
*/
|
||||||
|
public static AnimationAccessor<? extends StaticAnimation> resolveWithFallback(
|
||||||
|
ResourceLocation id
|
||||||
|
) {
|
||||||
|
if (id == null) {
|
||||||
|
// null ID : log + fallback. Pas de dedup (cas pathologique — le
|
||||||
|
// caller a un bug, pas un miss de datapack).
|
||||||
|
TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"[TiedUpAnimationRegistry] resolveWithFallback appelé avec id=null, "
|
||||||
|
+ "using EMPTY_ANIMATION fallback."
|
||||||
|
);
|
||||||
|
return TiedUpRigRegistry.EMPTY_ANIMATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimationAccessor<? extends StaticAnimation> anim = AnimationManager.byKey(id);
|
||||||
|
if (anim != null) {
|
||||||
|
return anim;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Miss — fallback + dedup warn. Set.add retourne true si l'ID n'était
|
||||||
|
// pas déjà dans le set → premier miss, on log. Sinon, silent no-op.
|
||||||
|
if (WARNED_MISSING_ANIMS.add(id)) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"[TiedUpAnimationRegistry] Animation not found: '{}', using EMPTY_ANIMATION fallback. "
|
||||||
|
+ "Check datapack JSON or run /reload.",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return TiedUpRigRegistry.EMPTY_ANIMATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset le set dedup des WARN missing. Utilisé dans deux contextes :
|
||||||
|
* <ul>
|
||||||
|
* <li>Tests unitaires — pour réinitialiser l'état statique entre test
|
||||||
|
* cases (sinon un test qui warn sur un ID pollue les suivants).</li>
|
||||||
|
* <li>Runtime reload (F3+T / datapack reload) — après un reload, des
|
||||||
|
* anims précédemment missing peuvent être dispo maintenant ; on
|
||||||
|
* veut pouvoir re-warn si elles retombent en miss après un autre
|
||||||
|
* reload.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Thread-safe via {@link ConcurrentHashMap#newKeySet()} — pas de
|
||||||
|
* synchronisation externe nécessaire.</p>
|
||||||
|
*/
|
||||||
|
public static void resetWarnedMissing() {
|
||||||
|
WARNED_MISSING_ANIMS.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
324
src/main/java/com/tiedup/remake/rig/TiedUpArmatures.java
Normal file
324
src/main/java/com/tiedup/remake/rig/TiedUpArmatures.java
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
import com.tiedup.remake.rig.armature.HumanoidArmature;
|
||||||
|
import com.tiedup.remake.rig.armature.Joint;
|
||||||
|
import com.tiedup.remake.rig.armature.datapack.ArmatureReloadListener;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry des armatures TiedUp exposé via {@link AssetAccessor} constants.
|
||||||
|
*
|
||||||
|
* <h2>Phase 2.4 — version procédurale</h2>
|
||||||
|
*
|
||||||
|
* <p>Cette classe construit le biped TiedUp <b>from scratch en Java</b>
|
||||||
|
* (hiérarchie + offsets identity). Suffisant pour débloquer le rendering RIG
|
||||||
|
* Phase 2.4 : les joints existent dans le map, {@code searchJointByName}
|
||||||
|
* fonctionne, le GLB → SkinnedMesh bridge a un mapping valide, etc.</p>
|
||||||
|
*
|
||||||
|
* <p><b>Phase 2.7 remplacera par un JSON Blender-authored hot-reloadable</b>.
|
||||||
|
* Pour l'instant, les joints sont tous à l'identité (offset/rotation nuls).
|
||||||
|
* Visuellement ça donnera un biped "effondré" sur le point d'origine si on
|
||||||
|
* rend sans animation — c'est acceptable car :</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Phase 2.4 n'a pas encore de renderer player patched complet (Phase 2.5)</li>
|
||||||
|
* <li>Phase 2.7 rechargera des offsets depuis {@code assets/tiedup/armatures/biped.json}
|
||||||
|
* co-authored via addon Blender (cf. MIGRATION.md §2.2.1)</li>
|
||||||
|
* <li>Les tests existants `GltfToSkinnedMeshTest` utilisent déjà le même pattern
|
||||||
|
* (Armature identity, {@code bakeOriginMatrices}) et sont verts</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Hiérarchie biped EF (20 joints)</h2>
|
||||||
|
*
|
||||||
|
* <pre>
|
||||||
|
* Root id=0
|
||||||
|
* ├─ Thigh_R ── Leg_R ── Knee_R id=1,2,3
|
||||||
|
* ├─ Thigh_L ── Leg_L ── Knee_L id=4,5,6
|
||||||
|
* └─ Torso id=7
|
||||||
|
* └─ Chest id=8
|
||||||
|
* ├─ Head id=9
|
||||||
|
* ├─ Shoulder_R ── Arm_R ── Elbow_R ── Hand_R ── Tool_R ids=10,11,14,12,13
|
||||||
|
* └─ Shoulder_L ── Arm_L ── Elbow_L ── Hand_L ── Tool_L ids=15,16,19,17,18
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* <p>Les IDs des bras ne suivent pas l'ordre hiérarchique parent→enfant : c'est
|
||||||
|
* voulu pour rester aligné avec le layout attendu par {@code VanillaModelTransformer}
|
||||||
|
* (upperJoint=Arm, lowerJoint=Hand, middleJoint=Elbow). Voir {@link #buildBiped()}.</p>
|
||||||
|
*
|
||||||
|
* <p><b>Noms conservés verbatim EF</b> (pas renommés en TiedUp style) car :</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Le {@code VanillaModelTransformer} forké EF (Phase 2.2) référence ces
|
||||||
|
* noms dans ses AABB / {@code WEIGHT_ALONG_Y} / {@code yClipCoord}</li>
|
||||||
|
* <li>Le bridge GLB ({@code LegacyJointNameMapper}) mappe déjà les joints
|
||||||
|
* PlayerAnimator legacy sur ces noms-là</li>
|
||||||
|
* <li>Re-authored serait un risque régression sans gain fonctionnel</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public final class TiedUpArmatures {
|
||||||
|
|
||||||
|
private TiedUpArmatures() {}
|
||||||
|
|
||||||
|
/** ResourceLocation registry pour l'accessor (même path que EF pour cohérence doc). */
|
||||||
|
private static final ResourceLocation BIPED_REGISTRY_NAME =
|
||||||
|
ResourceLocation.fromNamespaceAndPath(TiedUpRigConstants.MODID, "armature/biped");
|
||||||
|
|
||||||
|
/** Short-form ID utilisé par les invocation_commands data-driven (ex.
|
||||||
|
* {@code "tiedup:biped"} dans le bloc "constructor" des anim JSONs).
|
||||||
|
* C'est ce que l'utilisateur écrit dans les datapacks — plus concis que
|
||||||
|
* {@code tiedup:armature/biped} et aligné sur la convention EF
|
||||||
|
* (cf. {@code assets/epicfight/armatures/biped.json} référencé comme
|
||||||
|
* {@code "epicfight:biped"} dans les exports DatapackEditScreen).
|
||||||
|
*
|
||||||
|
* <p>Tant que Phase 2.7 n'a pas livré un vrai registry JSON
|
||||||
|
* {@link TiedUpRigRegistry}, {@link #get(ResourceLocation)} ne reconnaît
|
||||||
|
* que cet ID — tout autre ID retombe sur le fallback
|
||||||
|
* {@code InstantiateInvoker.getArmature} qui warn + renvoie BIPED.</p>
|
||||||
|
*/
|
||||||
|
private static final ResourceLocation BIPED_SHORT_ID =
|
||||||
|
ResourceLocation.fromNamespaceAndPath(TiedUpRigConstants.MODID, "biped");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holder idiome pour init lazy + thread-safe sans synchronized.
|
||||||
|
*
|
||||||
|
* <p>Le class loader JVM garantit qu'une classe est initialisée au plus une
|
||||||
|
* fois, et que l'init est visible à tous les threads (JLS §12.4.1 — the class
|
||||||
|
* initialization lock est acquis automatiquement). Deux threads qui touchent
|
||||||
|
* {@code Holder.INSTANCE} simultanément ne peuvent pas observer l'instance
|
||||||
|
* non-initialisée ni en créer deux exemplaires. Intégré SP (client + server
|
||||||
|
* threads concurrents sur la même JVM) safe.</p>
|
||||||
|
*
|
||||||
|
* <p>Raison du fix (review Phase 2.4, P0-BUG-002) : le pattern précédent
|
||||||
|
* {@code if (BIPED_INSTANCE == null) BIPED_INSTANCE = buildBiped();} est
|
||||||
|
* un double-init race — deux threads entrent tous les deux dans le if,
|
||||||
|
* les deux créent un HumanoidArmature distinct, dernier gagne et pollue
|
||||||
|
* le cache.</p>
|
||||||
|
*/
|
||||||
|
private static final class Holder {
|
||||||
|
static final HumanoidArmature INSTANCE;
|
||||||
|
static {
|
||||||
|
// Signal visible au dev que les joints sont en identity transform.
|
||||||
|
// Sans ça, Phase 2.6+ câblera le renderer et le mesh apparaîtra
|
||||||
|
// "effondré à l'origine" sans signal — debug cauchemar. Le warn
|
||||||
|
// n'apparaît qu'une fois (class-init lock JVM).
|
||||||
|
TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"TiedUpArmatures.BIPED initialized with IDENTITY joint transforms (Phase 2.4 stub). "
|
||||||
|
+ "Mesh will render collapsed-to-origin until Phase 2.7 provides biped.json "
|
||||||
|
+ "Blender-authored offsets. See docs/plans/rig/PHASE0_DEGRADATIONS.md "
|
||||||
|
+ "Phase 2.4 backlog entry #1."
|
||||||
|
);
|
||||||
|
INSTANCE = buildBiped();
|
||||||
|
}
|
||||||
|
private Holder() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AssetAccessor biped TiedUp. L'instance est construite lazy à la première
|
||||||
|
* référence {@code Holder.INSTANCE} (thread-safe via class-init lock JVM).
|
||||||
|
*
|
||||||
|
* <p>Utilisé par {@link com.tiedup.remake.rig.patch.PlayerPatch#getArmature()}
|
||||||
|
* et par les futurs {@code StaticAnimation(… , BIPED)} Phase 2.7+.</p>
|
||||||
|
*/
|
||||||
|
public static final AssetAccessor<HumanoidArmature> BIPED = new AssetAccessor<>() {
|
||||||
|
@Override
|
||||||
|
public HumanoidArmature get() {
|
||||||
|
return Holder.INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation registryName() {
|
||||||
|
return BIPED_REGISTRY_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean inRegistry() {
|
||||||
|
// Pas dans un JsonAssetLoader registry tant que Phase 2.7 n'a pas
|
||||||
|
// posé le biped.json. Une fois fait, ce flag repassera à true via
|
||||||
|
// un nouveau registry layer.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build procédural de la hiérarchie biped EF. 20 joints, IDs 0..19 assignés
|
||||||
|
* explicitement pour matcher le layout attendu par
|
||||||
|
* {@link com.tiedup.remake.rig.mesh.transformer.VanillaModelTransformer}
|
||||||
|
* (cf. constantes {@code RIGHT_ARM}/{@code LEFT_ARM} — upperJoint/lowerJoint/middleJoint).
|
||||||
|
*
|
||||||
|
* <p>Ordre d'insertion LinkedHashMap ≠ ordre des IDs pour les bras (hand
|
||||||
|
* vient AVANT elbow dans les IDs, pour aligner sur la sémantique EF où
|
||||||
|
* {@code middleJoint = elbow}). Voir commentaires inline.</p>
|
||||||
|
*
|
||||||
|
* <p>Toutes les transforms sont identity — Phase 2.7 remplacera par les
|
||||||
|
* offsets Blender mesurés (cf. doc header).</p>
|
||||||
|
*/
|
||||||
|
private static HumanoidArmature buildBiped() {
|
||||||
|
// ID Joint assigné explicitement au constructeur (pas par position dans
|
||||||
|
// la map) — Armature.jointById est construit depuis joint.getId().
|
||||||
|
// On utilise LinkedHashMap malgré tout pour garantir un ordre d'itération
|
||||||
|
// stable (utile pour le debug et pour OpenMatrix4f.allocateMatrixArray
|
||||||
|
// qui dimensionne sur jointCount).
|
||||||
|
Map<String, Joint> joints = new LinkedHashMap<>(20);
|
||||||
|
|
||||||
|
// Pattern EF : tous les joints démarrent avec une localTransform identity.
|
||||||
|
// bakeOriginMatrices() calcule ensuite les toOrigin relatives à la
|
||||||
|
// hiérarchie parent→enfant.
|
||||||
|
Joint root = joint(joints, "Root", 0);
|
||||||
|
|
||||||
|
// Jambes
|
||||||
|
Joint thighR = joint(joints, "Thigh_R", 1);
|
||||||
|
Joint legR = joint(joints, "Leg_R", 2);
|
||||||
|
Joint kneeR = joint(joints, "Knee_R", 3);
|
||||||
|
Joint thighL = joint(joints, "Thigh_L", 4);
|
||||||
|
Joint legL = joint(joints, "Leg_L", 5);
|
||||||
|
Joint kneeL = joint(joints, "Knee_L", 6);
|
||||||
|
|
||||||
|
// Tronc
|
||||||
|
Joint torso = joint(joints, "Torso", 7);
|
||||||
|
Joint chest = joint(joints, "Chest", 8);
|
||||||
|
Joint head = joint(joints, "Head", 9);
|
||||||
|
|
||||||
|
// Bras droit — IDs alignés sur le layout EF (VanillaModelTransformer.RIGHT_ARM
|
||||||
|
// encode upperJoint=11, lowerJoint=12, middleJoint=14, cf.
|
||||||
|
// VanillaModelTransformer:50). L'insertion dans le LinkedHashMap reste
|
||||||
|
// dans l'ordre hiérarchique (shoulder → arm → elbow → hand → tool) pour
|
||||||
|
// préserver la lisibilité de l'iteration ; les IDs déterminent le
|
||||||
|
// mapping jointById utilisé par VanillaModelTransformer + SimpleTransformer.
|
||||||
|
Joint shoulderR = joint(joints, "Shoulder_R", 10);
|
||||||
|
Joint armR = joint(joints, "Arm_R", 11);
|
||||||
|
Joint handR = joint(joints, "Hand_R", 12);
|
||||||
|
Joint toolR = joint(joints, "Tool_R", 13);
|
||||||
|
Joint elbowR = joint(joints, "Elbow_R", 14);
|
||||||
|
|
||||||
|
// Bras gauche — symétrique : Arm_L=16, Hand_L=17, Tool_L=18, Elbow_L=19
|
||||||
|
// (VanillaModelTransformer.LEFT_ARM upperJoint=16, lowerJoint=17, middleJoint=19).
|
||||||
|
Joint shoulderL = joint(joints, "Shoulder_L", 15);
|
||||||
|
Joint armL = joint(joints, "Arm_L", 16);
|
||||||
|
Joint handL = joint(joints, "Hand_L", 17);
|
||||||
|
Joint toolL = joint(joints, "Tool_L", 18);
|
||||||
|
Joint elbowL = joint(joints, "Elbow_L", 19);
|
||||||
|
|
||||||
|
// Hiérarchie. addSubJoints est idempotent (skip si déjà présent) — safe
|
||||||
|
// de le réappeler, utile si on étend plus tard.
|
||||||
|
root.addSubJoints(thighR, thighL, torso);
|
||||||
|
thighR.addSubJoints(legR);
|
||||||
|
legR.addSubJoints(kneeR);
|
||||||
|
thighL.addSubJoints(legL);
|
||||||
|
legL.addSubJoints(kneeL);
|
||||||
|
|
||||||
|
torso.addSubJoints(chest);
|
||||||
|
chest.addSubJoints(head, shoulderR, shoulderL);
|
||||||
|
|
||||||
|
shoulderR.addSubJoints(armR);
|
||||||
|
armR.addSubJoints(elbowR);
|
||||||
|
elbowR.addSubJoints(handR);
|
||||||
|
handR.addSubJoints(toolR);
|
||||||
|
|
||||||
|
shoulderL.addSubJoints(armL);
|
||||||
|
armL.addSubJoints(elbowL);
|
||||||
|
elbowL.addSubJoints(handL);
|
||||||
|
handL.addSubJoints(toolL);
|
||||||
|
|
||||||
|
HumanoidArmature arm = new HumanoidArmature("biped", joints.size(), root, joints);
|
||||||
|
|
||||||
|
// Calcule les toOrigin relatifs — obligatoire après la construction
|
||||||
|
// sinon Pose.orElseEmpty retournerait des matrices non initialisées.
|
||||||
|
arm.bakeOriginMatrices();
|
||||||
|
|
||||||
|
return arm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Joint joint(Map<String, Joint> target, String name, int id) {
|
||||||
|
Joint j = new Joint(name, id, new OpenMatrix4f());
|
||||||
|
target.put(name, j);
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lookup d'un {@link AssetAccessor} d'armature par ID datapack. Point
|
||||||
|
* d'entrée du data-driven : quand {@code AnimationManager.readResourcepackAnimation}
|
||||||
|
* parse le bloc {@code "constructor"} d'un JSON anim utilisateur et que
|
||||||
|
* {@link com.tiedup.remake.rig.util.InstantiateInvoker} rencontre un arg de
|
||||||
|
* type {@code Armature}, il appelle cette méthode via {@code getArmature(id)}.
|
||||||
|
*
|
||||||
|
* <p><b>Builtin</b> : seul l'ID canonique {@code tiedup:biped} (et son
|
||||||
|
* équivalent long-form {@code tiedup:armature/biped}) est résolu en dur —
|
||||||
|
* {@link HumanoidArmature} procédurale construite par {@link #buildBiped()}.
|
||||||
|
* Hardcodé pour performance + compat backward avec le
|
||||||
|
* {@link com.tiedup.remake.rig.mesh.transformer.VanillaModelTransformer}
|
||||||
|
* qui référence les IDs de joint verbatim.</p>
|
||||||
|
*
|
||||||
|
* <p><b>Datapack</b> : pour tout autre ID, on delegate au
|
||||||
|
* {@link ArmatureReloadListener} — chargement depuis
|
||||||
|
* {@code data/<ns>/tiedup/armatures/<name>.json} (cf. D6). Permet aux
|
||||||
|
* modders de définir quadruped / centaure / neko / ... sans coder Java.</p>
|
||||||
|
*
|
||||||
|
* <p>Retourne {@code null} si l'ID n'est ni le biped builtin ni présent
|
||||||
|
* dans le registry datapack — le caller décide du fallback
|
||||||
|
* (InstantiateInvoker log WARN + BIPED, cf. sa Javadoc).</p>
|
||||||
|
*
|
||||||
|
* @param id l'ID à résoudre (ex. {@code tiedup:biped},
|
||||||
|
* {@code tiedup:armature/biped} ou
|
||||||
|
* {@code mymod:quadruped}). Jamais null — caller check.
|
||||||
|
* @return l'{@link AssetAccessor} correspondant, ou {@code null} si l'ID
|
||||||
|
* n'est pas connu du registry. Pas de fallback automatique ici —
|
||||||
|
* c'est au caller de décider (InstantiateInvoker log+BIPED,
|
||||||
|
* test strict fail, etc.).
|
||||||
|
*/
|
||||||
|
public static AssetAccessor<? extends Armature> get(ResourceLocation id) {
|
||||||
|
if (BIPED_SHORT_ID.equals(id) || BIPED_REGISTRY_NAME.equals(id)) {
|
||||||
|
return BIPED;
|
||||||
|
}
|
||||||
|
Armature datapackArmature = ArmatureReloadListener.get(id);
|
||||||
|
if (datapackArmature != null) {
|
||||||
|
return wrapDatapackArmature(id, datapackArmature);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enveloppe une {@link Armature} datapack en {@link AssetAccessor}
|
||||||
|
* conformément à l'API de {@link #get(ResourceLocation)}. Le
|
||||||
|
* {@code AssetAccessor} porte l'{@link Armature} directement (pas de
|
||||||
|
* re-lookup à chaque {@code get()}) et {@code inRegistry()} retourne
|
||||||
|
* {@code true} pour signaler que l'armature provient d'un registry JSON.
|
||||||
|
*
|
||||||
|
* <p>Chaque appel crée une nouvelle instance de l'accessor — c'est un
|
||||||
|
* wrapper léger, non-cache. Si un caller veut dé-dupliquer il peut le
|
||||||
|
* faire lui-même. Cache en interne serait prématuré : les datapack
|
||||||
|
* armatures sont remplacées complètement à chaque {@code /reload}, donc
|
||||||
|
* un cache devrait se purger — préférable de laisser les callers
|
||||||
|
* re-résoudre.</p>
|
||||||
|
*/
|
||||||
|
private static AssetAccessor<? extends Armature> wrapDatapackArmature(
|
||||||
|
ResourceLocation id, Armature armature
|
||||||
|
) {
|
||||||
|
return new AssetAccessor<Armature>() {
|
||||||
|
@Override
|
||||||
|
public Armature get() {
|
||||||
|
return armature;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation registryName() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean inRegistry() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
126
src/main/java/com/tiedup/remake/rig/TiedUpRigConstants.java
Normal file
126
src/main/java/com/tiedup/remake/rig/TiedUpRigConstants.java
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig;
|
||||||
|
|
||||||
|
import com.mojang.logging.LogUtils;
|
||||||
|
import com.tiedup.remake.core.TiedUpMod;
|
||||||
|
import com.tiedup.remake.rig.anim.Animator;
|
||||||
|
import com.tiedup.remake.rig.anim.ServerAnimator;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remplace {@code yesman.epicfight.main.EpicFightMod} + {@code EpicFightSharedConstants}
|
||||||
|
* du fork upstream. Expose les singletons nécessaires au runtime RIG :
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link #MODID} — ID du mod TiedUp (tiedup)</li>
|
||||||
|
* <li>{@link #LOGGER} — logger commun RIG</li>
|
||||||
|
* <li>{@link #identifier(String)} — helper ResourceLocation</li>
|
||||||
|
* <li>{@link #ANIMATOR_PROVIDER} — factory client/server split pour instancier l'Animator</li>
|
||||||
|
* <li>{@link #isPhysicalClient()} — détection side runtime</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Pattern lazy method-ref : {@code ClientAnimator::getAnimator} n'est chargé
|
||||||
|
* que si {@link #isPhysicalClient()} est true. Sur serveur dedié, la classe
|
||||||
|
* client n'est jamais référencée, donc jamais chargée → pas de
|
||||||
|
* {@code NoClassDefFoundError}.</p>
|
||||||
|
*/
|
||||||
|
public final class TiedUpRigConstants {
|
||||||
|
|
||||||
|
public static final String MODID = TiedUpMod.MOD_ID;
|
||||||
|
public static final Logger LOGGER = LogUtils.getLogger();
|
||||||
|
|
||||||
|
/** Détection dev env (Gradle runClient) — utilisé pour les logs debug EF. */
|
||||||
|
public static final boolean IS_DEV_ENV = !FMLEnvironment.production;
|
||||||
|
|
||||||
|
/** Durée d'un tick MC en secondes (20 TPS). */
|
||||||
|
public static final float A_TICK = 1.0F / 20.0F;
|
||||||
|
|
||||||
|
/** Durée de transition inter-animation par défaut (en secondes — 0.15s = 3 ticks). */
|
||||||
|
public static final float GENERAL_ANIMATION_TRANSITION_TIME = 0.15F;
|
||||||
|
|
||||||
|
/** Nombre max de joints supportés par une armature (limite matrice pool). */
|
||||||
|
public static final int MAX_JOINTS = 128;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory lazy : crée un Animator approprié au side runtime courant.
|
||||||
|
* Client → {@link com.tiedup.remake.rig.anim.client.ClientAnimator#getAnimator}
|
||||||
|
* Server → {@link ServerAnimator#getAnimator} (forké verbatim EF)
|
||||||
|
*
|
||||||
|
* <p>Pattern lazy method-ref : {@code ClientAnimator::getAnimator} n'est
|
||||||
|
* chargé que si {@link #isPhysicalClient()} est true. Sur serveur dédié,
|
||||||
|
* la classe client n'est jamais référencée, donc jamais chargée → pas de
|
||||||
|
* {@code NoClassDefFoundError}.</p>
|
||||||
|
*/
|
||||||
|
public static final Function<LivingEntityPatch<?>, Animator> ANIMATOR_PROVIDER =
|
||||||
|
isPhysicalClient()
|
||||||
|
? com.tiedup.remake.rig.anim.client.ClientAnimator::getAnimator
|
||||||
|
: ServerAnimator::getAnimator;
|
||||||
|
|
||||||
|
private TiedUpRigConstants() {}
|
||||||
|
|
||||||
|
public static ResourceLocation identifier(String path) {
|
||||||
|
return ResourceLocation.fromNamespaceAndPath(MODID, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Alias d'{@link #identifier(String)} — équivalent TiedUpRigConstants.prefix upstream. */
|
||||||
|
public static ResourceLocation prefix(String path) {
|
||||||
|
return identifier(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPhysicalClient() {
|
||||||
|
return FMLEnvironment.dist == Dist.CLIENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* En dev env : log un message + throw l'exception fournie.
|
||||||
|
* En prod : log WARN seulement. Équivalent EF {@code TiedUpRigConstants.stacktraceIfDevSide}.
|
||||||
|
*/
|
||||||
|
public static <E extends RuntimeException> void stacktraceIfDevSide(
|
||||||
|
String message,
|
||||||
|
java.util.function.Function<String, E> exceptionFactory
|
||||||
|
) {
|
||||||
|
if (IS_DEV_ENV) {
|
||||||
|
throw exceptionFactory.apply(message);
|
||||||
|
} else {
|
||||||
|
LOGGER.warn(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* En dev env : log via le consumer + throw l'exception.
|
||||||
|
* En prod : log seulement. Équivalent EF {@code EpicFightMod.logAndStacktraceIfDevSide}.
|
||||||
|
*/
|
||||||
|
public static void logAndStacktraceIfDevSide(
|
||||||
|
java.util.function.BiConsumer<Logger, String> logAction,
|
||||||
|
String message,
|
||||||
|
java.util.function.Function<String, ? extends Throwable> exceptionFactory
|
||||||
|
) {
|
||||||
|
logAndStacktraceIfDevSide(logAction, message, exceptionFactory, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void logAndStacktraceIfDevSide(
|
||||||
|
java.util.function.BiConsumer<Logger, String> logAction,
|
||||||
|
String message,
|
||||||
|
java.util.function.Function<String, ? extends Throwable> exceptionFactory,
|
||||||
|
String stackTraceMessage
|
||||||
|
) {
|
||||||
|
logAction.accept(LOGGER, message);
|
||||||
|
if (IS_DEV_ENV) {
|
||||||
|
Throwable t = exceptionFactory.apply(stackTraceMessage);
|
||||||
|
if (t instanceof RuntimeException re) throw re;
|
||||||
|
if (t instanceof Error err) throw err;
|
||||||
|
throw new RuntimeException(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/main/java/com/tiedup/remake/rig/TiedUpRigRegistry.java
Normal file
50
src/main/java/com/tiedup/remake/rig/TiedUpRigRegistry.java
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DirectStaticAnimation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remplace les registries {@code yesman.epicfight.gameasset.Animations} et
|
||||||
|
* {@code yesman.epicfight.gameasset.Armatures} du fork upstream. TiedUp
|
||||||
|
* n'utilise PAS les animations combat EF (BIPED_IDLE, BIPED_WALK, etc. —
|
||||||
|
* ARR assets) — on authore les nôtres en Phase 4 via addon Blender.
|
||||||
|
*
|
||||||
|
* <p>Ce registry expose juste {@link #EMPTY_ANIMATION} — animation singleton
|
||||||
|
* "ne fait rien", référencée par LayerOffAnimation et StaticAnimation pour
|
||||||
|
* le défaut.</p>
|
||||||
|
*
|
||||||
|
* <p>Les vrais registries TiedUp (TiedUpAnimationRegistry, TiedUpArmatures,
|
||||||
|
* TiedUpMeshRegistry) sont prévus en Phase 2-3 et gèreront le scan resource
|
||||||
|
* pack + lookup par ResourceLocation.</p>
|
||||||
|
*/
|
||||||
|
public final class TiedUpRigRegistry {
|
||||||
|
|
||||||
|
private TiedUpRigRegistry() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animation singleton "ne fait rien". Utilisée par le runtime comme
|
||||||
|
* fallback quand aucune animation n'est active sur une layer.
|
||||||
|
*
|
||||||
|
* <p>Équivalent de {@code TiedUpRigRegistry.EMPTY_ANIMATION} du fork upstream
|
||||||
|
* (cf. Animations.java:27 EF).</p>
|
||||||
|
*/
|
||||||
|
public static final DirectStaticAnimation EMPTY_ANIMATION = new DirectStaticAnimation() {
|
||||||
|
public static final ResourceLocation EMPTY_ANIMATION_REGISTRY_NAME =
|
||||||
|
ResourceLocation.fromNamespaceAndPath(TiedUpRigConstants.MODID, "empty");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void loadAnimation() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation registryName() {
|
||||||
|
return EMPTY_ANIMATION_REGISTRY_NAME;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig;
|
||||||
|
|
||||||
|
import net.minecraft.server.packs.resources.ResourceManager;
|
||||||
|
import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
|
||||||
|
import net.minecraft.util.profiling.ProfilerFiller;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.client.event.RegisterClientReloadListenersEvent;
|
||||||
|
import net.minecraftforge.event.AddReloadListenerEvent;
|
||||||
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||||
|
import net.minecraftforge.fml.common.Mod;
|
||||||
|
|
||||||
|
import com.tiedup.remake.core.TiedUpMod;
|
||||||
|
import com.tiedup.remake.rig.anim.action.impl.SpawnParticleAction;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook {@code /reload} (serveur) + {@code F3+T} (client) pour reset le set dedup
|
||||||
|
* {@link TiedUpAnimationRegistry#resetWarnedMissing()}.
|
||||||
|
*
|
||||||
|
* <h2>Pourquoi ?</h2>
|
||||||
|
* <p>Le set statique {@code WARNED_MISSING_ANIMS} de
|
||||||
|
* {@link TiedUpAnimationRegistry} accumule les IDs d'animations introuvables
|
||||||
|
* pour éviter le spam log (un miss reloggué tick après tick). Sans reset, le
|
||||||
|
* set grandit sans borne en prod (N items cassés × K sessions × long uptime)
|
||||||
|
* et empêche de re-signaler un miss qui redevient cassé après un reload.</p>
|
||||||
|
*
|
||||||
|
* <h2>Scénario typique</h2>
|
||||||
|
* <ol>
|
||||||
|
* <li>Session N : un item data-driven référence {@code tiedup:broken_anim} →
|
||||||
|
* WARN log, ID ajouté au set.</li>
|
||||||
|
* <li>Modder corrige l'asset, {@code /reload}.</li>
|
||||||
|
* <li>Session N+1 : resolveWithFallback dispose de l'anim → pas de fallback,
|
||||||
|
* pas de WARN. Le set contient toujours l'ID de la session N (résiduel).</li>
|
||||||
|
* <li>Modder casse à nouveau l'anim, {@code /reload}.</li>
|
||||||
|
* <li>Session N+2 : miss de nouveau, <b>mais l'ID est déjà dans le set</b> →
|
||||||
|
* silencieux, aucun feedback modder.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>Avec ce listener, chaque reload clear le set — un miss sur une anim
|
||||||
|
* précédemment warnée redéclenche le WARN (comportement attendu : le modder
|
||||||
|
* doit voir l'erreur s'il re-casse l'asset).</p>
|
||||||
|
*
|
||||||
|
* <h2>Les deux sides</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link AddReloadListenerEvent} fire côté serveur à chaque
|
||||||
|
* {@code /reload} datapack.
|
||||||
|
* {@code resolveWithFallback} peut être appelé côté serveur (validation
|
||||||
|
* parse-time des items, checks administratifs). Les entrées set accumulées
|
||||||
|
* côté serveur sont cleared là.</li>
|
||||||
|
* <li>{@link RegisterClientReloadListenersEvent} fire une fois au setup client
|
||||||
|
* pour enregistrer un listener qui tourne à chaque resource reload client
|
||||||
|
* (F3+T ou resource pack swap).
|
||||||
|
* {@code resolveWithFallback} est majoritairement appelé côté client
|
||||||
|
* (pipeline rebuild animations, packet handlers). Les entrées set accumulées
|
||||||
|
* côté client sont cleared là.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Le set statique dans {@link TiedUpAnimationRegistry} est le MÊME sur les
|
||||||
|
* deux sides (même JVM pour un serveur intégré / solo) — les deux hooks
|
||||||
|
* clearent le même set, pas de double comptabilité. Sur serveur dédié
|
||||||
|
* seul le hook server fire, côté client pur (rare — lobby / fast reconnect)
|
||||||
|
* seul le hook client.</p>
|
||||||
|
*
|
||||||
|
* <h2>Threading</h2>
|
||||||
|
* <p>{@link TiedUpAnimationRegistry#resetWarnedMissing()} utilise
|
||||||
|
* {@link java.util.concurrent.ConcurrentHashMap#newKeySet()} donc safe à
|
||||||
|
* l'appel concurrent. Les reload events peuvent fire depuis le main thread
|
||||||
|
* (server tick) ou le render thread (client reload) — aucune contrainte
|
||||||
|
* supplémentaire.</p>
|
||||||
|
*/
|
||||||
|
@Mod.EventBusSubscriber(modid = TiedUpMod.MOD_ID)
|
||||||
|
public final class TiedUpRigRegistryReloadListener
|
||||||
|
extends SimplePreparableReloadListener<Void> {
|
||||||
|
|
||||||
|
private TiedUpRigRegistryReloadListener() {
|
||||||
|
// Instancié par les event handlers ci-dessous — pas d'API publique
|
||||||
|
// à part comme reload listener.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void prepare(ResourceManager mgr, ProfilerFiller profiler) {
|
||||||
|
// No-op — tout se fait dans apply(). Pas de I/O à faire off-thread,
|
||||||
|
// l'opération est juste un Set.clear() en O(1) amortisé.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void apply(Void nothing, ResourceManager mgr, ProfilerFiller profiler) {
|
||||||
|
TiedUpAnimationRegistry.resetWarnedMissing();
|
||||||
|
SpawnParticleAction.resetWarnedMissing();
|
||||||
|
TiedUpRigConstants.LOGGER.debug(
|
||||||
|
"[TiedUpRigRegistryReloadListener] WARNED_MISSING_ANIMS + WARNED_MISSING_JOINTS reset on reload"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook {@link AddReloadListenerEvent} — fire à chaque {@code /reload}
|
||||||
|
* datapack côté serveur (et aussi au server start). Enregistre un
|
||||||
|
* listener qui clear le set dedup WARN.
|
||||||
|
*
|
||||||
|
* <p>Note : {@code AddReloadListenerEvent} est sur le FORGE bus (pas MOD bus),
|
||||||
|
* mais le {@code @Mod.EventBusSubscriber} par défaut cible le MOD bus. On
|
||||||
|
* spécifie {@link Mod.EventBusSubscriber.Bus#FORGE} sur le subscriber ci-dessus ?
|
||||||
|
* Non : en 1.20.1, {@code AddReloadListenerEvent extends Event} bus-inferred
|
||||||
|
* automatiquement — le subscriber static-registered via annotation
|
||||||
|
* fonctionne sur le FORGE bus par défaut tant qu'on n'indique pas
|
||||||
|
* {@code Bus.MOD}. Par souci de clarté, on s'aligne sur le pattern du mod
|
||||||
|
* ({@code TiedUpMod.ForgeEvents} utilise le FORGE bus).</p>
|
||||||
|
*/
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onAddReloadListeners(AddReloadListenerEvent event) {
|
||||||
|
event.addListener(new TiedUpRigRegistryReloadListener());
|
||||||
|
TiedUpRigConstants.LOGGER.debug(
|
||||||
|
"[TiedUpRigRegistryReloadListener] Registered server-side reload listener for WARN dedup reset"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook {@link RegisterClientReloadListenersEvent} — fire une fois côté client
|
||||||
|
* pendant le setup pour enregistrer des listeners qui tourneront à chaque
|
||||||
|
* resource reload client (F3+T, resource pack swap).
|
||||||
|
*
|
||||||
|
* <p>Gardé {@link Dist#CLIENT} : sur serveur dédié, l'event n'existe pas
|
||||||
|
* (event client), le subscriber static est skip sans class-load.</p>
|
||||||
|
*
|
||||||
|
* <p>Ce subscriber est un inner static class pour isoler la gate de Dist —
|
||||||
|
* {@code @Mod.EventBusSubscriber(value = Dist.CLIENT)} sur la classe entière
|
||||||
|
* exclurait aussi le hook {@link #onAddReloadListeners} server-side.</p>
|
||||||
|
*/
|
||||||
|
@Mod.EventBusSubscriber(
|
||||||
|
modid = TiedUpMod.MOD_ID,
|
||||||
|
bus = Mod.EventBusSubscriber.Bus.MOD,
|
||||||
|
value = Dist.CLIENT
|
||||||
|
)
|
||||||
|
public static final class ClientReloadHook {
|
||||||
|
|
||||||
|
private ClientReloadHook() {
|
||||||
|
// holder class for @SubscribeEvent static
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onRegisterClientReloadListeners(
|
||||||
|
RegisterClientReloadListenersEvent event
|
||||||
|
) {
|
||||||
|
event.registerReloadListener(new TiedUpRigRegistryReloadListener());
|
||||||
|
TiedUpRigConstants.LOGGER.debug(
|
||||||
|
"[TiedUpRigRegistryReloadListener] Registered client-side reload listener for WARN dedup reset"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
140
src/main/java/com/tiedup/remake/rig/anim/AnimationClip.java
Normal file
140
src/main/java/com/tiedup/remake/rig/anim/AnimationClip.java
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.mutable.MutableInt;
|
||||||
|
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
|
||||||
|
public class AnimationClip {
|
||||||
|
public static final AnimationClip EMPTY_CLIP = new AnimationClip();
|
||||||
|
|
||||||
|
protected Map<String, TransformSheet> jointTransforms = new HashMap<> ();
|
||||||
|
protected float clipTime;
|
||||||
|
protected float[] bakedTimes;
|
||||||
|
|
||||||
|
/// To modify existing keyframes in runtime and keep the baked state, call [#setBaked] again
|
||||||
|
/// after finishing clip modification. (Frequent calls of this method will cause a performance issue)
|
||||||
|
public void addJointTransform(String jointName, TransformSheet sheet) {
|
||||||
|
this.jointTransforms.put(jointName, sheet);
|
||||||
|
this.bakedTimes = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasJointTransform(String jointName) {
|
||||||
|
return this.jointTransforms.containsKey(jointName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bakes all keyframes to optimize calculating current pose,
|
||||||
|
public void bakeKeyframes() {
|
||||||
|
Set<Float> timestamps = new HashSet<> ();
|
||||||
|
|
||||||
|
this.jointTransforms.values().forEach(transformSheet -> {
|
||||||
|
transformSheet.forEach((i, keyframe) -> {
|
||||||
|
timestamps.add(keyframe.time());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
float[] bakedTimestamps = new float[timestamps.size()];
|
||||||
|
MutableInt mi = new MutableInt(0);
|
||||||
|
|
||||||
|
timestamps.stream().sorted().toList().forEach(f -> {
|
||||||
|
bakedTimestamps[mi.getAndAdd(1)] = f;
|
||||||
|
});
|
||||||
|
|
||||||
|
Map<String, TransformSheet> bakedJointTransforms = new HashMap<> ();
|
||||||
|
|
||||||
|
this.jointTransforms.forEach((jointName, transformSheet) -> {
|
||||||
|
bakedJointTransforms.put(jointName, transformSheet.createInterpolated(bakedTimestamps));
|
||||||
|
});
|
||||||
|
|
||||||
|
this.jointTransforms = bakedJointTransforms;
|
||||||
|
this.bakedTimes = bakedTimestamps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bake keyframes supposing all keyframes are aligned (mainly used when creating link animations)
|
||||||
|
public void setBaked() {
|
||||||
|
TransformSheet transformSheet = this.jointTransforms.get("Root");
|
||||||
|
|
||||||
|
if (transformSheet != null) {
|
||||||
|
this.bakedTimes = new float[transformSheet.getKeyframes().length];
|
||||||
|
|
||||||
|
for (int i = 0; i < transformSheet.getKeyframes().length; i++) {
|
||||||
|
this.bakedTimes[i] = transformSheet.getKeyframes()[i].time();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformSheet getJointTransform(String jointName) {
|
||||||
|
return this.jointTransforms.get(jointName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Pose getPoseInTime(float time) {
|
||||||
|
Pose pose = new Pose();
|
||||||
|
|
||||||
|
if (time < 0.0F) {
|
||||||
|
time = this.clipTime + time;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.bakedTimes != null && this.bakedTimes.length > 0) {
|
||||||
|
// Binary search
|
||||||
|
int begin = 0, end = this.bakedTimes.length - 1;
|
||||||
|
|
||||||
|
while (end - begin > 1) {
|
||||||
|
int i = begin + (end - begin) / 2;
|
||||||
|
|
||||||
|
if (this.bakedTimes[i] <= time && this.bakedTimes[i+1] > time) {
|
||||||
|
begin = i;
|
||||||
|
end = i+1;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
if (this.bakedTimes[i] > time) {
|
||||||
|
end = i;
|
||||||
|
} else if (this.bakedTimes[i+1] <= time) {
|
||||||
|
begin = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float delta = Mth.clamp((time - this.bakedTimes[begin]) / (this.bakedTimes[end] - this.bakedTimes[begin]), 0.0F, 1.0F);
|
||||||
|
TransformSheet.InterpolationInfo iInfo = new TransformSheet.InterpolationInfo(begin, end, delta);
|
||||||
|
|
||||||
|
for (String jointName : this.jointTransforms.keySet()) {
|
||||||
|
pose.putJointData(jointName, this.jointTransforms.get(jointName).getInterpolatedTransform(iInfo));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (String jointName : this.jointTransforms.keySet()) {
|
||||||
|
pose.putJointData(jointName, this.jointTransforms.get(jointName).getInterpolatedTransform(time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pose;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @return returns protected keyframes of each joint to keep the baked state of keyframes.
|
||||||
|
public Map<String, TransformSheet> getJointTransforms() {
|
||||||
|
return Collections.unmodifiableMap(this.jointTransforms);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
this.jointTransforms.clear();
|
||||||
|
this.bakedTimes = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setClipTime(float clipTime) {
|
||||||
|
this.clipTime = clipTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getClipTime() {
|
||||||
|
return this.clipTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
450
src/main/java/com/tiedup/remake/rig/anim/AnimationManager.java
Normal file
450
src/main/java/com/tiedup/remake/rig/anim/AnimationManager.java
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
import net.minecraft.resources.FileToIdConverter;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.network.ServerGamePacketListenerImpl;
|
||||||
|
import net.minecraft.server.packs.resources.Resource;
|
||||||
|
import net.minecraft.server.packs.resources.ResourceManager;
|
||||||
|
import net.minecraft.server.packs.resources.SimplePreparableReloadListener;
|
||||||
|
import net.minecraft.util.GsonHelper;
|
||||||
|
import net.minecraft.util.profiling.ProfilerFiller;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
|
import net.minecraftforge.eventbus.api.Event;
|
||||||
|
import net.minecraftforge.fml.event.IModBusEvent;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.asset.JsonAssetLoader;
|
||||||
|
import com.tiedup.remake.rig.anim.client.AnimationSubFileReader;
|
||||||
|
import com.tiedup.remake.rig.exception.AssetLoadingException;
|
||||||
|
import com.tiedup.remake.rig.util.InstantiateInvoker;
|
||||||
|
import com.tiedup.remake.rig.util.MutableBoolean;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigRegistry;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public class AnimationManager extends SimplePreparableReloadListener<List<ResourceLocation>> {
|
||||||
|
private static final AnimationManager INSTANCE = new AnimationManager();
|
||||||
|
private static ResourceManager serverResourceManager = null;
|
||||||
|
private static final Gson GSON = new GsonBuilder().create();
|
||||||
|
private static final String DIRECTORY = "animmodels/animations";
|
||||||
|
|
||||||
|
public static AnimationManager getInstance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<Integer, AnimationAccessor<? extends StaticAnimation>> animationById = Maps.newHashMap();
|
||||||
|
private final Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>> animationByName = Maps.newHashMap();
|
||||||
|
private final Map<AnimationAccessor<? extends StaticAnimation>, StaticAnimation> animations = Maps.newHashMap();
|
||||||
|
private final Map<AnimationAccessor<? extends StaticAnimation>, String> resourcepackAnimationCommands = Maps.newHashMap();
|
||||||
|
|
||||||
|
public static boolean checkNull(AssetAccessor<? extends StaticAnimation> animation) {
|
||||||
|
if (animation == null || animation.isEmpty()) {
|
||||||
|
if (animation != null) {
|
||||||
|
TiedUpRigConstants.stacktraceIfDevSide("Empty animation accessor: " + animation.registryName(), NoSuchElementException::new);
|
||||||
|
} else {
|
||||||
|
TiedUpRigConstants.stacktraceIfDevSide("Null animation accessor", NoSuchElementException::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T extends StaticAnimation> AnimationAccessor<T> byKey(String registryName) {
|
||||||
|
return byKey(ResourceLocation.parse(registryName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T extends StaticAnimation> AnimationAccessor<T> byKey(ResourceLocation registryName) {
|
||||||
|
return (AnimationAccessor<T>)getInstance().animationByName.get(registryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T extends StaticAnimation> AnimationAccessor<T> byId(int animationId) {
|
||||||
|
return (AnimationAccessor<T>)getInstance().animationById.get(animationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>> getAnimations(Predicate<AssetAccessor<? extends StaticAnimation>> filter) {
|
||||||
|
Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>> filteredItems =
|
||||||
|
this.animationByName.entrySet().stream()
|
||||||
|
.filter(entry -> {
|
||||||
|
return filter.test(entry.getValue());
|
||||||
|
})
|
||||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
|
||||||
|
return ImmutableMap.copyOf(filteredItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnimationClip loadAnimationClip(StaticAnimation animation, BiFunction<JsonAssetLoader, StaticAnimation, AnimationClip> clipLoader) {
|
||||||
|
try {
|
||||||
|
if (getAnimationResourceManager() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonAssetLoader modelLoader = new JsonAssetLoader(getAnimationResourceManager(), animation.getLocation());
|
||||||
|
AnimationClip loadedClip = clipLoader.apply(modelLoader, animation);
|
||||||
|
|
||||||
|
return loadedClip;
|
||||||
|
} catch (AssetLoadingException e) {
|
||||||
|
throw new AssetLoadingException("Failed to load animation clip from: " + animation, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void readAnimationProperties(StaticAnimation animation) {
|
||||||
|
ResourceLocation dataLocation = getSubAnimationFileLocation(animation.getLocation(), AnimationSubFileReader.SUBFILE_CLIENT_PROPERTY);
|
||||||
|
ResourceLocation povLocation = getSubAnimationFileLocation(animation.getLocation(), AnimationSubFileReader.SUBFILE_POV_ANIMATION);
|
||||||
|
|
||||||
|
getAnimationResourceManager().getResource(dataLocation).ifPresent((rs) -> {
|
||||||
|
AnimationSubFileReader.readAndApply(animation, rs, AnimationSubFileReader.SUBFILE_CLIENT_PROPERTY);
|
||||||
|
});
|
||||||
|
|
||||||
|
getAnimationResourceManager().getResource(povLocation).ifPresent((rs) -> {
|
||||||
|
AnimationSubFileReader.readAndApply(animation, rs, AnimationSubFileReader.SUBFILE_POV_ANIMATION);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<ResourceLocation> prepare(ResourceManager resourceManager, ProfilerFiller profilerIn) {
|
||||||
|
if (!TiedUpRigConstants.isPhysicalClient() && serverResourceManager == null) {
|
||||||
|
serverResourceManager = resourceManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animations.clear();
|
||||||
|
this.animationById.entrySet().removeIf(entry -> !entry.getValue().inRegistry());
|
||||||
|
this.animationByName.entrySet().removeIf(entry -> !entry.getValue().inRegistry());
|
||||||
|
this.resourcepackAnimationCommands.clear();
|
||||||
|
|
||||||
|
List<ResourceLocation> directories = new ArrayList<> ();
|
||||||
|
scanDirectoryNames(resourceManager, directories);
|
||||||
|
|
||||||
|
return directories;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void scanDirectoryNames(ResourceManager resourceManager, List<ResourceLocation> output) {
|
||||||
|
FileToIdConverter filetoidconverter = FileToIdConverter.json(DIRECTORY);
|
||||||
|
filetoidconverter.listMatchingResources(resourceManager).keySet().stream().map(AnimationManager::pathToId).forEach(output::add);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void apply(List<ResourceLocation> objects, ResourceManager resourceManager, ProfilerFiller profilerIn) {
|
||||||
|
// RIG : Armatures.reload() (EF gameasset registry) retiré.
|
||||||
|
// TiedUpArmatures.reload() sera appelé ici en Phase 2 quand le registry
|
||||||
|
// sera créé. En Phase 0, no-op.
|
||||||
|
//
|
||||||
|
// PHASE 2 LISTENER ORDERING NOTE (BUG-RACE-01 prevention) :
|
||||||
|
// Quand cette classe sera enregistrée comme PreparableReloadListener via
|
||||||
|
// AddReloadListenerEvent (server) + RegisterClientReloadListenersEvent (client),
|
||||||
|
// elle DOIT être enregistrée APRÈS ces 3 listeners qui peuplent les registries
|
||||||
|
// consommés par les codecs de propriétés d'animation :
|
||||||
|
// - LivingMotionReloadListener (pour LIVING_MOTION_CODEC dans AnimationProperty)
|
||||||
|
// - ArmatureReloadListener (pour InstantiateInvoker.getArmature)
|
||||||
|
// - PoseTypeReloadListener (pour les bindings pose type)
|
||||||
|
// MC 1.20.1 SimpleReloadInstance sérialise les apply() dans l'ordre
|
||||||
|
// d'enregistrement de la liste — c'est notre seul levier d'ordering en
|
||||||
|
// Forge 1.20.1 (PreparableReloadListener.getDependencies() n'existe pas
|
||||||
|
// en cette version, c'est une API Fabric/NeoForge).
|
||||||
|
// Sites concernés : TiedUpMod.ForgeEvents.onAddReloadListeners + V2ClientSetup.onRegisterReloadListeners.
|
||||||
|
|
||||||
|
Set<ResourceLocation> registeredAnimation =
|
||||||
|
this.animationById.values().stream()
|
||||||
|
.reduce(
|
||||||
|
new HashSet<> (),
|
||||||
|
(set, accessor) -> {
|
||||||
|
set.add(accessor.registryName());
|
||||||
|
|
||||||
|
for (AssetAccessor<? extends StaticAnimation> subAnimAccessor : accessor.get().getSubAnimations()) {
|
||||||
|
set.add(subAnimAccessor.registryName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return set;
|
||||||
|
},
|
||||||
|
(set1, set2) -> {
|
||||||
|
set1.addAll(set2);
|
||||||
|
return set1;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load animations that are not registered by AnimationRegistryEvent
|
||||||
|
// Reads from /assets folder in physical client, /datapack in physical server.
|
||||||
|
objects.stream()
|
||||||
|
.filter(animId -> !registeredAnimation.contains(animId) && !animId.getPath().contains("/data/") && !animId.getPath().contains("/pov/"))
|
||||||
|
.sorted(Comparator.comparing(ResourceLocation::toString))
|
||||||
|
.forEach(animId -> {
|
||||||
|
Optional<Resource> resource = resourceManager.getResource(idToPath(animId));
|
||||||
|
|
||||||
|
try (Reader reader = resource.orElseThrow().openAsReader()) {
|
||||||
|
JsonElement jsonelement = GsonHelper.fromJson(GSON, reader, JsonElement.class);
|
||||||
|
this.readResourcepackAnimation(animId, jsonelement.getAsJsonObject());
|
||||||
|
} catch (IOException | JsonParseException | IllegalArgumentException resourceReadException) {
|
||||||
|
TiedUpRigConstants.LOGGER.error("Couldn't parse animation data from {}", animId, resourceReadException);
|
||||||
|
} catch (Exception e) {
|
||||||
|
TiedUpRigConstants.LOGGER.error("Failed at constructing {}", animId, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// RIG : upstream EF appelait ici yesman.epicfight.skill.SkillManager.reloadAllSkillsAnimations()
|
||||||
|
// (re-link des animations aux Skills Java). Combat system hors scope TiedUp → appel strippé.
|
||||||
|
|
||||||
|
this.animations.entrySet().stream()
|
||||||
|
.reduce(
|
||||||
|
new ArrayList<AssetAccessor<? extends StaticAnimation>>(),
|
||||||
|
(list, entry) -> {
|
||||||
|
MutableBoolean init = new MutableBoolean(true);
|
||||||
|
|
||||||
|
if (entry.getValue() == null || entry.getValue().getAccessor() == null) {
|
||||||
|
TiedUpRigConstants.logAndStacktraceIfDevSide(Logger::error, "Invalid animation implementation: " + entry.getKey(), AssetLoadingException::new);
|
||||||
|
init.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.getValue().getSubAnimations().forEach((subAnimation) -> {
|
||||||
|
if (subAnimation == null || subAnimation.get() == null) {
|
||||||
|
TiedUpRigConstants.logAndStacktraceIfDevSide(Logger::error, "Invalid sub animation implementation: " + entry.getKey(), AssetLoadingException::new);
|
||||||
|
init.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (init.value()) {
|
||||||
|
list.add(entry.getValue().getAccessor());
|
||||||
|
list.addAll(entry.getValue().getSubAnimations());
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
},
|
||||||
|
(list1, list2) -> {
|
||||||
|
list1.addAll(list2);
|
||||||
|
return list1;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.forEach(accessor -> {
|
||||||
|
accessor.doOrThrow(StaticAnimation::postInit);
|
||||||
|
|
||||||
|
if (TiedUpRigConstants.isPhysicalClient()) {
|
||||||
|
AnimationManager.readAnimationProperties(accessor.get());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResourceLocation getSubAnimationFileLocation(ResourceLocation location, AnimationSubFileReader.SubFileType<?> subFileType) {
|
||||||
|
int splitIdx = location.getPath().lastIndexOf('/');
|
||||||
|
|
||||||
|
if (splitIdx < 0) {
|
||||||
|
splitIdx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResourceLocation.fromNamespaceAndPath(location.getNamespace(), String.format("%s/" + subFileType.getDirectory() + "%s", location.getPath().substring(0, splitIdx), location.getPath().substring(splitIdx)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts animation id, acquired by [StaticAnimation#getRegistryName], to animation resource path acquired by [StaticAnimation#getLocation]
|
||||||
|
public static ResourceLocation idToPath(ResourceLocation rl) {
|
||||||
|
return rl.getPath().matches(DIRECTORY + "/.*\\.json") ? rl : ResourceLocation.fromNamespaceAndPath(rl.getNamespace(), DIRECTORY + "/" + rl.getPath() + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts animation resource path, acquired by [StaticAnimation#getLocation], to animation id acquired by [StaticAnimation#getRegistryName]
|
||||||
|
public static ResourceLocation pathToId(ResourceLocation rl) {
|
||||||
|
return ResourceLocation.fromNamespaceAndPath(rl.getNamespace(), rl.getPath().replace(DIRECTORY + "/", "").replace(".json", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setServerResourceManager(ResourceManager pResourceManager) {
|
||||||
|
serverResourceManager = pResourceManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResourceManager getAnimationResourceManager() {
|
||||||
|
return TiedUpRigConstants.isPhysicalClient() ? Minecraft.getInstance().getResourceManager() : serverResourceManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getResourcepackAnimationCount() {
|
||||||
|
return this.resourcepackAnimationCommands.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream<CompoundTag> getResourcepackAnimationStream() {
|
||||||
|
return this.resourcepackAnimationCommands.entrySet().stream().map((entry) -> {
|
||||||
|
CompoundTag compTag = new CompoundTag();
|
||||||
|
compTag.putString("registry_name", entry.getKey().registryName().toString());
|
||||||
|
compTag.putInt("id", entry.getKey().id());
|
||||||
|
compTag.putString("invoke_command", entry.getValue());
|
||||||
|
|
||||||
|
return compTag;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mandatoryPack : creates dummy animations for animations from the server without animation clips when the server has mandatory resource pack.
|
||||||
|
* custom weapon types & mob capabilities won't be created because they won't be able to find the animations from the server
|
||||||
|
* dummy animations will be automatically removed right after reloading resourced as the server forces using resource pack
|
||||||
|
*/
|
||||||
|
// RIG : processServerPacket + validateClientAnimationRegistry strippés.
|
||||||
|
// C'était le protocole datapack-sync EF pour valider que le client a les
|
||||||
|
// mêmes animations que le serveur au login (important pour les animations
|
||||||
|
// combat stockées en data/). TiedUp utilise resource pack uniquement
|
||||||
|
// (assets/) côté client, pas de sync datapack nécessaire.
|
||||||
|
// Ré-introduire Phase 2+ si on veut un warning quand un pack d'animations
|
||||||
|
// custom diverge.
|
||||||
|
|
||||||
|
private static final Set<String> NO_WARNING_MODID = Sets.newHashSet();
|
||||||
|
|
||||||
|
public static void addNoWarningModId(String modid) {
|
||||||
|
NO_WARNING_MODID.add(modid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**************************************************
|
||||||
|
* User-animation loader
|
||||||
|
**************************************************/
|
||||||
|
@SuppressWarnings({ "deprecation" })
|
||||||
|
private void readResourcepackAnimation(ResourceLocation rl, JsonObject json) throws Exception {
|
||||||
|
JsonElement constructorElement = json.get("constructor");
|
||||||
|
|
||||||
|
if (constructorElement == null) {
|
||||||
|
if (NO_WARNING_MODID.contains(rl.getNamespace())) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
TiedUpRigConstants.logAndStacktraceIfDevSide(
|
||||||
|
Logger::error
|
||||||
|
, "Datapack animation reading failed: No constructor information has provided: " + rl
|
||||||
|
, IllegalStateException::new
|
||||||
|
, "No constructor information has provided in User animation, " + rl + "\nPlease remove this resource if it's unnecessary to optimize your project."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject constructorObject = constructorElement.getAsJsonObject();
|
||||||
|
String invocationCommand = constructorObject.get("invocation_command").getAsString();
|
||||||
|
StaticAnimation animation = InstantiateInvoker.invoke(invocationCommand, StaticAnimation.class).getResult();
|
||||||
|
JsonElement propertiesElement = json.getAsJsonObject().get("properties");
|
||||||
|
|
||||||
|
if (propertiesElement != null) {
|
||||||
|
JsonObject propertiesObject = propertiesElement.getAsJsonObject();
|
||||||
|
|
||||||
|
for (Map.Entry<String, JsonElement> entry : propertiesObject.entrySet()) {
|
||||||
|
AnimationProperty<?> propertyKey = AnimationProperty.getSerializableProperty(entry.getKey());
|
||||||
|
Object value = propertyKey.parseFrom(entry.getValue());
|
||||||
|
animation.addPropertyUnsafe(propertyKey, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimationAccessor<StaticAnimation> accessor = AnimationAccessorImpl.create(rl, this.animations.size() + 1, false, null);
|
||||||
|
animation.setAccessor(accessor);
|
||||||
|
|
||||||
|
this.resourcepackAnimationCommands.put(accessor, invocationCommand);
|
||||||
|
this.animationById.put(accessor.id(), accessor);
|
||||||
|
this.animationByName.put(accessor.registryName(), accessor);
|
||||||
|
this.animations.put(accessor, animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public interface AnimationAccessor<A extends DynamicAnimation> extends AssetAccessor<A> {
|
||||||
|
int id();
|
||||||
|
|
||||||
|
default boolean idBetween(AnimationAccessor<? extends StaticAnimation> a1, AnimationAccessor<? extends StaticAnimation> a2) {
|
||||||
|
return a1.id() <= this.id() && a2.id() >= this.id();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static record AnimationAccessorImpl<A extends StaticAnimation> (ResourceLocation registryName, int id, boolean inRegistry, Function<AnimationAccessor<A>, A> onLoad) implements AnimationAccessor<A> {
|
||||||
|
private static <A extends StaticAnimation> AnimationAccessor<A> create(ResourceLocation registryName, int id, boolean inRegistry, Function<AnimationAccessor<A>, A> onLoad) {
|
||||||
|
return new AnimationAccessorImpl<A> (registryName, id, inRegistry, onLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public A get() {
|
||||||
|
if (!INSTANCE.animations.containsKey(this)) {
|
||||||
|
INSTANCE.animations.put(this, this.onLoad.apply(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (A)INSTANCE.animations.get(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return this.registryName.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int hashCode() {
|
||||||
|
return this.registryName.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj) {
|
||||||
|
return true;
|
||||||
|
} else if (obj instanceof AnimationAccessor armatureAccessor) {
|
||||||
|
return this.registryName.equals(armatureAccessor.registryName());
|
||||||
|
} else if (obj instanceof ResourceLocation rl) {
|
||||||
|
return this.registryName.equals(rl);
|
||||||
|
} else if (obj instanceof String name) {
|
||||||
|
return this.registryName.toString().equals(name);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AnimationRegistryEvent extends Event implements IModBusEvent {
|
||||||
|
private List<AnimationBuilder> builders = Lists.newArrayList();
|
||||||
|
private Set<String> namespaces = Sets.newHashSet();
|
||||||
|
|
||||||
|
public void newBuilder(String namespace, Consumer<AnimationBuilder> build) {
|
||||||
|
if (this.namespaces.contains(namespace)) {
|
||||||
|
throw new IllegalArgumentException("Animation builder namespace '" + namespace + "' already exists!");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.namespaces.add(namespace);
|
||||||
|
this.builders.add(new AnimationBuilder(namespace, build));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AnimationBuilder> getBuilders() {
|
||||||
|
return this.builders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static record AnimationBuilder(String namespace, Consumer<AnimationBuilder> task) {
|
||||||
|
public <T extends StaticAnimation> AnimationManager.AnimationAccessor<T> nextAccessor(String id, Function<AnimationManager.AnimationAccessor<T>, T> onLoad) {
|
||||||
|
AnimationAccessor<T> accessor = AnimationAccessorImpl.create(ResourceLocation.fromNamespaceAndPath(this.namespace, id), INSTANCE.animations.size() + 1, true, onLoad);
|
||||||
|
|
||||||
|
INSTANCE.animationById.put(accessor.id(), accessor);
|
||||||
|
INSTANCE.animationByName.put(accessor.registryName(), accessor);
|
||||||
|
INSTANCE.animations.put(accessor, null);
|
||||||
|
|
||||||
|
return accessor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
162
src/main/java/com/tiedup/remake/rig/anim/AnimationPlayer.java
Normal file
162
src/main/java/com/tiedup/remake/rig/anim/AnimationPlayer.java
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackSpeedModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackTimeModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.StaticAnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigRegistry;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
public class AnimationPlayer {
|
||||||
|
protected float elapsedTime;
|
||||||
|
protected float prevElapsedTime;
|
||||||
|
protected boolean isEnd;
|
||||||
|
protected boolean doNotResetTime;
|
||||||
|
protected boolean reversed;
|
||||||
|
protected AssetAccessor<? extends DynamicAnimation> play;
|
||||||
|
|
||||||
|
public AnimationPlayer() {
|
||||||
|
this.setPlayAnimation(TiedUpRigRegistry.EMPTY_ANIMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void tick(LivingEntityPatch<?> entitypatch) {
|
||||||
|
DynamicAnimation currentPlay = this.getAnimation().get();
|
||||||
|
DynamicAnimation currentPlayStatic = currentPlay.getRealAnimation().get();
|
||||||
|
this.prevElapsedTime = this.elapsedTime;
|
||||||
|
|
||||||
|
float playbackSpeed = currentPlay.getPlaySpeed(entitypatch, currentPlay);
|
||||||
|
PlaybackSpeedModifier playSpeedModifier = currentPlayStatic.getProperty(StaticAnimationProperty.PLAY_SPEED_MODIFIER).orElse(null);
|
||||||
|
|
||||||
|
if (playSpeedModifier != null) {
|
||||||
|
playbackSpeed = playSpeedModifier.modify(currentPlay, entitypatch, playbackSpeed, this.prevElapsedTime, this.elapsedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.elapsedTime += TiedUpRigConstants.A_TICK * playbackSpeed * (this.isReversed() && currentPlay.canBePlayedReverse() ? -1.0F : 1.0F);
|
||||||
|
PlaybackTimeModifier playTimeModifier = currentPlayStatic.getProperty(StaticAnimationProperty.ELAPSED_TIME_MODIFIER).orElse(null);
|
||||||
|
|
||||||
|
if (playTimeModifier != null) {
|
||||||
|
Pair<Float, Float> time = playTimeModifier.modify(currentPlay, entitypatch, playbackSpeed, this.prevElapsedTime, this.elapsedTime);
|
||||||
|
this.prevElapsedTime = time.getFirst();
|
||||||
|
this.elapsedTime = time.getSecond();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.elapsedTime > currentPlay.getTotalTime()) {
|
||||||
|
if (currentPlay.isRepeat()) {
|
||||||
|
this.prevElapsedTime = this.prevElapsedTime - currentPlay.getTotalTime();
|
||||||
|
this.elapsedTime %= currentPlay.getTotalTime();
|
||||||
|
} else {
|
||||||
|
this.elapsedTime = currentPlay.getTotalTime();
|
||||||
|
currentPlay.end(entitypatch, null, true);
|
||||||
|
this.isEnd = true;
|
||||||
|
}
|
||||||
|
} else if (this.elapsedTime < 0) {
|
||||||
|
if (currentPlay.isRepeat()) {
|
||||||
|
this.prevElapsedTime = currentPlay.getTotalTime() - this.elapsedTime;
|
||||||
|
this.elapsedTime = currentPlay.getTotalTime() + this.elapsedTime;
|
||||||
|
} else {
|
||||||
|
this.elapsedTime = 0.0F;
|
||||||
|
currentPlay.end(entitypatch, null, true);
|
||||||
|
this.isEnd = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
this.elapsedTime = 0;
|
||||||
|
this.prevElapsedTime = 0;
|
||||||
|
this.isEnd = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlayAnimation(AssetAccessor<? extends DynamicAnimation> animation) {
|
||||||
|
if (this.doNotResetTime) {
|
||||||
|
this.doNotResetTime = false;
|
||||||
|
this.isEnd = false;
|
||||||
|
} else {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.play = animation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pose getCurrentPose(LivingEntityPatch<?> entitypatch, float partialTicks) {
|
||||||
|
return this.play.get().getPoseByTime(entitypatch, this.prevElapsedTime + (this.elapsedTime - this.prevElapsedTime) * partialTicks, partialTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getElapsedTime() {
|
||||||
|
return this.elapsedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getPrevElapsedTime() {
|
||||||
|
return this.prevElapsedTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setElapsedTimeCurrent(float elapsedTime) {
|
||||||
|
this.elapsedTime = elapsedTime;
|
||||||
|
this.isEnd = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setElapsedTime(float elapsedTime) {
|
||||||
|
this.elapsedTime = elapsedTime;
|
||||||
|
this.prevElapsedTime = elapsedTime;
|
||||||
|
this.isEnd = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setElapsedTime(float prevElapsedTime, float elapsedTime) {
|
||||||
|
this.elapsedTime = elapsedTime;
|
||||||
|
this.prevElapsedTime = prevElapsedTime;
|
||||||
|
this.isEnd = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void begin(AssetAccessor<? extends DynamicAnimation> animation, LivingEntityPatch<?> entitypatch) {
|
||||||
|
animation.get().tick(entitypatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetAccessor<? extends DynamicAnimation> getAnimation() {
|
||||||
|
return this.play;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetAccessor<? extends StaticAnimation> getRealAnimation() {
|
||||||
|
return this.play.get().getRealAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markDoNotResetTime() {
|
||||||
|
this.doNotResetTime = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnd() {
|
||||||
|
return this.isEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void terminate(LivingEntityPatch<?> entitypatch) {
|
||||||
|
this.play.get().end(entitypatch, this.play, true);
|
||||||
|
this.isEnd = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReversed() {
|
||||||
|
return this.reversed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReversed(boolean reversed) {
|
||||||
|
this.reversed = reversed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return this.play == TiedUpRigRegistry.EMPTY_ANIMATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.getAnimation() + " " + this.prevElapsedTime + " " + this.elapsedTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RIG stub. Upstream EF : packet commun client/serveur pour sync des
|
||||||
|
* animation variables (variables partagées entre deux sides pendant une
|
||||||
|
* animation combat).
|
||||||
|
*
|
||||||
|
* <p>TiedUp Phase 0 : la classe est conservée en stub juste pour l'enum
|
||||||
|
* {@link Action} utilisé par {@code AnimationVariables.put/remove} et
|
||||||
|
* {@code SynchedAnimationVariableKey.sync}. Le vrai packet réseau n'est
|
||||||
|
* pas implémenté — les `sync()` calls sont no-ops côté runtime pour
|
||||||
|
* l'instant (cf. {@code SynchedAnimationVariableKey.sync}).</p>
|
||||||
|
*
|
||||||
|
* <p>Phase 2+ : si on a besoin de sync d'animation variables entre
|
||||||
|
* serveur et client (cas d'usage non identifié en TiedUp), implémenter
|
||||||
|
* un vrai packet. Sinon garder le stub et stripper {@code sync()} plus
|
||||||
|
* tard.</p>
|
||||||
|
*/
|
||||||
|
public class AnimationVariablePacket {
|
||||||
|
|
||||||
|
public enum Action {
|
||||||
|
PUT,
|
||||||
|
REMOVE,
|
||||||
|
}
|
||||||
|
}
|
||||||
267
src/main/java/com/tiedup/remake/rig/anim/AnimationVariables.java
Normal file
267
src/main/java/com/tiedup/remake/rig/anim/AnimationVariables.java
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.util.ParseUtil;
|
||||||
|
import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap;
|
||||||
|
import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap.TypeKey;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigRegistry;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationVariablePacket;
|
||||||
|
|
||||||
|
public class AnimationVariables {
|
||||||
|
protected final Animator animator;
|
||||||
|
protected final TypeFlexibleHashMap<AnimationVariableKey<?>> animationVariables = new TypeFlexibleHashMap<> (false);
|
||||||
|
|
||||||
|
public AnimationVariables(Animator animator) {
|
||||||
|
this.animator = animator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> Optional<T> getSharedVariable(SharedAnimationVariableKey<T> key) {
|
||||||
|
return Optional.ofNullable(this.animationVariables.get(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T getOrDefaultSharedVariable(SharedAnimationVariableKey<T> key) {
|
||||||
|
return ParseUtil.orElse((T)this.animationVariables.get(key), () -> key.defaultValue(this.animator));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> Optional<T> get(IndependentAnimationVariableKey<T> key, AssetAccessor<? extends StaticAnimation> animation) {
|
||||||
|
if (animation == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<ResourceLocation, Object> subMap = this.animationVariables.get(key);
|
||||||
|
|
||||||
|
if (subMap == null) {
|
||||||
|
return Optional.empty();
|
||||||
|
} else {
|
||||||
|
return Optional.ofNullable((T)subMap.get(animation.registryName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T getOrDefault(IndependentAnimationVariableKey<T> key, AssetAccessor<? extends StaticAnimation> animation) {
|
||||||
|
if (animation == null) {
|
||||||
|
return Objects.requireNonNull(key.defaultValue(this.animator), "Null value returned by default provider.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<ResourceLocation, Object> subMap = this.animationVariables.get(key);
|
||||||
|
|
||||||
|
if (subMap == null) {
|
||||||
|
return Objects.requireNonNull(key.defaultValue(this.animator), "Null value returned by default provider.");
|
||||||
|
} else {
|
||||||
|
return ParseUtil.orElse((T)subMap.get(animation.registryName()), () -> key.defaultValue(this.animator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void putDefaultSharedVariable(SharedAnimationVariableKey<T> key) {
|
||||||
|
T value = key.defaultValue(this.animator);
|
||||||
|
Objects.requireNonNull(value, "Null value returned by default provider.");
|
||||||
|
|
||||||
|
this.putSharedVariable(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void putSharedVariable(SharedAnimationVariableKey<T> key, T value) {
|
||||||
|
this.putSharedVariable(key, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Deprecated // Avoid direct use
|
||||||
|
public <T> void putSharedVariable(SharedAnimationVariableKey<T> key, T value, boolean synchronize) {
|
||||||
|
if (this.animationVariables.containsKey(key) && !key.mutable()) {
|
||||||
|
throw new UnsupportedOperationException("Can't modify a const variable");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animationVariables.put((AnimationVariableKey<?>)key, value);
|
||||||
|
|
||||||
|
if (synchronize && key instanceof SynchedAnimationVariableKey) {
|
||||||
|
SynchedAnimationVariableKey<T> synchedanimationvariablekey = (SynchedAnimationVariableKey<T>)key;
|
||||||
|
synchedanimationvariablekey.sync(this.animator.entitypatch, (AssetAccessor<? extends StaticAnimation>)null, value, AnimationVariablePacket.Action.PUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void putDefaultValue(IndependentAnimationVariableKey<T> key, AssetAccessor<? extends StaticAnimation> animation) {
|
||||||
|
T value = key.defaultValue(this.animator);
|
||||||
|
Objects.requireNonNull(value, "Null value returned by default provider.");
|
||||||
|
|
||||||
|
this.put(key, animation, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void put(IndependentAnimationVariableKey<T> key, AssetAccessor<? extends StaticAnimation> animation, T value) {
|
||||||
|
this.put(key, animation, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Deprecated // Avoid direct use
|
||||||
|
public <T> void put(IndependentAnimationVariableKey<T> key, AssetAccessor<? extends StaticAnimation> animation, T value, boolean synchronize) {
|
||||||
|
if (animation == TiedUpRigRegistry.EMPTY_ANIMATION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animationVariables.computeIfPresent(key, (k, v) -> {
|
||||||
|
Map<ResourceLocation, Object> variablesByAnimations = ((Map<ResourceLocation, Object>)v);
|
||||||
|
|
||||||
|
if (!key.mutable() && variablesByAnimations.containsKey(animation.registryName())) {
|
||||||
|
throw new UnsupportedOperationException("Can't modify a const variable");
|
||||||
|
}
|
||||||
|
|
||||||
|
variablesByAnimations.put(animation.registryName(), value);
|
||||||
|
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.animationVariables.computeIfAbsent(key, (k) -> {
|
||||||
|
return new HashMap<> (Map.of(animation.registryName(), value));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (synchronize && key instanceof SynchedAnimationVariableKey) {
|
||||||
|
SynchedAnimationVariableKey<T> synchedanimationvariablekey = (SynchedAnimationVariableKey<T>)key;
|
||||||
|
synchedanimationvariablekey.sync(this.animator.entitypatch, animation, value, AnimationVariablePacket.Action.PUT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T removeSharedVariable(SharedAnimationVariableKey<T> key) {
|
||||||
|
return this.removeSharedVariable(key, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Deprecated // Avoid direct use
|
||||||
|
public <T> T removeSharedVariable(SharedAnimationVariableKey<T> key, boolean synchronize) {
|
||||||
|
if (!key.mutable()) {
|
||||||
|
throw new UnsupportedOperationException("Can't remove a const variable");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (synchronize && key instanceof SynchedAnimationVariableKey) {
|
||||||
|
SynchedAnimationVariableKey<T> synchedanimationvariablekey = (SynchedAnimationVariableKey<T>)key;
|
||||||
|
synchedanimationvariablekey.sync(this.animator.entitypatch, null, null, AnimationVariablePacket.Action.REMOVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (T)this.animationVariables.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void removeAll(AnimationAccessor<? extends StaticAnimation> animation) {
|
||||||
|
if (animation == TiedUpRigRegistry.EMPTY_ANIMATION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<AnimationVariableKey<?>, Object> entry : this.animationVariables.entrySet()) {
|
||||||
|
if (entry.getKey().isSharedKey()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<ResourceLocation, Object> map = (Map<ResourceLocation, Object>)entry.getValue();
|
||||||
|
|
||||||
|
if (map != null) {
|
||||||
|
map.remove(animation.registryName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(IndependentAnimationVariableKey<?> key, AssetAccessor<? extends StaticAnimation> animation) {
|
||||||
|
this.remove(key, animation, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Deprecated // Avoid direct use
|
||||||
|
public void remove(IndependentAnimationVariableKey<?> key, AssetAccessor<? extends StaticAnimation> animation, boolean synchronize) {
|
||||||
|
if (animation == TiedUpRigRegistry.EMPTY_ANIMATION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<ResourceLocation, Object> map = (Map<ResourceLocation, Object>)this.animationVariables.get(key);
|
||||||
|
|
||||||
|
if (map != null) {
|
||||||
|
map.remove(animation.registryName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (synchronize && key instanceof SynchedAnimationVariableKey) {
|
||||||
|
SynchedAnimationVariableKey<?> synchedanimationvariablekey = (SynchedAnimationVariableKey<?>)key;
|
||||||
|
synchedanimationvariablekey.sync(this.animator.entitypatch, null, null, AnimationVariablePacket.Action.REMOVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> SharedAnimationVariableKey<T> shared(Function<Animator, T> defaultValueSupplier, boolean mutable) {
|
||||||
|
return new SharedAnimationVariableKey<> (defaultValueSupplier, mutable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> IndependentAnimationVariableKey<T> independent(Function<Animator, T> defaultValueSupplier, boolean mutable) {
|
||||||
|
return new IndependentAnimationVariableKey<> (defaultValueSupplier, mutable);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract static class AnimationVariableKey<T> implements TypeKey<T> {
|
||||||
|
protected final Function<Animator, T> defaultValueSupplier;
|
||||||
|
protected final boolean mutable;
|
||||||
|
|
||||||
|
protected AnimationVariableKey(Function<Animator, T> defaultValueSupplier, boolean mutable) {
|
||||||
|
this.defaultValueSupplier = defaultValueSupplier;
|
||||||
|
this.mutable = mutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public T defaultValue(Animator animator) {
|
||||||
|
return this.defaultValueSupplier.apply(animator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean mutable() {
|
||||||
|
return this.mutable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T defaultValue() {
|
||||||
|
throw new UnsupportedOperationException("Use defaultValue(Animator animator) to get default value of animation variable key");
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract boolean isSharedKey();
|
||||||
|
public abstract boolean isSynched();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SharedAnimationVariableKey<T> extends AnimationVariableKey<T> {
|
||||||
|
protected SharedAnimationVariableKey(Function<Animator, T> initValueSupplier, boolean mutable) {
|
||||||
|
super(initValueSupplier, mutable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSharedKey() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSynched() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class IndependentAnimationVariableKey<T> extends AnimationVariableKey<T> {
|
||||||
|
protected IndependentAnimationVariableKey(Function<Animator, T> initValueSupplier, boolean mutable) {
|
||||||
|
super(initValueSupplier, mutable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSharedKey() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSynched() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
148
src/main/java/com/tiedup/remake/rig/anim/Animator.java
Normal file
148
src/main/java/com/tiedup/remake/rig/anim/Animator.java
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
|
||||||
|
import net.minecraftforge.common.MinecraftForge;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.EntityState;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.event.InitAnimatorEvent;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigRegistry;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
public abstract class Animator {
|
||||||
|
protected final Map<LivingMotion, AssetAccessor<? extends StaticAnimation>> livingAnimations = Maps.newHashMap();
|
||||||
|
protected final AnimationVariables animationVariables = new AnimationVariables(this);
|
||||||
|
protected final LivingEntityPatch<?> entitypatch;
|
||||||
|
|
||||||
|
public Animator(LivingEntityPatch<?> entitypatch) {
|
||||||
|
this.entitypatch = entitypatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play an animation
|
||||||
|
*
|
||||||
|
* @param nextAnimation the animation that is meant to be played.
|
||||||
|
* @param transitionTimeModifier extends the transition time if positive value provided, or starts in time as an amount of time (e.g. -0.1F starts in 0.1F frame time)
|
||||||
|
*/
|
||||||
|
public abstract void playAnimation(AssetAccessor<? extends StaticAnimation> nextAnimation, float transitionTimeModifier);
|
||||||
|
|
||||||
|
public final void playAnimation(int id, float transitionTimeModifier) {
|
||||||
|
this.playAnimation(AnimationManager.byId(id), transitionTimeModifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a given animation without transition animation.
|
||||||
|
* @param nextAnimation
|
||||||
|
*/
|
||||||
|
public abstract void playAnimationInstantly(AssetAccessor<? extends StaticAnimation> nextAnimation);
|
||||||
|
|
||||||
|
public final void playAnimationInstantly(int id) {
|
||||||
|
this.playAnimationInstantly(AnimationManager.byId(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reserve a given animation until the current animation ends.
|
||||||
|
* If the given animation has a higher priority than current animation, it terminates the current animation by force and play the next animation
|
||||||
|
* @param nextAnimation
|
||||||
|
*/
|
||||||
|
public abstract void reserveAnimation(AssetAccessor<? extends StaticAnimation> nextAnimation);
|
||||||
|
|
||||||
|
public final void reserveAnimation(int id) {
|
||||||
|
this.reserveAnimation(AnimationManager.byId(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop playing given animation if exist
|
||||||
|
* @param targetAnimation
|
||||||
|
* @return true when found and successfully stop the target animation
|
||||||
|
*/
|
||||||
|
public abstract boolean stopPlaying(AssetAccessor<? extends StaticAnimation> targetAnimation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play an shooting animation to end aiming pose
|
||||||
|
*/
|
||||||
|
public abstract void playShootingAnimation();
|
||||||
|
|
||||||
|
public final boolean stopPlaying(int id) {
|
||||||
|
return this.stopPlaying(AnimationManager.byId(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void setSoftPause(boolean paused);
|
||||||
|
public abstract void setHardPause(boolean paused);
|
||||||
|
public abstract void tick();
|
||||||
|
|
||||||
|
public abstract EntityState getEntityState();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches an animation player playing the given animation parameter or return base layer if it's null
|
||||||
|
* Secure non-null but returned animation player won't match with a given animation
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public abstract AnimationPlayer getPlayerFor(@Nullable AssetAccessor<? extends DynamicAnimation> playingAnimation);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches an animation player playing the given animation parameter
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public abstract Optional<AnimationPlayer> getPlayer(AssetAccessor<? extends DynamicAnimation> playingAnimation);
|
||||||
|
|
||||||
|
public abstract <T> Pair<AnimationPlayer, T> findFor(Class<T> animationType);
|
||||||
|
public abstract Pose getPose(float partialTicks);
|
||||||
|
|
||||||
|
public void postInit() {
|
||||||
|
InitAnimatorEvent initAnimatorEvent = new InitAnimatorEvent(this.entitypatch, this);
|
||||||
|
MinecraftForge.EVENT_BUS.post(initAnimatorEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playDeathAnimation() {
|
||||||
|
// RIG : Animations.BIPED_DEATH (EF combat asset ARR) retiré.
|
||||||
|
// Fallback sur EMPTY_ANIMATION — TiedUp authored ses propres
|
||||||
|
// anims de mort Phase 4 et les binde via addLivingAnimation(DEATH, ...).
|
||||||
|
this.playAnimation(this.livingAnimations.getOrDefault(LivingMotions.DEATH, TiedUpRigRegistry.EMPTY_ANIMATION), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addLivingAnimation(LivingMotion livingMotion, AssetAccessor<? extends StaticAnimation> animation) {
|
||||||
|
if (AnimationManager.checkNull(animation)) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn("Unable to put an empty animation for " + livingMotion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.livingAnimations.put(livingMotion, animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetAccessor<? extends StaticAnimation> getLivingAnimation(LivingMotion livingMotion, AssetAccessor<? extends StaticAnimation> defaultGetter) {
|
||||||
|
return this.livingAnimations.getOrDefault(livingMotion, defaultGetter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<LivingMotion, AssetAccessor<? extends StaticAnimation>> getLivingAnimations() {
|
||||||
|
return ImmutableMap.copyOf(this.livingAnimations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnimationVariables getVariables() {
|
||||||
|
return this.animationVariables;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LivingEntityPatch<?> getEntityPatch() {
|
||||||
|
return this.entitypatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetLivingAnimations() {
|
||||||
|
this.livingAnimations.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Motion {@link LivingMotion} ajoute via datapack, sans code Java.
|
||||||
|
*
|
||||||
|
* <p>Modder path : deposer un fichier JSON dans
|
||||||
|
* {@code data/<ns>/tiedup/living_motions/<name>.json}. Le {@link
|
||||||
|
* LivingMotionReloadListener} le detecte au chargement des datapacks, l'enregistre
|
||||||
|
* dans {@link LivingMotion#ENUM_MANAGER} (meme ordinal pool que les enums Java
|
||||||
|
* builtin {@link LivingMotions} et {@link TiedUpLivingMotions}), et le rend
|
||||||
|
* resolvable via {@link com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemParser}.</p>
|
||||||
|
*
|
||||||
|
* <p>L'id de la motion est derive du path du fichier :
|
||||||
|
* {@code data/mymod/tiedup/living_motions/orgasm_shake.json} devient
|
||||||
|
* {@code mymod:orgasm_shake}. Le {@code toString()} renvoie la ResourceLocation
|
||||||
|
* complete — {@link com.tiedup.remake.rig.util.ExtendableEnumManager#assign}
|
||||||
|
* utilise {@code toString()} lowercased comme cle unique, donc deux motions
|
||||||
|
* de namespace differents coexistent sans collision (p.ex.
|
||||||
|
* {@code mymod:orgasm_shake} vs {@code tiedup:orgasm_shake}).</p>
|
||||||
|
*
|
||||||
|
* <h2>Choix de design : classe, pas record</h2>
|
||||||
|
* <p>Le {@code universalOrdinal()} doit etre assigne par
|
||||||
|
* {@link com.tiedup.remake.rig.util.ExtendableEnumManager#assign} APRES
|
||||||
|
* construction de l'instance (l'{@code ExtendableEnumManager} prend le
|
||||||
|
* {@link LivingMotion} en parametre, lit sa cle via {@code toString()}, et
|
||||||
|
* retourne l'ordinal a posteriori). Un {@code record} Java a tous ses champs
|
||||||
|
* immuables — il faudrait donc construire deux instances (placeholder +
|
||||||
|
* final), ce qui laisse un {@code placeholder.universalOrdinal() == -1} stocke
|
||||||
|
* dans les maps internes du {@code ExtendableEnumManager}. Une classe
|
||||||
|
* mutable-at-first-call (pattern identique a {@link LivingMotions#id}) evite
|
||||||
|
* ce double-hop et garantit que l'instance stockee dans
|
||||||
|
* {@code ENUM_MANAGER.enumMapByName} porte le bon ordinal.</p>
|
||||||
|
*/
|
||||||
|
public final class DataDrivenLivingMotion implements LivingMotion {
|
||||||
|
|
||||||
|
private final ResourceLocation id;
|
||||||
|
private final String description;
|
||||||
|
@Nullable
|
||||||
|
private final String category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ordinal attribue par {@link LivingMotion#ENUM_MANAGER}. Non-final : set
|
||||||
|
* une seule fois par {@link LivingMotionReloadListener} juste apres
|
||||||
|
* {@code assign()}. {@code volatile} garantit visibilite cross-thread si
|
||||||
|
* un client lit {@code universalOrdinal()} pendant que le server thread
|
||||||
|
* est en train de finir l'assignation (peu probable en pratique — assign
|
||||||
|
* est sous {@code synchronized}).
|
||||||
|
*/
|
||||||
|
private volatile int ordinal = -1;
|
||||||
|
|
||||||
|
public DataDrivenLivingMotion(
|
||||||
|
ResourceLocation id,
|
||||||
|
String description,
|
||||||
|
@Nullable String category
|
||||||
|
) {
|
||||||
|
this.id = Objects.requireNonNull(id, "id");
|
||||||
|
this.description = Objects.requireNonNull(description, "description");
|
||||||
|
this.category = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appele exactement une fois par {@link LivingMotionReloadListener} pour
|
||||||
|
* poser l'ordinal retourne par {@code ExtendableEnumManager.assign}.
|
||||||
|
*
|
||||||
|
* @param ordinal ordinal >= 0
|
||||||
|
* @throws IllegalStateException si deja assigne
|
||||||
|
*/
|
||||||
|
void setOrdinal(int ordinal) {
|
||||||
|
if (this.ordinal != -1) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"DataDrivenLivingMotion " + this.id + " ordinal already set to "
|
||||||
|
+ this.ordinal + " (tried to set " + ordinal + ")"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (ordinal < 0) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Negative ordinal " + ordinal + " for " + this.id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.ordinal = ordinal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceLocation id() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String description() {
|
||||||
|
return this.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String category() {
|
||||||
|
return this.category;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int universalOrdinal() {
|
||||||
|
return this.ordinal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* La cle de dedup dans {@link com.tiedup.remake.rig.util.ExtendableEnumManager}
|
||||||
|
* est {@code toString().toLowerCase()}. On expose la RL complete pour garantir
|
||||||
|
* l'unicite cross-namespace ({@code mymod:foo} != {@code tiedup:foo}).
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof DataDrivenLivingMotion other)) return false;
|
||||||
|
return this.id.equals(other.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.id.hashCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/main/java/com/tiedup/remake/rig/anim/Keyframe.java
Normal file
49
src/main/java/com/tiedup/remake/rig/anim/Keyframe.java
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
|
||||||
|
public class Keyframe {
|
||||||
|
private float timeStamp;
|
||||||
|
private final JointTransform transform;
|
||||||
|
|
||||||
|
public Keyframe(float timeStamp, JointTransform trasnform) {
|
||||||
|
this.timeStamp = timeStamp;
|
||||||
|
this.transform = trasnform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Keyframe(Keyframe original) {
|
||||||
|
this.transform = JointTransform.empty();
|
||||||
|
this.copyFrom(original);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copyFrom(Keyframe target) {
|
||||||
|
this.timeStamp = target.timeStamp;
|
||||||
|
this.transform.copyFrom(target.transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float time() {
|
||||||
|
return this.timeStamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTime(float time) {
|
||||||
|
this.timeStamp = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointTransform transform() {
|
||||||
|
return this.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return "Keyframe[Time: " + this.timeStamp + ", " + (this.transform == null ? "null" : this.transform.toString()) + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Keyframe empty() {
|
||||||
|
return new Keyframe(0.0F, JointTransform.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main/java/com/tiedup/remake/rig/anim/LivingMotion.java
Normal file
24
src/main/java/com/tiedup/remake/rig/anim/LivingMotion.java
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.util.ExtendableEnum;
|
||||||
|
import com.tiedup.remake.rig.util.ExtendableEnumManager;
|
||||||
|
|
||||||
|
public interface LivingMotion extends ExtendableEnum {
|
||||||
|
ExtendableEnumManager<LivingMotion> ENUM_MANAGER = new ExtendableEnumManager<> ("living_motion");
|
||||||
|
|
||||||
|
default boolean isSame(LivingMotion livingMotion) {
|
||||||
|
if (this == LivingMotions.IDLE && livingMotion == LivingMotions.INACTION) {
|
||||||
|
return true;
|
||||||
|
} else if (this == LivingMotions.INACTION && livingMotion == LivingMotions.IDLE) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this == livingMotion;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.packs.resources.ResourceManager;
|
||||||
|
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
|
||||||
|
import net.minecraft.util.profiling.ProfilerFiller;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scanne les fichiers JSON {@code data/<ns>/tiedup/living_motions/*.json} et
|
||||||
|
* enregistre chacun en tant que {@link DataDrivenLivingMotion} dans le
|
||||||
|
* registre partage {@link LivingMotion#ENUM_MANAGER}.
|
||||||
|
*
|
||||||
|
* <h2>But</h2>
|
||||||
|
* <p>Permettre a un modder/resourcepack-maker d'ajouter de nouvelles
|
||||||
|
* {@link LivingMotion} (ex. {@code mymod:orgasm_shake}) sans coder un enum
|
||||||
|
* Java + sans appel explicite a {@code LivingMotion.ENUM_MANAGER.registerEnumCls}.
|
||||||
|
* Workflow 100% data-driven.</p>
|
||||||
|
*
|
||||||
|
* <h2>Format JSON attendu</h2>
|
||||||
|
* <pre>{@code
|
||||||
|
* {
|
||||||
|
* "description": "Orgasm shake shiver anim — fired on VX state",
|
||||||
|
* "category": "vx_reactions"
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
* <p>Le champ {@code description} est obligatoire (humain + logs). Le champ
|
||||||
|
* {@code category} est optionnel et sert au regroupement editorial uniquement
|
||||||
|
* (ex. {@code locomotion}, {@code vx_reactions}, {@code restraint}...).</p>
|
||||||
|
*
|
||||||
|
* <h2>Ordinal stability cross-reload</h2>
|
||||||
|
* <p>Le {@link com.tiedup.remake.rig.util.ExtendableEnumManager#assign} refuse
|
||||||
|
* d'enregistrer deux fois la meme cle (throw {@link IllegalArgumentException}).
|
||||||
|
* On garde donc une vue persistante {@link #PERSISTENT_REGISTRY} : au premier
|
||||||
|
* load d'un id, l'ordinal est attribue et le {@link DataDrivenLivingMotion}
|
||||||
|
* est cache ; les reloads ulterieurs re-utilisent la meme instance (et donc
|
||||||
|
* le meme ordinal). Le cache survit aux {@code /reload} (static + JVM lifetime),
|
||||||
|
* mais PAS aux restart de serveur — voir section "Limitations".</p>
|
||||||
|
*
|
||||||
|
* <h2>Limitations connues</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Les ordinals sont stables pendant une session JVM mais re-attribues
|
||||||
|
* apres restart — l'ordre de decouverte des fichiers JSON (alpha-sorted
|
||||||
|
* par ResourceLocation) determine l'ordinal initial. Si un modder
|
||||||
|
* serialise l'ordinal (ex. network packet ou NBT), la reference cassera
|
||||||
|
* apres restart si un nouveau motion est ajoute avant dans l'ordre de
|
||||||
|
* scan. En pratique, tous les consumers internes TiedUp! referencent
|
||||||
|
* les motions par {@link ResourceLocation}, pas par ordinal — le
|
||||||
|
* probleme ne se manifeste que si un mod tiers persiste l'ordinal.</li>
|
||||||
|
* <li>Un JSON mal forme (pas de {@code description} ou type invalide) est
|
||||||
|
* skip avec un WARN ; le reste du batch continue.</li>
|
||||||
|
* <li>Un meme id re-charge avec une description differente emet un WARN
|
||||||
|
* mais garde la PREMIERE description en memoire (immuable). La nouvelle
|
||||||
|
* description apparait au prochain restart JVM.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Side & threading</h2>
|
||||||
|
* <p>{@link SimpleJsonResourceReloadListener#apply} s'execute cote serveur a
|
||||||
|
* chaque {@code /reload} (+ worldload), et cote client au resource reload
|
||||||
|
* (F3+T). Les deux sides partagent le meme {@link LivingMotion#ENUM_MANAGER}
|
||||||
|
* (static JVM-wide) — sur serveur integre, les sides pointent vers le meme
|
||||||
|
* registre ; pas de double comptabilite. {@code synchronized} sur
|
||||||
|
* {@code ExtendableEnumManager.assign} absorbe le risque theorique de race
|
||||||
|
* entre le thread de reload serveur et le thread client.</p>
|
||||||
|
*/
|
||||||
|
public class LivingMotionReloadListener extends SimpleJsonResourceReloadListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache JVM-wide des motions data-driven : une entree = un ordinal
|
||||||
|
* definitivement reserve. Le mutex {@link LivingMotion#ENUM_MANAGER} (via
|
||||||
|
* {@code synchronized} sur {@code assign}) protege les inserts ; cette
|
||||||
|
* {@link ConcurrentHashMap} supporte les {@code get} concurrents sans
|
||||||
|
* bloquer.
|
||||||
|
*/
|
||||||
|
private static final Map<ResourceLocation, DataDrivenLivingMotion> PERSISTENT_REGISTRY =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache des descriptions pour permettre un WARN quand un reload change la
|
||||||
|
* description d'un motion existant (l'instance en memoire ne peut pas etre
|
||||||
|
* mise a jour — ordinal deja consomme et enregistre dans le ENUM_MANAGER).
|
||||||
|
*/
|
||||||
|
private static final Map<ResourceLocation, String> LAST_SEEN_DESCRIPTIONS =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/** Dossier scanne : {@code data/<ns>/tiedup/living_motions/*.json}. */
|
||||||
|
public static final String DIRECTORY = "tiedup/living_motions";
|
||||||
|
|
||||||
|
public LivingMotionReloadListener() {
|
||||||
|
super(new GsonBuilder().create(), DIRECTORY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resout une motion data-driven par sa ResourceLocation.
|
||||||
|
*
|
||||||
|
* @param id identifiant namespace:path (ex. {@code mymod:orgasm_shake})
|
||||||
|
* @return la motion enregistree, ou {@code null} si jamais vue
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static DataDrivenLivingMotion get(ResourceLocation id) {
|
||||||
|
return PERSISTENT_REGISTRY.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nombre de motions data-driven actuellement connues. */
|
||||||
|
public static int size() {
|
||||||
|
return PERSISTENT_REGISTRY.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vue immutable du registre, exposee pour debug / tests.
|
||||||
|
*/
|
||||||
|
public static Map<ResourceLocation, DataDrivenLivingMotion> view() {
|
||||||
|
return Collections.unmodifiableMap(PERSISTENT_REGISTRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test hook — vide le registre data-driven. NE vide PAS le
|
||||||
|
* {@link LivingMotion#ENUM_MANAGER} sous-jacent (qui ne le supporte pas
|
||||||
|
* nativement), donc a n'utiliser que dans des tests isoles ou le reset
|
||||||
|
* d'ordinal ne cause pas de collision avec les enum builtin.
|
||||||
|
*
|
||||||
|
* <p>En prod, le registre ne se vide jamais — c'est intentionnel
|
||||||
|
* (preservation des ordinals pendant la session JVM).</p>
|
||||||
|
*/
|
||||||
|
static void clearForTests() {
|
||||||
|
PERSISTENT_REGISTRY.clear();
|
||||||
|
LAST_SEEN_DESCRIPTIONS.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void apply(
|
||||||
|
Map<ResourceLocation, JsonElement> objectIn,
|
||||||
|
ResourceManager resourceManager,
|
||||||
|
ProfilerFiller profileFiller
|
||||||
|
) {
|
||||||
|
// Ordre alphabetique stable : si deux JSON sont vus pour la premiere
|
||||||
|
// fois au meme reload, l'ordre de ResourceLocation.compareTo
|
||||||
|
// determine l'ordre d'assignation. Reproductible entre deux boot JVM
|
||||||
|
// avec le meme ensemble de fichiers (evite les ordinals qui dansent).
|
||||||
|
Map<ResourceLocation, JsonElement> sorted = new TreeMap<>(objectIn);
|
||||||
|
|
||||||
|
int added = 0;
|
||||||
|
int reloaded = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
|
||||||
|
for (Map.Entry<ResourceLocation, JsonElement> entry : sorted.entrySet()) {
|
||||||
|
ResourceLocation id = entry.getKey();
|
||||||
|
JsonElement element = entry.getValue();
|
||||||
|
|
||||||
|
if (!element.isJsonObject()) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"[LivingMotionReloadListener] Skipping {} : top-level JSON is not an object",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject obj = element.getAsJsonObject();
|
||||||
|
String description = readStringOrNull(obj, "description");
|
||||||
|
if (description == null) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"[LivingMotionReloadListener] Skipping {} : missing or invalid 'description'",
|
||||||
|
id
|
||||||
|
);
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String category = readStringOrNull(obj, "category");
|
||||||
|
|
||||||
|
// Deja connu ? Reutilise l'instance — ordinal stable, pas de
|
||||||
|
// double-assign (qui throw IAE dans ExtendableEnumManager).
|
||||||
|
DataDrivenLivingMotion existing = PERSISTENT_REGISTRY.get(id);
|
||||||
|
if (existing != null) {
|
||||||
|
String lastDesc = LAST_SEEN_DESCRIPTIONS.get(id);
|
||||||
|
if (!Objects.equals(lastDesc, description)) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"[LivingMotionReloadListener] Motion {} reloaded with a different description "
|
||||||
|
+ "(was '{}', now '{}') — ordinal remains {}, new description takes effect "
|
||||||
|
+ "at next JVM restart",
|
||||||
|
id, lastDesc, description, existing.universalOrdinal()
|
||||||
|
);
|
||||||
|
LAST_SEEN_DESCRIPTIONS.put(id, description);
|
||||||
|
}
|
||||||
|
reloaded++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nouveau motion : construction -> assign() -> pose ordinal.
|
||||||
|
// Le {@code DataDrivenLivingMotion} est construit avec ordinal=-1,
|
||||||
|
// puis {@code ExtendableEnumManager.assign} l'indexe par son
|
||||||
|
// {@code toString()} (la RL full) et retourne l'ordinal concret.
|
||||||
|
// On pose ensuite l'ordinal sur l'instance via {@code setOrdinal}.
|
||||||
|
// L'instance stockee dans les maps internes du ENUM_MANAGER EST
|
||||||
|
// la meme reference que celle dans PERSISTENT_REGISTRY — le set
|
||||||
|
// est donc visible a travers tous les lookups.
|
||||||
|
DataDrivenLivingMotion motion =
|
||||||
|
new DataDrivenLivingMotion(id, description, category);
|
||||||
|
int assignedOrdinal;
|
||||||
|
try {
|
||||||
|
assignedOrdinal = LivingMotion.ENUM_MANAGER.assign(motion);
|
||||||
|
} catch (IllegalArgumentException dup) {
|
||||||
|
// Le ENUM_MANAGER contient deja un motion avec cette cle
|
||||||
|
// (cas limite : conflit avec un enum builtin qui reserverait
|
||||||
|
// deliberement le meme toString(), p.ex. un modder qui
|
||||||
|
// appelle {@code mymod:orgasm_shake} un enum Java ET un JSON).
|
||||||
|
TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"[LivingMotionReloadListener] Skipping {} : name collision in ENUM_MANAGER ({})",
|
||||||
|
id, dup.getMessage()
|
||||||
|
);
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
motion.setOrdinal(assignedOrdinal);
|
||||||
|
PERSISTENT_REGISTRY.put(id, motion);
|
||||||
|
LAST_SEEN_DESCRIPTIONS.put(id, description);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
|
||||||
|
TiedUpRigConstants.LOGGER.info(
|
||||||
|
"[LivingMotionReloadListener] Reload done : {} new motion(s) registered, "
|
||||||
|
+ "{} motion(s) reloaded (ordinal preserved), {} skipped",
|
||||||
|
added, reloaded, skipped
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static String readStringOrNull(JsonObject obj, String key) {
|
||||||
|
if (!obj.has(key) || obj.get(key).isJsonNull()) return null;
|
||||||
|
JsonElement elem = obj.get(key);
|
||||||
|
if (!elem.isJsonPrimitive() || !elem.getAsJsonPrimitive().isString()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return elem.getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook d'apply direct pour tests — {@link SimpleJsonResourceReloadListener#apply}
|
||||||
|
* est {@code protected}, ce helper l'expose en public pour que les tests
|
||||||
|
* cross-package ({@code DataDrivenItemParserAnimationsTest}) puissent
|
||||||
|
* alimenter le registry sans bootstrap MC.
|
||||||
|
*
|
||||||
|
* <p>Le {@code ResourceManager} et le {@code ProfilerFiller} ne sont
|
||||||
|
* pas lus par notre {@code apply}, on peut passer {@code null} en test.</p>
|
||||||
|
*/
|
||||||
|
public void applyForTests(Map<ResourceLocation, JsonElement> data) {
|
||||||
|
this.apply(data, null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/com/tiedup/remake/rig/anim/LivingMotions.java
Normal file
23
src/main/java/com/tiedup/remake/rig/anim/LivingMotions.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
public enum LivingMotions implements LivingMotion {
|
||||||
|
ALL, // Datapack edit option
|
||||||
|
INACTION, IDLE, CONFRONT, ANGRY, FLOAT, WALK, RUN, SWIM, FLY, SNEAK, KNEEL, FALL, SIT, MOUNT, DEATH, CHASE, SPELLCAST, JUMP, CELEBRATE, LANDING_RECOVERY, CREATIVE_FLY, CREATIVE_IDLE, SLEEP, // Base
|
||||||
|
DIGGING, ADMIRE, CLIMB, DRINK, EAT, NONE, AIM, BLOCK, BLOCK_SHIELD, RELOAD, SHOT, SPECTATE; // Mix
|
||||||
|
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
LivingMotions() {
|
||||||
|
this.id = LivingMotion.ENUM_MANAGER.assign(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int universalOrdinal() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
115
src/main/java/com/tiedup/remake/rig/anim/Pose.java
Normal file
115
src/main/java/com/tiedup/remake/rig/anim/Pose.java
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
import com.google.common.base.Predicate;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
|
||||||
|
public class Pose {
|
||||||
|
public static final Pose EMPTY_POSE = new Pose();
|
||||||
|
|
||||||
|
public static Pose interpolatePose(Pose pose1, Pose pose2, float pregression) {
|
||||||
|
Pose pose = new Pose();
|
||||||
|
|
||||||
|
Set<String> mergedSet = new HashSet<>(pose1.jointTransformData.keySet());
|
||||||
|
mergedSet.addAll(pose2.jointTransformData.keySet());
|
||||||
|
|
||||||
|
for (String jointName : mergedSet) {
|
||||||
|
pose.putJointData(jointName, JointTransform.interpolate(pose1.orElseEmpty(jointName), pose2.orElseEmpty(jointName), pregression));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pose;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final Map<String, JointTransform> jointTransformData;
|
||||||
|
|
||||||
|
public Pose() {
|
||||||
|
this(Maps.newHashMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pose(Map<String, JointTransform> jointTransforms) {
|
||||||
|
this.jointTransformData = jointTransforms;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putJointData(String name, JointTransform transform) {
|
||||||
|
this.jointTransformData.put(name, transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, JointTransform> getJointTransformData() {
|
||||||
|
return this.jointTransformData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disableJoint(Predicate<? super Map.Entry<String, JointTransform>> predicate) {
|
||||||
|
this.jointTransformData.entrySet().removeIf(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disableAllJoints() {
|
||||||
|
this.jointTransformData.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasTransform(String jointName) {
|
||||||
|
return this.jointTransformData.containsKey(jointName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointTransform get(String jointName) {
|
||||||
|
return this.jointTransformData.get(jointName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointTransform orElseEmpty(String jointName) {
|
||||||
|
return this.jointTransformData.getOrDefault(jointName, JointTransform.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointTransform orElse(String jointName, JointTransform orElse) {
|
||||||
|
return this.jointTransformData.getOrDefault(jointName, orElse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forEachEnabledTransforms(BiConsumer<String, JointTransform> task) {
|
||||||
|
this.jointTransformData.forEach(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void load(Pose pose, LoadOperation operation) {
|
||||||
|
switch (operation) {
|
||||||
|
case SET -> {
|
||||||
|
this.disableAllJoints();
|
||||||
|
pose.forEachEnabledTransforms(this::putJointData);
|
||||||
|
}
|
||||||
|
case OVERWRITE -> {
|
||||||
|
pose.forEachEnabledTransforms(this::putJointData);
|
||||||
|
}
|
||||||
|
case APPEND_ABSENT -> {
|
||||||
|
pose.forEachEnabledTransforms((name, transform) -> {
|
||||||
|
if (!this.hasTransform(name)) {
|
||||||
|
this.putJointData(name, transform);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Pose: ");
|
||||||
|
|
||||||
|
for (Map.Entry<String, JointTransform> entry : this.jointTransformData.entrySet()) {
|
||||||
|
sb.append(String.format("%s{%s, %s}, ", entry.getKey(), entry.getValue().translation().toString(), entry.getValue().rotation().toString()) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LoadOperation {
|
||||||
|
SET, OVERWRITE, APPEND_ABSENT
|
||||||
|
}
|
||||||
|
}
|
||||||
160
src/main/java/com/tiedup/remake/rig/anim/ServerAnimator.java
Normal file
160
src/main/java/com/tiedup/remake/rig/anim/ServerAnimator.java
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.EntityState;
|
||||||
|
import com.tiedup.remake.rig.anim.types.LinkAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigRegistry;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
public class ServerAnimator extends Animator {
|
||||||
|
public static Animator getAnimator(LivingEntityPatch<?> entitypatch) {
|
||||||
|
return new ServerAnimator(entitypatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final LinkAnimation linkAnimation;
|
||||||
|
public final AnimationPlayer animationPlayer;
|
||||||
|
|
||||||
|
protected AssetAccessor<? extends DynamicAnimation> nextAnimation;
|
||||||
|
public boolean hardPaused = false;
|
||||||
|
public boolean softPaused = false;
|
||||||
|
|
||||||
|
public ServerAnimator(LivingEntityPatch<?> entitypatch) {
|
||||||
|
super(entitypatch);
|
||||||
|
|
||||||
|
this.linkAnimation = new LinkAnimation();
|
||||||
|
this.animationPlayer = new AnimationPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Play an animation by animation instance **/
|
||||||
|
@Override
|
||||||
|
public void playAnimation(AssetAccessor<? extends StaticAnimation> nextAnimation, float transitionTimeModifier) {
|
||||||
|
this.softPaused = false;
|
||||||
|
Pose lastPose = this.animationPlayer.getAnimation().get().getPoseByTime(this.entitypatch, 0.0F, 0.0F);
|
||||||
|
|
||||||
|
if (!this.animationPlayer.isEnd()) {
|
||||||
|
this.animationPlayer.getAnimation().get().end(this.entitypatch, nextAnimation, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextAnimation.get().begin(this.entitypatch);
|
||||||
|
|
||||||
|
if (!nextAnimation.get().isMetaAnimation()) {
|
||||||
|
nextAnimation.get().setLinkAnimation(this.animationPlayer.getAnimation(), lastPose, true, transitionTimeModifier, this.entitypatch, this.linkAnimation);
|
||||||
|
this.linkAnimation.getAnimationClip().setBaked();
|
||||||
|
this.linkAnimation.putOnPlayer(this.animationPlayer, this.entitypatch);
|
||||||
|
this.entitypatch.updateEntityState();
|
||||||
|
this.nextAnimation = nextAnimation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playAnimationInstantly(AssetAccessor<? extends StaticAnimation> nextAnimation) {
|
||||||
|
this.softPaused = false;
|
||||||
|
|
||||||
|
if (!this.animationPlayer.isEnd()) {
|
||||||
|
this.animationPlayer.getAnimation().get().end(this.entitypatch, nextAnimation, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextAnimation.get().begin(this.entitypatch);
|
||||||
|
nextAnimation.get().putOnPlayer(this.animationPlayer, this.entitypatch);
|
||||||
|
this.entitypatch.updateEntityState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reserveAnimation(AssetAccessor<? extends StaticAnimation> nextAnimation) {
|
||||||
|
this.softPaused = false;
|
||||||
|
this.nextAnimation = nextAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean stopPlaying(AssetAccessor<? extends StaticAnimation> targetAnimation) {
|
||||||
|
if (this.animationPlayer.getRealAnimation() == targetAnimation) {
|
||||||
|
this.animationPlayer.terminate(this.entitypatch);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playShootingAnimation() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick() {
|
||||||
|
if (this.hardPaused || this.softPaused) {
|
||||||
|
this.entitypatch.updateEntityState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animationPlayer.tick(this.entitypatch);
|
||||||
|
this.entitypatch.updateEntityState();
|
||||||
|
|
||||||
|
if (this.animationPlayer.isEnd()) {
|
||||||
|
if (this.nextAnimation == null) {
|
||||||
|
TiedUpRigRegistry.EMPTY_ANIMATION.putOnPlayer(this.animationPlayer, this.entitypatch);
|
||||||
|
this.softPaused = true;
|
||||||
|
} else {
|
||||||
|
if (!this.animationPlayer.getAnimation().get().isLinkAnimation() && !this.nextAnimation.get().isLinkAnimation()) {
|
||||||
|
this.nextAnimation.get().begin(this.entitypatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nextAnimation.get().putOnPlayer(this.animationPlayer, this.entitypatch);
|
||||||
|
this.nextAnimation = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.animationPlayer.getAnimation().get().tick(this.entitypatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pose getPose(float partialTicks) {
|
||||||
|
return this.animationPlayer.getCurrentPose(this.entitypatch, partialTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimationPlayer getPlayerFor(AssetAccessor<? extends DynamicAnimation> playingAnimation) {
|
||||||
|
return this.animationPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<AnimationPlayer> getPlayer(AssetAccessor<? extends DynamicAnimation> playingAnimation) {
|
||||||
|
if (this.animationPlayer.getRealAnimation() == playingAnimation.get().getRealAnimation()) {
|
||||||
|
return Optional.of(this.animationPlayer);
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> Pair<AnimationPlayer, T> findFor(Class<T> animationType) {
|
||||||
|
return animationType.isAssignableFrom(this.animationPlayer.getAnimation().getClass()) ? Pair.of(this.animationPlayer, (T)this.animationPlayer.getAnimation()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EntityState getEntityState() {
|
||||||
|
return this.animationPlayer.getAnimation().get().getState(this.entitypatch, this.animationPlayer.getElapsedTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSoftPause(boolean paused) {
|
||||||
|
this.softPaused = paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHardPause(boolean paused) {
|
||||||
|
this.hardPaused = paused;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import net.minecraft.core.IdMapper;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraftforge.registries.IForgeRegistry;
|
||||||
|
import net.minecraftforge.registries.IForgeRegistryInternal;
|
||||||
|
import net.minecraftforge.registries.RegistryManager;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationVariables.IndependentAnimationVariableKey;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationVariables.SharedAnimationVariableKey;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.util.PacketBufferCodec;
|
||||||
|
import com.tiedup.remake.rig.util.datastruct.ClearableIdMapper;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationVariablePacket;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationVariablePacket;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationVariablePacket;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
public interface SynchedAnimationVariableKey<T> {
|
||||||
|
public static <T> SynchedSharedAnimationVariableKey<T> shared(Function<Animator, T> defaultValueSupplier, boolean mutable, PacketBufferCodec<T> codec) {
|
||||||
|
return new SynchedSharedAnimationVariableKey<> (defaultValueSupplier, mutable, codec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> SynchedIndependentAnimationVariableKey<T> independent(Function<Animator, T> defaultValueSupplier, boolean mutable, PacketBufferCodec<T> codec) {
|
||||||
|
return new SynchedIndependentAnimationVariableKey<> (defaultValueSupplier, mutable, codec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final ResourceLocation BY_ID_REGISTRY = TiedUpRigConstants.identifier("variablekeytoid");
|
||||||
|
|
||||||
|
public static class SynchedAnimationVariableKeyCallbacks implements IForgeRegistry.BakeCallback<SynchedAnimationVariableKey<?>>, IForgeRegistry.CreateCallback<SynchedAnimationVariableKey<?>>, IForgeRegistry.ClearCallback<SynchedAnimationVariableKey<?>> {
|
||||||
|
private static final SynchedAnimationVariableKeyCallbacks INSTANCE = new SynchedAnimationVariableKeyCallbacks();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void onBake(IForgeRegistryInternal<SynchedAnimationVariableKey<?>> owner, RegistryManager stage) {
|
||||||
|
final ClearableIdMapper<SynchedAnimationVariableKey<?>> synchedanimationvariablekeybyid = owner.getSlaveMap(BY_ID_REGISTRY, ClearableIdMapper.class);
|
||||||
|
owner.forEach(synchedanimationvariablekeybyid::add);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(IForgeRegistryInternal<SynchedAnimationVariableKey<?>> owner, RegistryManager stage) {
|
||||||
|
owner.setSlaveMap(BY_ID_REGISTRY, new ClearableIdMapper<SynchedAnimationVariableKey<?>> (owner.getKeys().size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClear(IForgeRegistryInternal<SynchedAnimationVariableKey<?>> owner, RegistryManager stage) {
|
||||||
|
owner.getSlaveMap(BY_ID_REGISTRY, ClearableIdMapper.class).clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SynchedAnimationVariableKeyCallbacks getRegistryCallback() {
|
||||||
|
return SynchedAnimationVariableKeyCallbacks.INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static IdMapper<SynchedAnimationVariableKey<?>> getIdMap() {
|
||||||
|
return SynchedAnimationVariableKeys.REGISTRY.get().getSlaveMap(BY_ID_REGISTRY, IdMapper.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> SynchedAnimationVariableKey<T> byId(int id) {
|
||||||
|
return (SynchedAnimationVariableKey<T>)getIdMap().byId(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PacketBufferCodec<T> getPacketBufferCodec();
|
||||||
|
|
||||||
|
public boolean isSharedKey();
|
||||||
|
|
||||||
|
default int getId() {
|
||||||
|
return getIdMap().getId(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
default void sync(LivingEntityPatch<?> entitypatch, @Nullable AssetAccessor<? extends StaticAnimation> animation, T value, AnimationVariablePacket.Action action) {
|
||||||
|
// RIG : sync réseau des animation variables strippé.
|
||||||
|
// Pas d'usage bondage identifié — ré-implémenter Phase 2 avec packet
|
||||||
|
// dédié si besoin. Voir AnimationVariablePacket stub.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SynchedSharedAnimationVariableKey<T> extends SharedAnimationVariableKey<T> implements SynchedAnimationVariableKey<T> {
|
||||||
|
private final PacketBufferCodec<T> packetBufferCodec;
|
||||||
|
|
||||||
|
protected SynchedSharedAnimationVariableKey(Function<Animator, T> defaultValueSupplier, boolean mutable, PacketBufferCodec<T> packetBufferCodec) {
|
||||||
|
super(defaultValueSupplier, mutable);
|
||||||
|
this.packetBufferCodec = packetBufferCodec;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSynched() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PacketBufferCodec<T> getPacketBufferCodec() {
|
||||||
|
return this.packetBufferCodec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SynchedIndependentAnimationVariableKey<T> extends IndependentAnimationVariableKey<T> implements SynchedAnimationVariableKey<T> {
|
||||||
|
private final PacketBufferCodec<T> packetBufferCodec;
|
||||||
|
|
||||||
|
protected SynchedIndependentAnimationVariableKey(Function<Animator, T> defaultValueSupplier, boolean mutable, PacketBufferCodec<T> packetBufferCodec) {
|
||||||
|
super(defaultValueSupplier, mutable);
|
||||||
|
this.packetBufferCodec = packetBufferCodec;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSharedKey() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PacketBufferCodec<T> getPacketBufferCodec() {
|
||||||
|
return this.packetBufferCodec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import net.minecraftforge.registries.DeferredRegister;
|
||||||
|
import net.minecraftforge.registries.IForgeRegistry;
|
||||||
|
import net.minecraftforge.registries.RegistryBuilder;
|
||||||
|
import net.minecraftforge.registries.RegistryObject;
|
||||||
|
import com.tiedup.remake.rig.anim.SynchedAnimationVariableKey.SynchedIndependentAnimationVariableKey;
|
||||||
|
import com.tiedup.remake.rig.util.PacketBufferCodec;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
|
||||||
|
public class SynchedAnimationVariableKeys {
|
||||||
|
private static final Supplier<RegistryBuilder<SynchedAnimationVariableKey<?>>> BUILDER = () -> new RegistryBuilder<SynchedAnimationVariableKey<?>>().addCallback(SynchedAnimationVariableKey.getRegistryCallback());
|
||||||
|
|
||||||
|
public static final DeferredRegister<SynchedAnimationVariableKey<?>> SYNCHED_ANIMATION_VARIABLE_KEYS = DeferredRegister.create(TiedUpRigConstants.identifier("synched_animation_variable_keys"), TiedUpRigConstants.MODID);
|
||||||
|
public static final Supplier<IForgeRegistry<SynchedAnimationVariableKey<?>>> REGISTRY = SYNCHED_ANIMATION_VARIABLE_KEYS.makeRegistry(BUILDER);
|
||||||
|
|
||||||
|
public static final RegistryObject<SynchedIndependentAnimationVariableKey<Vec3>> DESTINATION = SYNCHED_ANIMATION_VARIABLE_KEYS.register("destination", () ->
|
||||||
|
SynchedAnimationVariableKey.independent(animator -> animator.getEntityPatch().getOriginal().position(), true, PacketBufferCodec.VEC3));
|
||||||
|
|
||||||
|
public static final RegistryObject<SynchedIndependentAnimationVariableKey<Integer>> TARGET_ENTITY = SYNCHED_ANIMATION_VARIABLE_KEYS.register("target_entity", () ->
|
||||||
|
SynchedAnimationVariableKey.independent(animator -> (Integer)null, true, PacketBufferCodec.INTEGER));
|
||||||
|
|
||||||
|
public static final RegistryObject<SynchedIndependentAnimationVariableKey<Integer>> CHARGING_TICKS = SYNCHED_ANIMATION_VARIABLE_KEYS.register("animation_playing_speed", () ->
|
||||||
|
SynchedAnimationVariableKey.independent(animator -> 0, true, PacketBufferCodec.INTEGER));
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Motions custom TiedUp! — extension de {@link LivingMotions} (motions vanilla EF).
|
||||||
|
*
|
||||||
|
* Chaque valeur partage le meme {@link LivingMotion#ENUM_MANAGER} que
|
||||||
|
* {@link LivingMotions} : les universalOrdinal() sont assignes a la suite, sans
|
||||||
|
* collision, a condition que les deux enums soient class-loaded avant usage.
|
||||||
|
*
|
||||||
|
* Les 8 premieres motions correspondent au design original RIG (cf.
|
||||||
|
* docs/plans/rig/). Les 3 dernieres sont des ajouts UX (P0/P1) :
|
||||||
|
* - POSE_SLEEP_BOUND : sleep avec restraints (P0)
|
||||||
|
* - POSE_UNCONSCIOUS : steady-state post-capture (P0)
|
||||||
|
* - FALL_BOUND : fall sans flailing (P1)
|
||||||
|
*
|
||||||
|
* Class-load force dans {@code TiedUpMod.commonSetup} via {@link #values()} —
|
||||||
|
* sans ca, les ordinals ne sont pas assignes tant que l'enum n'est pas touche
|
||||||
|
* (JLS : init lazy).
|
||||||
|
*/
|
||||||
|
public enum TiedUpLivingMotions implements LivingMotion {
|
||||||
|
POSE_DOG,
|
||||||
|
POSE_PET_BED_SIT,
|
||||||
|
POSE_PET_BED_SLEEP,
|
||||||
|
POSE_FURNITURE_SEAT,
|
||||||
|
POSE_KNEEL_BOUND,
|
||||||
|
STRUGGLE_BOUND,
|
||||||
|
WALK_BOUND,
|
||||||
|
SNEAK_BOUND,
|
||||||
|
POSE_SLEEP_BOUND, // UX P0 — sleep avec restraints
|
||||||
|
POSE_UNCONSCIOUS, // UX P0 — steady-state post-capture
|
||||||
|
FALL_BOUND; // UX P1 — no flailing en chute
|
||||||
|
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
TiedUpLivingMotions() {
|
||||||
|
this.id = LivingMotion.ENUM_MANAGER.assign(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int universalOrdinal() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
302
src/main/java/com/tiedup/remake/rig/anim/TransformSheet.java
Normal file
302
src/main/java/com/tiedup/remake/rig/anim/TransformSheet.java
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.joml.Quaternionf;
|
||||||
|
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
import com.tiedup.remake.rig.math.MathUtils;
|
||||||
|
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||||
|
import com.tiedup.remake.rig.math.Vec3f;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
public class TransformSheet {
|
||||||
|
public static final TransformSheet EMPTY_SHEET = new TransformSheet(List.of(new Keyframe(0.0F, JointTransform.empty()), new Keyframe(Float.MAX_VALUE, JointTransform.empty())));
|
||||||
|
public static final Function<Vec3, TransformSheet> EMPTY_SHEET_PROVIDER = translation -> {
|
||||||
|
return new TransformSheet(List.of(new Keyframe(0.0F, JointTransform.translation(new Vec3f(translation))), new Keyframe(Float.MAX_VALUE, JointTransform.empty())));
|
||||||
|
};
|
||||||
|
|
||||||
|
private Keyframe[] keyframes;
|
||||||
|
|
||||||
|
public TransformSheet() {
|
||||||
|
this(new Keyframe[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformSheet(int size) {
|
||||||
|
this(new Keyframe[size]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformSheet(List<Keyframe> keyframeList) {
|
||||||
|
this(keyframeList.toArray(new Keyframe[0]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformSheet(Keyframe[] keyframes) {
|
||||||
|
this.keyframes = keyframes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Keyframe[] getKeyframes() {
|
||||||
|
return this.keyframes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformSheet copyAll() {
|
||||||
|
return this.copy(0, this.keyframes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformSheet copy(int start, int end) {
|
||||||
|
int len = end - start;
|
||||||
|
Keyframe[] newKeyframes = new Keyframe[len];
|
||||||
|
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
Keyframe kf = this.keyframes[i + start];
|
||||||
|
newKeyframes[i] = new Keyframe(kf);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TransformSheet(newKeyframes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformSheet readFrom(TransformSheet opponent) {
|
||||||
|
if (opponent.keyframes.length != this.keyframes.length) {
|
||||||
|
this.keyframes = new Keyframe[opponent.keyframes.length];
|
||||||
|
|
||||||
|
for (int i = 0; i < this.keyframes.length; i++) {
|
||||||
|
this.keyframes[i] = Keyframe.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < this.keyframes.length; i++) {
|
||||||
|
this.keyframes[i].copyFrom(opponent.keyframes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformSheet createInterpolated(float[] timestamp) {
|
||||||
|
TransformSheet interpolationCreated = new TransformSheet(timestamp.length);
|
||||||
|
|
||||||
|
for (int i = 0; i < timestamp.length; i++) {
|
||||||
|
interpolationCreated.keyframes[i] = new Keyframe(timestamp[i], this.getInterpolatedTransform(timestamp[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return interpolationCreated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform each joint
|
||||||
|
*/
|
||||||
|
public void forEach(BiConsumer<Integer, Keyframe> task) {
|
||||||
|
this.forEach(task, 0, this.keyframes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forEach(BiConsumer<Integer, Keyframe> task, int start, int end) {
|
||||||
|
end = Math.min(end, this.keyframes.length);
|
||||||
|
|
||||||
|
for (int i = start; i < end; i++) {
|
||||||
|
task.accept(i, this.keyframes[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec3f getInterpolatedTranslation(float currentTime) {
|
||||||
|
InterpolationInfo interpolInfo = this.getInterpolationInfo(currentTime);
|
||||||
|
|
||||||
|
if (interpolInfo == InterpolationInfo.INVALID) {
|
||||||
|
return new Vec3f();
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3f vec3f = MathUtils.lerpVector(this.keyframes[interpolInfo.prev].transform().translation(), this.keyframes[interpolInfo.next].transform().translation(), interpolInfo.delta);
|
||||||
|
return vec3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointTransform getInterpolatedTransform(float currentTime) {
|
||||||
|
return this.getInterpolatedTransform(this.getInterpolationInfo(currentTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointTransform getInterpolatedTransform(InterpolationInfo interpolationInfo) {
|
||||||
|
if (interpolationInfo == InterpolationInfo.INVALID) {
|
||||||
|
return JointTransform.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
JointTransform trasnform = JointTransform.interpolate(this.keyframes[interpolationInfo.prev].transform(), this.keyframes[interpolationInfo.next].transform(), interpolationInfo.delta);
|
||||||
|
return trasnform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformSheet extend(TransformSheet target) {
|
||||||
|
int newKeyLength = this.keyframes.length + target.keyframes.length;
|
||||||
|
Keyframe[] newKeyfrmaes = new Keyframe[newKeyLength];
|
||||||
|
|
||||||
|
for (int i = 0; i < this.keyframes.length; i++) {
|
||||||
|
newKeyfrmaes[i] = this.keyframes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = this.keyframes.length; i < newKeyLength; i++) {
|
||||||
|
newKeyfrmaes[i] = new Keyframe(target.keyframes[i - this.keyframes.length]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.keyframes = newKeyfrmaes;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformSheet getFirstFrame() {
|
||||||
|
TransformSheet part = this.copy(0, 2);
|
||||||
|
Keyframe[] keyframes = part.getKeyframes();
|
||||||
|
keyframes[1].transform().copyFrom(keyframes[0].transform());
|
||||||
|
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void correctAnimationByNewPosition(Vec3f startpos, Vec3f startToEnd, Vec3f modifiedStart, Vec3f modifiedStartToEnd) {
|
||||||
|
Keyframe[] keyframes = this.getKeyframes();
|
||||||
|
Keyframe startKeyframe = keyframes[0];
|
||||||
|
Keyframe endKeyframe = keyframes[keyframes.length - 1];
|
||||||
|
float pitchDeg = (float) Math.toDegrees(Mth.atan2(modifiedStartToEnd.y - startToEnd.y, modifiedStartToEnd.length()));
|
||||||
|
float yawDeg = (float) MathUtils.getAngleBetween(modifiedStartToEnd.copy().multiply(1.0F, 0.0F, 1.0F), startToEnd.copy().multiply(1.0F, 0.0F, 1.0F));
|
||||||
|
|
||||||
|
for (Keyframe kf : keyframes) {
|
||||||
|
float lerp = (kf.time() - startKeyframe.time()) / (endKeyframe.time() - startKeyframe.time());
|
||||||
|
Vec3f line = MathUtils.lerpVector(new Vec3f(0F, 0F, 0F), startToEnd, lerp);
|
||||||
|
Vec3f modifiedLine = MathUtils.lerpVector(new Vec3f(0F, 0F, 0F), modifiedStartToEnd, lerp);
|
||||||
|
Vec3f keyTransform = kf.transform().translation();
|
||||||
|
Vec3f startToKeyTransform = keyTransform.copy().sub(startpos).multiply(-1.0F, 1.0F, -1.0F);
|
||||||
|
Vec3f animOnLine = startToKeyTransform.copy().sub(line);
|
||||||
|
OpenMatrix4f rotator = OpenMatrix4f.createRotatorDeg(pitchDeg, Vec3f.X_AXIS).mulFront(OpenMatrix4f.createRotatorDeg(yawDeg, Vec3f.Y_AXIS));
|
||||||
|
Vec3f toNewKeyTransform = modifiedLine.add(OpenMatrix4f.transform3v(rotator, animOnLine, null));
|
||||||
|
keyTransform.set(modifiedStart.copy().add((toNewKeyTransform)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the animation coord system to world coord system regarding origin point as @param worldDest
|
||||||
|
*
|
||||||
|
* @param entitypatch
|
||||||
|
* @param worldStart
|
||||||
|
* @param worldDest
|
||||||
|
* @param xRot
|
||||||
|
* @param entityYRot
|
||||||
|
* @param startFrame
|
||||||
|
* @param endFrame
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public TransformSheet transformToWorldCoordOriginAsDest(LivingEntityPatch<?> entitypatch, Vec3 startInWorld, Vec3 destInWorld, float entityYRot, float destYRot, int startFrmae, int destFrame) {
|
||||||
|
TransformSheet byStart = this.copy(0, destFrame + 1);
|
||||||
|
TransformSheet byDest = this.copy(0, destFrame + 1);
|
||||||
|
TransformSheet result = new TransformSheet(destFrame + 1);
|
||||||
|
Vec3 toTargetInWorld = destInWorld.subtract(startInWorld);
|
||||||
|
double worldMagnitude = toTargetInWorld.horizontalDistance();
|
||||||
|
double animMagnitude = this.keyframes[0].transform().translation().horizontalDistance();
|
||||||
|
float scale = (float)(worldMagnitude / animMagnitude);
|
||||||
|
|
||||||
|
byStart.forEach((idx, keyframe) -> {
|
||||||
|
keyframe.transform().translation().sub(this.keyframes[0].transform().translation());
|
||||||
|
keyframe.transform().translation().multiply(1.0F, 1.0F, scale);
|
||||||
|
keyframe.transform().translation().rotate(-entityYRot, Vec3f.Y_AXIS);
|
||||||
|
keyframe.transform().translation().multiply(-1.0F, 1.0F, -1.0F);
|
||||||
|
keyframe.transform().translation().add(startInWorld);
|
||||||
|
});
|
||||||
|
|
||||||
|
byDest.forEach((idx, keyframe) -> {
|
||||||
|
keyframe.transform().translation().multiply(1.0F, 1.0F, Mth.lerp((idx / (float)destFrame), scale, 1.0F));
|
||||||
|
keyframe.transform().translation().rotate(-destYRot, Vec3f.Y_AXIS);
|
||||||
|
keyframe.transform().translation().multiply(-1.0F, 1.0F, -1.0F);
|
||||||
|
keyframe.transform().translation().add(destInWorld);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (int i = 0; i < destFrame + 1; i++) {
|
||||||
|
if (i <= startFrmae) {
|
||||||
|
result.getKeyframes()[i] = new Keyframe(this.keyframes[i].time(), JointTransform.translation(byStart.getKeyframes()[i].transform().translation()));
|
||||||
|
} else {
|
||||||
|
float lerp = this.keyframes[i].time() == 0.0F ? 0.0F : this.keyframes[i].time() / this.keyframes[destFrame].time();
|
||||||
|
Vec3f lerpTranslation = Vec3f.interpolate(byStart.getKeyframes()[i].transform().translation(), byDest.getKeyframes()[i].transform().translation(), lerp, null);
|
||||||
|
result.getKeyframes()[i] = new Keyframe(this.keyframes[i].time(), JointTransform.translation(lerpTranslation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.keyframes.length > destFrame) {
|
||||||
|
TransformSheet behindDestination = this.copy(destFrame + 1, this.keyframes.length);
|
||||||
|
|
||||||
|
behindDestination.forEach((idx, keyframe) -> {
|
||||||
|
keyframe.transform().translation().sub(this.keyframes[destFrame].transform().translation());
|
||||||
|
keyframe.transform().translation().rotate(entityYRot, Vec3f.Y_AXIS);
|
||||||
|
keyframe.transform().translation().multiply(-1.0F, 1.0F, -1.0F);
|
||||||
|
keyframe.transform().translation().add(result.getKeyframes()[destFrame].transform().translation());
|
||||||
|
});
|
||||||
|
|
||||||
|
result.extend(behindDestination);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InterpolationInfo getInterpolationInfo(float currentTime) {
|
||||||
|
if (this.keyframes.length == 0) {
|
||||||
|
return InterpolationInfo.INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTime < 0.0F) {
|
||||||
|
currentTime = this.keyframes[this.keyframes.length - 1].time() + currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary search
|
||||||
|
int begin = 0, end = this.keyframes.length - 1;
|
||||||
|
|
||||||
|
while (end - begin > 1) {
|
||||||
|
int i = begin + (end - begin) / 2;
|
||||||
|
|
||||||
|
if (this.keyframes[i].time() <= currentTime && this.keyframes[i+1].time() > currentTime) {
|
||||||
|
begin = i;
|
||||||
|
end = i+1;
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
if (this.keyframes[i].time() > currentTime) {
|
||||||
|
end = i;
|
||||||
|
} else if (this.keyframes[i+1].time() <= currentTime) {
|
||||||
|
begin = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float progression = Mth.clamp((currentTime - this.keyframes[begin].time()) / (this.keyframes[end].time() - this.keyframes[begin].time()), 0.0F, 1.0F);
|
||||||
|
return new InterpolationInfo(begin, end, Float.isNaN(progression) ? 1.0F : progression);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float maxFrameTime() {
|
||||||
|
float maxFrameTime = -1.0F;
|
||||||
|
|
||||||
|
for (Keyframe kf : this.keyframes) {
|
||||||
|
if (kf.time() > maxFrameTime) {
|
||||||
|
maxFrameTime = kf.time();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxFrameTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int idx = 0;
|
||||||
|
|
||||||
|
for (Keyframe kf : this.keyframes) {
|
||||||
|
sb.append(kf);
|
||||||
|
|
||||||
|
if (++idx < this.keyframes.length) {
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static record InterpolationInfo(int prev, int next, float delta) {
|
||||||
|
public static final InterpolationInfo INVALID = new InterpolationInfo(-1, -1, -1.0F);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.action;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
import com.tiedup.remake.rig.util.CodecDispatchRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 D2 — biggest artist unlock : an {@code AnimationAction} is a
|
||||||
|
* data-driven unit of behaviour triggered at animation begin / end / a specific
|
||||||
|
* frame / a period, authored from a datapack JSON block without any Java code.
|
||||||
|
*
|
||||||
|
* <p>Serialization goes through {@link AnimationActionRegistry#dispatchCodec()}
|
||||||
|
* which reads a {@code "type"} field and dispatches to the codec registered
|
||||||
|
* for that {@link ResourceLocation}. Example JSON :
|
||||||
|
* <pre>{@code
|
||||||
|
* { "type": "tiedup:play_sound",
|
||||||
|
* "sound": "minecraft:entity.player.levelup",
|
||||||
|
* "volume": 0.8 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Each implementation is responsible for its own sidedness — actions that
|
||||||
|
* mutate world state should early-return on the wrong side, actions that spawn
|
||||||
|
* client particles should early-return on server. This is by design : the outer
|
||||||
|
* {@link com.tiedup.remake.rig.anim.property.AnimationEvent.Side} filter still
|
||||||
|
* applies but individual actions can tighten it further when the outer event
|
||||||
|
* is configured as {@code BOTH}.
|
||||||
|
*
|
||||||
|
* <p>The {@code prevElapsed} / {@code elapsed} arguments are forwarded verbatim
|
||||||
|
* from the outer {@link com.tiedup.remake.rig.anim.property.AnimationEvent#execute}
|
||||||
|
* call so that timing-aware actions (future extension) can key off the exact
|
||||||
|
* trigger frame. Today's core actions ignore them — they execute atomically
|
||||||
|
* on trigger.
|
||||||
|
*/
|
||||||
|
public interface AnimationAction extends CodecDispatchRegistry.Typed {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch codec — reads the {@code "type"} field of the JSON object and
|
||||||
|
* delegates to the codec of the matching registered action. Unknown types
|
||||||
|
* return a {@link com.mojang.serialization.DataResult} error (logged via
|
||||||
|
* {@link com.tiedup.remake.rig.anim.property.AnimationProperty#parseFrom}
|
||||||
|
* and the containing list entry is dropped).
|
||||||
|
*
|
||||||
|
* <p>Uses {@code partialDispatch} rather than {@code dispatch} because
|
||||||
|
* {@code dispatch} requires a total function {@code ResourceLocation ->
|
||||||
|
* Codec<? extends AnimationAction>}, which cannot express «unknown
|
||||||
|
* type» without throwing.
|
||||||
|
*/
|
||||||
|
Codec<AnimationAction> CODEC = AnimationActionRegistry.INSTANCE.dispatchCodec();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The registered type id of this action (e.g. {@code tiedup:play_sound}).
|
||||||
|
* Used by the dispatch codec to serialize back to JSON.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
ResourceLocation type();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute this action against the entity playing {@code animation}.
|
||||||
|
*
|
||||||
|
* @param patch the living entity patch currently playing the animation
|
||||||
|
* @param animation the animation accessor (forwarded from the outer event)
|
||||||
|
* @param prevElapsed previous frame time in the animation (seconds)
|
||||||
|
* @param elapsed current frame time in the animation (seconds)
|
||||||
|
*/
|
||||||
|
void execute(
|
||||||
|
LivingEntityPatch<?> patch,
|
||||||
|
AssetAccessor<? extends DynamicAnimation> animation,
|
||||||
|
float prevElapsed,
|
||||||
|
float elapsed
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.action;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.action.impl.ApplyEffectAction;
|
||||||
|
import com.tiedup.remake.rig.anim.action.impl.DamageEntityAction;
|
||||||
|
import com.tiedup.remake.rig.anim.action.impl.PlaySoundAction;
|
||||||
|
import com.tiedup.remake.rig.anim.action.impl.SpawnParticleAction;
|
||||||
|
import com.tiedup.remake.rig.util.CodecDispatchRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry of {@link AnimationAction} type ids → codecs, used by the dispatch
|
||||||
|
* codec exposed via {@link AnimationAction#CODEC}.
|
||||||
|
*
|
||||||
|
* <p>The four core actions ({@code play_sound}, {@code spawn_particle},
|
||||||
|
* {@code apply_effect}, {@code damage_entity}) are registered in the static
|
||||||
|
* initializer of this class so that a single reference to
|
||||||
|
* {@link AnimationAction#CODEC} in a parse path is enough to bootstrap the
|
||||||
|
* full dispatch table.
|
||||||
|
*
|
||||||
|
* <p>Third-party mods may register additional action types by calling
|
||||||
|
* {@link #register} on {@link #INSTANCE} from their common setup event
|
||||||
|
* (post-{@code FMLCommonSetup} to avoid class-loading order surprises with
|
||||||
|
* the static init of this class).
|
||||||
|
*
|
||||||
|
* <p>Plumbing (map, register, dispatchCodec) lives in
|
||||||
|
* {@link CodecDispatchRegistry} since {@code SMELL-CODEC-01}.
|
||||||
|
*/
|
||||||
|
public final class AnimationActionRegistry extends CodecDispatchRegistry<AnimationAction> {
|
||||||
|
|
||||||
|
public static final AnimationActionRegistry INSTANCE = new AnimationActionRegistry();
|
||||||
|
|
||||||
|
private AnimationActionRegistry() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String registryName() {
|
||||||
|
return "AnimationAction";
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
INSTANCE.register(PlaySoundAction.ID, PlaySoundAction.CODEC);
|
||||||
|
INSTANCE.register(SpawnParticleAction.ID, SpawnParticleAction.CODEC);
|
||||||
|
INSTANCE.register(ApplyEffectAction.ID, ApplyEffectAction.CODEC);
|
||||||
|
INSTANCE.register(DamageEntityAction.ID, DamageEntityAction.CODEC);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.action;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationEvent;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationEvent.E0;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationEvent.InPeriodEvent;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationEvent.InTimeEvent;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationEvent.SimpleEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 D2 — adapter layer between JSON-authored event blocks and the
|
||||||
|
* runtime {@link AnimationEvent} hierarchy. Three serialized event shapes are
|
||||||
|
* supported :
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link SimpleSerializedEvent} — fires on animation begin or end
|
||||||
|
* (no timing predicate).</li>
|
||||||
|
* <li>{@link TimeSerializedEvent} — fires once when the animation crosses
|
||||||
|
* the {@code frame} timestamp (seconds since anim start).</li>
|
||||||
|
* <li>{@link PeriodSerializedEvent} — fires every tick while the animation
|
||||||
|
* is between {@code start} and {@code end}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Each shape carries a list of {@link AnimationAction}s that all run when
|
||||||
|
* the event fires. Actions drive their own sidedness — see individual impls —
|
||||||
|
* so the outer {@link AnimationEvent.Side} defaults to {@link AnimationEvent.Side#BOTH}
|
||||||
|
* and can be overridden via an optional {@code "side"} field
|
||||||
|
* ({@code "CLIENT"}, {@code "SERVER"}, {@code "LOCAL_CLIENT"}, {@code "BOTH"}).
|
||||||
|
*
|
||||||
|
* <p>The adapter returns thin {@code E0} lambdas : the outer AnimationEvent
|
||||||
|
* machinery keeps handling side filtering + time-window checking, and the
|
||||||
|
* lambda body simply iterates the action list. This keeps the runtime path
|
||||||
|
* identical to the hand-coded Java path (no perf regression, no new class
|
||||||
|
* to pool).
|
||||||
|
*/
|
||||||
|
public final class DataDrivenAnimationEvents {
|
||||||
|
|
||||||
|
private DataDrivenAnimationEvents() {}
|
||||||
|
|
||||||
|
private static final Codec<AnimationEvent.Side> SIDE_CODEC = Codec.STRING.xmap(
|
||||||
|
s -> AnimationEvent.Side.valueOf(s.toUpperCase()),
|
||||||
|
Enum::name
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialized shape for {@code on_begin} / {@code on_end} events.
|
||||||
|
*/
|
||||||
|
public record SimpleSerializedEvent(
|
||||||
|
List<AnimationAction> actions,
|
||||||
|
AnimationEvent.Side side
|
||||||
|
) {
|
||||||
|
|
||||||
|
public static final Codec<SimpleSerializedEvent> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
AnimationAction.CODEC.listOf().fieldOf("actions").forGetter(SimpleSerializedEvent::actions),
|
||||||
|
SIDE_CODEC.optionalFieldOf("side", AnimationEvent.Side.BOTH).forGetter(SimpleSerializedEvent::side)
|
||||||
|
).apply(i, SimpleSerializedEvent::new));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sugar accepting either a full object {@code {"actions":[...], "side":...}}
|
||||||
|
* or a bare action list {@code [...]} — in which case {@code side}
|
||||||
|
* defaults to {@link AnimationEvent.Side#BOTH}. This is the shape most
|
||||||
|
* artists reach for.
|
||||||
|
*/
|
||||||
|
public static final Codec<SimpleSerializedEvent> SUGAR_CODEC = Codec.either(
|
||||||
|
AnimationAction.CODEC.listOf(),
|
||||||
|
CODEC
|
||||||
|
).xmap(
|
||||||
|
either -> either.map(
|
||||||
|
actions -> new SimpleSerializedEvent(actions, AnimationEvent.Side.BOTH),
|
||||||
|
full -> full
|
||||||
|
),
|
||||||
|
ev -> com.mojang.datafixers.util.Either.right(ev)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this serialized record into the runtime {@link SimpleEvent}
|
||||||
|
* representation.
|
||||||
|
*/
|
||||||
|
public SimpleEvent<E0> toRuntime() {
|
||||||
|
final List<AnimationAction> captured = List.copyOf(this.actions);
|
||||||
|
E0 fire = (patch, anim, params) -> {
|
||||||
|
for (AnimationAction action : captured) {
|
||||||
|
action.execute(patch, anim, 0.0F, 0.0F);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return SimpleEvent.create(fire, this.side);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialized shape for a {@code tick_events} entry that fires once when
|
||||||
|
* the animation crosses {@code frame} (seconds).
|
||||||
|
*/
|
||||||
|
public record TimeSerializedEvent(
|
||||||
|
float frame,
|
||||||
|
List<AnimationAction> actions,
|
||||||
|
AnimationEvent.Side side
|
||||||
|
) {
|
||||||
|
|
||||||
|
public static final Codec<TimeSerializedEvent> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
Codec.FLOAT.fieldOf("frame").forGetter(TimeSerializedEvent::frame),
|
||||||
|
AnimationAction.CODEC.listOf().fieldOf("actions").forGetter(TimeSerializedEvent::actions),
|
||||||
|
SIDE_CODEC.optionalFieldOf("side", AnimationEvent.Side.BOTH).forGetter(TimeSerializedEvent::side)
|
||||||
|
).apply(i, TimeSerializedEvent::new));
|
||||||
|
|
||||||
|
public InTimeEvent<E0> toRuntime() {
|
||||||
|
final List<AnimationAction> captured = List.copyOf(this.actions);
|
||||||
|
E0 fire = (patch, anim, params) -> {
|
||||||
|
for (AnimationAction action : captured) {
|
||||||
|
action.execute(patch, anim, 0.0F, 0.0F);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return InTimeEvent.create(this.frame, fire, this.side);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialized shape for a {@code tick_events} entry that fires every tick
|
||||||
|
* while the animation elapsed-time is between {@code start} and {@code end}.
|
||||||
|
*/
|
||||||
|
public record PeriodSerializedEvent(
|
||||||
|
float start,
|
||||||
|
float end,
|
||||||
|
List<AnimationAction> actions,
|
||||||
|
AnimationEvent.Side side
|
||||||
|
) {
|
||||||
|
|
||||||
|
public static final Codec<PeriodSerializedEvent> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
Codec.FLOAT.fieldOf("start").forGetter(PeriodSerializedEvent::start),
|
||||||
|
Codec.FLOAT.fieldOf("end").forGetter(PeriodSerializedEvent::end),
|
||||||
|
AnimationAction.CODEC.listOf().fieldOf("actions").forGetter(PeriodSerializedEvent::actions),
|
||||||
|
SIDE_CODEC.optionalFieldOf("side", AnimationEvent.Side.BOTH).forGetter(PeriodSerializedEvent::side)
|
||||||
|
).apply(i, PeriodSerializedEvent::new));
|
||||||
|
|
||||||
|
public InPeriodEvent<E0> toRuntime() {
|
||||||
|
final List<AnimationAction> captured = List.copyOf(this.actions);
|
||||||
|
E0 fire = (patch, anim, params) -> {
|
||||||
|
for (AnimationAction action : captured) {
|
||||||
|
action.execute(patch, anim, 0.0F, 0.0F);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return InPeriodEvent.create(this.start, this.end, fire, this.side);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discriminator codec for a single {@code tick_events} entry — picks
|
||||||
|
* between time and period by looking for the {@code "frame"} vs
|
||||||
|
* {@code "start"}/{@code "end"} keys. Implemented as an {@link Codec#either}
|
||||||
|
* because the two shapes are structurally disjoint (a time event cannot
|
||||||
|
* also be a period event).
|
||||||
|
*/
|
||||||
|
public static final Codec<AnimationEvent<?, ?>> TICK_EVENT_ENTRY_CODEC = Codec.either(
|
||||||
|
TimeSerializedEvent.CODEC,
|
||||||
|
PeriodSerializedEvent.CODEC
|
||||||
|
).xmap(
|
||||||
|
either -> either.map(TimeSerializedEvent::toRuntime, PeriodSerializedEvent::toRuntime),
|
||||||
|
// Encode path — best-effort : runtime events don't carry enough
|
||||||
|
// structural info to re-serialize losslessly (the action list is lost
|
||||||
|
// inside the opaque E0 lambda). We fall through to the period shape
|
||||||
|
// which is the richer of the two — encoding is not a supported
|
||||||
|
// round-trip path for these properties (see D2 scope notes).
|
||||||
|
ev -> com.mojang.datafixers.util.Either.right(new PeriodSerializedEvent(0.0F, 0.0F, List.of(), AnimationEvent.Side.BOTH))
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Codec for the full {@code tick_events} list property.
|
||||||
|
*/
|
||||||
|
public static final Codec<List<AnimationEvent<?, ?>>> TICK_EVENTS_CODEC =
|
||||||
|
TICK_EVENT_ENTRY_CODEC.listOf().xmap(
|
||||||
|
ArrayList::new,
|
||||||
|
ArrayList::new
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Codec for an {@code on_begin} / {@code on_end} list property. The entry
|
||||||
|
* codec accepts either a bare action list or a full object, see
|
||||||
|
* {@link SimpleSerializedEvent#SUGAR_CODEC}.
|
||||||
|
*/
|
||||||
|
public static final Codec<List<SimpleEvent<?>>> BEGIN_END_EVENTS_CODEC =
|
||||||
|
SimpleSerializedEvent.SUGAR_CODEC.listOf().xmap(
|
||||||
|
list -> {
|
||||||
|
List<SimpleEvent<?>> out = new ArrayList<>(list.size());
|
||||||
|
for (SimpleSerializedEvent ev : list) {
|
||||||
|
out.add(ev.toRuntime());
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
// Lossy encode — runtime SimpleEvent<?> doesn't retain action list
|
||||||
|
// after toRuntime wraps them in an opaque E0 lambda. Return empty
|
||||||
|
// serialized list (see class Javadoc).
|
||||||
|
runtime -> {
|
||||||
|
List<SimpleSerializedEvent> out = new ArrayList<>(runtime.size());
|
||||||
|
for (int i = 0; i < runtime.size(); i++) {
|
||||||
|
out.add(new SimpleSerializedEvent(List.of(), AnimationEvent.Side.BOTH));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.action.impl;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.effect.MobEffect;
|
||||||
|
import net.minecraft.world.effect.MobEffectInstance;
|
||||||
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
import net.minecraftforge.registries.ForgeRegistries;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.action.AnimationAction;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a potion effect to the animating entity. Server-side authoritative —
|
||||||
|
* {@code LivingEntity.addEffect} is a no-op on the client (the effect will
|
||||||
|
* be synced down when the server accepts the change).
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* { "type": "tiedup:apply_effect",
|
||||||
|
* "effect": "minecraft:slowness",
|
||||||
|
* "duration_ticks": 60,
|
||||||
|
* "amplifier": 1,
|
||||||
|
* "ambient": false,
|
||||||
|
* "show_particles": true,
|
||||||
|
* "show_icon": true }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>{@code amplifier} defaults to {@code 0} (level 1), {@code ambient} /
|
||||||
|
* {@code show_particles} / {@code show_icon} mirror {@link MobEffectInstance}'s
|
||||||
|
* defaults ({@code false} / {@code true} / {@code true}). Unknown effect ids
|
||||||
|
* are no-op + WARN.
|
||||||
|
*/
|
||||||
|
public record ApplyEffectAction(
|
||||||
|
ResourceLocation effect,
|
||||||
|
int durationTicks,
|
||||||
|
int amplifier,
|
||||||
|
boolean ambient,
|
||||||
|
boolean showParticles,
|
||||||
|
boolean showIcon
|
||||||
|
) implements AnimationAction {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("apply_effect");
|
||||||
|
|
||||||
|
public static final Codec<ApplyEffectAction> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
ResourceLocation.CODEC.fieldOf("effect").forGetter(ApplyEffectAction::effect),
|
||||||
|
Codec.INT.fieldOf("duration_ticks").forGetter(ApplyEffectAction::durationTicks),
|
||||||
|
Codec.INT.optionalFieldOf("amplifier", 0).forGetter(ApplyEffectAction::amplifier),
|
||||||
|
Codec.BOOL.optionalFieldOf("ambient", false).forGetter(ApplyEffectAction::ambient),
|
||||||
|
Codec.BOOL.optionalFieldOf("show_particles", true).forGetter(ApplyEffectAction::showParticles),
|
||||||
|
Codec.BOOL.optionalFieldOf("show_icon", true).forGetter(ApplyEffectAction::showIcon)
|
||||||
|
).apply(i, ApplyEffectAction::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(
|
||||||
|
LivingEntityPatch<?> patch,
|
||||||
|
AssetAccessor<? extends DynamicAnimation> animation,
|
||||||
|
float prevElapsed,
|
||||||
|
float elapsed
|
||||||
|
) {
|
||||||
|
MobEffect mobEffect = ForgeRegistries.MOB_EFFECTS.getValue(this.effect);
|
||||||
|
if (mobEffect == null) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn("ApplyEffectAction : unknown mob effect {}", this.effect);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LivingEntity entity = patch.getOriginal();
|
||||||
|
entity.addEffect(new MobEffectInstance(
|
||||||
|
mobEffect,
|
||||||
|
this.durationTicks,
|
||||||
|
this.amplifier,
|
||||||
|
this.ambient,
|
||||||
|
this.showParticles,
|
||||||
|
this.showIcon
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.action.impl;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.damagesource.DamageSource;
|
||||||
|
import net.minecraft.world.damagesource.DamageSources;
|
||||||
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.action.AnimationAction;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Damage the animating entity for {@link #amount} half-hearts using the named
|
||||||
|
* damage source. Server-side authoritative — {@code LivingEntity.hurt} is a
|
||||||
|
* no-op on the client.
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* { "type": "tiedup:damage_entity",
|
||||||
|
* "amount": 2.0,
|
||||||
|
* "source": "generic" }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>The {@code source} field maps to a helper method on {@link DamageSources}
|
||||||
|
* (see the {@link SourceType} enum). Defaults to {@code generic}. In 1.20.1
|
||||||
|
* damage types are registry-driven {@link net.minecraft.resources.ResourceKey},
|
||||||
|
* so the direct JSON-to-ResourceKey path would require a {@code RegistryAccess}
|
||||||
|
* we don't have in a codec — using the helper names keeps authoring ergonomic
|
||||||
|
* and side-steps the registry plumbing. Mods that want a custom damage type
|
||||||
|
* can register an {@link AnimationAction} subclass that consumes their id
|
||||||
|
* directly.
|
||||||
|
*/
|
||||||
|
public record DamageEntityAction(
|
||||||
|
float amount,
|
||||||
|
SourceType source
|
||||||
|
) implements AnimationAction {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("damage_entity");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safe codec over {@link SourceType} : uppercases + looks up the enum
|
||||||
|
* constant, returning a {@link DataResult#error} for unknown names rather
|
||||||
|
* than throwing. {@code flatXmap} is used (over {@code xmap}) precisely to
|
||||||
|
* surface the error through the Codec pipeline.
|
||||||
|
*/
|
||||||
|
private static final Codec<SourceType> SOURCE_TYPE_CODEC = Codec.STRING.flatXmap(
|
||||||
|
s -> {
|
||||||
|
try {
|
||||||
|
return DataResult.success(SourceType.valueOf(s.toUpperCase()));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return DataResult.error(() -> "Unknown damage source type : " + s);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
t -> DataResult.success(t.name().toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
public static final Codec<DamageEntityAction> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
Codec.FLOAT.fieldOf("amount").forGetter(DamageEntityAction::amount),
|
||||||
|
SOURCE_TYPE_CODEC.optionalFieldOf("source", SourceType.GENERIC).forGetter(DamageEntityAction::source)
|
||||||
|
).apply(i, DamageEntityAction::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(
|
||||||
|
LivingEntityPatch<?> patch,
|
||||||
|
AssetAccessor<? extends DynamicAnimation> animation,
|
||||||
|
float prevElapsed,
|
||||||
|
float elapsed
|
||||||
|
) {
|
||||||
|
LivingEntity entity = patch.getOriginal();
|
||||||
|
if (entity.level().isClientSide()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DamageSource damageSource = resolveDamageSource(entity.damageSources(), this.source);
|
||||||
|
if (damageSource == null) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn("DamageEntityAction : could not resolve damage source {}", this.source);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.hurt(damageSource, this.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DamageSource resolveDamageSource(DamageSources sources, SourceType type) {
|
||||||
|
return switch (type) {
|
||||||
|
case GENERIC -> sources.generic();
|
||||||
|
case MAGIC -> sources.magic();
|
||||||
|
case FALL -> sources.fall();
|
||||||
|
case IN_FIRE -> sources.inFire();
|
||||||
|
case ON_FIRE -> sources.onFire();
|
||||||
|
case LAVA -> sources.lava();
|
||||||
|
case DROWN -> sources.drown();
|
||||||
|
case STARVE -> sources.starve();
|
||||||
|
case CACTUS -> sources.cactus();
|
||||||
|
case CRAMMING -> sources.cramming();
|
||||||
|
case IN_WALL -> sources.inWall();
|
||||||
|
case WITHER -> sources.wither();
|
||||||
|
case FREEZE -> sources.freeze();
|
||||||
|
case DRY_OUT -> sources.dryOut();
|
||||||
|
case SWEET_BERRY_BUSH -> sources.sweetBerryBush();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whitelist of vanilla damage sources addressable from JSON. Artist-facing
|
||||||
|
* names are lowercase ({@code "generic"}, {@code "magic"}, ...) — the codec
|
||||||
|
* uppercases before looking up the enum.
|
||||||
|
*/
|
||||||
|
public enum SourceType {
|
||||||
|
GENERIC,
|
||||||
|
MAGIC,
|
||||||
|
FALL,
|
||||||
|
IN_FIRE,
|
||||||
|
ON_FIRE,
|
||||||
|
LAVA,
|
||||||
|
DROWN,
|
||||||
|
STARVE,
|
||||||
|
CACTUS,
|
||||||
|
CRAMMING,
|
||||||
|
IN_WALL,
|
||||||
|
WITHER,
|
||||||
|
FREEZE,
|
||||||
|
DRY_OUT,
|
||||||
|
SWEET_BERRY_BUSH
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.action.impl;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
|
import com.mojang.serialization.MapCodec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.sounds.SoundEvent;
|
||||||
|
import net.minecraft.sounds.SoundSource;
|
||||||
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
import net.minecraftforge.registries.ForgeRegistries;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.action.AnimationAction;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a sound at the entity's position. Server-side authoritative — the
|
||||||
|
* {@code level.playSound(null, ...)} call broadcasts to all clients within
|
||||||
|
* the default spatial radius.
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* { "type": "tiedup:play_sound",
|
||||||
|
* "sound": "minecraft:entity.player.levelup",
|
||||||
|
* "volume": 0.8,
|
||||||
|
* "pitch": 1.1,
|
||||||
|
* "category": "neutral" }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>{@code volume} defaults to {@code 1.0}, {@code pitch} defaults to
|
||||||
|
* {@code 1.0}, {@code category} defaults to {@code neutral}. Unknown sound
|
||||||
|
* ids are silently no-op — they log a WARN via the surrounding parse path
|
||||||
|
* and skip execution (safer than crashing the animation).
|
||||||
|
*/
|
||||||
|
public record PlaySoundAction(
|
||||||
|
ResourceLocation sound,
|
||||||
|
float volume,
|
||||||
|
float pitch,
|
||||||
|
SoundSource category
|
||||||
|
) implements AnimationAction {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("play_sound");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a single string into a {@link SoundSource}, accepting the
|
||||||
|
* canonical lowercase {@link SoundSource#getName()} form (e.g.
|
||||||
|
* {@code "master"}, {@code "record"}, {@code "block"}, {@code "player"}
|
||||||
|
* — note these are the singular forms, not the enum constant names like
|
||||||
|
* {@code RECORDS} / {@code BLOCKS} / {@code PLAYERS}).
|
||||||
|
*/
|
||||||
|
private static DataResult<SoundSource> parseSoundSource(String s) {
|
||||||
|
String normalized = s.toLowerCase(Locale.ROOT);
|
||||||
|
for (SoundSource src : SoundSource.values()) {
|
||||||
|
if (src.getName().equals(normalized)) {
|
||||||
|
return DataResult.success(src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String valid = Arrays.stream(SoundSource.values())
|
||||||
|
.map(SoundSource::getName)
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
return DataResult.error(() -> "Unknown SoundSource: '" + s + "'. Valid values: " + valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Codec for {@link SoundSource} as a non-optional field.
|
||||||
|
*
|
||||||
|
* <p>Uses {@link Codec#comapFlatMap} so that an unknown name produces a
|
||||||
|
* {@link DataResult#error} with a descriptive message rather than an
|
||||||
|
* uncaught {@link IllegalArgumentException}. Without this, a typo like
|
||||||
|
* {@code "ambiant"} would crash the whole animation parse with no clue
|
||||||
|
* which field was at fault.
|
||||||
|
*/
|
||||||
|
private static final Codec<SoundSource> SOURCE_CODEC = Codec.STRING.comapFlatMap(
|
||||||
|
PlaySoundAction::parseSoundSource,
|
||||||
|
source -> source.getName()
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MapCodec for the optional {@code category} field, with strict error
|
||||||
|
* propagation.
|
||||||
|
*
|
||||||
|
* <p><strong>Why not {@code SOURCE_CODEC.optionalFieldOf(...)}?</strong>
|
||||||
|
* In DFU 6.0.8 (the version shipped with Forge 1.20.1) the
|
||||||
|
* {@code OptionalFieldCodec.decode} implementation is
|
||||||
|
* <em>lenient by default</em> — when the inner codec returns
|
||||||
|
* {@code DataResult.error} for a present value, the optional codec
|
||||||
|
* silently swallows it and yields {@code Optional.empty()}. The strict
|
||||||
|
* {@code lenient=false} flag was only added in a later DFU release.
|
||||||
|
*
|
||||||
|
* <p>To surface artist typos like {@code "category": "ambiant"} as a
|
||||||
|
* real parse error (RISK-001), we instead first decode the raw string
|
||||||
|
* via {@code Codec.STRING.optionalFieldOf("category")} (which always
|
||||||
|
* succeeds for strings) and then validate via {@code flatXmap} which
|
||||||
|
* properly propagates {@link DataResult#error} from the
|
||||||
|
* {@link #parseSoundSource} validator.
|
||||||
|
*/
|
||||||
|
private static final MapCodec<SoundSource> CATEGORY_FIELD = Codec.STRING
|
||||||
|
.optionalFieldOf("category")
|
||||||
|
.flatXmap(
|
||||||
|
rawOpt -> rawOpt.isPresent()
|
||||||
|
? parseSoundSource(rawOpt.get()).map(s -> s)
|
||||||
|
: DataResult.success(SoundSource.NEUTRAL),
|
||||||
|
source -> DataResult.success(Optional.of(source.getName()))
|
||||||
|
);
|
||||||
|
|
||||||
|
public static final Codec<PlaySoundAction> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
ResourceLocation.CODEC.fieldOf("sound").forGetter(PlaySoundAction::sound),
|
||||||
|
Codec.FLOAT.optionalFieldOf("volume", 1.0F).forGetter(PlaySoundAction::volume),
|
||||||
|
Codec.FLOAT.optionalFieldOf("pitch", 1.0F).forGetter(PlaySoundAction::pitch),
|
||||||
|
CATEGORY_FIELD.forGetter(PlaySoundAction::category)
|
||||||
|
).apply(i, PlaySoundAction::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(
|
||||||
|
LivingEntityPatch<?> patch,
|
||||||
|
AssetAccessor<? extends DynamicAnimation> animation,
|
||||||
|
float prevElapsed,
|
||||||
|
float elapsed
|
||||||
|
) {
|
||||||
|
SoundEvent event = ForgeRegistries.SOUND_EVENTS.getValue(this.sound);
|
||||||
|
if (event == null) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn("PlaySoundAction : unknown sound event {}", this.sound);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LivingEntity entity = patch.getOriginal();
|
||||||
|
entity.level().playSound(
|
||||||
|
null,
|
||||||
|
entity.getX(), entity.getY(), entity.getZ(),
|
||||||
|
event,
|
||||||
|
this.category,
|
||||||
|
this.volume,
|
||||||
|
this.pitch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.action.impl;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.core.particles.ParticleOptions;
|
||||||
|
import net.minecraft.core.particles.ParticleType;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
import net.minecraft.world.level.Level;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import net.minecraftforge.registries.ForgeRegistries;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.action.AnimationAction;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
import com.tiedup.remake.rig.armature.Joint;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawn a particle burst at the entity's root position or at a named joint.
|
||||||
|
* Client-side only — the {@code level.addParticle} API requires a ClientLevel.
|
||||||
|
* Actions declared on a server-side outer event are no-op.
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* { "type": "tiedup:spawn_particle",
|
||||||
|
* "particle": "minecraft:smoke",
|
||||||
|
* "at": "Root",
|
||||||
|
* "count": 5,
|
||||||
|
* "speed": 0.05,
|
||||||
|
* "offset_x": 0.0,
|
||||||
|
* "offset_y": 1.2,
|
||||||
|
* "offset_z": 0.0 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>{@code at} defaults to «root joint position = entity position» —
|
||||||
|
* if the specified joint does not exist in the armature, a WARN is logged and
|
||||||
|
* the particle spawns at the entity origin. {@code count} defaults to {@code 1},
|
||||||
|
* {@code speed} defaults to {@code 0.0}, offsets default to zero.
|
||||||
|
*
|
||||||
|
* <p><b>Design note :</b> the particle type must implement {@link ParticleOptions}
|
||||||
|
* directly — vanilla «simple» particles like {@code minecraft:smoke}
|
||||||
|
* satisfy this because {@link ParticleType} extends {@link ParticleOptions}
|
||||||
|
* when the particle carries no extra data. Complex particles that require data
|
||||||
|
* parameters (block / item / dust color) cannot currently be authored through
|
||||||
|
* this action — that's a follow-up (would require a particle data sub-codec).
|
||||||
|
*/
|
||||||
|
public record SpawnParticleAction(
|
||||||
|
ResourceLocation particle,
|
||||||
|
String joint,
|
||||||
|
int count,
|
||||||
|
float speed,
|
||||||
|
float offsetX,
|
||||||
|
float offsetY,
|
||||||
|
float offsetZ
|
||||||
|
) implements AnimationAction {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("spawn_particle");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set (thread-safe) des couples {@code armatureName + ":" + joint} pour
|
||||||
|
* lesquels un WARN « joint inconnu » a déjà été émis. Évite le spam log :
|
||||||
|
* les period events fire à 20 Hz × duration, donc une typo modder
|
||||||
|
* ({@code "at": "wrongJoint"}) sans dedup polluerait {@code latest.log}
|
||||||
|
* d'un WARN par tick et masquerait les vrais bugs.
|
||||||
|
*
|
||||||
|
* <p>Pattern identique à {@code TiedUpAnimationRegistry.WARNED_MISSING_ANIMS}
|
||||||
|
* — {@link ConcurrentHashMap#newKeySet()} pour la sûreté en cas d'appels
|
||||||
|
* concurrents (le code execute peut tourner sur le client tick thread comme
|
||||||
|
* sur un thread de network handler).</p>
|
||||||
|
*
|
||||||
|
* <p>Le reset est wired dans
|
||||||
|
* {@code TiedUpRigRegistryReloadListener.apply()} : à chaque {@code /reload}
|
||||||
|
* (datapack) ou F3+T (resource pack client) on purge le set, ce qui ré-active
|
||||||
|
* le WARN pour le cas modder corrige son JSON puis le re-casse.</p>
|
||||||
|
*/
|
||||||
|
static final Set<String> WARNED_MISSING_JOINTS = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
public static final Codec<SpawnParticleAction> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
ResourceLocation.CODEC.fieldOf("particle").forGetter(SpawnParticleAction::particle),
|
||||||
|
Codec.STRING.optionalFieldOf("at", "").forGetter(SpawnParticleAction::joint),
|
||||||
|
Codec.INT.optionalFieldOf("count", 1).forGetter(SpawnParticleAction::count),
|
||||||
|
Codec.FLOAT.optionalFieldOf("speed", 0.0F).forGetter(SpawnParticleAction::speed),
|
||||||
|
Codec.FLOAT.optionalFieldOf("offset_x", 0.0F).forGetter(SpawnParticleAction::offsetX),
|
||||||
|
Codec.FLOAT.optionalFieldOf("offset_y", 0.0F).forGetter(SpawnParticleAction::offsetY),
|
||||||
|
Codec.FLOAT.optionalFieldOf("offset_z", 0.0F).forGetter(SpawnParticleAction::offsetZ)
|
||||||
|
).apply(i, SpawnParticleAction::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(
|
||||||
|
LivingEntityPatch<?> patch,
|
||||||
|
AssetAccessor<? extends DynamicAnimation> animation,
|
||||||
|
float prevElapsed,
|
||||||
|
float elapsed
|
||||||
|
) {
|
||||||
|
LivingEntity entity = patch.getOriginal();
|
||||||
|
Level level = entity.level();
|
||||||
|
|
||||||
|
// Client-side guard : addParticle is a no-op on server but we short
|
||||||
|
// circuit early to avoid the registry lookup overhead.
|
||||||
|
if (!level.isClientSide()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParticleType<?> particleType = ForgeRegistries.PARTICLE_TYPES.getValue(this.particle);
|
||||||
|
if (particleType == null) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn("SpawnParticleAction : unknown particle type {}", this.particle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(particleType instanceof ParticleOptions options)) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"SpawnParticleAction : particle {} does not implement ParticleOptions (complex particles not yet supported)",
|
||||||
|
this.particle
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 origin = resolveOrigin(patch, entity);
|
||||||
|
|
||||||
|
for (int n = 0; n < this.count; n++) {
|
||||||
|
level.addParticle(
|
||||||
|
options,
|
||||||
|
origin.x + this.offsetX,
|
||||||
|
origin.y + this.offsetY,
|
||||||
|
origin.z + this.offsetZ,
|
||||||
|
0.0, this.speed, 0.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the spawn origin. If {@link #joint} is non-empty and matches an
|
||||||
|
* armature joint, we would ideally transform the joint's local position
|
||||||
|
* into world space — but the public {@link LivingEntityPatch} API does not
|
||||||
|
* yet expose a joint-world-position helper (tracked separately). For now
|
||||||
|
* we only validate the joint exists and fall back to the entity position.
|
||||||
|
*/
|
||||||
|
private Vec3 resolveOrigin(LivingEntityPatch<?> patch, LivingEntity entity) {
|
||||||
|
Vec3 entityPos = new Vec3(entity.getX(), entity.getY(), entity.getZ());
|
||||||
|
|
||||||
|
if (this.joint == null || this.joint.isEmpty()) {
|
||||||
|
return entityPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
Armature armature = patch.getArmature();
|
||||||
|
if (armature == null) {
|
||||||
|
return entityPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
Joint target = armature.searchJointByName(this.joint);
|
||||||
|
if (target == null) {
|
||||||
|
// Dedup : period events fire à 20 Hz, sans dedup une typo modder
|
||||||
|
// produirait un WARN par tick × duration. Un seul WARN par couple
|
||||||
|
// (armature, joint) jusqu'au prochain reload.
|
||||||
|
if (tryWarnMissingJoint(armature.toString(), this.joint)) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"SpawnParticleAction : unknown joint '{}' on armature '{}', falling back to entity position "
|
||||||
|
+ "(further occurrences for this armature+joint suppressed until next /reload)",
|
||||||
|
this.joint, armature
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return entityPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO (D2 follow-up) : apply joint's model-space transform to entityPos
|
||||||
|
// via patch.getModelMatrix(partialTick). Needs a partialTick plumb
|
||||||
|
// through AnimationAction.execute which today only forwards elapsed.
|
||||||
|
return entityPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper de dedup pour le WARN « joint inconnu ». Retourne {@code true} si
|
||||||
|
* le couple {@code (armatureName, joint)} n'avait pas encore été warné — le
|
||||||
|
* caller doit alors logger ; sinon retourne {@code false} (déjà warné, on
|
||||||
|
* skip le log).
|
||||||
|
*
|
||||||
|
* <p>Extrait en helper static package-private pour pouvoir tester la
|
||||||
|
* sémantique de dedup directement (les tests purs ne peuvent pas exercer
|
||||||
|
* {@link #execute} qui requiert un {@link LivingEntityPatch} mocké et un
|
||||||
|
* {@link Level} client réels — runtime MC nécessaire). Le test exerce ce
|
||||||
|
* helper, et l'execute() délègue à ce même helper, donc la couverture de la
|
||||||
|
* logique critique (dedup) est complète.</p>
|
||||||
|
*
|
||||||
|
* @param armatureName le nom de l'armature (généralement
|
||||||
|
* {@code armature.toString()} qui retourne {@code this.name})
|
||||||
|
* @param joint le nom du joint introuvable
|
||||||
|
* @return {@code true} si c'est le premier miss pour ce couple
|
||||||
|
* (le caller doit logger), {@code false} sinon (déjà warné).
|
||||||
|
*/
|
||||||
|
static boolean tryWarnMissingJoint(String armatureName, String joint) {
|
||||||
|
return WARNED_MISSING_JOINTS.add(armatureName + ":" + joint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset le set dedup des WARN « joint inconnu ». Appelé par
|
||||||
|
* {@code TiedUpRigRegistryReloadListener.apply()} à chaque reload datapack /
|
||||||
|
* resource-pack, et par les tests pour garantir l'isolation entre cas.
|
||||||
|
*
|
||||||
|
* <p>Sans reset après {@code /reload}, un modder qui corrige son JSON puis
|
||||||
|
* re-casse l'asset ne reverrait jamais le WARN (l'ID resterait dans le set).
|
||||||
|
* Avec reset, le warn redéclenche après chaque reload — comportement attendu
|
||||||
|
* pour le feedback modder.</p>
|
||||||
|
*
|
||||||
|
* <p>Thread-safe via {@link ConcurrentHashMap#newKeySet()} — pas de
|
||||||
|
* synchronisation externe nécessaire.</p>
|
||||||
|
*/
|
||||||
|
public static void resetWarnedMissing() {
|
||||||
|
WARNED_MISSING_JOINTS.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.client;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
import com.google.gson.internal.Streams;
|
||||||
|
import com.google.gson.stream.JsonReader;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.packs.resources.Resource;
|
||||||
|
import net.minecraft.util.GsonHelper;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationManager;
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotion;
|
||||||
|
import com.tiedup.remake.rig.anim.TransformSheet;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.StaticAnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.anim.types.ActionAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DirectStaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.JsonAssetLoader;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.JointMaskEntry;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.JointMaskReloadListener;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.LayerInfo;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.TrailInfo;
|
||||||
|
import com.tiedup.remake.rig.exception.AssetLoadingException;
|
||||||
|
import com.tiedup.remake.rig.util.ParseUtil;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
public class AnimationSubFileReader {
|
||||||
|
public static final SubFileType<ClientProperty> SUBFILE_CLIENT_PROPERTY = new ClientPropertyType();
|
||||||
|
public static final SubFileType<PovSettings> SUBFILE_POV_ANIMATION = new PovAnimationType();
|
||||||
|
|
||||||
|
public static void readAndApply(StaticAnimation animation, Resource iresource, SubFileType<?> subFileType) {
|
||||||
|
InputStream inputstream = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
inputstream = iresource.open();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert inputstream != null : "Input stream is null";
|
||||||
|
|
||||||
|
try {
|
||||||
|
subFileType.apply(inputstream, animation);
|
||||||
|
} catch (JsonParseException e) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn("Can't read sub file " + subFileType.directory + " for " + animation);
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static abstract class SubFileType<T> {
|
||||||
|
private final String directory;
|
||||||
|
private final AnimationSubFileDeserializer<T> deserializer;
|
||||||
|
|
||||||
|
private SubFileType(String directory, AnimationSubFileDeserializer<T> deserializer) {
|
||||||
|
this.directory = directory;
|
||||||
|
this.deserializer = deserializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize from input stream
|
||||||
|
public void apply(InputStream inputstream, StaticAnimation animation) {
|
||||||
|
Reader reader = new InputStreamReader(inputstream, StandardCharsets.UTF_8);
|
||||||
|
JsonReader jsonReader = new JsonReader(reader);
|
||||||
|
jsonReader.setLenient(true);
|
||||||
|
T deserialized = this.deserializer.deserialize(animation, Streams.parse(jsonReader));
|
||||||
|
this.applySubFileInfo(deserialized, animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize from json object
|
||||||
|
public void apply(JsonElement jsonElement, StaticAnimation animation) {
|
||||||
|
T deserialized = this.deserializer.deserialize(animation, jsonElement);
|
||||||
|
this.applySubFileInfo(deserialized, animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void applySubFileInfo(T deserialized, StaticAnimation animation);
|
||||||
|
|
||||||
|
public String getDirectory() {
|
||||||
|
return this.directory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ClientProperty(LayerInfo layerInfo, LayerInfo multilayerInfo, List<TrailInfo> trailInfo) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ClientPropertyType extends SubFileType<ClientProperty> {
|
||||||
|
private ClientPropertyType() {
|
||||||
|
super("data", new AnimationSubFileReader.ClientAnimationPropertyDeserializer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applySubFileInfo(ClientProperty deserialized, StaticAnimation animation) {
|
||||||
|
if (deserialized.layerInfo() != null) {
|
||||||
|
if (deserialized.layerInfo().jointMaskEntry.isValid()) {
|
||||||
|
animation.addProperty(ClientAnimationProperties.JOINT_MASK, deserialized.layerInfo().jointMaskEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
animation.addProperty(ClientAnimationProperties.LAYER_TYPE, deserialized.layerInfo().layerType);
|
||||||
|
animation.addProperty(ClientAnimationProperties.PRIORITY, deserialized.layerInfo().priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deserialized.multilayerInfo() != null) {
|
||||||
|
DirectStaticAnimation multilayerAnimation = new DirectStaticAnimation(animation.getLocation(), animation.getTransitionTime(), animation.isRepeat(), animation.getRegistryName().toString() + "_multilayer", animation.getArmature());
|
||||||
|
|
||||||
|
if (deserialized.multilayerInfo().jointMaskEntry.isValid()) {
|
||||||
|
multilayerAnimation.addProperty(ClientAnimationProperties.JOINT_MASK, deserialized.multilayerInfo().jointMaskEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
multilayerAnimation.addProperty(ClientAnimationProperties.LAYER_TYPE, deserialized.multilayerInfo().layerType);
|
||||||
|
multilayerAnimation.addProperty(ClientAnimationProperties.PRIORITY, deserialized.multilayerInfo().priority);
|
||||||
|
multilayerAnimation.addProperty(StaticAnimationProperty.ELAPSED_TIME_MODIFIER, (self, entitypatch, speed, prevElapsedTime, elapsedTime) -> {
|
||||||
|
Layer baseLayer = entitypatch.getClientAnimator().baseLayer;
|
||||||
|
|
||||||
|
if (baseLayer.animationPlayer.getAnimation().get().getRealAnimation().get() != animation) {
|
||||||
|
return Pair.of(prevElapsedTime, elapsedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self.isStaticAnimation() && baseLayer.animationPlayer.getAnimation().get().isStaticAnimation()) {
|
||||||
|
return Pair.of(prevElapsedTime + speed, elapsedTime + speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pair.of(baseLayer.animationPlayer.getPrevElapsedTime(), baseLayer.animationPlayer.getElapsedTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
animation.addProperty(ClientAnimationProperties.MULTILAYER_ANIMATION, multilayerAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deserialized.trailInfo().size() > 0) {
|
||||||
|
animation.addProperty(ClientAnimationProperties.TRAIL_EFFECT, deserialized.trailInfo());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ClientAnimationPropertyDeserializer implements AnimationSubFileDeserializer<ClientProperty> {
|
||||||
|
private static LayerInfo deserializeLayerInfo(JsonObject jsonObject) {
|
||||||
|
return deserializeLayerInfo(jsonObject, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LayerInfo deserializeLayerInfo(JsonObject jsonObject, @Nullable Layer.LayerType defaultLayerType) {
|
||||||
|
JointMaskEntry.Builder builder = JointMaskEntry.builder();
|
||||||
|
Layer.Priority priority = jsonObject.has("priority") ? Layer.Priority.valueOf(GsonHelper.getAsString(jsonObject, "priority")) : null;
|
||||||
|
Layer.LayerType layerType = jsonObject.has("layer") ? Layer.LayerType.valueOf(GsonHelper.getAsString(jsonObject, "layer")) : Layer.LayerType.BASE_LAYER;
|
||||||
|
|
||||||
|
if (jsonObject.has("masks")) {
|
||||||
|
JsonArray maskArray = jsonObject.get("masks").getAsJsonArray();
|
||||||
|
|
||||||
|
if (!maskArray.isEmpty()) {
|
||||||
|
builder.defaultMask(JointMaskReloadListener.getNoneMask());
|
||||||
|
|
||||||
|
maskArray.forEach(element -> {
|
||||||
|
JsonObject jointMaskEntry = element.getAsJsonObject();
|
||||||
|
String livingMotionName = GsonHelper.getAsString(jointMaskEntry, "livingmotion");
|
||||||
|
String type = GsonHelper.getAsString(jointMaskEntry, "type");
|
||||||
|
|
||||||
|
if (!type.contains(":")) {
|
||||||
|
type = (new StringBuilder(TiedUpRigConstants.MODID)).append(":").append(type).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (livingMotionName.equals("ALL")) {
|
||||||
|
builder.defaultMask(JointMaskReloadListener.getJointMaskEntry(type));
|
||||||
|
} else {
|
||||||
|
builder.mask((LivingMotion) LivingMotion.ENUM_MANAGER.getOrThrow(livingMotionName), JointMaskReloadListener.getJointMaskEntry(type));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LayerInfo(builder.create(), priority, (defaultLayerType == null) ? layerType : defaultLayerType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientProperty deserialize(StaticAnimation animation, JsonElement json) throws JsonParseException {
|
||||||
|
JsonObject jsonObject = json.getAsJsonObject();
|
||||||
|
LayerInfo layerInfo = null;
|
||||||
|
LayerInfo multilayerInfo = null;
|
||||||
|
|
||||||
|
if (jsonObject.has("multilayer")) {
|
||||||
|
JsonObject multiplayerJson = jsonObject.get("multilayer").getAsJsonObject();
|
||||||
|
layerInfo = deserializeLayerInfo(multiplayerJson.get("base").getAsJsonObject());
|
||||||
|
multilayerInfo = deserializeLayerInfo(multiplayerJson.get("composite").getAsJsonObject(), Layer.LayerType.COMPOSITE_LAYER);
|
||||||
|
} else {
|
||||||
|
layerInfo = deserializeLayerInfo(jsonObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TrailInfo> trailInfos = Lists.newArrayList();
|
||||||
|
|
||||||
|
if (jsonObject.has("trail_effects")) {
|
||||||
|
JsonArray trailArray = jsonObject.get("trail_effects").getAsJsonArray();
|
||||||
|
trailArray.forEach(element -> trailInfos.add(TrailInfo.deserialize(element)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClientProperty(layerInfo, multilayerInfo, trailInfos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static record PovSettings(
|
||||||
|
@Nullable TransformSheet cameraTransform,
|
||||||
|
Map<String, Boolean> visibilities,
|
||||||
|
RootTransformation rootTransformation,
|
||||||
|
@Nullable ViewLimit viewLimit,
|
||||||
|
boolean visibilityOthers,
|
||||||
|
boolean hasUniqueAnimation,
|
||||||
|
boolean syncFrame
|
||||||
|
) {
|
||||||
|
public enum RootTransformation {
|
||||||
|
CAMERA, WORLD
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ViewLimit(float xRotMin, float xRotMax, float yRotMin, float yRotMax) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PovAnimationType extends SubFileType<PovSettings> {
|
||||||
|
private PovAnimationType() {
|
||||||
|
super("pov", new AnimationSubFileReader.PovAnimationDeserializer());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void applySubFileInfo(PovSettings deserialized, StaticAnimation animation) {
|
||||||
|
ResourceLocation povAnimationLocation = deserialized.hasUniqueAnimation() ? AnimationManager.getSubAnimationFileLocation(animation.getLocation(), SUBFILE_POV_ANIMATION) : animation.getLocation();
|
||||||
|
DirectStaticAnimation povAnimation = new DirectStaticAnimation(povAnimationLocation, animation.getTransitionTime(), animation.isRepeat(), animation.getRegistryName().toString() + "_pov", animation.getArmature()) {
|
||||||
|
@Override
|
||||||
|
public float getPlaySpeed(LivingEntityPatch<?> entitypatch, DynamicAnimation pAnimation) {
|
||||||
|
return animation.getPlaySpeed(entitypatch, pAnimation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
animation.getProperty(StaticAnimationProperty.PLAY_SPEED_MODIFIER).ifPresent(speedModifier -> {
|
||||||
|
povAnimation.addProperty(StaticAnimationProperty.PLAY_SPEED_MODIFIER, speedModifier);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deserialized.syncFrame()) {
|
||||||
|
animation.getProperty(StaticAnimationProperty.ELAPSED_TIME_MODIFIER).ifPresent(elapsedTimeModifier -> {
|
||||||
|
povAnimation.addProperty(StaticAnimationProperty.ELAPSED_TIME_MODIFIER, elapsedTimeModifier);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
animation.addProperty(ClientAnimationProperties.POV_ANIMATION, povAnimation);
|
||||||
|
animation.addProperty(ClientAnimationProperties.POV_SETTINGS, deserialized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class PovAnimationDeserializer implements AnimationSubFileDeserializer<PovSettings> {
|
||||||
|
@Override
|
||||||
|
public PovSettings deserialize(StaticAnimation animation, JsonElement json) throws AssetLoadingException, JsonParseException {
|
||||||
|
JsonObject jObject = json.getAsJsonObject();
|
||||||
|
TransformSheet cameraTransform = null;
|
||||||
|
PovSettings.ViewLimit viewLimit = null;
|
||||||
|
PovSettings.RootTransformation rootTrasnformation = null;
|
||||||
|
|
||||||
|
if (jObject.has("root")) {
|
||||||
|
rootTrasnformation = PovSettings.RootTransformation.valueOf(ParseUtil.toUpperCase(GsonHelper.getAsString(jObject, "root")));
|
||||||
|
} else {
|
||||||
|
if (animation instanceof ActionAnimation) {
|
||||||
|
rootTrasnformation = PovSettings.RootTransformation.WORLD;
|
||||||
|
} else {
|
||||||
|
rootTrasnformation = PovSettings.RootTransformation.CAMERA;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jObject.has("camera")) {
|
||||||
|
JsonObject cameraTransformJObject = jObject.getAsJsonObject("camera");
|
||||||
|
cameraTransform = JsonAssetLoader.getTransformSheet(cameraTransformJObject, null, false, JsonAssetLoader.TransformFormat.ATTRIBUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImmutableMap.Builder<String, Boolean> visibilitiesBuilder = ImmutableMap.builder();
|
||||||
|
boolean others = false;
|
||||||
|
|
||||||
|
if (jObject.has("visibilities")) {
|
||||||
|
JsonObject visibilitiesObject = jObject.getAsJsonObject("visibilities");
|
||||||
|
visibilitiesObject.entrySet().stream().filter((e) -> !"others".equals(e.getKey())).forEach((entry) -> visibilitiesBuilder.put(entry.getKey(), entry.getValue().getAsBoolean()));
|
||||||
|
others = visibilitiesObject.get("others").getAsBoolean();
|
||||||
|
} else {
|
||||||
|
visibilitiesBuilder.put("leftArm", true);
|
||||||
|
visibilitiesBuilder.put("leftSleeve", true);
|
||||||
|
visibilitiesBuilder.put("rightArm", true);
|
||||||
|
visibilitiesBuilder.put("rightSleeve", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jObject.has("limited_view_degrees")) {
|
||||||
|
JsonObject limitedViewDegrees = jObject.getAsJsonObject("limited_view_degrees");
|
||||||
|
JsonArray xRot = limitedViewDegrees.get("xRot").getAsJsonArray();
|
||||||
|
JsonArray yRot = limitedViewDegrees.get("yRot").getAsJsonArray();
|
||||||
|
|
||||||
|
float xRotMin = Math.min(xRot.get(0).getAsFloat(), xRot.get(1).getAsFloat());
|
||||||
|
float xRotMax = Math.max(xRot.get(0).getAsFloat(), xRot.get(1).getAsFloat());
|
||||||
|
float yRotMin = Math.min(yRot.get(0).getAsFloat(), yRot.get(1).getAsFloat());
|
||||||
|
float yRotMax = Math.max(yRot.get(0).getAsFloat(), yRot.get(1).getAsFloat());
|
||||||
|
viewLimit = new PovSettings.ViewLimit(xRotMin, xRotMax, yRotMin, yRotMax);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PovSettings(cameraTransform, visibilitiesBuilder.build(), rootTrasnformation, viewLimit, others, jObject.has("animation"), GsonHelper.getAsBoolean(jObject, "sync_frame", false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface AnimationSubFileDeserializer<T> {
|
||||||
|
public T deserialize(StaticAnimation animation, JsonElement json) throws JsonParseException;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,562 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.client;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
|
||||||
|
import net.minecraft.client.renderer.MultiBufferSource;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationManager;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationPlayer;
|
||||||
|
import com.tiedup.remake.rig.anim.Animator;
|
||||||
|
import com.tiedup.remake.rig.armature.Joint;
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotion;
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotions;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.ServerAnimator;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.EntityState;
|
||||||
|
import com.tiedup.remake.rig.anim.types.EntityState.StateFactor;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.anim.client.Layer.Priority;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.JointMask.BindModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.JointMask.JointMaskSet;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.JointMaskEntry;
|
||||||
|
import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigRegistry;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
public class ClientAnimator extends Animator {
|
||||||
|
public static Animator getAnimator(LivingEntityPatch<?> entitypatch) {
|
||||||
|
return entitypatch.isLogicalClient() ? new ClientAnimator(entitypatch) : ServerAnimator.getAnimator(entitypatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<LivingMotion, AssetAccessor<? extends StaticAnimation>> compositeLivingAnimations;
|
||||||
|
private final Map<LivingMotion, AssetAccessor<? extends StaticAnimation>> defaultLivingAnimations;
|
||||||
|
private final Map<LivingMotion, AssetAccessor<? extends StaticAnimation>> defaultCompositeLivingAnimations;
|
||||||
|
public final Layer.BaseLayer baseLayer;
|
||||||
|
private LivingMotion currentMotion;
|
||||||
|
private LivingMotion currentCompositeMotion;
|
||||||
|
private boolean hardPaused;
|
||||||
|
|
||||||
|
public ClientAnimator(LivingEntityPatch<?> entitypatch) {
|
||||||
|
this(entitypatch, Layer.BaseLayer::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientAnimator(LivingEntityPatch<?> entitypatch, Supplier<Layer.BaseLayer> layerSupplier) {
|
||||||
|
super(entitypatch);
|
||||||
|
|
||||||
|
this.currentMotion = LivingMotions.IDLE;
|
||||||
|
this.currentCompositeMotion = LivingMotions.IDLE;
|
||||||
|
this.compositeLivingAnimations = Maps.newHashMap();
|
||||||
|
this.defaultLivingAnimations = Maps.newHashMap();
|
||||||
|
this.defaultCompositeLivingAnimations = Maps.newHashMap();
|
||||||
|
this.baseLayer = layerSupplier.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Play an animation by animation instance **/
|
||||||
|
@Override
|
||||||
|
public void playAnimation(AssetAccessor<? extends StaticAnimation> nextAnimation, float transitionTimeModifier) {
|
||||||
|
Layer layer = nextAnimation.get().getLayerType() == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(nextAnimation.get().getPriority());
|
||||||
|
layer.paused = false;
|
||||||
|
layer.playAnimation(nextAnimation, this.entitypatch, transitionTimeModifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
// RIG : playAnimationAt(..., AnimatorControlPacket.Layer, AnimatorControlPacket.Priority)
|
||||||
|
// strippé — re-implem Phase 2 avec packet dédié. Voir AnimationVariablePacket.
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playAnimationInstantly(AssetAccessor<? extends StaticAnimation> nextAnimation) {
|
||||||
|
Layer layer = nextAnimation.get().getLayerType() == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(nextAnimation.get().getPriority());
|
||||||
|
layer.paused = false;
|
||||||
|
layer.playAnimationInstantly(nextAnimation, this.entitypatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reserveAnimation(AssetAccessor<? extends StaticAnimation> nextAnimation) {
|
||||||
|
Layer layer = nextAnimation.get().getLayerType() == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(nextAnimation.get().getPriority());
|
||||||
|
|
||||||
|
if (nextAnimation.get().getPriority().isHigherThan(layer.animationPlayer.getRealAnimation().get().getPriority())) {
|
||||||
|
if (!layer.animationPlayer.isEnd() && layer.animationPlayer.getAnimation() != null) {
|
||||||
|
layer.animationPlayer.getAnimation().get().end(this.entitypatch, nextAnimation, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.animationPlayer.terminate(this.entitypatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.nextAnimation = nextAnimation;
|
||||||
|
layer.paused = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean stopPlaying(AssetAccessor<? extends StaticAnimation> targetAnimation) {
|
||||||
|
Layer layer = targetAnimation.get().getLayerType() == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(targetAnimation.get().getPriority());
|
||||||
|
|
||||||
|
if (layer.animationPlayer.getRealAnimation() == targetAnimation) {
|
||||||
|
layer.animationPlayer.terminate(this.entitypatch);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSoftPause(boolean paused) {
|
||||||
|
this.iterAllLayers(layer -> layer.paused = paused);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHardPause(boolean paused) {
|
||||||
|
this.hardPaused = paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addLivingAnimation(LivingMotion livingMotion, AssetAccessor<? extends StaticAnimation> animation) {
|
||||||
|
if (AnimationManager.checkNull(animation)) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn("Unable to put an empty animation for " + livingMotion);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Layer.LayerType layerType = animation.get().getLayerType();
|
||||||
|
boolean isBaseLayer = (layerType == Layer.LayerType.BASE_LAYER);
|
||||||
|
|
||||||
|
Map<LivingMotion, AssetAccessor<? extends StaticAnimation>> storage = layerType == Layer.LayerType.BASE_LAYER ? this.livingAnimations : this.compositeLivingAnimations;
|
||||||
|
LivingMotion compareMotion = layerType == Layer.LayerType.BASE_LAYER ? this.currentMotion : this.currentCompositeMotion;
|
||||||
|
Layer layer = layerType == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(animation.get().getPriority());
|
||||||
|
storage.put(livingMotion, animation);
|
||||||
|
|
||||||
|
if (livingMotion == compareMotion) {
|
||||||
|
EntityState state = this.getEntityState();
|
||||||
|
|
||||||
|
if (!state.inaction()) {
|
||||||
|
layer.playLivingAnimation(animation, this.entitypatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBaseLayer) {
|
||||||
|
animation.get().getProperty(ClientAnimationProperties.MULTILAYER_ANIMATION).ifPresent(multilayerAnimation -> {
|
||||||
|
this.compositeLivingAnimations.put(livingMotion, multilayerAnimation);
|
||||||
|
|
||||||
|
if (livingMotion == this.currentCompositeMotion) {
|
||||||
|
EntityState state = this.getEntityState();
|
||||||
|
|
||||||
|
if (!state.inaction()) {
|
||||||
|
layer.playLivingAnimation(multilayerAnimation, this.entitypatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentMotionsAsDefault() {
|
||||||
|
this.defaultLivingAnimations.putAll(this.livingAnimations);
|
||||||
|
this.defaultCompositeLivingAnimations.putAll(this.compositeLivingAnimations);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resetLivingAnimations() {
|
||||||
|
super.resetLivingAnimations();
|
||||||
|
this.compositeLivingAnimations.clear();
|
||||||
|
this.defaultLivingAnimations.forEach((key, val) -> this.addLivingAnimation(key, val));
|
||||||
|
this.defaultCompositeLivingAnimations.forEach((key, val) -> this.addLivingAnimation(key, val));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetAccessor<? extends StaticAnimation> getLivingMotion(LivingMotion motion) {
|
||||||
|
return this.livingAnimations.getOrDefault(motion, this.livingAnimations.get(LivingMotions.IDLE));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetAccessor<? extends StaticAnimation> getCompositeLivingMotion(LivingMotion motion) {
|
||||||
|
return this.compositeLivingAnimations.get(motion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit() {
|
||||||
|
super.postInit();
|
||||||
|
|
||||||
|
this.setCurrentMotionsAsDefault();
|
||||||
|
|
||||||
|
AssetAccessor<? extends StaticAnimation> idleMotion = this.livingAnimations.get(this.currentMotion);
|
||||||
|
this.baseLayer.playAnimationInstantly(idleMotion, this.entitypatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick() {
|
||||||
|
/**
|
||||||
|
// Layer debugging
|
||||||
|
for (Layer layer : this.getAllLayers()) {
|
||||||
|
System.out.println(layer);
|
||||||
|
}
|
||||||
|
System.out.println();
|
||||||
|
**/
|
||||||
|
|
||||||
|
if (this.hardPaused) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.baseLayer.update(this.entitypatch);
|
||||||
|
|
||||||
|
if (this.baseLayer.animationPlayer.isEnd() && this.baseLayer.nextAnimation == null && this.currentMotion != LivingMotions.DEATH) {
|
||||||
|
this.entitypatch.updateMotion(false);
|
||||||
|
|
||||||
|
if (this.compositeLivingAnimations.containsKey(this.entitypatch.currentCompositeMotion)) {
|
||||||
|
this.playAnimation(this.getCompositeLivingMotion(this.entitypatch.currentCompositeMotion), 0.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.baseLayer.playAnimation(this.getLivingMotion(this.entitypatch.currentLivingMotion), this.entitypatch, 0.0F);
|
||||||
|
} else {
|
||||||
|
if (!this.compareCompositeMotion(this.entitypatch.currentCompositeMotion)) {
|
||||||
|
/* Turns off the multilayer of the base layer */
|
||||||
|
this.getLivingMotion(this.currentCompositeMotion).get().getProperty(ClientAnimationProperties.MULTILAYER_ANIMATION).ifPresent((multilayerAnimation) -> {
|
||||||
|
if (!this.compositeLivingAnimations.containsKey(this.entitypatch.currentCompositeMotion)) {
|
||||||
|
this.getCompositeLayer(multilayerAnimation.get().getPriority()).off(this.entitypatch);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.compositeLivingAnimations.containsKey(this.currentCompositeMotion)) {
|
||||||
|
AssetAccessor<? extends StaticAnimation> nextLivingAnimation = this.getCompositeLivingMotion(this.entitypatch.currentCompositeMotion);
|
||||||
|
|
||||||
|
if (nextLivingAnimation == null || nextLivingAnimation.get().getPriority() != this.getCompositeLivingMotion(this.currentCompositeMotion).get().getPriority()) {
|
||||||
|
this.getCompositeLayer(this.getCompositeLivingMotion(this.currentCompositeMotion).get().getPriority()).off(this.entitypatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.compositeLivingAnimations.containsKey(this.entitypatch.currentCompositeMotion)) {
|
||||||
|
this.playAnimation(this.getCompositeLivingMotion(this.entitypatch.currentCompositeMotion), 0.0F);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.compareMotion(this.entitypatch.currentLivingMotion) && this.entitypatch.currentLivingMotion != LivingMotions.DEATH) {
|
||||||
|
if (this.livingAnimations.containsKey(this.entitypatch.currentLivingMotion)) {
|
||||||
|
this.baseLayer.playAnimation(this.getLivingMotion(this.entitypatch.currentLivingMotion), this.entitypatch, 0.0F);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentMotion = this.entitypatch.currentLivingMotion;
|
||||||
|
this.currentCompositeMotion = this.entitypatch.currentCompositeMotion;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playDeathAnimation() {
|
||||||
|
if (!this.getPlayerFor(null).getAnimation().get().getProperty(ActionAnimationProperty.IS_DEATH_ANIMATION).orElse(false)) {
|
||||||
|
this.playAnimation(this.livingAnimations.getOrDefault(LivingMotions.DEATH, TiedUpRigRegistry.EMPTY_ANIMATION), 0.0F);
|
||||||
|
this.currentMotion = LivingMotions.DEATH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Layer getCompositeLayer(Layer.Priority priority) {
|
||||||
|
return this.baseLayer.compositeLayers.get(priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void renderDebuggingInfoForAllLayers(PoseStack poseStack, MultiBufferSource buffer, float partialTicks) {
|
||||||
|
this.iterAllLayers((layer) -> {
|
||||||
|
if (layer.isOff()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimationPlayer animPlayer = layer.animationPlayer;
|
||||||
|
float playTime = Mth.lerp(partialTicks, animPlayer.getPrevElapsedTime(), animPlayer.getElapsedTime());
|
||||||
|
animPlayer.getAnimation().get().renderDebugging(poseStack, buffer, entitypatch, playTime, partialTicks);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates all layers
|
||||||
|
* @param task
|
||||||
|
*/
|
||||||
|
public void iterAllLayers(Consumer<Layer> task) {
|
||||||
|
task.accept(this.baseLayer);
|
||||||
|
this.baseLayer.compositeLayers.values().forEach(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates all activated layers from the highest layer
|
||||||
|
* when base layer = highest, iterates only base layer
|
||||||
|
* when base layer = middle, iterates base layer and highest composite layer
|
||||||
|
* when base layer = lowest, iterates base layer and all composite layers
|
||||||
|
*
|
||||||
|
* @param task
|
||||||
|
* @return true if all layers didn't return false by @param task
|
||||||
|
*/
|
||||||
|
public boolean iterVisibleLayersUntilFalse(Function<Layer, Boolean> task) {
|
||||||
|
Layer.Priority[] highers = this.baseLayer.baseLayerPriority.highers();
|
||||||
|
|
||||||
|
for (int i = highers.length - 1; i >= 0; i--) {
|
||||||
|
Layer layer = this.baseLayer.getLayer(highers[i]);
|
||||||
|
|
||||||
|
if (layer.isDisabled() || layer.animationPlayer.isEmpty()) {
|
||||||
|
if (highers[i] == this.baseLayer.baseLayerPriority) {
|
||||||
|
return task.apply(this.baseLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!task.apply(layer)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (highers[i] == this.baseLayer.baseLayerPriority) {
|
||||||
|
return task.apply(this.baseLayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pose getPose(float partialTicks) {
|
||||||
|
return this.getPose(partialTicks, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pose getPose(float partialTicks, boolean useCurrentMotion) {
|
||||||
|
Pose composedPose = new Pose();
|
||||||
|
Pose baseLayerPose = this.baseLayer.getEnabledPose(this.entitypatch, useCurrentMotion, partialTicks);
|
||||||
|
|
||||||
|
Map<Layer.Priority, Pair<AssetAccessor<? extends DynamicAnimation>, Pose>> layerPoses = Maps.newLinkedHashMap();
|
||||||
|
composedPose.load(baseLayerPose, Pose.LoadOperation.OVERWRITE);
|
||||||
|
|
||||||
|
for (Layer.Priority priority : this.baseLayer.baseLayerPriority.highers()) {
|
||||||
|
Layer compositeLayer = this.baseLayer.compositeLayers.get(priority);
|
||||||
|
|
||||||
|
if (!compositeLayer.isDisabled() && !compositeLayer.animationPlayer.isEmpty()) {
|
||||||
|
Pose layerPose = compositeLayer.getEnabledPose(this.entitypatch, useCurrentMotion, partialTicks);
|
||||||
|
layerPoses.put(priority, Pair.of(compositeLayer.animationPlayer.getAnimation(), layerPose));
|
||||||
|
composedPose.load(layerPose, Pose.LoadOperation.OVERWRITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Joint rootJoint = this.entitypatch.getArmature().rootJoint;
|
||||||
|
this.applyBindModifier(baseLayerPose, composedPose, rootJoint, layerPoses, useCurrentMotion);
|
||||||
|
|
||||||
|
return composedPose;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pose getComposedLayerPoseBelow(Layer.Priority priorityLimit, float partialTicks) {
|
||||||
|
Pose composedPose = this.baseLayer.getEnabledPose(this.entitypatch, true, partialTicks);
|
||||||
|
Pose baseLayerPose = this.baseLayer.getEnabledPose(this.entitypatch, true, partialTicks);
|
||||||
|
Map<Layer.Priority, Pair<AssetAccessor<? extends DynamicAnimation>, Pose>> layerPoses = Maps.newLinkedHashMap();
|
||||||
|
|
||||||
|
for (Layer.Priority priority : priorityLimit.lowers()) {
|
||||||
|
Layer compositeLayer = this.baseLayer.compositeLayers.get(priority);
|
||||||
|
|
||||||
|
if (!compositeLayer.isDisabled()) {
|
||||||
|
Pose layerPose = compositeLayer.getEnabledPose(this.entitypatch, true, partialTicks);
|
||||||
|
layerPoses.put(priority, Pair.of(compositeLayer.animationPlayer.getAnimation(), layerPose));
|
||||||
|
composedPose.load(layerPose, Pose.LoadOperation.OVERWRITE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!layerPoses.isEmpty()) {
|
||||||
|
this.applyBindModifier(baseLayerPose, composedPose, this.entitypatch.getArmature().rootJoint, layerPoses, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return composedPose;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyBindModifier(Pose basePose, Pose result, Joint joint, Map<Layer.Priority, Pair<AssetAccessor<? extends DynamicAnimation>, Pose>> poses, boolean useCurrentMotion) {
|
||||||
|
List<Priority> list = Lists.newArrayList(poses.keySet());
|
||||||
|
Collections.reverse(list);
|
||||||
|
|
||||||
|
for (Layer.Priority priority : list) {
|
||||||
|
AssetAccessor<? extends DynamicAnimation> nowPlaying = poses.get(priority).getFirst();
|
||||||
|
JointMaskEntry jointMaskEntry = nowPlaying.get().getJointMaskEntry(this.entitypatch, useCurrentMotion).orElse(null);
|
||||||
|
|
||||||
|
if (jointMaskEntry != null) {
|
||||||
|
LivingMotion livingMotion = this.getCompositeLayer(priority).getLivingMotion(this.entitypatch, useCurrentMotion);
|
||||||
|
|
||||||
|
if (nowPlaying.get().hasTransformFor(joint.getName()) && !jointMaskEntry.isMasked(livingMotion, joint.getName())) {
|
||||||
|
JointMaskSet jointmaskset = jointMaskEntry.getMask(livingMotion);
|
||||||
|
BindModifier bindModifier = jointmaskset.getBindModifier(joint.getName());
|
||||||
|
|
||||||
|
if (bindModifier != null) {
|
||||||
|
bindModifier.modify(this.entitypatch, basePose, result, livingMotion, jointMaskEntry, priority, joint, poses);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Joint subJoints : joint.getSubJoints()) {
|
||||||
|
this.applyBindModifier(basePose, result, subJoints, poses, useCurrentMotion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean compareMotion(LivingMotion motion) {
|
||||||
|
return this.currentMotion.isSame(motion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean compareCompositeMotion(LivingMotion motion) {
|
||||||
|
return this.currentCompositeMotion.isSame(motion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forceResetBeforeAction(LivingMotion livingMotion, LivingMotion compositeLivingMotion) {
|
||||||
|
if (!this.currentMotion.equals(livingMotion)) {
|
||||||
|
if (this.livingAnimations.containsKey(livingMotion)) {
|
||||||
|
this.baseLayer.playAnimation(this.getLivingMotion(livingMotion), this.entitypatch, 0.0F);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.entitypatch.currentLivingMotion = livingMotion;
|
||||||
|
this.currentMotion = livingMotion;
|
||||||
|
|
||||||
|
if (!this.currentCompositeMotion.equals(compositeLivingMotion)) {
|
||||||
|
if (this.compositeLivingAnimations.containsKey(this.currentCompositeMotion)) {
|
||||||
|
this.getCompositeLayer(this.getCompositeLivingMotion(this.currentCompositeMotion).get().getPriority()).off(this.entitypatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.compositeLivingAnimations.containsKey(compositeLivingMotion)) {
|
||||||
|
this.playAnimation(this.getCompositeLivingMotion(compositeLivingMotion), 0.0F);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentCompositeMotion = LivingMotions.NONE;
|
||||||
|
this.entitypatch.currentCompositeMotion = LivingMotions.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetMotion(boolean resetPrevMotion) {
|
||||||
|
if (resetPrevMotion) this.currentMotion = LivingMotions.IDLE;
|
||||||
|
this.entitypatch.currentLivingMotion = LivingMotions.IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetCompositeMotion() {
|
||||||
|
if (this.currentCompositeMotion != LivingMotions.IDLE && this.compositeLivingAnimations.containsKey(this.currentCompositeMotion)) {
|
||||||
|
AssetAccessor<? extends StaticAnimation> currentPlaying = this.getCompositeLivingMotion(this.currentCompositeMotion);
|
||||||
|
AssetAccessor<? extends StaticAnimation> resetPlaying = this.getCompositeLivingMotion(LivingMotions.IDLE);
|
||||||
|
|
||||||
|
if (resetPlaying != null && currentPlaying != resetPlaying) {
|
||||||
|
this.playAnimation(resetPlaying, 0.0F);
|
||||||
|
} else if (currentPlaying != null) {
|
||||||
|
this.getCompositeLayer(currentPlaying.get().getPriority()).off(this.entitypatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentCompositeMotion = LivingMotions.NONE;
|
||||||
|
this.entitypatch.currentCompositeMotion = LivingMotions.NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void offAllLayers() {
|
||||||
|
for (Layer layer : this.baseLayer.compositeLayers.values()) {
|
||||||
|
layer.off(this.entitypatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playShootingAnimation() {
|
||||||
|
if (this.compositeLivingAnimations.containsKey(LivingMotions.SHOT)) {
|
||||||
|
this.playAnimation(this.compositeLivingAnimations.get(LivingMotions.SHOT), 0.0F);
|
||||||
|
this.entitypatch.currentCompositeMotion = LivingMotions.NONE;
|
||||||
|
this.currentCompositeMotion = LivingMotions.NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimationPlayer getPlayerFor(AssetAccessor<? extends DynamicAnimation> playingAnimation) {
|
||||||
|
if (playingAnimation == null) {
|
||||||
|
return this.baseLayer.animationPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
DynamicAnimation animation = playingAnimation.get();
|
||||||
|
|
||||||
|
if (animation instanceof StaticAnimation staticAnimation) {
|
||||||
|
Layer layer = staticAnimation.getLayerType() == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(staticAnimation.getPriority());
|
||||||
|
if (layer.animationPlayer.getAnimation() == playingAnimation) return layer.animationPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Layer layer : this.baseLayer.compositeLayers.values()) {
|
||||||
|
if (layer.animationPlayer.getRealAnimation().equals(playingAnimation)) {
|
||||||
|
return layer.animationPlayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.baseLayer.animationPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<AnimationPlayer> getPlayer(AssetAccessor<? extends DynamicAnimation> playingAnimation) {
|
||||||
|
DynamicAnimation animation = playingAnimation.get();
|
||||||
|
|
||||||
|
if (animation instanceof StaticAnimation staticAnimation) {
|
||||||
|
Layer layer = staticAnimation.getLayerType() == Layer.LayerType.BASE_LAYER ? this.baseLayer : this.baseLayer.compositeLayers.get(staticAnimation.getPriority());
|
||||||
|
|
||||||
|
if (layer.animationPlayer.getRealAnimation().equals(playingAnimation)) {
|
||||||
|
return Optional.of(layer.animationPlayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.baseLayer.animationPlayer.getRealAnimation().equals(playingAnimation.get().getRealAnimation())) {
|
||||||
|
return Optional.of(this.baseLayer.animationPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Layer layer : this.baseLayer.compositeLayers.values()) {
|
||||||
|
if (layer.animationPlayer.getRealAnimation().equals(playingAnimation.get().getRealAnimation())) {
|
||||||
|
return Optional.of(layer.animationPlayer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LivingMotion currentMotion() {
|
||||||
|
return this.currentMotion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LivingMotion currentCompositeMotion() {
|
||||||
|
return this.currentCompositeMotion;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> Pair<AnimationPlayer, T> findFor(Class<T> animationType) {
|
||||||
|
for (Layer layer : this.baseLayer.compositeLayers.values()) {
|
||||||
|
if (animationType.isAssignableFrom(layer.animationPlayer.getAnimation().getClass())) {
|
||||||
|
return Pair.of(layer.animationPlayer, (T)layer.animationPlayer.getAnimation());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return animationType.isAssignableFrom(this.baseLayer.animationPlayer.getAnimation().getClass()) ? Pair.of(this.baseLayer.animationPlayer, (T)this.baseLayer.animationPlayer.getAnimation()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EntityState getEntityState() {
|
||||||
|
TypeFlexibleHashMap<StateFactor<?>> stateMap = new TypeFlexibleHashMap<> (false);
|
||||||
|
|
||||||
|
for (Layer layer : this.baseLayer.compositeLayers.values()) {
|
||||||
|
if (this.baseLayer.baseLayerPriority.isHigherThan(layer.priority)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!layer.isOff()) {
|
||||||
|
stateMap.putAll(layer.animationPlayer.getAnimation().get().getStatesMap(this.entitypatch, layer.animationPlayer.getElapsedTime()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// put base layer states
|
||||||
|
if (layer.priority == this.baseLayer.baseLayerPriority) {
|
||||||
|
stateMap.putAll(this.baseLayer.animationPlayer.getAnimation().get().getStatesMap(this.entitypatch, this.baseLayer.animationPlayer.getElapsedTime()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EntityState(stateMap);
|
||||||
|
}
|
||||||
|
}
|
||||||
359
src/main/java/com/tiedup/remake/rig/anim/client/Layer.java
Normal file
359
src/main/java/com/tiedup/remake/rig/anim/client/Layer.java
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.client;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationPlayer;
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotion;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.types.ConcurrentLinkAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.LayerOffAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.LinkAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigRegistry;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
public class Layer {
|
||||||
|
protected AssetAccessor<? extends StaticAnimation> nextAnimation;
|
||||||
|
protected final LinkAnimation linkAnimation;
|
||||||
|
protected final ConcurrentLinkAnimation concurrentLinkAnimation;
|
||||||
|
protected final LayerOffAnimation layerOffAnimation;
|
||||||
|
protected final Layer.Priority priority;
|
||||||
|
protected boolean disabled;
|
||||||
|
protected boolean paused;
|
||||||
|
public final AnimationPlayer animationPlayer;
|
||||||
|
|
||||||
|
public Layer(Priority priority) {
|
||||||
|
this(priority, AnimationPlayer::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Layer(Priority priority, Supplier<AnimationPlayer> animationPlayerProvider) {
|
||||||
|
this.animationPlayer = animationPlayerProvider.get();
|
||||||
|
this.linkAnimation = new LinkAnimation();
|
||||||
|
this.concurrentLinkAnimation = new ConcurrentLinkAnimation();
|
||||||
|
this.layerOffAnimation = new LayerOffAnimation(priority);
|
||||||
|
this.priority = priority;
|
||||||
|
this.disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void playAnimation(AssetAccessor<? extends StaticAnimation> nextAnimation, LivingEntityPatch<?> entitypatch, float transitionTimeModifier) {
|
||||||
|
// Get pose before StaticAnimation#end is called
|
||||||
|
Pose lastPose = this.getCurrentPose(entitypatch);
|
||||||
|
|
||||||
|
if (!this.animationPlayer.isEnd()) {
|
||||||
|
this.animationPlayer.getAnimation().get().end(entitypatch, nextAnimation, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resume();
|
||||||
|
nextAnimation.get().begin(entitypatch);
|
||||||
|
|
||||||
|
if (!nextAnimation.get().isMetaAnimation()) {
|
||||||
|
this.setLinkAnimation(nextAnimation, entitypatch, lastPose, transitionTimeModifier);
|
||||||
|
this.linkAnimation.putOnPlayer(this.animationPlayer, entitypatch);
|
||||||
|
entitypatch.updateEntityState();
|
||||||
|
this.nextAnimation = nextAnimation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays an animation without a link animation
|
||||||
|
*/
|
||||||
|
public void playAnimationInstantly(AssetAccessor<? extends DynamicAnimation> nextAnimation, LivingEntityPatch<?> entitypatch) {
|
||||||
|
if (!this.animationPlayer.isEnd()) {
|
||||||
|
this.animationPlayer.getAnimation().get().end(entitypatch, nextAnimation, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resume();
|
||||||
|
|
||||||
|
nextAnimation.get().begin(entitypatch);
|
||||||
|
nextAnimation.get().putOnPlayer(this.animationPlayer, entitypatch);
|
||||||
|
entitypatch.updateEntityState();
|
||||||
|
this.nextAnimation = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void playLivingAnimation(AssetAccessor<? extends StaticAnimation> nextAnimation, LivingEntityPatch<?> entitypatch) {
|
||||||
|
if (!this.animationPlayer.isEnd()) {
|
||||||
|
this.animationPlayer.getAnimation().get().end(entitypatch, nextAnimation, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resume();
|
||||||
|
nextAnimation.get().begin(entitypatch);
|
||||||
|
|
||||||
|
if (!nextAnimation.get().isMetaAnimation()) {
|
||||||
|
this.concurrentLinkAnimation.acceptFrom(this.animationPlayer.getRealAnimation(), nextAnimation, this.animationPlayer.getElapsedTime());
|
||||||
|
this.concurrentLinkAnimation.putOnPlayer(this.animationPlayer, entitypatch);
|
||||||
|
entitypatch.updateEntityState();
|
||||||
|
this.nextAnimation = nextAnimation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Pose getCurrentPose(LivingEntityPatch<?> entitypatch) {
|
||||||
|
return entitypatch.getClientAnimator().getPose(0.0F, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setLinkAnimation(AssetAccessor<? extends StaticAnimation> nextAnimation, LivingEntityPatch<?> entitypatch, Pose lastPose, float transitionTimeModifier) {
|
||||||
|
AssetAccessor<? extends DynamicAnimation> fromAnimation = this.animationPlayer.isEmpty() ? entitypatch.getClientAnimator().baseLayer.animationPlayer.getAnimation() : this.animationPlayer.getAnimation();
|
||||||
|
|
||||||
|
if (fromAnimation.get() instanceof LinkAnimation linkAnimation) {
|
||||||
|
fromAnimation = linkAnimation.getFromAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
nextAnimation.get().setLinkAnimation(fromAnimation, lastPose, !this.animationPlayer.isEmpty(), transitionTimeModifier, entitypatch, this.linkAnimation);
|
||||||
|
this.linkAnimation.getAnimationClip().setBaked();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(LivingEntityPatch<?> entitypatch) {
|
||||||
|
if (this.paused) {
|
||||||
|
this.animationPlayer.setElapsedTime(this.animationPlayer.getElapsedTime());
|
||||||
|
} else {
|
||||||
|
this.animationPlayer.tick(entitypatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.animationPlayer.isEnd()) {
|
||||||
|
this.animationPlayer.getAnimation().get().tick(entitypatch);
|
||||||
|
} else if (!this.paused) {
|
||||||
|
if (this.nextAnimation != null) {
|
||||||
|
if (!this.animationPlayer.getAnimation().get().isLinkAnimation() && !this.nextAnimation.get().isLinkAnimation()) {
|
||||||
|
this.nextAnimation.get().begin(entitypatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.nextAnimation.get().putOnPlayer(this.animationPlayer, entitypatch);
|
||||||
|
this.nextAnimation = null;
|
||||||
|
} else {
|
||||||
|
if (this.animationPlayer.getAnimation() instanceof LayerOffAnimation) {
|
||||||
|
this.animationPlayer.getAnimation().get().end(entitypatch, TiedUpRigRegistry.EMPTY_ANIMATION, true);
|
||||||
|
} else {
|
||||||
|
this.off(entitypatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isBaseLayer()) {
|
||||||
|
entitypatch.updateEntityState();
|
||||||
|
entitypatch.updateMotion(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pause() {
|
||||||
|
this.paused = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resume() {
|
||||||
|
this.paused = false;
|
||||||
|
this.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isDisabled() {
|
||||||
|
return this.disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOff() {
|
||||||
|
return this.isDisabled() || this.animationPlayer.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean isBaseLayer() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copyLayerTo(Layer layer, float playbackTime) {
|
||||||
|
AssetAccessor<? extends DynamicAnimation> animation;
|
||||||
|
|
||||||
|
if (this.animationPlayer.getAnimation() == this.linkAnimation) {
|
||||||
|
this.linkAnimation.copyTo(layer.linkAnimation);
|
||||||
|
animation = layer.linkAnimation;
|
||||||
|
} else {
|
||||||
|
animation = this.animationPlayer.getAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.animationPlayer.setPlayAnimation(animation);
|
||||||
|
layer.animationPlayer.setElapsedTime(this.animationPlayer.getPrevElapsedTime() + playbackTime, this.animationPlayer.getElapsedTime() + playbackTime);
|
||||||
|
layer.nextAnimation = this.nextAnimation;
|
||||||
|
layer.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LivingMotion getLivingMotion(LivingEntityPatch<?> entitypatch, boolean current) {
|
||||||
|
return current ? entitypatch.currentLivingMotion : entitypatch.getClientAnimator().currentMotion();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pose getEnabledPose(LivingEntityPatch<?> entitypatch, boolean useCurrentMotion, float partialTick) {
|
||||||
|
Pose pose = this.animationPlayer.getCurrentPose(entitypatch, partialTick);
|
||||||
|
this.animationPlayer.getAnimation().get().getJointMaskEntry(entitypatch, useCurrentMotion).ifPresent((jointEntry) -> pose.disableJoint((entry) -> jointEntry.isMasked(this.getLivingMotion(entitypatch, useCurrentMotion), entry.getKey())));
|
||||||
|
|
||||||
|
return pose;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void off(LivingEntityPatch<?> entitypatch) {
|
||||||
|
if (!this.isDisabled() && !(this.animationPlayer.getAnimation() instanceof LayerOffAnimation)) {
|
||||||
|
if (this.priority == null) {
|
||||||
|
this.disableLayer();
|
||||||
|
} else {
|
||||||
|
float transitionTimeModifier = entitypatch.getClientAnimator().baseLayer.animationPlayer.getAnimation().get().getTransitionTime();
|
||||||
|
setLayerOffAnimation(this.animationPlayer.getAnimation(), this.getEnabledPose(entitypatch, false, 1.0F), this.layerOffAnimation, transitionTimeModifier);
|
||||||
|
this.playAnimationInstantly(this.layerOffAnimation, entitypatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disableLayer() {
|
||||||
|
this.disabled = true;
|
||||||
|
this.animationPlayer.setPlayAnimation(TiedUpRigRegistry.EMPTY_ANIMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setLayerOffAnimation(AssetAccessor<? extends DynamicAnimation> currentAnimation, Pose currentPose, LayerOffAnimation offAnimation, float transitionTimeModifier) {
|
||||||
|
offAnimation.setLastAnimation(currentAnimation.get().getRealAnimation());
|
||||||
|
offAnimation.setLastPose(currentPose);
|
||||||
|
offAnimation.setTotalTime(transitionTimeModifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetAccessor<? extends DynamicAnimation> getNextAnimation() {
|
||||||
|
return this.nextAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
|
sb.append(this.isBaseLayer() ? "Base Layer(" + ((BaseLayer)this).baseLayerPriority + ") : " : " Composite Layer(" + this.priority + ") : ");
|
||||||
|
sb.append(this.animationPlayer.getAnimation() + " ");
|
||||||
|
sb.append(", prev elapsed time: " + this.animationPlayer.getPrevElapsedTime() + " ");
|
||||||
|
sb.append(", elapsed time: " + this.animationPlayer.getElapsedTime() + " ");
|
||||||
|
sb.append(", total time: " + this.animationPlayer.getAnimation().get().getTotalTime() + " ");
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BaseLayer extends Layer {
|
||||||
|
protected Map<Layer.Priority, Layer> compositeLayers = Maps.newLinkedHashMap();
|
||||||
|
protected Layer.Priority baseLayerPriority;
|
||||||
|
|
||||||
|
public BaseLayer() {
|
||||||
|
this(AnimationPlayer::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BaseLayer(Supplier<AnimationPlayer> animationPlayerProvider) {
|
||||||
|
super(null, animationPlayerProvider);
|
||||||
|
|
||||||
|
for (Priority priority : Priority.values()) {
|
||||||
|
this.compositeLayers.computeIfAbsent(priority, Layer::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.baseLayerPriority = Priority.LOWEST;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void playAnimation(AssetAccessor<? extends StaticAnimation> nextAnimation, LivingEntityPatch<?> entitypatch, float transitionTimeModifier) {
|
||||||
|
this.offCompositeLayersLowerThan(entitypatch, nextAnimation);
|
||||||
|
super.playAnimation(nextAnimation, entitypatch, transitionTimeModifier);
|
||||||
|
this.baseLayerPriority = nextAnimation.get().getPriority();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void playLivingAnimation(AssetAccessor<? extends StaticAnimation> nextAnimation, LivingEntityPatch<?> entitypatch) {
|
||||||
|
if (!this.animationPlayer.isEnd()) {
|
||||||
|
this.animationPlayer.getAnimation().get().end(entitypatch, nextAnimation, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resume();
|
||||||
|
nextAnimation.get().begin(entitypatch);
|
||||||
|
|
||||||
|
if (!nextAnimation.get().isMetaAnimation()) {
|
||||||
|
this.concurrentLinkAnimation.acceptFrom(this.animationPlayer.getRealAnimation(), nextAnimation, this.animationPlayer.getElapsedTime());
|
||||||
|
this.concurrentLinkAnimation.putOnPlayer(this.animationPlayer, entitypatch);
|
||||||
|
entitypatch.updateEntityState();
|
||||||
|
this.nextAnimation = nextAnimation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(LivingEntityPatch<?> entitypatch) {
|
||||||
|
super.update(entitypatch);
|
||||||
|
|
||||||
|
for (Layer layer : this.compositeLayers.values()) {
|
||||||
|
layer.update(entitypatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void offCompositeLayersLowerThan(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> nextAnimation) {
|
||||||
|
Priority[] layersToOff = nextAnimation.get().isMainFrameAnimation() ? nextAnimation.get().getPriority().lowersAndEqual() : nextAnimation.get().getPriority().lowers();
|
||||||
|
|
||||||
|
for (Priority p : layersToOff) {
|
||||||
|
this.compositeLayers.get(p).off(entitypatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disableLayer(Priority priority) {
|
||||||
|
this.compositeLayers.get(priority).disableLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Layer getLayer(Priority priority) {
|
||||||
|
return this.compositeLayers.get(priority);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Priority getBaseLayerPriority() {
|
||||||
|
return this.baseLayerPriority;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void off(LivingEntityPatch<?> entitypatch) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isDisabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isBaseLayer() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LayerType {
|
||||||
|
BASE_LAYER, COMPOSITE_LAYER
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Priority {
|
||||||
|
/**
|
||||||
|
* The common usage of each layer
|
||||||
|
*
|
||||||
|
* LOWEST: Most of living cycle animations. Also a default value for animations doesn't inherit {@link MainFrameAnimation.class}
|
||||||
|
* LOW: A few {@link ActionAnimation.class} that allows showing living cycle animations. e.g. step
|
||||||
|
* MIDDLE: Most of composite living cycle animations. e.g. weapon holding animations
|
||||||
|
* HIGH: A few composite animations that doesn't repeat. e.g. Uchigatana sheathing, Shield hit
|
||||||
|
* HIGHEST: Most of {@link MainFrameAnimation.class} and a few living cycle animations. e.g. ladder animation
|
||||||
|
**/
|
||||||
|
LOWEST, LOW, MIDDLE, HIGH, HIGHEST;
|
||||||
|
|
||||||
|
public Priority[] lowers() {
|
||||||
|
return Arrays.copyOfRange(Priority.values(), 0, this.ordinal());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Priority[] lowersAndEqual() {
|
||||||
|
return Arrays.copyOfRange(Priority.values(), 0, this.ordinal() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Priority[] highers() {
|
||||||
|
return Arrays.copyOfRange(Priority.values(), this.ordinal(), Priority.values().length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHigherThan(Priority priority) {
|
||||||
|
return this.ordinal() > priority.ordinal();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHigherOrEqual(Priority priority) {
|
||||||
|
return this.ordinal() >= priority.ordinal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.client.property;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.StaticAnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DirectStaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.client.AnimationSubFileReader;
|
||||||
|
import com.tiedup.remake.rig.anim.client.Layer;
|
||||||
|
|
||||||
|
public class ClientAnimationProperties {
|
||||||
|
/**
|
||||||
|
* Layer type. (BASE: Living, attack animations, COMPOSITE: Aiming, weapon holding, digging animation)
|
||||||
|
*
|
||||||
|
* <p>Artist-freedom Wave A : serializable via a {@link Codec} mapping to
|
||||||
|
* the {@link Layer.LayerType} enum constant name (case-insensitive
|
||||||
|
* tolerance — accepts {@code "base_layer"} or {@code "BASE_LAYER"}). This
|
||||||
|
* allows animation JSON authors to override the property through the
|
||||||
|
* top-level {@code "properties"} block:
|
||||||
|
* <pre>{@code
|
||||||
|
* "properties": {
|
||||||
|
* "layer_type": "COMPOSITE_LAYER"
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Upstream Epic Fight deserializes this field exclusively through
|
||||||
|
* {@link AnimationSubFileReader#deserializeLayerInfo} from a {@code
|
||||||
|
* "layer"} key in a sub-file. We keep that legacy path working and add
|
||||||
|
* this {@code name+Codec} registration as an additional access route
|
||||||
|
* (the two surface names — {@code layer_type} here, {@code layer} in the
|
||||||
|
* sub-file — do not conflict because they live in different JSON blocks).
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<Layer.LayerType> LAYER_TYPE = new StaticAnimationProperty<Layer.LayerType>(
|
||||||
|
"layer_type",
|
||||||
|
Codec.STRING.xmap(
|
||||||
|
s -> Layer.LayerType.valueOf(s.toUpperCase()),
|
||||||
|
Enum::name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Priority of composite layer.
|
||||||
|
*
|
||||||
|
* <p>Artist-freedom Wave A : serializable via a {@link Codec} mapping to
|
||||||
|
* the {@link Layer.Priority} enum constant name. Accepted values are
|
||||||
|
* {@code "LOWEST"}, {@code "LOW"}, {@code "MIDDLE"}, {@code "HIGH"},
|
||||||
|
* {@code "HIGHEST"} (case-insensitive).
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<Layer.Priority> PRIORITY = new StaticAnimationProperty<Layer.Priority>(
|
||||||
|
"priority",
|
||||||
|
Codec.STRING.xmap(
|
||||||
|
s -> Layer.Priority.valueOf(s.toUpperCase()),
|
||||||
|
Enum::name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joint mask for composite layer.
|
||||||
|
*
|
||||||
|
* <p>Artist-freedom Wave A : serializable via a {@link Codec} that encodes
|
||||||
|
* a {@link JointMaskEntry} as a single {@link net.minecraft.resources.ResourceLocation}
|
||||||
|
* string (namespaced path of a joint-mask JSON file in
|
||||||
|
* {@code animmodels/joint_mask/...}). At parse time the string is resolved
|
||||||
|
* through {@link JointMaskReloadListener#getJointMaskEntry(String)} into a
|
||||||
|
* {@link JointMask.JointMaskSet}, then wrapped into a {@link JointMaskEntry}
|
||||||
|
* with that set as the default mask and no per-motion overrides.
|
||||||
|
*
|
||||||
|
* <p>Round-trip encoding returns {@code "tiedup:none"} when the entry is
|
||||||
|
* unnamed or reference-equal to the vanilla {@code none} fallback. This is
|
||||||
|
* a best-effort lossy encode — a {@link JointMaskEntry} with per-motion
|
||||||
|
* overrides cannot be reduced to a single joint-mask ID without structural
|
||||||
|
* loss, and round-tripping that through the Codec is explicitly NOT
|
||||||
|
* supported (the richer upstream sub-file format in
|
||||||
|
* {@link AnimationSubFileReader} remains the authoring path for those).
|
||||||
|
*
|
||||||
|
* <p><b>Intent</b> : the {@code properties} JSON block is the
|
||||||
|
* «simple» override path that 99% of artists use — a single
|
||||||
|
* default mask is the only practical shape to serialize there. For
|
||||||
|
* per-motion mask entries the artist still writes a {@code *.data.json}
|
||||||
|
* sub-file alongside the anim.
|
||||||
|
*
|
||||||
|
* <p>Unknown joint-mask IDs resolve to the {@code tiedup:none} fallback
|
||||||
|
* entry (see {@link JointMaskReloadListener#getNoneMask()}) — no crash
|
||||||
|
* but a WARN log is emitted at deserialization.
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<JointMaskEntry> JOINT_MASK = new StaticAnimationProperty<JointMaskEntry>(
|
||||||
|
"joint_mask",
|
||||||
|
Codec.STRING.xmap(
|
||||||
|
ClientAnimationProperties::decodeJointMaskEntry,
|
||||||
|
ClientAnimationProperties::encodeJointMaskEntry
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trail particle information
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<List<TrailInfo>> TRAIL_EFFECT = new StaticAnimationProperty<List<TrailInfo>> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An animation clip being played in first person.
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<DirectStaticAnimation> POV_ANIMATION = new StaticAnimationProperty<DirectStaticAnimation> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An animation clip being played in first person.
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<AnimationSubFileReader.PovSettings> POV_SETTINGS = new StaticAnimationProperty<AnimationSubFileReader.PovSettings> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multilayer for living animations (e.g. Greatsword holding animation should be played simultaneously with jumping animation)
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<DirectStaticAnimation> MULTILAYER_ANIMATION = new StaticAnimationProperty<DirectStaticAnimation> ();
|
||||||
|
|
||||||
|
// === Wave A helpers : JOINT_MASK codec (package-private for unit testing) ===
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a joint-mask ID string into a {@link JointMaskEntry} whose default
|
||||||
|
* mask is the set registered under that ID. Unknown IDs fall back to the
|
||||||
|
* {@code tiedup:none} empty mask (already handled by
|
||||||
|
* {@link JointMaskReloadListener#getJointMaskEntry(String)}).
|
||||||
|
*
|
||||||
|
* <p>Package-private for direct unit-test coverage — the Codec re-wires
|
||||||
|
* this through {@link Codec#xmap} but the test can exercise the pure
|
||||||
|
* lookup logic without bootstrap.
|
||||||
|
*/
|
||||||
|
static JointMaskEntry decodeJointMaskEntry(String id) {
|
||||||
|
JointMask.JointMaskSet set = JointMaskReloadListener.getJointMaskEntry(id);
|
||||||
|
return JointMaskEntry.builder().defaultMask(set).create();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode a {@link JointMaskEntry} by looking up its default-mask ID in the
|
||||||
|
* {@link JointMaskReloadListener} reverse bimap. Falls back to
|
||||||
|
* {@code "tiedup:none"} when the mask is unregistered (either never
|
||||||
|
* reloaded or built programmatically with a custom set).
|
||||||
|
*
|
||||||
|
* <p>Per-motion override masks inside the entry are intentionally NOT
|
||||||
|
* serialized here — see class-level Javadoc on {@link #JOINT_MASK} for
|
||||||
|
* the rationale.
|
||||||
|
*/
|
||||||
|
static String encodeJointMaskEntry(JointMaskEntry entry) {
|
||||||
|
if (entry == null || entry.getDefaultMask() == null) {
|
||||||
|
return "tiedup:none";
|
||||||
|
}
|
||||||
|
net.minecraft.resources.ResourceLocation key =
|
||||||
|
JointMaskReloadListener.getKey(entry.getDefaultMask());
|
||||||
|
return key != null ? key.toString() : "tiedup:none";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.client.property;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.armature.Joint;
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotion;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.anim.client.Layer;
|
||||||
|
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||||
|
import com.tiedup.remake.rig.math.Vec3f;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
public class JointMask {
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface BindModifier {
|
||||||
|
public void modify(LivingEntityPatch<?> entitypatch, Pose baseLayerPose, Pose resultPose, LivingMotion livingMotion, JointMaskEntry wholeEntry, Layer.Priority priority, Joint joint, Map<Layer.Priority, Pair<AssetAccessor<? extends DynamicAnimation>, Pose>> poses);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final BindModifier KEEP_CHILD_LOCROT = (entitypatch, baseLayerPose, result, livingMotion, wholeEntry, priority, joint, poses) -> {
|
||||||
|
Pose currentPose = poses.get(priority).getSecond();
|
||||||
|
JointTransform lowestTransform = baseLayerPose.orElseEmpty(joint.getName());
|
||||||
|
JointTransform currentTransform = currentPose.orElseEmpty(joint.getName());
|
||||||
|
result.orElseEmpty(joint.getName()).translation().y = lowestTransform.translation().y;
|
||||||
|
|
||||||
|
OpenMatrix4f lowestMatrix = lowestTransform.toMatrix();
|
||||||
|
OpenMatrix4f currentMatrix = currentTransform.toMatrix();
|
||||||
|
OpenMatrix4f currentToLowest = OpenMatrix4f.mul(OpenMatrix4f.invert(currentMatrix, null), lowestMatrix, null);
|
||||||
|
|
||||||
|
for (Joint subJoint : joint.getSubJoints()) {
|
||||||
|
if (wholeEntry.isMasked(livingMotion, subJoint.getName())) {
|
||||||
|
OpenMatrix4f lowestLocalTransform = OpenMatrix4f.mul(joint.getLocalTransform(), lowestMatrix, null);
|
||||||
|
OpenMatrix4f currentLocalTransform = OpenMatrix4f.mul(joint.getLocalTransform(), currentMatrix, null);
|
||||||
|
OpenMatrix4f childTransform = OpenMatrix4f.mul(subJoint.getLocalTransform(), result.orElseEmpty(subJoint.getName()).toMatrix(), null);
|
||||||
|
OpenMatrix4f lowestFinal = OpenMatrix4f.mul(lowestLocalTransform, childTransform, null);
|
||||||
|
OpenMatrix4f currentFinal = OpenMatrix4f.mul(currentLocalTransform, childTransform, null);
|
||||||
|
Vec3f vec = new Vec3f((currentFinal.m30 - lowestFinal.m30) * 0.5F, currentFinal.m31 - lowestFinal.m31, currentFinal.m32 - lowestFinal.m32);
|
||||||
|
JointTransform jt = result.orElseEmpty(subJoint.getName());
|
||||||
|
jt.parent(JointTransform.translation(vec), OpenMatrix4f::mul);
|
||||||
|
jt.jointLocal(JointTransform.fromMatrixWithoutScale(currentToLowest), OpenMatrix4f::mul);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static JointMask of(String jointName, BindModifier bindModifier) {
|
||||||
|
return new JointMask(jointName, bindModifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointMask of(String jointName) {
|
||||||
|
return new JointMask(jointName, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String jointName;
|
||||||
|
private final BindModifier bindModifier;
|
||||||
|
|
||||||
|
private JointMask(String jointName, BindModifier bindModifier) {
|
||||||
|
this.jointName = jointName;
|
||||||
|
this.bindModifier = bindModifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class JointMaskSet {
|
||||||
|
final Map<String, BindModifier> masks = Maps.newHashMap();
|
||||||
|
|
||||||
|
public boolean contains(String name) {
|
||||||
|
return this.masks.containsKey(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BindModifier getBindModifier(String jointName) {
|
||||||
|
return this.masks.get(jointName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointMaskSet of(JointMask... masks) {
|
||||||
|
JointMaskSet jointMaskSet = new JointMaskSet();
|
||||||
|
|
||||||
|
for (JointMask jointMask : masks) {
|
||||||
|
jointMaskSet.masks.put(jointMask.jointName, jointMask.bindModifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jointMaskSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointMaskSet of(Set<JointMask> jointMasks) {
|
||||||
|
JointMaskSet jointMaskSet = new JointMaskSet();
|
||||||
|
|
||||||
|
for (JointMask jointMask : jointMasks) {
|
||||||
|
jointMaskSet.masks.put(jointMask.jointName, jointMask.bindModifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jointMaskSet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.client.property;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotion;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.JointMask.JointMaskSet;
|
||||||
|
|
||||||
|
public class JointMaskEntry {
|
||||||
|
public static final JointMaskSet BIPED_UPPER_JOINTS_WITH_ROOT = JointMaskSet.of(
|
||||||
|
JointMask.of("Root", JointMask.KEEP_CHILD_LOCROT), JointMask.of("Torso"),
|
||||||
|
JointMask.of("Chest"), JointMask.of("Head"),
|
||||||
|
JointMask.of("Shoulder_R"), JointMask.of("Arm_R"),
|
||||||
|
JointMask.of("Hand_R"), JointMask.of("Elbow_R"),
|
||||||
|
JointMask.of("Tool_R"), JointMask.of("Shoulder_L"),
|
||||||
|
JointMask.of("Arm_L"), JointMask.of("Hand_L"),
|
||||||
|
JointMask.of("Elbow_L"), JointMask.of("Tool_L")
|
||||||
|
);
|
||||||
|
|
||||||
|
public static final JointMaskEntry BASIC_ATTACK_MASK = JointMaskEntry.builder().defaultMask(JointMaskEntry.BIPED_UPPER_JOINTS_WITH_ROOT).create();
|
||||||
|
|
||||||
|
private final Map<LivingMotion, JointMaskSet> masks = Maps.newHashMap();
|
||||||
|
private final JointMaskSet defaultMask;
|
||||||
|
|
||||||
|
public JointMaskEntry(JointMaskSet defaultMask, List<Pair<LivingMotion, JointMaskSet>> masks) {
|
||||||
|
this.defaultMask = defaultMask;
|
||||||
|
|
||||||
|
for (Pair<LivingMotion, JointMaskSet> mask : masks) {
|
||||||
|
this.masks.put(mask.getLeft(), mask.getRight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointMaskSet getMask(LivingMotion livingmotion) {
|
||||||
|
return this.masks.getOrDefault(livingmotion, this.defaultMask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMasked(LivingMotion livingmotion, String jointName) {
|
||||||
|
return !this.masks.getOrDefault(livingmotion, this.defaultMask).contains(jointName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Map.Entry<LivingMotion, JointMaskSet>> getEntries() {
|
||||||
|
return this.masks.entrySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointMaskSet getDefaultMask() {
|
||||||
|
return this.defaultMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isValid() {
|
||||||
|
return this.defaultMask != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointMaskEntry.Builder builder() {
|
||||||
|
return new JointMaskEntry.Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
|
||||||
|
for (Map.Entry<LivingMotion, JointMaskSet> entry : this.masks.entrySet()) {
|
||||||
|
builder.append(entry.getKey() + ": ");
|
||||||
|
builder.append(JointMaskReloadListener.getKey(entry.getValue()) + ", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
ResourceLocation maskKey = JointMaskReloadListener.getKey(this.defaultMask);
|
||||||
|
|
||||||
|
if (maskKey == null) {
|
||||||
|
builder.append("default: custom");
|
||||||
|
} else {
|
||||||
|
builder.append("default: ");
|
||||||
|
builder.append(JointMaskReloadListener.getKey(this.defaultMask));
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private final List<Pair<LivingMotion, JointMaskSet>> masks = Lists.newArrayList();
|
||||||
|
private JointMaskSet defaultMask = null;
|
||||||
|
|
||||||
|
public JointMaskEntry.Builder mask(LivingMotion motion, JointMaskSet masks) {
|
||||||
|
this.masks.add(Pair.of(motion, masks));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointMaskEntry.Builder defaultMask(JointMaskSet masks) {
|
||||||
|
this.defaultMask = masks;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointMaskEntry create() {
|
||||||
|
return new JointMaskEntry(this.defaultMask, this.masks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.client.property;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import com.google.common.collect.BiMap;
|
||||||
|
import com.google.common.collect.HashBiMap;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.packs.resources.ResourceManager;
|
||||||
|
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
|
||||||
|
import net.minecraft.util.profiling.ProfilerFiller;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.JointMask.BindModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.JointMask.JointMaskSet;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
|
||||||
|
public class JointMaskReloadListener extends SimpleJsonResourceReloadListener {
|
||||||
|
private static final BiMap<ResourceLocation, JointMaskSet> JOINT_MASKS = HashBiMap.create();
|
||||||
|
private static final Map<String, JointMask.BindModifier> BIND_MODIFIERS = Maps.newHashMap();
|
||||||
|
private static final ResourceLocation NONE_MASK = TiedUpRigConstants.identifier("none");
|
||||||
|
|
||||||
|
static {
|
||||||
|
BIND_MODIFIERS.put("keep_child_locrot", JointMask.KEEP_CHILD_LOCROT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointMaskSet getJointMaskEntry(String type) {
|
||||||
|
ResourceLocation rl = ResourceLocation.parse(type);
|
||||||
|
return JOINT_MASKS.getOrDefault(rl, JOINT_MASKS.get(NONE_MASK));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointMaskSet getNoneMask() {
|
||||||
|
return JOINT_MASKS.get(NONE_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ResourceLocation getKey(JointMaskSet type) {
|
||||||
|
return JOINT_MASKS.inverse().get(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<Map.Entry<ResourceLocation, JointMaskSet>> entries() {
|
||||||
|
return JOINT_MASKS.entrySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointMaskReloadListener() {
|
||||||
|
super((new GsonBuilder()).create(), "animmodels/joint_mask");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void apply(Map<ResourceLocation, JsonElement> objectIn, ResourceManager resourceManager, ProfilerFiller profileFiller) {
|
||||||
|
JOINT_MASKS.clear();
|
||||||
|
|
||||||
|
for (Map.Entry<ResourceLocation, JsonElement> entry : objectIn.entrySet()) {
|
||||||
|
Set<JointMask> masks = Sets.newHashSet();
|
||||||
|
JsonObject object = entry.getValue().getAsJsonObject();
|
||||||
|
JsonArray joints = object.getAsJsonArray("joints");
|
||||||
|
JsonObject bindModifiers = object.has("bind_modifiers") ? object.getAsJsonObject("bind_modifiers") : null;
|
||||||
|
|
||||||
|
for (JsonElement joint : joints) {
|
||||||
|
String jointName = joint.getAsString();
|
||||||
|
BindModifier modifier = null;
|
||||||
|
|
||||||
|
if (bindModifiers != null) {
|
||||||
|
String modifierName = bindModifiers.has(jointName) ? bindModifiers.get(jointName).getAsString() : null;
|
||||||
|
modifier = BIND_MODIFIERS.get(modifierName);
|
||||||
|
}
|
||||||
|
|
||||||
|
masks.add(JointMask.of(jointName, modifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
String path = entry.getKey().toString();
|
||||||
|
ResourceLocation key = ResourceLocation.fromNamespaceAndPath(entry.getKey().getNamespace(), path.substring(path.lastIndexOf("/") + 1));
|
||||||
|
|
||||||
|
JOINT_MASKS.put(key, JointMaskSet.of(masks));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.client.property;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.client.Layer;
|
||||||
|
|
||||||
|
public class LayerInfo {
|
||||||
|
public final JointMaskEntry jointMaskEntry;
|
||||||
|
public final Layer.Priority priority;
|
||||||
|
public final Layer.LayerType layerType;
|
||||||
|
|
||||||
|
public LayerInfo(JointMaskEntry jointMaskEntry, Layer.Priority priority, Layer.LayerType layerType) {
|
||||||
|
this.jointMaskEntry = jointMaskEntry;
|
||||||
|
this.priority = priority;
|
||||||
|
this.layerType = layerType;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.client.property;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
|
||||||
|
import net.minecraft.core.particles.SimpleParticleType;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub RIG Phase 0 — combat weapon particle trail. Pas utilisé dans TiedUp
|
||||||
|
* (bondage, pas d'armes actives), mais on garde l'API typée pour JSON compat.
|
||||||
|
* {@code deserialize} retourne toujours un trail neutre non-playable, donc
|
||||||
|
* le block dans StaticAnimation est court-circuité (voir {@link #playable()}).
|
||||||
|
*/
|
||||||
|
public record TrailInfo(String joint, SimpleParticleType particle, boolean playable) {
|
||||||
|
public static TrailInfo deserialize(JsonElement element) {
|
||||||
|
return new TrailInfo("", null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec3 start() { return Vec3.ZERO; }
|
||||||
|
public Vec3 end() { return Vec3.ZERO; }
|
||||||
|
public float startTime() { return 0.0F; }
|
||||||
|
public float endTime() { return 0.0F; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackSpeedModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
import com.tiedup.remake.rig.util.CodecDispatchRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 D1 — data-driven concrete implementation of
|
||||||
|
* {@link PlaybackSpeedModifier}. The base functional interface takes five
|
||||||
|
* parameters ; datapack authors never need all of them, so concrete impls
|
||||||
|
* ignore the irrelevant ones.
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* "play_speed_modifier": { "type": "tiedup:constant_factor", "factor": 0.5 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Consumer : {@link com.tiedup.remake.rig.anim.AnimationPlayer#tick} reads
|
||||||
|
* the modifier and multiplies the current {@code playbackSpeed} by whatever
|
||||||
|
* the modifier returns each tick — so a modifier that returns {@code 0.5F}
|
||||||
|
* halves the animation speed.
|
||||||
|
*/
|
||||||
|
public interface PlaybackSpeedModifierImpl extends PlaybackSpeedModifier, CodecDispatchRegistry.Typed {
|
||||||
|
|
||||||
|
Codec<PlaybackSpeedModifierImpl> CODEC = PlaybackSpeedModifierRegistry.INSTANCE.dispatchCodec();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
ResourceLocation type();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
float modify(DynamicAnimation self, LivingEntityPatch<?> entitypatch, float speed, float prevElapsedTime, float elapsedTime);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.ConstantFactorSpeedModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.LinearRampSpeedModifier;
|
||||||
|
import com.tiedup.remake.rig.util.CodecDispatchRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry of {@link PlaybackSpeedModifierImpl} type ids → codecs. Same
|
||||||
|
* dispatch pattern as {@link PoseModifierRegistry}.
|
||||||
|
*
|
||||||
|
* <p>Base impls :
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code tiedup:constant_factor} — multiply speed by a fixed factor</li>
|
||||||
|
* <li>{@code tiedup:linear_ramp} — linear interpolation between a start and
|
||||||
|
* end factor over {@code elapsedTime} from 0 to {@code duration}</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Plumbing lives in {@link CodecDispatchRegistry} since {@code SMELL-CODEC-01}.
|
||||||
|
*/
|
||||||
|
public final class PlaybackSpeedModifierRegistry extends CodecDispatchRegistry<PlaybackSpeedModifierImpl> {
|
||||||
|
|
||||||
|
public static final PlaybackSpeedModifierRegistry INSTANCE = new PlaybackSpeedModifierRegistry();
|
||||||
|
|
||||||
|
private PlaybackSpeedModifierRegistry() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String registryName() {
|
||||||
|
return "PlaybackSpeedModifier";
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
INSTANCE.register(ConstantFactorSpeedModifier.ID, ConstantFactorSpeedModifier.CODEC);
|
||||||
|
INSTANCE.register(LinearRampSpeedModifier.ID, LinearRampSpeedModifier.CODEC);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackTimeModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
import com.tiedup.remake.rig.util.CodecDispatchRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 D1 — data-driven concrete implementation of
|
||||||
|
* {@link PlaybackTimeModifier}. The base interface returns a
|
||||||
|
* {@code Pair<Float, Float>} of {@code (prevElapsed, elapsed)}, typically used
|
||||||
|
* to loop a sub-section of an animation (set {@code elapsed} back to the
|
||||||
|
* looppoint when it crosses a threshold).
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* "elapsed_time_modifier": { "type": "tiedup:loop_section",
|
||||||
|
* "loop_start": 0.3, "loop_end": 0.8 }
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
public interface PlaybackTimeModifierImpl extends PlaybackTimeModifier, CodecDispatchRegistry.Typed {
|
||||||
|
|
||||||
|
Codec<PlaybackTimeModifierImpl> CODEC = PlaybackTimeModifierRegistry.INSTANCE.dispatchCodec();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
ResourceLocation type();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
Pair<Float, Float> modify(DynamicAnimation self, LivingEntityPatch<?> entitypatch, float speed, float prevElapsedTime, float elapsedTime);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.LoopSectionTimeModifier;
|
||||||
|
import com.tiedup.remake.rig.util.CodecDispatchRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry of {@link PlaybackTimeModifierImpl} type ids → codecs.
|
||||||
|
*
|
||||||
|
* <p>One base impl today :
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code tiedup:loop_section} — rewind elapsed time back to a loop
|
||||||
|
* start when it crosses a loop end threshold</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Not a ton of base impls because the common «replay this window»
|
||||||
|
* behaviour is what 95% of bondage loops need ; more exotic time warps
|
||||||
|
* (ping-pong, jitter) can be added later without a schema break.
|
||||||
|
*
|
||||||
|
* <p>Plumbing lives in {@link CodecDispatchRegistry} since {@code SMELL-CODEC-01}.
|
||||||
|
*/
|
||||||
|
public final class PlaybackTimeModifierRegistry extends CodecDispatchRegistry<PlaybackTimeModifierImpl> {
|
||||||
|
|
||||||
|
public static final PlaybackTimeModifierRegistry INSTANCE = new PlaybackTimeModifierRegistry();
|
||||||
|
|
||||||
|
private PlaybackTimeModifierRegistry() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String registryName() {
|
||||||
|
return "PlaybackTimeModifier";
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
INSTANCE.register(LoopSectionTimeModifier.ID, LoopSectionTimeModifier.CODEC);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.PoseModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
import com.tiedup.remake.rig.util.CodecDispatchRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 D1 — data-driven concrete implementation of the functional
|
||||||
|
* {@link PoseModifier} interface.
|
||||||
|
*
|
||||||
|
* <p>{@link PoseModifier} is a raw lambda {@code (self, pose, patch, elapsed,
|
||||||
|
* partialTick) -> void}, which is not serializable on its own. This interface
|
||||||
|
* extends it with a {@link #type()} identifier and a dispatch codec so that
|
||||||
|
* datapack authors can write concrete modifiers in JSON :
|
||||||
|
* <pre>{@code
|
||||||
|
* "pose_modifier": { "type": "tiedup:joint_rotation_offset",
|
||||||
|
* "joint": "upper_arm_left",
|
||||||
|
* "pitch": 15.0, "yaw": 0.0, "roll": 0.0 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Unlike {@link com.tiedup.remake.rig.anim.action.AnimationAction} which
|
||||||
|
* supports lists (a single animation can fire many actions), a single
|
||||||
|
* {@code pose_modifier} slot accepts exactly one modifier — because
|
||||||
|
* {@code StaticAnimationProperty.POSE_MODIFIER} is declared as {@code <PoseModifier>}
|
||||||
|
* (singular) and the consumer
|
||||||
|
* ({@link com.tiedup.remake.rig.anim.types.StaticAnimation#modifyPose}) calls
|
||||||
|
* {@code poseModifier.modify(...)} exactly once. Authors who want composition
|
||||||
|
* should chain via {@link com.tiedup.remake.rig.anim.modifier.impl.ChainedPoseModifier}.
|
||||||
|
*
|
||||||
|
* <p>All implementations must be side-safe : {@link PoseModifier#modify} is
|
||||||
|
* invoked in both client render and server logic ticks. Implementations that
|
||||||
|
* mutate the pose must do so deterministically from the inputs only, no IO or
|
||||||
|
* world state.
|
||||||
|
*/
|
||||||
|
public interface PoseModifierImpl extends PoseModifier, CodecDispatchRegistry.Typed {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch codec — reads the {@code "type"} field and delegates to the
|
||||||
|
* codec registered for that {@link ResourceLocation}.
|
||||||
|
*/
|
||||||
|
Codec<PoseModifierImpl> CODEC = PoseModifierRegistry.INSTANCE.dispatchCodec();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The registered type id of this modifier (e.g.
|
||||||
|
* {@code tiedup:joint_rotation_offset}).
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
ResourceLocation type();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forwarded from {@link PoseModifier#modify}. Subinterface so that the
|
||||||
|
* generic functional contract stays visible at the implementation site
|
||||||
|
* without Java default-method ambiguity.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
void modify(DynamicAnimation self, Pose pose, LivingEntityPatch<?> entitypatch, float elapsedTime, float partialTick);
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.ChainedPoseModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.JointRotationOffsetModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.impl.JointTranslationOffsetModifier;
|
||||||
|
import com.tiedup.remake.rig.util.CodecDispatchRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry of {@link PoseModifierImpl} type ids → codecs. Mirrors the design
|
||||||
|
* of {@link com.tiedup.remake.rig.anim.action.AnimationActionRegistry}.
|
||||||
|
*
|
||||||
|
* <p>Three base impls are registered in the static initializer :
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code tiedup:joint_rotation_offset} — nudge a single joint's rotation</li>
|
||||||
|
* <li>{@code tiedup:joint_translation_offset} — nudge a single joint's translation</li>
|
||||||
|
* <li>{@code tiedup:chain} — run a list of modifiers in order</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Registering via the static init means a single reference to
|
||||||
|
* {@link PoseModifierImpl#CODEC} in a parse path is enough to bootstrap the
|
||||||
|
* dispatch table, same bootstrap contract as the action registry.
|
||||||
|
*
|
||||||
|
* <p>Plumbing lives in {@link CodecDispatchRegistry} since {@code SMELL-CODEC-01}.
|
||||||
|
*/
|
||||||
|
public final class PoseModifierRegistry extends CodecDispatchRegistry<PoseModifierImpl> {
|
||||||
|
|
||||||
|
public static final PoseModifierRegistry INSTANCE = new PoseModifierRegistry();
|
||||||
|
|
||||||
|
private PoseModifierRegistry() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String registryName() {
|
||||||
|
return "PoseModifier";
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
INSTANCE.register(JointRotationOffsetModifier.ID, JointRotationOffsetModifier.CODEC);
|
||||||
|
INSTANCE.register(JointTranslationOffsetModifier.ID, JointTranslationOffsetModifier.CODEC);
|
||||||
|
INSTANCE.register(ChainedPoseModifier.ID, ChainedPoseModifier.CODEC);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier.impl;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
|
import com.mojang.serialization.Decoder;
|
||||||
|
import com.mojang.serialization.DynamicOps;
|
||||||
|
import com.mojang.serialization.Encoder;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PoseModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composite modifier — runs a list of modifiers sequentially on the same pose.
|
||||||
|
* Compensates for the singular {@code POSE_MODIFIER} property slot (only one
|
||||||
|
* modifier can be attached to an animation, but authors often need several
|
||||||
|
* per-joint nudges).
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* { "type": "tiedup:chain",
|
||||||
|
* "modifiers": [
|
||||||
|
* { "type": "tiedup:joint_rotation_offset", "joint": "upper_arm_left", "pitch": 15.0 },
|
||||||
|
* { "type": "tiedup:joint_translation_offset","joint": "hand_right", "y": -0.05 }
|
||||||
|
* ] }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Nested chains are allowed (no cycle detection — an author-written cycle
|
||||||
|
* would manifest as a {@link StackOverflowError} at tick time, which is
|
||||||
|
* identifiable and not worth defensive code). Execution order is list order.
|
||||||
|
*/
|
||||||
|
public record ChainedPoseModifier(
|
||||||
|
List<PoseModifierImpl> modifiers
|
||||||
|
) implements PoseModifierImpl {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("chain");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hand-rolled lazy codec. We cannot use a {@link RecordCodecBuilder} that
|
||||||
|
* references {@link PoseModifierImpl#CODEC} at static-init time because
|
||||||
|
* that would create a cycle :
|
||||||
|
* {@code ChainedPoseModifier.CODEC → PoseModifierImpl.CODEC
|
||||||
|
* → PoseModifierRegistry.<clinit> → ChainedPoseModifier.CODEC}
|
||||||
|
* which leaves ChainedPoseModifier.CODEC null during the registry
|
||||||
|
* registration call. Instead, we defer the recursive lookup by calling
|
||||||
|
* {@link PoseModifierImpl#CODEC} inside the encode/decode bodies, which
|
||||||
|
* run only at (de)serialization time — long after all static inits have
|
||||||
|
* completed.
|
||||||
|
*/
|
||||||
|
public static final Codec<ChainedPoseModifier> CODEC = Codec.of(
|
||||||
|
new Encoder<ChainedPoseModifier>() {
|
||||||
|
@Override
|
||||||
|
public <T> DataResult<T> encode(ChainedPoseModifier input, DynamicOps<T> ops, T prefix) {
|
||||||
|
return PoseModifierImpl.CODEC.listOf()
|
||||||
|
.encodeStart(ops, input.modifiers())
|
||||||
|
.flatMap(list -> ops.mergeToMap(prefix, ops.createString("modifiers"), list));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Decoder<ChainedPoseModifier>() {
|
||||||
|
@Override
|
||||||
|
public <T> DataResult<com.mojang.datafixers.util.Pair<ChainedPoseModifier, T>> decode(DynamicOps<T> ops, T input) {
|
||||||
|
return ops.getMap(input).flatMap(map -> {
|
||||||
|
T modifiersField = map.get("modifiers");
|
||||||
|
if (modifiersField == null) {
|
||||||
|
return DataResult.error(() -> "Missing field 'modifiers' in tiedup:chain");
|
||||||
|
}
|
||||||
|
return PoseModifierImpl.CODEC.listOf()
|
||||||
|
.parse(ops, modifiersField)
|
||||||
|
.map(list -> com.mojang.datafixers.util.Pair.of(new ChainedPoseModifier(list), input));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modify(DynamicAnimation self, Pose pose, LivingEntityPatch<?> entitypatch, float elapsedTime, float partialTick) {
|
||||||
|
for (PoseModifierImpl m : this.modifiers) {
|
||||||
|
m.modify(self, pose, entitypatch, elapsedTime, partialTick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier.impl;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PlaybackSpeedModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multiply the current playback speed by a fixed factor every tick.
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* "play_speed_modifier": { "type": "tiedup:constant_factor", "factor": 0.5 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>A {@code factor} of {@code 1.0} is a no-op. Negative factors technically
|
||||||
|
* work (animation plays backward) but the reverse flag in
|
||||||
|
* {@link com.tiedup.remake.rig.anim.AnimationPlayer} already handles that
|
||||||
|
* cleanly — prefer that over a negative speed modifier.
|
||||||
|
*/
|
||||||
|
public record ConstantFactorSpeedModifier(float factor) implements PlaybackSpeedModifierImpl {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("constant_factor");
|
||||||
|
|
||||||
|
public static final Codec<ConstantFactorSpeedModifier> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
Codec.FLOAT.fieldOf("factor").forGetter(ConstantFactorSpeedModifier::factor)
|
||||||
|
).apply(i, ConstantFactorSpeedModifier::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float modify(DynamicAnimation self, LivingEntityPatch<?> entitypatch, float speed, float prevElapsedTime, float elapsedTime) {
|
||||||
|
return speed * this.factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier.impl;
|
||||||
|
|
||||||
|
import org.joml.Quaternionf;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PoseModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate a single named joint by a fixed Euler offset (degrees) at every tick
|
||||||
|
* of the animation. Bondage pipeline use-case : lock a restrained arm joint to
|
||||||
|
* a tighter angle than the authored animation provides.
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* { "type": "tiedup:joint_rotation_offset",
|
||||||
|
* "joint": "upper_arm_left",
|
||||||
|
* "pitch": 15.0,
|
||||||
|
* "yaw": 0.0,
|
||||||
|
* "roll": 0.0 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Angles default to zero if omitted — a {@code joint_rotation_offset} with
|
||||||
|
* all three zero is a no-op (author error, logged nowhere — by design, since
|
||||||
|
* it's a cheap operation and a valid edge case during iteration).
|
||||||
|
*
|
||||||
|
* <p>If the joint is absent from the pose, this modifier is a silent no-op :
|
||||||
|
* the authored animation simply doesn't touch that joint on this frame. This
|
||||||
|
* matches the semantics of {@link Pose#orElseEmpty} used elsewhere.
|
||||||
|
*/
|
||||||
|
public record JointRotationOffsetModifier(
|
||||||
|
String joint,
|
||||||
|
float pitch,
|
||||||
|
float yaw,
|
||||||
|
float roll
|
||||||
|
) implements PoseModifierImpl {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("joint_rotation_offset");
|
||||||
|
|
||||||
|
public static final Codec<JointRotationOffsetModifier> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
Codec.STRING.fieldOf("joint").forGetter(JointRotationOffsetModifier::joint),
|
||||||
|
Codec.FLOAT.optionalFieldOf("pitch", 0.0F).forGetter(JointRotationOffsetModifier::pitch),
|
||||||
|
Codec.FLOAT.optionalFieldOf("yaw", 0.0F).forGetter(JointRotationOffsetModifier::yaw),
|
||||||
|
Codec.FLOAT.optionalFieldOf("roll", 0.0F).forGetter(JointRotationOffsetModifier::roll)
|
||||||
|
).apply(i, JointRotationOffsetModifier::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modify(DynamicAnimation self, Pose pose, LivingEntityPatch<?> entitypatch, float elapsedTime, float partialTick) {
|
||||||
|
if (!pose.hasTransform(this.joint)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JointTransform jt = pose.get(this.joint);
|
||||||
|
Quaternionf offset = new Quaternionf().rotationXYZ(
|
||||||
|
this.pitch * Mth.DEG_TO_RAD,
|
||||||
|
this.yaw * Mth.DEG_TO_RAD,
|
||||||
|
this.roll * Mth.DEG_TO_RAD
|
||||||
|
);
|
||||||
|
jt.rotation().mul(offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier.impl;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PoseModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shift a single named joint by a fixed translation offset at every tick of
|
||||||
|
* the animation. Use-case : pull a constrained wrist joint closer to the
|
||||||
|
* furniture anchor than the authored animation puts it.
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* { "type": "tiedup:joint_translation_offset",
|
||||||
|
* "joint": "hand_right",
|
||||||
|
* "x": 0.0, "y": -0.05, "z": 0.02 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>All three axes default to zero — a fully-zero offset is a no-op.
|
||||||
|
* Missing joint is a silent no-op (same contract as the rotation variant).
|
||||||
|
*/
|
||||||
|
public record JointTranslationOffsetModifier(
|
||||||
|
String joint,
|
||||||
|
float x,
|
||||||
|
float y,
|
||||||
|
float z
|
||||||
|
) implements PoseModifierImpl {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("joint_translation_offset");
|
||||||
|
|
||||||
|
public static final Codec<JointTranslationOffsetModifier> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
Codec.STRING.fieldOf("joint").forGetter(JointTranslationOffsetModifier::joint),
|
||||||
|
Codec.FLOAT.optionalFieldOf("x", 0.0F).forGetter(JointTranslationOffsetModifier::x),
|
||||||
|
Codec.FLOAT.optionalFieldOf("y", 0.0F).forGetter(JointTranslationOffsetModifier::y),
|
||||||
|
Codec.FLOAT.optionalFieldOf("z", 0.0F).forGetter(JointTranslationOffsetModifier::z)
|
||||||
|
).apply(i, JointTranslationOffsetModifier::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modify(DynamicAnimation self, Pose pose, LivingEntityPatch<?> entitypatch, float elapsedTime, float partialTick) {
|
||||||
|
if (!pose.hasTransform(this.joint)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JointTransform jt = pose.get(this.joint);
|
||||||
|
jt.translation().add(this.x, this.y, this.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier.impl;
|
||||||
|
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PlaybackSpeedModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ramp the playback speed factor linearly from {@code from} to {@code to} over
|
||||||
|
* the time window {@code [startTime, endTime]} (in seconds of animation
|
||||||
|
* elapsed). Outside the window, clamps to the boundary value.
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* "play_speed_modifier": { "type": "tiedup:linear_ramp",
|
||||||
|
* "from": 0.2, "to": 1.0,
|
||||||
|
* "start_time": 0.0, "end_time": 0.5 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>Use-case : hesitant-start animations (slow initial frames, then ramp to
|
||||||
|
* full speed for the payoff). The modifier's output is multiplied against the
|
||||||
|
* base speed by {@link com.tiedup.remake.rig.anim.AnimationPlayer#tick}, so
|
||||||
|
* {@code from=1.0, to=1.0} is a no-op.
|
||||||
|
*
|
||||||
|
* <p>{@code end_time <= start_time} degenerates to returning {@code to} for
|
||||||
|
* all {@code elapsedTime >= start_time} (clamped path above saturation).
|
||||||
|
*/
|
||||||
|
public record LinearRampSpeedModifier(
|
||||||
|
float from,
|
||||||
|
float to,
|
||||||
|
float startTime,
|
||||||
|
float endTime
|
||||||
|
) implements PlaybackSpeedModifierImpl {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("linear_ramp");
|
||||||
|
|
||||||
|
public static final Codec<LinearRampSpeedModifier> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
Codec.FLOAT.fieldOf("from").forGetter(LinearRampSpeedModifier::from),
|
||||||
|
Codec.FLOAT.fieldOf("to").forGetter(LinearRampSpeedModifier::to),
|
||||||
|
Codec.FLOAT.optionalFieldOf("start_time", 0.0F).forGetter(LinearRampSpeedModifier::startTime),
|
||||||
|
Codec.FLOAT.optionalFieldOf("end_time", 1.0F).forGetter(LinearRampSpeedModifier::endTime)
|
||||||
|
).apply(i, LinearRampSpeedModifier::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float modify(DynamicAnimation self, LivingEntityPatch<?> entitypatch, float speed, float prevElapsedTime, float elapsedTime) {
|
||||||
|
float factor;
|
||||||
|
if (elapsedTime <= this.startTime) {
|
||||||
|
factor = this.from;
|
||||||
|
} else if (elapsedTime >= this.endTime || this.endTime <= this.startTime) {
|
||||||
|
factor = this.to;
|
||||||
|
} else {
|
||||||
|
float progress = (elapsedTime - this.startTime) / (this.endTime - this.startTime);
|
||||||
|
factor = Mth.lerp(progress, this.from, this.to);
|
||||||
|
}
|
||||||
|
return speed * factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.modifier.impl;
|
||||||
|
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PlaybackTimeModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rewind the animation playhead back to {@code loopStart} every time it
|
||||||
|
* crosses {@code loopEnd}. Matches the «sustain loop» pattern used e.g. for
|
||||||
|
* breathing idles inside a rest phase — the intro plays once, a middle section
|
||||||
|
* loops, and the outro is triggered by a separate state transition (not this
|
||||||
|
* modifier).
|
||||||
|
*
|
||||||
|
* <p>JSON schema :
|
||||||
|
* <pre>{@code
|
||||||
|
* "elapsed_time_modifier": { "type": "tiedup:loop_section",
|
||||||
|
* "loop_start": 0.3, "loop_end": 0.8 }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <p>This modifier is a pure function of {@code elapsedTime} — it does not
|
||||||
|
* mutate the animation. The {@code prevElapsedTime} is rewound symmetrically
|
||||||
|
* so event triggers using {@code [prev, elapsed]} windows still fire cleanly
|
||||||
|
* after the rewind.
|
||||||
|
*
|
||||||
|
* <p>Invariant : {@code loopStart < loopEnd}. Violating this yields undefined
|
||||||
|
* behaviour (probably a NaN or an instant loop) — no defensive check because
|
||||||
|
* the author would notice immediately on first playback.
|
||||||
|
*/
|
||||||
|
public record LoopSectionTimeModifier(
|
||||||
|
float loopStart,
|
||||||
|
float loopEnd
|
||||||
|
) implements PlaybackTimeModifierImpl {
|
||||||
|
|
||||||
|
public static final ResourceLocation ID = TiedUpRigConstants.identifier("loop_section");
|
||||||
|
|
||||||
|
public static final Codec<LoopSectionTimeModifier> CODEC = RecordCodecBuilder.create(i -> i.group(
|
||||||
|
Codec.FLOAT.fieldOf("loop_start").forGetter(LoopSectionTimeModifier::loopStart),
|
||||||
|
Codec.FLOAT.fieldOf("loop_end").forGetter(LoopSectionTimeModifier::loopEnd)
|
||||||
|
).apply(i, LoopSectionTimeModifier::new));
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation type() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pair<Float, Float> modify(DynamicAnimation self, LivingEntityPatch<?> entitypatch, float speed, float prevElapsedTime, float elapsedTime) {
|
||||||
|
if (elapsedTime >= this.loopEnd) {
|
||||||
|
float overshoot = elapsedTime - this.loopEnd;
|
||||||
|
float sectionLen = this.loopEnd - this.loopStart;
|
||||||
|
float wrapped = this.loopStart + (overshoot % sectionLen);
|
||||||
|
float prevWrapped = prevElapsedTime - (elapsedTime - wrapped);
|
||||||
|
return Pair.of(prevWrapped, wrapped);
|
||||||
|
}
|
||||||
|
return Pair.of(prevElapsedTime, elapsedTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.property;
|
||||||
|
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
public abstract class AnimationEvent<EVENT extends AnimationEvent.Event<?, ?, ?, ?, ?, ?, ?, ?, ?, ?>, T extends AnimationEvent<EVENT, T>> {
|
||||||
|
protected final AnimationEvent.Side side;
|
||||||
|
protected final EVENT event;
|
||||||
|
protected AnimationParameters params;
|
||||||
|
|
||||||
|
private AnimationEvent(AnimationEvent.Side executionSide, EVENT event) {
|
||||||
|
this.side = executionSide;
|
||||||
|
this.event = event;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract boolean checkCondition(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, float prevElapsed, float elapsed);
|
||||||
|
|
||||||
|
public void execute(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, float prevElapsed, float elapsed) {
|
||||||
|
if (this.side.predicate.test(entitypatch.getOriginal()) && this.checkCondition(entitypatch, animation, prevElapsed, elapsed)) {
|
||||||
|
this.event.fire(entitypatch, animation, this.params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void executeWithNewParams(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, float prevElapsed, float elapsed, AnimationParameters parameters) {
|
||||||
|
if (this.side.predicate.test(entitypatch.getOriginal()) && this.checkCondition(entitypatch, animation, prevElapsed, elapsed)) {
|
||||||
|
this.event.fire(entitypatch, animation, parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SimpleEvent<EVENT extends AnimationEvent.Event<?, ?, ?, ?, ?, ?, ?, ?, ?, ?>> extends AnimationEvent<EVENT, SimpleEvent<EVENT>> {
|
||||||
|
private SimpleEvent(AnimationEvent.Side executionSide, EVENT event) {
|
||||||
|
super(executionSide, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean checkCondition(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, float prevElapsed, float elapsed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <E extends Event<?, ?, ?, ?, ?, ?, ?, ?, ?, ?>> SimpleEvent<E> create(E event, AnimationEvent.Side isRemote) {
|
||||||
|
return new SimpleEvent<> (isRemote, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class InTimeEvent<EVENT extends AnimationEvent.Event<?, ?, ?, ?, ?, ?, ?, ?, ?, ?>> extends AnimationEvent<EVENT, InTimeEvent<EVENT>> implements Comparable<InTimeEvent<EVENT>> {
|
||||||
|
final float time;
|
||||||
|
|
||||||
|
private InTimeEvent(float time, AnimationEvent.Side executionSide, EVENT event) {
|
||||||
|
super(executionSide, event);
|
||||||
|
this.time = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkCondition(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, float prevElapsed, float elapsed) {
|
||||||
|
return this.time >= prevElapsed && this.time < elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(InTimeEvent<EVENT> arg0) {
|
||||||
|
if(this.time == arg0.time) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return this.time > arg0.time ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <E extends Event<?, ?, ?, ?, ?, ?, ?, ?, ?, ?>> InTimeEvent<E> create(float time, E event, AnimationEvent.Side isRemote) {
|
||||||
|
return new InTimeEvent<> (time, isRemote, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class InPeriodEvent<EVENT extends AnimationEvent.Event<?, ?, ?, ?, ?, ?, ?, ?, ?, ?>> extends AnimationEvent<EVENT, InPeriodEvent<EVENT>> implements Comparable<InPeriodEvent<EVENT>> {
|
||||||
|
final float start;
|
||||||
|
final float end;
|
||||||
|
|
||||||
|
private InPeriodEvent(float start, float end, AnimationEvent.Side executionSide, EVENT event) {
|
||||||
|
super(executionSide, event);
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkCondition(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, float prevElapsed, float elapsed) {
|
||||||
|
return this.start <= elapsed && this.end > elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(InPeriodEvent<EVENT> arg0) {
|
||||||
|
if (this.start == arg0.start) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return this.start > arg0.start ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <E extends Event<?, ?, ?, ?, ?, ?, ?, ?, ?, ?>> InPeriodEvent<E> create(float start, float end, E event, AnimationEvent.Side isRemote) {
|
||||||
|
return new InPeriodEvent<> (start, end, isRemote, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Side {
|
||||||
|
CLIENT((entity) -> entity.level().isClientSide),
|
||||||
|
SERVER((entity) -> !entity.level().isClientSide), BOTH((entity) -> true),
|
||||||
|
LOCAL_CLIENT((entity) -> {
|
||||||
|
if (entity instanceof Player player) {
|
||||||
|
return player.isLocalPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
Predicate<Entity> predicate;
|
||||||
|
|
||||||
|
Side(Predicate<Entity> predicate) {
|
||||||
|
this.predicate = predicate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnimationParameters<?, ?, ?, ?, ?, ?, ?, ?, ?, ?> getParameters() {
|
||||||
|
return this.params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <A> T params(A p1) {
|
||||||
|
this.params = AnimationParameters.of(p1);
|
||||||
|
return (T)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <A, B> T params(A p1, B p2) {
|
||||||
|
this.params = AnimationParameters.of(p1, p2);
|
||||||
|
return (T)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <A, B, C> T params(A p1, B p2, C p3) {
|
||||||
|
this.params = AnimationParameters.of(p1, p2, p3);
|
||||||
|
return (T)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <A, B, C, D> T params(A p1, B p2, C p3, D p4) {
|
||||||
|
this.params = AnimationParameters.of(p1, p2, p3, p4);
|
||||||
|
return (T)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <A, B, C, D, E> T params(A p1, B p2, C p3, D p4, E p5) {
|
||||||
|
this.params = AnimationParameters.of(p1, p2, p3, p4, p5);
|
||||||
|
return (T)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <A, B, C, D, E, F> T params(A p1, B p2, C p3, D p4, E p5, F p6) {
|
||||||
|
this.params = AnimationParameters.of(p1, p2, p3, p4, p5, p6);
|
||||||
|
return (T)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <A, B, C, D, E, F, G> T params(A p1, B p2, C p3, D p4, E p5, F p6, G p7) {
|
||||||
|
this.params = AnimationParameters.of(p1, p2, p3, p4, p5, p6, p7);
|
||||||
|
return (T)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <A, B, C, D, E, F, G, H> T params(A p1, B p2, C p3, D p4, E p5, F p6, G p7, H p8) {
|
||||||
|
this.params = AnimationParameters.of(p1, p2, p3, p4, p5, p6, p7, p8);
|
||||||
|
return (T)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <A, B, C, D, E, F, G, H, I> T params(A p1, B p2, C p3, D p4, E p5, F p6, G p7, H p8, I p9) {
|
||||||
|
this.params = AnimationParameters.of(p1, p2, p3, p4, p5, p6, p7, p8, p9);
|
||||||
|
return (T)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <A, B, C, D, E, F, G, H, I, J> T params(A p1, B p2, C p3, D p4, E p5, F p6, G p7, H p8, I p9, J p10) {
|
||||||
|
this.params = AnimationParameters.of(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10);
|
||||||
|
return (T)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Event<A, B, C, D, E, F, G, H, I, J> {
|
||||||
|
void fire(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, AnimationParameters<A, B, C, D, E, F, G, H, I, J> params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface E0 extends Event<Void, Void, Void, Void, Void, Void, Void, Void, Void, Void> {
|
||||||
|
void fire(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, AnimationParameters<Void, Void, Void, Void, Void, Void, Void, Void, Void, Void> params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface E1<A> extends Event<A, Void, Void, Void, Void, Void, Void, Void, Void, Void> {
|
||||||
|
void fire(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, AnimationParameters<A, Void, Void, Void, Void, Void, Void, Void, Void, Void> params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface E2<A, B> extends Event<A, B, Void, Void, Void, Void, Void, Void, Void, Void> {
|
||||||
|
void fire(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, AnimationParameters<A, B, Void, Void, Void, Void, Void, Void, Void, Void> params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface E3<A, B, C> extends Event<A, B, C, Void, Void, Void, Void, Void, Void, Void> {
|
||||||
|
void fire(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, AnimationParameters<A, B, C, Void, Void, Void, Void, Void, Void, Void> params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface E4<A, B, C, D> extends Event<A, B, C, D, Void, Void, Void, Void, Void, Void> {
|
||||||
|
void fire(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, AnimationParameters<A, B, C, D, Void, Void, Void, Void, Void, Void> params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface E5<A, B, C, D, E> extends Event<A, B, C, D, E, Void, Void, Void, Void, Void> {
|
||||||
|
void fire(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, AnimationParameters<A, B, C, D, E, Void, Void, Void, Void, Void> params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface E6<A, B, C, D, E, F> extends Event<A, B, C, D, E, F, Void, Void, Void, Void> {
|
||||||
|
void fire(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, AnimationParameters<A, B, C, D, E, F, Void, Void, Void, Void> params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface E7<A, B, C, D, E, F, G> extends Event<A, B, C, D, E, F, G, Void, Void, Void> {
|
||||||
|
void fire(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, AnimationParameters<A, B, C, D, E, F, G, Void, Void, Void> params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface E8<A, B, C, D, E, F, G, H> extends Event<A, B, C, D, E, F, G, H, Void, Void> {
|
||||||
|
void fire(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, AnimationParameters<A, B, C, D, E, F, G, H, Void, Void> params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface E9<A, B, C, D, E, F, G, H, I> extends Event<A, B, C, D, E, F, G, H, I, Void> {
|
||||||
|
void fire(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, AnimationParameters<A, B, C, D, E, F, G, H, I, Void> params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface E10<A, B, C, D, E, F, G, H, I, J> extends Event<A, B, C, D, E, F, G, H, I, J> {
|
||||||
|
void fire(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends StaticAnimation> animation, AnimationParameters<A, B, C, D, E, F, G, H, I, J> params);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.property;
|
||||||
|
|
||||||
|
public record AnimationParameters<A, B, C, D, E, F, G, H, I, J> (
|
||||||
|
A first,
|
||||||
|
B second,
|
||||||
|
C third,
|
||||||
|
D fourth,
|
||||||
|
E fifth,
|
||||||
|
F sixth,
|
||||||
|
G seventh,
|
||||||
|
H eighth,
|
||||||
|
I ninth,
|
||||||
|
J tenth
|
||||||
|
) {
|
||||||
|
public static <A> AnimationParameters<A, Void, Void, Void, Void, Void, Void, Void, Void, Void> of(A first) {
|
||||||
|
return new AnimationParameters<> (first, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <A, B> AnimationParameters<A, B, Void, Void, Void, Void, Void, Void, Void, Void> of(A first, B second) {
|
||||||
|
return new AnimationParameters<> (first, second, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <A, B, C> AnimationParameters<A, B, C, Void, Void, Void, Void, Void, Void, Void> of(A first, B second, C third) {
|
||||||
|
return new AnimationParameters<> (first, second, third, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <A, B, C, D> AnimationParameters<A, B, C, D, Void, Void, Void, Void, Void, Void> of(A first, B second, C third, D fourth) {
|
||||||
|
return new AnimationParameters<> (first, second, third, fourth, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <A, B, C, D, E> AnimationParameters<A, B, C, D, E, Void, Void, Void, Void, Void> of(A first, B second, C third, D fourth, E fifth) {
|
||||||
|
return new AnimationParameters<> (first, second, third, fourth, fifth, (Void)null, (Void)null, (Void)null, (Void)null, (Void)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <A, B, C, D, E, F> AnimationParameters<A, B, C, D, E, F, Void, Void, Void, Void> of(A first, B second, C third, D fourth, E fifth, F sixth) {
|
||||||
|
return new AnimationParameters<> (first, second, third, fourth, fifth, sixth, (Void)null, (Void)null, (Void)null, (Void)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <A, B, C, D, E, F, G> AnimationParameters<A, B, C, D, E, F, G, Void, Void, Void> of(A first, B second, C third, D fourth, E fifth, F sixth, G seventh) {
|
||||||
|
return new AnimationParameters<> (first, second, third, fourth, fifth, sixth, seventh, (Void)null, (Void)null, (Void)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <A, B, C, D, E, F, G, H> AnimationParameters<A, B, C, D, E, F, G, H, Void, Void> of(A first, B second, C third, D fourth, E fifth, F sixth, G seventh, H eighth) {
|
||||||
|
return new AnimationParameters<> (first, second, third, fourth, fifth, sixth, seventh, eighth, (Void)null, (Void)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <A, B, C, D, E, F, G, H, I> AnimationParameters<A, B, C, D, E, F, G, H, I, Void> of(A first, B second, C third, D fourth, E fifth, F sixth, G seventh, H eighth, I ninth) {
|
||||||
|
return new AnimationParameters<> (first, second, third, fourth, fifth, sixth, seventh, eighth, ninth, (Void)null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <A, B, C, D, E, F, G, H, I, J> AnimationParameters<A, B, C, D, E, F, G, H, I, J> of(A first, B second, C third, D fourth, E fifth, F sixth, G seventh, H eighth, I ninth, J tenth) {
|
||||||
|
return new AnimationParameters<> (first, second, third, fourth, fifth, sixth, seventh, eighth, ninth, tenth);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <A, B, C, D, E, F, G, H, I, J, N> AnimationParameters<?, ?, ?, ?, ?, ?, ?, ?, ?, ?> addParameter(AnimationParameters<A, B, C, D, E, F, G, H, I, J> parameters, N newParam) {
|
||||||
|
if (parameters.first() == null) {
|
||||||
|
return new AnimationParameters<N, Void, Void, Void, Void, Void, Void, Void, Void, Void> (newParam, null, null, null, null, null, null, null, null, null);
|
||||||
|
} else if (parameters.second() == null) {
|
||||||
|
return new AnimationParameters<A, N, Void, Void, Void, Void, Void, Void, Void, Void> (parameters.first(), newParam, null, null, null, null, null, null, null, null);
|
||||||
|
} else if (parameters.third() == null) {
|
||||||
|
return new AnimationParameters<A, B, N, Void, Void, Void, Void, Void, Void, Void> (parameters.first(), parameters.second(), newParam, null, null, null, null, null, null, null);
|
||||||
|
} else if (parameters.fourth() == null) {
|
||||||
|
return new AnimationParameters<A, B, C, N, Void, Void, Void, Void, Void, Void> (parameters.first(), parameters.second(), parameters.third(), newParam, null, null, null, null, null, null);
|
||||||
|
} else if (parameters.fifth() == null) {
|
||||||
|
return new AnimationParameters<A, B, C, D, N, Void, Void, Void, Void, Void> (parameters.first(), parameters.second(), parameters.third(), parameters.fourth(), newParam, null, null, null, null, null);
|
||||||
|
} else if (parameters.sixth() == null) {
|
||||||
|
return new AnimationParameters<A, B, C, D, E, N, Void, Void, Void, Void> (parameters.first(), parameters.second(), parameters.third(), parameters.fourth(), parameters.fifth(), newParam, null, null, null, null);
|
||||||
|
} else if (parameters.seventh() == null) {
|
||||||
|
return new AnimationParameters<A, B, C, D, E, F, N, Void, Void, Void> (parameters.first(), parameters.second(), parameters.third(), parameters.fourth(), parameters.fifth(), parameters.sixth(), newParam, null, null, null);
|
||||||
|
} else if (parameters.eighth() == null) {
|
||||||
|
return new AnimationParameters<A, B, C, D, E, F, G, N, Void, Void> (parameters.first(), parameters.second(), parameters.third(), parameters.fourth(), parameters.fifth(), parameters.sixth(), parameters.seventh(), newParam, null, null);
|
||||||
|
} else if (parameters.ninth() == null) {
|
||||||
|
return new AnimationParameters<A, B, C, D, E, F, G, H, N, Void> (parameters.first(), parameters.second(), parameters.third(), parameters.fourth(), parameters.fifth(), parameters.sixth(), parameters.seventh(), parameters.eighth(), newParam, null);
|
||||||
|
} else if (parameters.tenth() == null) {
|
||||||
|
return new AnimationParameters<A, B, C, D, E, F, G, H, I, N> (parameters.first(), parameters.second(), parameters.third(), parameters.fourth(), parameters.fifth(), parameters.sixth(), parameters.seventh(), parameters.eighth(), parameters.ninth(), newParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnsupportedOperationException("Parameters are full!");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,623 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.property;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
import com.mojang.serialization.Codec;
|
||||||
|
import com.mojang.serialization.DataResult;
|
||||||
|
import com.mojang.serialization.JsonOps;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.sounds.SoundEvent;
|
||||||
|
import net.minecraft.tags.TagKey;
|
||||||
|
import net.minecraft.world.damagesource.DamageType;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotion;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.TransformSheet;
|
||||||
|
import com.tiedup.remake.rig.anim.action.DataDrivenAnimationEvents;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PlaybackSpeedModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PlaybackTimeModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.modifier.PoseModifierImpl;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationEvent.SimpleEvent;
|
||||||
|
import com.tiedup.remake.rig.anim.property.MoveCoordFunctions.MoveCoordGetter;
|
||||||
|
import com.tiedup.remake.rig.anim.property.MoveCoordFunctions.MoveCoordSetter;
|
||||||
|
import com.tiedup.remake.rig.anim.types.ActionAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.LinkAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.physics.ik.InverseKinematicsSimulator.BakedInverseKinematicsDefinition;
|
||||||
|
import com.tiedup.remake.rig.physics.ik.InverseKinematicsSimulator.InverseKinematicsDefinition;
|
||||||
|
import com.tiedup.remake.rig.util.TimePairList;
|
||||||
|
import com.tiedup.remake.rig.math.ValueModifier;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
import com.tiedup.remake.rig.patch.item.CapabilityItem;
|
||||||
|
|
||||||
|
public abstract class AnimationProperty<T> {
|
||||||
|
private static final Map<String, AnimationProperty<?>> SERIALIZABLE_ANIMATION_PROPERTY_KEYS = Maps.newHashMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 D1 — {@link LivingMotion} codec. {@code LivingMotion} is an
|
||||||
|
* {@link com.tiedup.remake.rig.util.ExtendableEnum} so its string form is
|
||||||
|
* the {@link #toString()} lowercased ; we deserialize via
|
||||||
|
* {@code ENUM_MANAGER.getOrThrow(String)} which throws if unknown.
|
||||||
|
*/
|
||||||
|
public static final Codec<LivingMotion> LIVING_MOTION_CODEC = Codec.STRING.flatXmap(
|
||||||
|
name -> {
|
||||||
|
try {
|
||||||
|
return DataResult.success(LivingMotion.ENUM_MANAGER.getOrThrow(name));
|
||||||
|
} catch (java.util.NoSuchElementException e) {
|
||||||
|
return DataResult.error(() -> "Unknown living motion: " + name);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
motion -> DataResult.success(motion.toString().toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 3 D1 — {@link TimePairList} codec. Authored as a flat list of
|
||||||
|
* floats that must have an even length (pairs of {@code begin, end}).
|
||||||
|
* Odd-length lists surface as a codec error (logged + property skipped).
|
||||||
|
*
|
||||||
|
* <p>JSON shape : {@code [0.0, 0.3, 0.6, 0.9]} → two pairs, {@code [0,0.3]}
|
||||||
|
* and {@code [0.6,0.9]}. We go through a float[] because {@code
|
||||||
|
* TimePairList.create} is varargs. Decoding via
|
||||||
|
* {@link TimePairList#create(float...)} surfaces the odd-count invariant
|
||||||
|
* via {@link IllegalArgumentException} — wrapped into a {@link DataResult}
|
||||||
|
* error.
|
||||||
|
*/
|
||||||
|
public static final Codec<TimePairList> TIME_PAIR_LIST_CODEC = Codec.FLOAT.listOf().flatXmap(
|
||||||
|
list -> {
|
||||||
|
if ((list.size() & 1) != 0) {
|
||||||
|
return DataResult.error(() -> "TimePairList must have an even number of floats, got " + list.size());
|
||||||
|
}
|
||||||
|
float[] arr = new float[list.size()];
|
||||||
|
for (int i = 0; i < arr.length; i++) {
|
||||||
|
arr[i] = list.get(i);
|
||||||
|
}
|
||||||
|
return DataResult.success(TimePairList.create(arr));
|
||||||
|
},
|
||||||
|
// Encode back : no public accessor on TimePairList's internal pairs,
|
||||||
|
// and the property pipeline is read-only from JSON (never re-serialized
|
||||||
|
// to disk) — we therefore refuse encoding. If round-trip becomes a
|
||||||
|
// requirement, expose TimePairList#asFloatArray() first.
|
||||||
|
tpl -> DataResult.error(() -> "TimePairList encoding is not supported (read-only datapack property)")
|
||||||
|
);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> AnimationProperty<T> getSerializableProperty(String name) {
|
||||||
|
if (!SERIALIZABLE_ANIMATION_PROPERTY_KEYS.containsKey(name)) {
|
||||||
|
throw new IllegalStateException("No property key named " + name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (AnimationProperty<T>) SERIALIZABLE_ANIMATION_PROPERTY_KEYS.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Codec<T> codecs;
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
public AnimationProperty(String name, @Nullable Codec<T> codecs) {
|
||||||
|
this.codecs = codecs;
|
||||||
|
this.name = name;
|
||||||
|
|
||||||
|
if (name != null) {
|
||||||
|
if (SERIALIZABLE_ANIMATION_PROPERTY_KEYS.containsKey(name)) {
|
||||||
|
throw new IllegalStateException("Animation property key " + name + " is already registered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
SERIALIZABLE_ANIMATION_PROPERTY_KEYS.put(name, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnimationProperty(String name) {
|
||||||
|
this(name, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T parseFrom(JsonElement e) {
|
||||||
|
return this.codecs.parse(JsonOps.INSTANCE, e).resultOrPartial((errm) -> TiedUpRigConstants.LOGGER.warn("Failed to parse property " + this.name + " because of " + errm)).orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Codec<T> getCodecs() {
|
||||||
|
return this.codecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class StaticAnimationProperty<T> extends AnimationProperty<T> {
|
||||||
|
public StaticAnimationProperty(String rl, @Nullable Codec<T> codecs) {
|
||||||
|
super(rl, codecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StaticAnimationProperty() {
|
||||||
|
this(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Events that are fired in every tick.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D2 — serializable from a datapack JSON
|
||||||
|
* {@code "tick_events"} block. Each entry is either a «time»
|
||||||
|
* event ({@code {"frame":0.15, "actions":[...]}}) or a
|
||||||
|
* «period» event
|
||||||
|
* ({@code {"start":0.0, "end":1.0, "actions":[...]}}). See
|
||||||
|
* {@link DataDrivenAnimationEvents#TICK_EVENTS_CODEC}.
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<List<AnimationEvent<?, ?>>> TICK_EVENTS = new StaticAnimationProperty<List<AnimationEvent<?, ?>>> (
|
||||||
|
"tick_events",
|
||||||
|
DataDrivenAnimationEvents.TICK_EVENTS_CODEC
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Events that are fired when the animation starts.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D2 — serializable from a datapack JSON {@code "on_begin"}
|
||||||
|
* block. Each entry is an «action list»
|
||||||
|
* ({@code [{"type":"tiedup:play_sound", ...}, ...]}) or a full object
|
||||||
|
* ({@code {"actions":[...], "side":"SERVER"}}). See
|
||||||
|
* {@link DataDrivenAnimationEvents#BEGIN_END_EVENTS_CODEC}.
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<List<SimpleEvent<?>>> ON_BEGIN_EVENTS = new StaticAnimationProperty<List<SimpleEvent<?>>> (
|
||||||
|
"on_begin",
|
||||||
|
DataDrivenAnimationEvents.BEGIN_END_EVENTS_CODEC
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Events that are fired when the animation ends.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D2 — serializable from a datapack JSON {@code "on_end"}
|
||||||
|
* block. Same shape as {@link #ON_BEGIN_EVENTS}.
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<List<SimpleEvent<?>>> ON_END_EVENTS = new StaticAnimationProperty<List<SimpleEvent<?>>> (
|
||||||
|
"on_end",
|
||||||
|
DataDrivenAnimationEvents.BEGIN_END_EVENTS_CODEC
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event triggered when entity changes an item in hand.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED. The item-change event is a
|
||||||
|
* combat/equipment hook from Epic Fight upstream ; no bondage pipeline
|
||||||
|
* consumer reads this property today. Re-enable (register a codec) when
|
||||||
|
* weapon/equipment reactive animations are reintroduced.
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<SimpleEvent<AnimationEvent.E2<CapabilityItem, CapabilityItem>>> ON_ITEM_CHANGE_EVENT = new StaticAnimationProperty<SimpleEvent<AnimationEvent.E2<CapabilityItem, CapabilityItem>>> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can modify the playback speed of the animation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — serializable via the
|
||||||
|
* {@link PlaybackSpeedModifierImpl#CODEC} dispatch codec (Category B).
|
||||||
|
* JSON example :
|
||||||
|
* <pre>{@code
|
||||||
|
* "play_speed_modifier": { "type": "tiedup:constant_factor", "factor": 0.5 }
|
||||||
|
* }</pre>
|
||||||
|
* The upcast {@code PlaybackSpeedModifierImpl → PlaybackSpeedModifier}
|
||||||
|
* is implicit (sub-interface) so the property stays typed as the wider
|
||||||
|
* functional type for backward-compat with existing consumers in
|
||||||
|
* {@link com.tiedup.remake.rig.anim.AnimationPlayer#tick} and
|
||||||
|
* {@link com.tiedup.remake.rig.anim.client.AnimationSubFileReader}.
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<PlaybackSpeedModifier> PLAY_SPEED_MODIFIER = new StaticAnimationProperty<PlaybackSpeedModifier> (
|
||||||
|
"play_speed_modifier",
|
||||||
|
PlaybackSpeedModifierImpl.CODEC.xmap(
|
||||||
|
impl -> (PlaybackSpeedModifier) impl,
|
||||||
|
base -> {
|
||||||
|
if (base instanceof PlaybackSpeedModifierImpl impl) return impl;
|
||||||
|
throw new IllegalStateException("PlaybackSpeedModifier value is not a serializable impl: " + base);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can modify the elapsed playback time of the animation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — serializable via the
|
||||||
|
* {@link PlaybackTimeModifierImpl#CODEC} dispatch codec (Category B).
|
||||||
|
* JSON example :
|
||||||
|
* <pre>{@code
|
||||||
|
* "elapsed_time_modifier": { "type": "tiedup:loop_section",
|
||||||
|
* "loop_start": 0.3, "loop_end": 0.8 }
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<PlaybackTimeModifier> ELAPSED_TIME_MODIFIER = new StaticAnimationProperty<PlaybackTimeModifier> (
|
||||||
|
"elapsed_time_modifier",
|
||||||
|
PlaybackTimeModifierImpl.CODEC.xmap(
|
||||||
|
impl -> (PlaybackTimeModifier) impl,
|
||||||
|
base -> {
|
||||||
|
if (base instanceof PlaybackTimeModifierImpl impl) return impl;
|
||||||
|
throw new IllegalStateException("PlaybackTimeModifier value is not a serializable impl: " + base);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property will be called both in client and server when modifying the pose.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — serializable via the
|
||||||
|
* {@link PoseModifierImpl#CODEC} dispatch codec (Category B). Key
|
||||||
|
* artist unlock for bondage : per-joint nudges can be authored
|
||||||
|
* from JSON :
|
||||||
|
* <pre>{@code
|
||||||
|
* "pose_modifier": { "type": "tiedup:chain",
|
||||||
|
* "modifiers": [
|
||||||
|
* { "type": "tiedup:joint_rotation_offset", "joint": "upper_arm_left", "pitch": 15.0 },
|
||||||
|
* { "type": "tiedup:joint_translation_offset", "joint": "hand_right", "y": -0.05 }
|
||||||
|
* ] }
|
||||||
|
* }</pre>
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<PoseModifier> POSE_MODIFIER = new StaticAnimationProperty<PoseModifier> (
|
||||||
|
"pose_modifier",
|
||||||
|
PoseModifierImpl.CODEC.xmap(
|
||||||
|
impl -> (PoseModifier) impl,
|
||||||
|
base -> {
|
||||||
|
if (base instanceof PoseModifierImpl impl) return impl;
|
||||||
|
throw new IllegalStateException("PoseModifier value is not a serializable impl: " + base);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix the head rotation to the player's body rotation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, trivial boolean codec.
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<Boolean> FIXED_HEAD_ROTATION = new StaticAnimationProperty<Boolean> (
|
||||||
|
"fixed_head_rotation",
|
||||||
|
Codec.BOOL
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines static animations as link animation when the animation is followed by a specific animation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category D, SKIPPED. The map value is an
|
||||||
|
* {@link AnimationAccessor} which is an internal registry handle (not a
|
||||||
|
* data-describable object) ; resolving accessors from ids requires the
|
||||||
|
* {@link com.tiedup.remake.rig.anim.AnimationManager} to already be
|
||||||
|
* populated, which is only true after full resource-pack load. The
|
||||||
|
* existing sub-file reader path handles transitions correctly — moving
|
||||||
|
* the parse here would create a chicken-and-egg bootstrapping issue.
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>>> TRANSITION_ANIMATIONS_FROM = new StaticAnimationProperty<Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>>> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines static animations as link animation when the animation is following a specific animation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category D, SKIPPED (same reason as
|
||||||
|
* {@link #TRANSITION_ANIMATIONS_FROM}).
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>>> TRANSITION_ANIMATIONS_TO = new StaticAnimationProperty<Map<ResourceLocation, AnimationAccessor<? extends StaticAnimation>>> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable physics while playing animation
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<Boolean> NO_PHYSICS = new StaticAnimationProperty<Boolean> ("no_physics", Codec.BOOL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inverse kinematics information.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category D, SKIPPED. {@link InverseKinematicsDefinition}
|
||||||
|
* holds complex baked state (joint chains, pole targets, constraint
|
||||||
|
* weights) that is authored via a dedicated IK definition file, not
|
||||||
|
* via the animation properties block. See the sub-file reader in
|
||||||
|
* {@link com.tiedup.remake.rig.anim.client.AnimationSubFileReader}.
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<List<InverseKinematicsDefinition>> IK_DEFINITION = new StaticAnimationProperty<List<InverseKinematicsDefinition>> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property automatically baked when animation is loaded.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category D, SKIPPED. Never authored in JSON : this
|
||||||
|
* slot is populated at load time by
|
||||||
|
* {@link com.tiedup.remake.rig.anim.types.StaticAnimation#loadAnimation}
|
||||||
|
* from {@link #IK_DEFINITION} which is then cleared.
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<List<BakedInverseKinematicsDefinition>> BAKED_IK_DEFINITION = new StaticAnimationProperty<List<BakedInverseKinematicsDefinition>> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property reset the entity's living motion.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, codec uses the shared
|
||||||
|
* {@link AnimationProperty#LIVING_MOTION_CODEC} which resolves against
|
||||||
|
* {@link LivingMotion#ENUM_MANAGER}. JSON example :
|
||||||
|
* <pre>{@code "reset_living_motion": "idle"}</pre>
|
||||||
|
*/
|
||||||
|
public static final StaticAnimationProperty<LivingMotion> RESET_LIVING_MOTION = new StaticAnimationProperty<LivingMotion> (
|
||||||
|
"reset_living_motion",
|
||||||
|
LIVING_MOTION_CODEC
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ActionAnimationProperty<T> extends StaticAnimationProperty<T> {
|
||||||
|
public ActionAnimationProperty(String rl, @Nullable Codec<T> codecs) {
|
||||||
|
super(rl, codecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActionAnimationProperty() {
|
||||||
|
this(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property will set the entity's delta movement to (0, 0, 0) at the beginning of an animation if true.
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<Boolean> STOP_MOVEMENT = new ActionAnimationProperty<Boolean> ("stop_movements", Codec.BOOL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property will set the entity's delta movement to (0, 0, 0) at the beginning of an animation if true.
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<Boolean> REMOVE_DELTA_MOVEMENT = new ActionAnimationProperty<Boolean> ("revmoe_delta_move", Codec.BOOL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property will move entity's coord also as y axis if true.
|
||||||
|
* Don't recommend using this property because it's old system. Use the coord joint instead.
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<Boolean> MOVE_VERTICAL = new ActionAnimationProperty<Boolean> ("move_vertically", Codec.BOOL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property determines the time of entity not affected by gravity.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, codec uses the shared
|
||||||
|
* {@link AnimationProperty#TIME_PAIR_LIST_CODEC}. JSON example :
|
||||||
|
* <pre>{@code "no_gravity_time": [0.1, 0.4, 0.6, 0.9]}</pre>
|
||||||
|
* (two no-gravity windows : [0.1..0.4] and [0.6..0.9]).
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<TimePairList> NO_GRAVITY_TIME = new ActionAnimationProperty<TimePairList> (
|
||||||
|
"no_gravity_time",
|
||||||
|
TIME_PAIR_LIST_CODEC
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coord of action animation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED. {@link TransformSheet} is
|
||||||
|
* populated from the animation's baked keyframes by
|
||||||
|
* {@link com.tiedup.remake.rig.asset.JsonAssetLoader} at load time
|
||||||
|
* (not authored in the properties block). Exposing a codec here would
|
||||||
|
* double-write an already-computed value.
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<TransformSheet> COORD = new ActionAnimationProperty<TransformSheet> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property determines whether to move the entity in link animation or not.
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<Boolean> MOVE_ON_LINK = new ActionAnimationProperty<Boolean> ("move_during_link", Codec.BOOL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* You can specify the coord movement time in action animation. Must be registered in order of time.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, codec uses
|
||||||
|
* {@link AnimationProperty#TIME_PAIR_LIST_CODEC}.
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<TimePairList> MOVE_TIME = new ActionAnimationProperty<TimePairList> (
|
||||||
|
"move_time",
|
||||||
|
TIME_PAIR_LIST_CODEC
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the dynamic coordinates of {@link ActionAnimation}. Called before creation of {@link LinkAnimation}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED. {@link MoveCoordSetter} is a
|
||||||
|
* combat/action-movement hook from Epic Fight upstream ; the bondage
|
||||||
|
* pipeline never authors these at the datapack level (coord work is
|
||||||
|
* done via coord joints and the sub-file reader).
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<MoveCoordSetter> COORD_SET_BEGIN = new ActionAnimationProperty<MoveCoordSetter> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the dynamic coordinates of {@link ActionAnimation}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED (same reason as
|
||||||
|
* {@link #COORD_SET_BEGIN}).
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<MoveCoordSetter> COORD_SET_TICK = new ActionAnimationProperty<MoveCoordSetter> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the coordinates of action animation.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED (same reason as
|
||||||
|
* {@link #COORD_SET_BEGIN}).
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<MoveCoordGetter> COORD_GET = new ActionAnimationProperty<MoveCoordGetter> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property determines if the speed effect will increase the move distance.
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<Boolean> AFFECT_SPEED = new ActionAnimationProperty<Boolean> ("move_speed_based_distance", Codec.BOOL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property determines if the movement can be canceled by {@link LivingEntityPatch#shouldBlockMoving()}.
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<Boolean> CANCELABLE_MOVE = new ActionAnimationProperty<Boolean> ("cancellable_movement", Codec.BOOL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Death animations won't be played if this value is true
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<Boolean> IS_DEATH_ANIMATION = new ActionAnimationProperty<Boolean> ("is_death", Codec.BOOL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property determines the update time of {@link ActionAnimationProperty#COORD_SET_TICK}. If the current time out of the bound it uses {@link MoveCoordFunctions#RAW_COORD and MoveCoordFunctions#DIFF_FROM_PREV_COORD}}
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, codec uses
|
||||||
|
* {@link AnimationProperty#TIME_PAIR_LIST_CODEC}. Still serialized
|
||||||
|
* despite being tied to {@link #COORD_SET_TICK} (Category C) because
|
||||||
|
* the underlying datatype is data and authoring the time windows
|
||||||
|
* without the setter is still legal (it degrades to a no-op, cheap).
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<TimePairList> COORD_UPDATE_TIME = new ActionAnimationProperty<TimePairList> (
|
||||||
|
"coord_update_time",
|
||||||
|
TIME_PAIR_LIST_CODEC
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property determines if it reset the player basic attack combo counter or not.
|
||||||
|
* RIG : BasicAttack strippé, flag conservé pour compat JSON.
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<Boolean> RESET_PLAYER_COMBO_COUNTER = new ActionAnimationProperty<Boolean> ("reset_combo_attack_counter", Codec.BOOL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide destination of action animation {@link MoveCoordFunctions}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED. Combat-movement hook ; the
|
||||||
|
* bondage pipeline would need a whole new destination-provider
|
||||||
|
* registry to expose this via JSON, and there is no artist use-case
|
||||||
|
* today.
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<DestLocationProvider> DEST_LOCATION_PROVIDER = new ActionAnimationProperty<DestLocationProvider> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide y rotation of entity {@link MoveCoordFunctions}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED (combat-rotation hook).
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<YRotProvider> ENTITY_YROT_PROVIDER = new ActionAnimationProperty<YRotProvider> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide y rotation of tracing coord {@link MoveCoordFunctions}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category C, SKIPPED (combat-rotation hook).
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<YRotProvider> DEST_COORD_YROT_PROVIDER = new ActionAnimationProperty<YRotProvider> ();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decides the index of start key frame for coord transform, See also with {@link MoveCoordFunctions#TRACE_ORIGIN_AS_DESTINATION}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, trivial int codec.
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<Integer> COORD_START_KEYFRAME_INDEX = new ActionAnimationProperty<Integer> (
|
||||||
|
"coord_start_keyframe_index",
|
||||||
|
Codec.INT
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decides the index of destination key frame for coord transform, See also with {@link MoveCoordFunctions#TRACE_ORIGIN_AS_DESTINATION}.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — Category A, trivial int codec.
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<Integer> COORD_DEST_KEYFRAME_INDEX = new ActionAnimationProperty<Integer> (
|
||||||
|
"coord_dest_keyframe_index",
|
||||||
|
Codec.INT
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if an entity should look where a camera is looking at the beginning of an animation (player only)
|
||||||
|
*/
|
||||||
|
public static final ActionAnimationProperty<Boolean> SYNC_CAMERA = new ActionAnimationProperty<Boolean> ("sync_camera", Codec.BOOL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AttackAnimationProperty<T> extends ActionAnimationProperty<T> {
|
||||||
|
public AttackAnimationProperty(String rl, @Nullable Codec<T> codecs) {
|
||||||
|
super(rl, codecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AttackAnimationProperty() {
|
||||||
|
this(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property determines if the animation has a fixed amount of move distance not depending on the distance between attacker and target entity
|
||||||
|
*/
|
||||||
|
public static final AttackAnimationProperty<Boolean> FIXED_MOVE_DISTANCE = new AttackAnimationProperty<Boolean> ("fixed_movement_distance", Codec.BOOL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property determines how much the playback speed will be affected by entity's attack speed.
|
||||||
|
*/
|
||||||
|
public static final AttackAnimationProperty<Float> ATTACK_SPEED_FACTOR = new AttackAnimationProperty<Float> ("attack_speed_factor", Codec.FLOAT);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property determines the basis of the speed factor. Default basis is the total animation time.
|
||||||
|
*/
|
||||||
|
public static final AttackAnimationProperty<Float> BASIS_ATTACK_SPEED = new AttackAnimationProperty<Float> ("basis_attack_speed", Codec.FLOAT);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property adds interpolated colliders when detecting colliding entities by using @MultiCollider.
|
||||||
|
*/
|
||||||
|
public static final AttackAnimationProperty<Integer> EXTRA_COLLIDERS = new AttackAnimationProperty<Integer> ("extra_colliders", Codec.INT);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This property determines a minimal distance between attacker and target.
|
||||||
|
*/
|
||||||
|
public static final AttackAnimationProperty<Float> REACH = new AttackAnimationProperty<Float> ("reach", Codec.FLOAT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combat-phase properties for attack animations.
|
||||||
|
*
|
||||||
|
* <p>Phase 3 D1 — entire class body is Category C (combat-only, EF
|
||||||
|
* legacy). The constructor no longer registers properties in the shared
|
||||||
|
* dispatch map (see the commented {@code super(...)} call below) — this
|
||||||
|
* was deliberately neutered in a prior audit because the bondage pipeline
|
||||||
|
* never reads {@link AttackPhaseProperty} values. The four
|
||||||
|
* {@link ValueModifier} constants kept their names purely for static
|
||||||
|
* reference by surviving combat-adjacent code paths ; the sound/tag/
|
||||||
|
* location properties are forever null-codec. Do not add codecs here
|
||||||
|
* until a combat feature is reintroduced (see V3-REW-11+ in the
|
||||||
|
* roadmap) — instead, resurrect the commented {@code super} call first
|
||||||
|
* so the dispatch map picks them up consistently.
|
||||||
|
*/
|
||||||
|
public static class AttackPhaseProperty<T> {
|
||||||
|
public AttackPhaseProperty(String rl, @Nullable Codec<? extends T> codecs) {
|
||||||
|
//super(rl, codecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AttackPhaseProperty() {
|
||||||
|
//this(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final AttackPhaseProperty<ValueModifier> MAX_STRIKES_MODIFIER = new AttackPhaseProperty<ValueModifier> ("max_strikes", ValueModifier.CODEC);
|
||||||
|
public static final AttackPhaseProperty<ValueModifier> DAMAGE_MODIFIER = new AttackPhaseProperty<ValueModifier> ("damage", ValueModifier.CODEC);
|
||||||
|
public static final AttackPhaseProperty<ValueModifier> ARMOR_NEGATION_MODIFIER = new AttackPhaseProperty<ValueModifier> ("armor_negation", ValueModifier.CODEC);
|
||||||
|
public static final AttackPhaseProperty<ValueModifier> IMPACT_MODIFIER = new AttackPhaseProperty<ValueModifier> ("impact", ValueModifier.CODEC);
|
||||||
|
// RIG : EXTRA_DAMAGE, STUN_TYPE, PARTICLE strippés (combat).
|
||||||
|
public static final AttackPhaseProperty<SoundEvent> SWING_SOUND = new AttackPhaseProperty<SoundEvent> ();
|
||||||
|
public static final AttackPhaseProperty<SoundEvent> HIT_SOUND = new AttackPhaseProperty<SoundEvent> ();
|
||||||
|
public static final AttackPhaseProperty<Set<TagKey<DamageType>>> SOURCE_TAG = new AttackPhaseProperty<Set<TagKey<DamageType>>> ();
|
||||||
|
public static final AttackPhaseProperty<Function<LivingEntityPatch<?>, Vec3>> SOURCE_LOCATION_PROVIDER = new AttackPhaseProperty<Function<LivingEntityPatch<?>, Vec3>> ();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface Registerer<T> {
|
||||||
|
void register(Map<AnimationProperty<T>, Object> properties, AnimationProperty<T> key, T object);
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************
|
||||||
|
* Static Animation Properties
|
||||||
|
******************************/
|
||||||
|
/**
|
||||||
|
* elapsedTime contains partial tick
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface PoseModifier {
|
||||||
|
void modify(DynamicAnimation self, Pose pose, LivingEntityPatch<?> entitypatch, float elapsedTime, float partialTick);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface PlaybackSpeedModifier {
|
||||||
|
float modify(DynamicAnimation self, LivingEntityPatch<?> entitypatch, float speed, float prevElapsedTime, float elapsedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface PlaybackTimeModifier {
|
||||||
|
Pair<Float, Float> modify(DynamicAnimation self, LivingEntityPatch<?> entitypatch, float speed, float prevElapsedTime, float elapsedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface DestLocationProvider {
|
||||||
|
Vec3 get(DynamicAnimation self, LivingEntityPatch<?> entitypatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface YRotProvider {
|
||||||
|
float get(DynamicAnimation self, LivingEntityPatch<?> entitypatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,480 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.property;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import net.minecraft.core.BlockPos;
|
||||||
|
import net.minecraft.tags.BlockTags;
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import net.minecraft.world.entity.Entity;
|
||||||
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
|
||||||
|
import net.minecraft.world.entity.ai.attributes.Attributes;
|
||||||
|
import net.minecraft.world.item.enchantment.EnchantmentHelper;
|
||||||
|
import net.minecraft.world.item.enchantment.Enchantments;
|
||||||
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationPlayer;
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
import com.tiedup.remake.rig.anim.Keyframe;
|
||||||
|
import com.tiedup.remake.rig.anim.SynchedAnimationVariableKeys;
|
||||||
|
import com.tiedup.remake.rig.anim.TransformSheet;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.AttackAnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.DestLocationProvider;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.YRotProvider;
|
||||||
|
import com.tiedup.remake.rig.anim.types.ActionAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.AttackAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.AttackAnimation.Phase;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.EntityState;
|
||||||
|
// RIG : GrapplingAttackAnimation strippé (combat grappling hook), ref javadoc conservée
|
||||||
|
import com.tiedup.remake.rig.math.MathUtils;
|
||||||
|
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||||
|
import com.tiedup.remake.rig.math.Vec3f;
|
||||||
|
import com.tiedup.remake.rig.math.Vec4f;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
import com.tiedup.remake.rig.patch.MobPatch;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry complet des constantes {@code MoveCoord*} consommées par les datapacks EF tiers
|
||||||
|
* via réflection ({@code StaticFieldArgument}). Ne pas purger individuellement sans couper
|
||||||
|
* le support datapack — l'absence d'un nom au runtime crash le chargement JSON.
|
||||||
|
*/
|
||||||
|
public class MoveCoordFunctions {
|
||||||
|
/**
|
||||||
|
* Defines a function that how to interpret given coordinate and return the movement vector from entity's current position
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface MoveCoordGetter {
|
||||||
|
Vec3f get(DynamicAnimation animation, LivingEntityPatch<?> entitypatch, TransformSheet transformSheet, float prevElapsedTime, float elapsedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a function that how to build the coordinate of {@link ActionAnimation}
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface MoveCoordSetter {
|
||||||
|
void set(DynamicAnimation animation, LivingEntityPatch<?> entitypatch, TransformSheet transformSheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MODEL_COORD
|
||||||
|
* - Calculates the coordinate gap between previous and current elapsed time
|
||||||
|
* - the coordinate doesn't reflect the entity's rotation
|
||||||
|
*/
|
||||||
|
public static final MoveCoordGetter MODEL_COORD = (animation, entitypatch, coord, prevElapsedTime, elapsedTime) -> {
|
||||||
|
LivingEntity livingentity = entitypatch.getOriginal();
|
||||||
|
JointTransform oJt = coord.getInterpolatedTransform(prevElapsedTime);
|
||||||
|
JointTransform jt = coord.getInterpolatedTransform(elapsedTime);
|
||||||
|
Vec4f prevpos = new Vec4f(oJt.translation());
|
||||||
|
Vec4f currentpos = new Vec4f(jt.translation());
|
||||||
|
|
||||||
|
OpenMatrix4f rotationTransform = entitypatch.getModelMatrix(1.0F).removeTranslation().removeScale();
|
||||||
|
OpenMatrix4f localTransform = entitypatch.getArmature().searchJointByName("Root").getLocalTransform().removeTranslation();
|
||||||
|
rotationTransform.mulBack(localTransform);
|
||||||
|
currentpos.transform(rotationTransform);
|
||||||
|
prevpos.transform(rotationTransform);
|
||||||
|
|
||||||
|
boolean hasNoGravity = entitypatch.getOriginal().isNoGravity();
|
||||||
|
boolean moveVertical = animation.getProperty(ActionAnimationProperty.MOVE_VERTICAL).orElse(false) || animation.getProperty(ActionAnimationProperty.COORD).isPresent();
|
||||||
|
float dx = prevpos.x - currentpos.x;
|
||||||
|
float dy = (moveVertical || hasNoGravity) ? currentpos.y - prevpos.y : 0.0F;
|
||||||
|
float dz = prevpos.z - currentpos.z;
|
||||||
|
dx = Math.abs(dx) > 0.0001F ? dx : 0.0F;
|
||||||
|
dz = Math.abs(dz) > 0.0001F ? dz : 0.0F;
|
||||||
|
|
||||||
|
BlockPos blockpos = new BlockPos.MutableBlockPos(livingentity.getX(), livingentity.getBoundingBox().minY - 1.0D, livingentity.getZ());
|
||||||
|
BlockState blockState = livingentity.level().getBlockState(blockpos);
|
||||||
|
AttributeInstance movementSpeed = livingentity.getAttribute(Attributes.MOVEMENT_SPEED);
|
||||||
|
boolean soulboost = blockState.is(BlockTags.SOUL_SPEED_BLOCKS) && EnchantmentHelper.getEnchantmentLevel(Enchantments.SOUL_SPEED, livingentity) > 0;
|
||||||
|
float speedFactor = (float)(soulboost ? 1.0D : livingentity.level().getBlockState(blockpos).getBlock().getSpeedFactor());
|
||||||
|
float moveMultiplier = (float)(animation.getProperty(ActionAnimationProperty.AFFECT_SPEED).orElse(false) ? (movementSpeed.getValue() / movementSpeed.getBaseValue()) : 1.0F);
|
||||||
|
|
||||||
|
return new Vec3f(dx * moveMultiplier * speedFactor, dy, dz * moveMultiplier * speedFactor);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WORLD_COORD
|
||||||
|
* - Calculates the coordinate of current elapsed time
|
||||||
|
* - the coordinate is the world position
|
||||||
|
*/
|
||||||
|
public static final MoveCoordGetter WORLD_COORD = (animation, entitypatch, coord, prevElapsedTime, elapsedTime) -> {
|
||||||
|
JointTransform jt = coord.getInterpolatedTransform(elapsedTime);
|
||||||
|
Vec3 entityPos = entitypatch.getOriginal().position();
|
||||||
|
|
||||||
|
return jt.translation().copy().sub(Vec3f.fromDoubleVector(entityPos));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ATTACHED
|
||||||
|
* Calculates the relative position of a grappling target entity.
|
||||||
|
* - especially used by {@link GrapplingAttackAnimation}
|
||||||
|
* - read by {@link MoveCoordFunctions#RAW_COORD}
|
||||||
|
*/
|
||||||
|
public static final MoveCoordGetter ATTACHED = (animation, entitypatch, coord, prevElapsedTime, elapsedTime) -> {
|
||||||
|
LivingEntity target = entitypatch.getGrapplingTarget();
|
||||||
|
|
||||||
|
if (target == null) {
|
||||||
|
return MODEL_COORD.get(animation, entitypatch, coord, prevElapsedTime, elapsedTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
TransformSheet rootCoord = animation.getCoord();
|
||||||
|
LivingEntity livingentity = entitypatch.getOriginal();
|
||||||
|
Vec3f model = rootCoord.getInterpolatedTransform(elapsedTime).translation();
|
||||||
|
Vec3f world = OpenMatrix4f.transform3v(OpenMatrix4f.createRotatorDeg(-target.getYRot(), Vec3f.Y_AXIS), model, null);
|
||||||
|
Vec3f dst = Vec3f.fromDoubleVector(target.position()).add(world);
|
||||||
|
entitypatch.setYRot(Mth.wrapDegrees(target.getYRot() + 180.0F));
|
||||||
|
|
||||||
|
return dst.sub(Vec3f.fromDoubleVector(livingentity.position()));
|
||||||
|
};
|
||||||
|
|
||||||
|
/******************************************************
|
||||||
|
* Action animation properties
|
||||||
|
******************************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No destination
|
||||||
|
*/
|
||||||
|
public static final DestLocationProvider NO_DEST = (DynamicAnimation self, LivingEntityPatch<?> entitypatch) -> {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Location of the current attack target
|
||||||
|
*/
|
||||||
|
public static final DestLocationProvider ATTACK_TARGET_LOCATION = (DynamicAnimation self, LivingEntityPatch<?> entitypatch) -> {
|
||||||
|
return entitypatch.getTarget() == null ? null : entitypatch.getTarget().position();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Location set by Animation Variable
|
||||||
|
*/
|
||||||
|
public static final DestLocationProvider SYNCHED_DEST_VARIABLE = (DynamicAnimation self, LivingEntityPatch<?> entitypatch) -> {
|
||||||
|
return entitypatch.getAnimator().getVariables().getOrDefault(SynchedAnimationVariableKeys.DESTINATION.get(), self.getRealAnimation());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Location of current attack target that is provided by animation variable
|
||||||
|
*/
|
||||||
|
public static final DestLocationProvider SYNCHED_TARGET_ENTITY_LOCATION_VARIABLE = (DynamicAnimation self, LivingEntityPatch<?> entitypatch) -> {
|
||||||
|
Optional<Integer> targetEntityId = entitypatch.getAnimator().getVariables().get(SynchedAnimationVariableKeys.TARGET_ENTITY.get(), self.getRealAnimation());
|
||||||
|
|
||||||
|
if (targetEntityId.isPresent()) {
|
||||||
|
Entity entity = entitypatch.getOriginal().level().getEntity(targetEntityId.get());
|
||||||
|
|
||||||
|
if (entity != null) {
|
||||||
|
return entity.position();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entitypatch.getOriginal().position();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looking direction from an action beginning location to a destination location
|
||||||
|
*/
|
||||||
|
public static final YRotProvider LOOK_DEST = (DynamicAnimation self, LivingEntityPatch<?> entitypatch) -> {
|
||||||
|
Vec3 destLocation = self.getRealAnimation().get().getProperty(ActionAnimationProperty.DEST_LOCATION_PROVIDER).orElse(NO_DEST).get(self, entitypatch);
|
||||||
|
|
||||||
|
if (destLocation != null) {
|
||||||
|
Vec3 startInWorld = entitypatch.getAnimator().getVariables().getOrDefault(ActionAnimation.BEGINNING_LOCATION, self.getRealAnimation());
|
||||||
|
|
||||||
|
if (startInWorld == null) {
|
||||||
|
startInWorld = entitypatch.getOriginal().position();
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 toDestWorld = destLocation.subtract(startInWorld);
|
||||||
|
float yRot = (float)Mth.wrapDegrees(MathUtils.getYRotOfVector(toDestWorld));
|
||||||
|
float entityYRot = MathUtils.rotlerp(entitypatch.getYRot(), yRot, entitypatch.getYRotLimit());
|
||||||
|
|
||||||
|
return entityYRot;
|
||||||
|
} else {
|
||||||
|
return entitypatch.getYRot();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate an entity toward target for attack animations
|
||||||
|
*/
|
||||||
|
public static final YRotProvider MOB_ATTACK_TARGET_LOOK = (DynamicAnimation self, LivingEntityPatch<?> entitypatch) -> {
|
||||||
|
if (!entitypatch.isLogicalClient() && entitypatch instanceof MobPatch<?> mobpatch) {
|
||||||
|
AnimationPlayer player = entitypatch.getAnimator().getPlayerFor(self.getAccessor());
|
||||||
|
float elapsedTime = player.getElapsedTime();
|
||||||
|
EntityState state = self.getState(entitypatch, elapsedTime);
|
||||||
|
|
||||||
|
if (state.getLevel() == 1 && !state.turningLocked()) {
|
||||||
|
mobpatch.getOriginal().getNavigation().stop();
|
||||||
|
entitypatch.getOriginal().attackAnim = 2;
|
||||||
|
LivingEntity target = entitypatch.getTarget();
|
||||||
|
|
||||||
|
if (target != null) {
|
||||||
|
float currentYRot = Mth.wrapDegrees(entitypatch.getOriginal().getYRot());
|
||||||
|
float clampedYRot = entitypatch.getYRotDeltaTo(target);
|
||||||
|
|
||||||
|
return currentYRot + clampedYRot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entitypatch.getYRot();
|
||||||
|
};
|
||||||
|
|
||||||
|
/******************************************************
|
||||||
|
* MoveCoordSetters
|
||||||
|
* Consider that getAnimationPlayer(self) returns null at the beginning.
|
||||||
|
******************************************************/
|
||||||
|
/**
|
||||||
|
* Sets a raw animation coordinate as action animation's coord
|
||||||
|
* - read by {@link MoveCoordFunctions#MODEL_COORD}
|
||||||
|
*/
|
||||||
|
public static final MoveCoordSetter RAW_COORD = (self, entitypatch, transformSheet) -> {
|
||||||
|
transformSheet.readFrom(self.getCoord().copyAll());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a raw animation coordinate multiplied by entity's pitch as action animation's coord
|
||||||
|
* - read by {@link MoveCoordFunctions#MODEL_COORD}
|
||||||
|
*/
|
||||||
|
public static final MoveCoordSetter RAW_COORD_WITH_X_ROT = (self, entitypatch, transformSheet) -> {
|
||||||
|
TransformSheet sheet = self.getCoord().copyAll();
|
||||||
|
float xRot = entitypatch.getOriginal().getXRot();
|
||||||
|
|
||||||
|
for (Keyframe kf : sheet.getKeyframes()) {
|
||||||
|
kf.transform().translation().rotate(-xRot, Vec3f.X_AXIS);
|
||||||
|
}
|
||||||
|
|
||||||
|
transformSheet.readFrom(sheet);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trace the origin point(0, 0, 0) in blender coord system as the destination
|
||||||
|
* - specify the {@link ActionAnimationProperty#DEST_LOCATION_PROVIDER} or it will act as {@link MoveCoordFunctions#RAW_COORD}.
|
||||||
|
* - the first keyframe's location is where the entity is in world
|
||||||
|
* - you can specify target frame distance by {@link ActionAnimationProperty#COORD_START_KEYFRAME_INDEX}, {@link ActionAnimationProperty#COORD_DEST_KEYFRAME_INDEX}
|
||||||
|
* - the coord after destination frame will not be scaled or rotated by distance gap between start location and end location in world coord
|
||||||
|
* - entity's x rotation is not affected by this coord function
|
||||||
|
* - entity's y rotation is the direction toward a destination, or you can give specific rotation value by {@link ActionAnimation#ENTITY_Y_ROT AnimationProperty}
|
||||||
|
* - no movements in link animation
|
||||||
|
* - read by {@link MoveCoordFunctions#WORLD_COORD}
|
||||||
|
*/
|
||||||
|
public static final MoveCoordSetter TRACE_ORIGIN_AS_DESTINATION = (self, entitypatch, transformSheet) -> {
|
||||||
|
if (self.isLinkAnimation()) {
|
||||||
|
transformSheet.readFrom(TransformSheet.EMPTY_SHEET_PROVIDER.apply(entitypatch.getOriginal().position()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Keyframe[] coordKeyframes = self.getCoord().getKeyframes();
|
||||||
|
int startFrame = self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_START_KEYFRAME_INDEX).orElse(0);
|
||||||
|
int destFrame = self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_DEST_KEYFRAME_INDEX).orElse(coordKeyframes.length - 1);
|
||||||
|
Vec3 destInWorld = self.getRealAnimation().get().getProperty(ActionAnimationProperty.DEST_LOCATION_PROVIDER).orElse(NO_DEST).get(self, entitypatch);
|
||||||
|
|
||||||
|
if (destInWorld == null) {
|
||||||
|
Vec3f beginningPosition = coordKeyframes[0].transform().translation().copy().multiply(1.0F, 1.0F, -1.0F);
|
||||||
|
beginningPosition.rotate(-entitypatch.getYRot(), Vec3f.Y_AXIS);
|
||||||
|
destInWorld = entitypatch.getOriginal().position().add(-beginningPosition.x, -beginningPosition.y, -beginningPosition.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 startInWorld = entitypatch.getAnimator().getVariables().getOrDefault(ActionAnimation.BEGINNING_LOCATION, self.getRealAnimation());
|
||||||
|
|
||||||
|
if (startInWorld == null) {
|
||||||
|
startInWorld = entitypatch.getOriginal().position();
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3 toTargetInWorld = destInWorld.subtract(startInWorld);
|
||||||
|
float yRot = (float)Mth.wrapDegrees(MathUtils.getYRotOfVector(toTargetInWorld));
|
||||||
|
Optional<YRotProvider> destYRotProvider = self.getRealAnimation().get().getProperty(ActionAnimationProperty.DEST_COORD_YROT_PROVIDER);
|
||||||
|
float destYRot = destYRotProvider.isEmpty() ? yRot : destYRotProvider.get().get(self, entitypatch);
|
||||||
|
|
||||||
|
TransformSheet result = self.getCoord().transformToWorldCoordOriginAsDest(entitypatch, startInWorld, destInWorld, yRot, destYRot, startFrame, destFrame);
|
||||||
|
transformSheet.readFrom(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trace the target entity's position (use it with MODEL_COORD)
|
||||||
|
* - the location of the last keyfram is basis to limit maximum distance
|
||||||
|
* - rotation is where the entity is looking
|
||||||
|
*/
|
||||||
|
public static final MoveCoordSetter TRACE_TARGET_DISTANCE = (self, entitypatch, transformSheet) -> {
|
||||||
|
Vec3 destLocation = self.getRealAnimation().get().getProperty(ActionAnimationProperty.DEST_LOCATION_PROVIDER).orElse(NO_DEST).get(self, entitypatch);
|
||||||
|
|
||||||
|
if (destLocation != null) {
|
||||||
|
TransformSheet transform = self.getCoord().copyAll();
|
||||||
|
Keyframe[] coord = transform.getKeyframes();
|
||||||
|
Keyframe[] realAnimationCoord = self.getRealAnimation().get().getCoord().getKeyframes();
|
||||||
|
Vec3 startInWorld = entitypatch.getAnimator().getVariables().getOrDefault(ActionAnimation.BEGINNING_LOCATION, self.getRealAnimation());
|
||||||
|
|
||||||
|
if (startInWorld == null) {
|
||||||
|
startInWorld = entitypatch.getOriginal().position();
|
||||||
|
}
|
||||||
|
|
||||||
|
int startFrame = self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_START_KEYFRAME_INDEX).orElse(0);
|
||||||
|
int realAnimationEndFrame = self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_DEST_KEYFRAME_INDEX).orElse(self.getRealAnimation().get().getCoord().getKeyframes().length - 1);
|
||||||
|
Vec3 toDestWorld = destLocation.subtract(startInWorld);
|
||||||
|
Vec3f toDestAnim = realAnimationCoord[realAnimationEndFrame].transform().translation();
|
||||||
|
LivingEntity attackTarget = entitypatch.getTarget();
|
||||||
|
|
||||||
|
// Calculate Entity-Entity collide radius
|
||||||
|
float entityRadius = 0.0F;
|
||||||
|
|
||||||
|
if (attackTarget != null) {
|
||||||
|
float reach = 0.0F;
|
||||||
|
|
||||||
|
if (self.getRealAnimation().get() instanceof AttackAnimation attackAnimation) {
|
||||||
|
Optional<Float> reachOpt = attackAnimation.getProperty(AttackAnimationProperty.REACH);
|
||||||
|
|
||||||
|
if (reachOpt.isPresent()) {
|
||||||
|
reach = reachOpt.get();
|
||||||
|
} else {
|
||||||
|
AnimationPlayer player = entitypatch.getAnimator().getPlayerFor(self.getAccessor());
|
||||||
|
|
||||||
|
if (player != null) {
|
||||||
|
Phase phase = attackAnimation.getPhaseByTime(player.getElapsedTime());
|
||||||
|
reach = entitypatch.getReach(phase.hand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entityRadius = (attackTarget.getBbWidth() + entitypatch.getOriginal().getBbWidth()) * 0.7F + reach;
|
||||||
|
}
|
||||||
|
|
||||||
|
float worldLength = Math.max((float)toDestWorld.length() - entityRadius, 0.0F);
|
||||||
|
float animLength = toDestAnim.length();
|
||||||
|
|
||||||
|
float dot = entitypatch.getAnimator().getVariables().getOrDefault(ActionAnimation.INITIAL_LOOK_VEC_DOT, self.getRealAnimation());
|
||||||
|
float lookLength = Mth.lerp(dot, animLength, worldLength);
|
||||||
|
float scale = Math.min(lookLength / animLength, 1.0F);
|
||||||
|
|
||||||
|
if (self.isLinkAnimation()) {
|
||||||
|
scale *= coord[coord.length - 1].transform().translation().length() / animLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
int endFrame = self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_DEST_KEYFRAME_INDEX).orElse(coord.length - 1);
|
||||||
|
|
||||||
|
for (int i = startFrame; i <= endFrame; i++) {
|
||||||
|
Vec3f translation = coord[i].transform().translation();
|
||||||
|
translation.x *= scale;
|
||||||
|
|
||||||
|
if (translation.z < 0.0F) {
|
||||||
|
translation.z *= scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transformSheet.readFrom(transform);
|
||||||
|
} else {
|
||||||
|
transformSheet.readFrom(self.getCoord().copyAll());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trace the target entity's position (use it MODEL_COORD)
|
||||||
|
* - the location of the last keyframe is a basis to limit maximum distance
|
||||||
|
* - rotation is the direction toward a target entity
|
||||||
|
*/
|
||||||
|
public static final MoveCoordSetter TRACE_TARGET_LOCATION_ROTATION = (self, entitypatch, transformSheet) -> {
|
||||||
|
Vec3 destLocation = self.getRealAnimation().get().getProperty(ActionAnimationProperty.DEST_LOCATION_PROVIDER).orElse(NO_DEST).get(self, entitypatch);
|
||||||
|
|
||||||
|
if (destLocation != null) {
|
||||||
|
TransformSheet transform = self.getCoord().copyAll();
|
||||||
|
Keyframe[] coord = transform.getKeyframes();
|
||||||
|
Keyframe[] realAnimationCoord = self.getRealAnimation().get().getCoord().getKeyframes();
|
||||||
|
Vec3 startInWorld = entitypatch.getAnimator().getVariables().getOrDefault(ActionAnimation.BEGINNING_LOCATION, self.getRealAnimation());
|
||||||
|
|
||||||
|
if (startInWorld == null) {
|
||||||
|
startInWorld = entitypatch.getOriginal().position();
|
||||||
|
}
|
||||||
|
|
||||||
|
int startFrame = self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_START_KEYFRAME_INDEX).orElse(0);
|
||||||
|
int endFrame = self.isLinkAnimation() ? coord.length - 1 : self.getRealAnimation().get().getProperty(ActionAnimationProperty.COORD_DEST_KEYFRAME_INDEX).orElse(coord.length - 1);
|
||||||
|
Vec3 toDestWorld = destLocation.subtract(startInWorld);
|
||||||
|
Vec3f toDestAnim = realAnimationCoord[endFrame].transform().translation();
|
||||||
|
LivingEntity attackTarget = entitypatch.getTarget();
|
||||||
|
|
||||||
|
// Calculate Entity-Entity collide radius
|
||||||
|
float entityRadius = 0.0F;
|
||||||
|
|
||||||
|
if (attackTarget != null) {
|
||||||
|
float reach = 0.0F;
|
||||||
|
|
||||||
|
if (self.getRealAnimation().get() instanceof AttackAnimation attackAnimation) {
|
||||||
|
Optional<Float> reachOpt = attackAnimation.getProperty(AttackAnimationProperty.REACH);
|
||||||
|
|
||||||
|
if (reachOpt.isPresent()) {
|
||||||
|
reach = reachOpt.get();
|
||||||
|
} else {
|
||||||
|
AnimationPlayer player = entitypatch.getAnimator().getPlayerFor(self.getAccessor());
|
||||||
|
|
||||||
|
if (player != null) {
|
||||||
|
Phase phase = attackAnimation.getPhaseByTime(player.getElapsedTime());
|
||||||
|
reach = entitypatch.getReach(phase.hand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entityRadius = (attackTarget.getBbWidth() + entitypatch.getOriginal().getBbWidth()) * 0.7F + reach;
|
||||||
|
}
|
||||||
|
|
||||||
|
float worldLength = Math.max((float)toDestWorld.length() - entityRadius, 0.0F);
|
||||||
|
float animLength = toDestAnim.length();
|
||||||
|
float scale = Math.min(worldLength / animLength, 1.0F);
|
||||||
|
|
||||||
|
if (self.isLinkAnimation()) {
|
||||||
|
scale *= coord[endFrame].transform().translation().length() / animLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = startFrame; i <= endFrame; i++) {
|
||||||
|
Vec3f translation = coord[i].transform().translation();
|
||||||
|
translation.x *= scale;
|
||||||
|
|
||||||
|
if (translation.z < 0.0F) {
|
||||||
|
translation.z *= scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transformSheet.readFrom(transform);
|
||||||
|
} else {
|
||||||
|
transformSheet.readFrom(self.getCoord().copyAll());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final MoveCoordSetter VEX_TRACE = (self, entitypatch, transformSheet) -> {
|
||||||
|
if (!self.isLinkAnimation()) {
|
||||||
|
TransformSheet transform = self.getCoord().copyAll();
|
||||||
|
|
||||||
|
if (entitypatch.getTarget() != null) {
|
||||||
|
Keyframe[] keyframes = transform.getKeyframes();
|
||||||
|
Vec3 pos = entitypatch.getOriginal().position();
|
||||||
|
Vec3 targetpos = entitypatch.getTarget().getEyePosition();
|
||||||
|
double flyDistance = Math.max(5.0D, targetpos.subtract(pos).length() * 2);
|
||||||
|
|
||||||
|
transform.forEach((index, keyframe) -> {
|
||||||
|
keyframe.transform().translation().scale((float)(flyDistance / Math.abs(keyframes[keyframes.length - 1].transform().translation().z)));
|
||||||
|
});
|
||||||
|
|
||||||
|
Vec3 toTarget = targetpos.subtract(pos);
|
||||||
|
float xRot = (float)-MathUtils.getXRotOfVector(toTarget);
|
||||||
|
float yRot = (float)MathUtils.getYRotOfVector(toTarget);
|
||||||
|
|
||||||
|
entitypatch.setYRot(yRot);
|
||||||
|
|
||||||
|
transform.forEach((index, keyframe) -> {
|
||||||
|
keyframe.transform().translation().rotateDegree(Vec3f.X_AXIS, xRot);
|
||||||
|
keyframe.transform().translation().rotateDegree(Vec3f.Y_AXIS, 180.0F - yRot);
|
||||||
|
keyframe.transform().translation().add(entitypatch.getOriginal().position());
|
||||||
|
});
|
||||||
|
|
||||||
|
transformSheet.readFrom(transform);
|
||||||
|
} else {
|
||||||
|
transform.forEach((index, keyframe) -> {
|
||||||
|
keyframe.transform().translation().rotateDegree(Vec3f.Y_AXIS, 180.0F - entitypatch.getYRot());
|
||||||
|
keyframe.transform().translation().add(entitypatch.getOriginal().position());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.types;
|
||||||
|
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationVariables;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationVariables.IndependentAnimationVariableKey;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RIG stub. Upstream EF : ActionAnimation ajoute un système de COORD
|
||||||
|
* TransformSheet pour les mouvements d'attaque en espace monde + des
|
||||||
|
* packets sync serveur/client via {@code CPSyncPlayerAnimationPosition}.
|
||||||
|
* Strippé pour TiedUp — garde juste l'API {@code addProperty} utilisée par
|
||||||
|
* {@link com.tiedup.remake.rig.asset.JsonAssetLoader} (ligne 621).
|
||||||
|
*/
|
||||||
|
public class ActionAnimation extends MainFrameAnimation {
|
||||||
|
|
||||||
|
// Variables indépendantes propagées à MoveCoordFunctions (position de départ
|
||||||
|
// en coord monde + produit scalaire lookVec initial pour lerp anim→world).
|
||||||
|
public static final IndependentAnimationVariableKey<Vec3> BEGINNING_LOCATION =
|
||||||
|
AnimationVariables.independent((animator) -> animator.getEntityPatch().getOriginal().position(), true);
|
||||||
|
public static final IndependentAnimationVariableKey<Float> INITIAL_LOOK_VEC_DOT =
|
||||||
|
AnimationVariables.independent((animator) -> 1.0F, true);
|
||||||
|
|
||||||
|
public ActionAnimation(float transitionTime, boolean isRepeat, String registryName, AssetAccessor<? extends Armature> armature) {
|
||||||
|
super(transitionTime, isRepeat, registryName, armature);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ActionAnimation(boolean isRepeat, String registryName, AssetAccessor<? extends Armature> armature) {
|
||||||
|
super(isRepeat, registryName, armature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une propriété action-specific (COORD TransformSheet typiquement).
|
||||||
|
* Version stub : no-op, juste pour que les refs JsonAssetLoader compilent.
|
||||||
|
*/
|
||||||
|
public <V> ActionAnimation addProperty(ActionAnimationProperty<V> property, V value) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub Phase 0 — LinkAnimation.modifyPose appelle ça pour zero-out X/Z de la
|
||||||
|
* Root joint en espace monde (empêche le sliding visuel pendant la transition
|
||||||
|
* vers une ActionAnimation sans keyframe "Coord"). Voir EF
|
||||||
|
* {@code yesman.epicfight.api.animation.types.ActionAnimation:209-222}.
|
||||||
|
*
|
||||||
|
* <p>Safe Phase 1 (idle/walk sont des StaticAnimation, pas ActionAnimation →
|
||||||
|
* jamais appelée). À re-implémenter Phase 2 dès qu'on introduit de vraies
|
||||||
|
* ActionAnimations bondage — sinon : sliding visible pendant transitionTime
|
||||||
|
* frames à chaque entrée dans l'action anim.</p>
|
||||||
|
*/
|
||||||
|
public void correctRootJoint(
|
||||||
|
com.tiedup.remake.rig.anim.types.DynamicAnimation animation,
|
||||||
|
com.tiedup.remake.rig.anim.Pose pose,
|
||||||
|
com.tiedup.remake.rig.patch.LivingEntityPatch<?> entitypatch,
|
||||||
|
float time,
|
||||||
|
float partialTicks
|
||||||
|
) {
|
||||||
|
if (com.tiedup.remake.rig.TiedUpRigConstants.IS_DEV_ENV) {
|
||||||
|
com.tiedup.remake.rig.TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"correctRootJoint no-op appelé (Phase 0 stub) — si ActionAnimation jouée, "
|
||||||
|
+ "sliding visuel attendu. Voir docs/plans/rig/PHASE0_DEGRADATIONS.md D-07."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.types;
|
||||||
|
|
||||||
|
import net.minecraft.world.InteractionHand;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RIG stub. Upstream EF : AttackAnimation = système d'attaque combat (570L,
|
||||||
|
* Phase timings, hitboxes, damage source, attack events, colliders). 100%
|
||||||
|
* combat, strippé en stub minimal pour TiedUp.
|
||||||
|
*
|
||||||
|
* <p>On conserve :</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Le type {@code AttackAnimation} pour les {@code instanceof} dans
|
||||||
|
* {@link com.tiedup.remake.rig.asset.JsonAssetLoader}:584</li>
|
||||||
|
* <li>Le field {@code phases} (liste vide) pour la boucle d'itération
|
||||||
|
* en JsonAssetLoader:591</li>
|
||||||
|
* <li>La classe interne {@link Phase} avec un {@code getColliders()}
|
||||||
|
* no-op pour JsonAssetLoader:592</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class AttackAnimation extends ActionAnimation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liste des phases d'attaque. Toujours vide en TiedUp — on ne joue
|
||||||
|
* jamais d'animation attaque. Mais le field doit exister pour que
|
||||||
|
* JsonAssetLoader.java ligne 591 puisse itérer dessus sans NPE.
|
||||||
|
*
|
||||||
|
* <p>Type {@code Phase[]} (et non {@code List<Phase>}) pour s'aligner
|
||||||
|
* sur la signature upstream EF — facilite le re-port de fixes EF.</p>
|
||||||
|
*/
|
||||||
|
public final Phase[] phases = new Phase[0];
|
||||||
|
|
||||||
|
public AttackAnimation(float transitionTime, boolean isRepeat, String registryName, AssetAccessor<? extends Armature> armature) {
|
||||||
|
super(transitionTime, isRepeat, registryName, armature);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AttackAnimation(boolean isRepeat, String registryName, AssetAccessor<? extends Armature> armature) {
|
||||||
|
super(isRepeat, registryName, armature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub — MoveCoordFunctions appelle ça pour calculer la reach mid-anim.
|
||||||
|
* On retourne toujours une Phase neutre (mainHand, pas de colliders),
|
||||||
|
* donc le reach retombe sur {@code entitypatch.getReach(MAIN_HAND)}.
|
||||||
|
*/
|
||||||
|
public Phase getPhaseByTime(float elapsedTime) {
|
||||||
|
return new Phase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase d'attaque. Stub pour satisfaire {@code phase.getColliders()}
|
||||||
|
* en JsonAssetLoader + {@code phase.hand} en MoveCoordFunctions.
|
||||||
|
*/
|
||||||
|
public static class Phase {
|
||||||
|
public final InteractionHand hand = InteractionHand.MAIN_HAND;
|
||||||
|
|
||||||
|
public JointColliderPair[] getColliders() {
|
||||||
|
return new JointColliderPair[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub — (Joint, Collider) pair pour les hitboxes combat. Non utilisé
|
||||||
|
* en TiedUp, juste un placeholder typé pour que JsonAssetLoader:592 compile.
|
||||||
|
*/
|
||||||
|
public static class JointColliderPair {
|
||||||
|
public com.tiedup.remake.rig.armature.Joint getFirst() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.types;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationClip;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.types.EntityState.StateFactor;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.anim.client.Layer;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.JointMaskEntry;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public class ConcurrentLinkAnimation extends DynamicAnimation implements AnimationAccessor<ConcurrentLinkAnimation> {
|
||||||
|
protected AssetAccessor<? extends StaticAnimation> nextAnimation;
|
||||||
|
protected AssetAccessor<? extends DynamicAnimation> currentAnimation;
|
||||||
|
protected float startsAt;
|
||||||
|
|
||||||
|
public ConcurrentLinkAnimation() {
|
||||||
|
this.animationClip = new AnimationClip();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void acceptFrom(AssetAccessor<? extends DynamicAnimation> currentAnimation, AssetAccessor<? extends StaticAnimation> nextAnimation, float time) {
|
||||||
|
this.currentAnimation = currentAnimation;
|
||||||
|
this.nextAnimation = nextAnimation;
|
||||||
|
this.startsAt = time;
|
||||||
|
this.setTotalTime(nextAnimation.get().getTransitionTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick(LivingEntityPatch<?> entitypatch) {
|
||||||
|
this.nextAnimation.get().linkTick(entitypatch, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void end(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends DynamicAnimation> nextAnimation, boolean isEnd) {
|
||||||
|
if (!isEnd) {
|
||||||
|
this.nextAnimation.get().end(entitypatch, nextAnimation, isEnd);
|
||||||
|
} else {
|
||||||
|
if (this.startsAt > 0.0F) {
|
||||||
|
entitypatch.getAnimator().getPlayer(this).ifPresent(player -> {
|
||||||
|
player.setElapsedTime(this.startsAt);
|
||||||
|
player.markDoNotResetTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.startsAt = 0.0F;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EntityState getState(LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
return this.nextAnimation.get().getState(entitypatch, 0.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getState(StateFactor<T> stateFactor, LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
return this.nextAnimation.get().getState(stateFactor, entitypatch, 0.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pose getPoseByTime(LivingEntityPatch<?> entitypatch, float time, float partialTicks) {
|
||||||
|
float elapsed = time + this.startsAt;
|
||||||
|
float currentElapsed = elapsed % this.currentAnimation.get().getTotalTime();
|
||||||
|
float nextElapsed = elapsed % this.nextAnimation.get().getTotalTime();
|
||||||
|
Pose currentAnimPose = this.currentAnimation.get().getPoseByTime(entitypatch, currentElapsed, 1.0F);
|
||||||
|
Pose nextAnimPose = this.nextAnimation.get().getPoseByTime(entitypatch, nextElapsed, 1.0F);
|
||||||
|
float interpolate = time / this.getTotalTime();
|
||||||
|
|
||||||
|
Pose interpolatedPose = Pose.interpolatePose(currentAnimPose, nextAnimPose, interpolate);
|
||||||
|
JointMaskEntry maskEntry = this.nextAnimation.get().getJointMaskEntry(entitypatch, true).orElse(null);
|
||||||
|
|
||||||
|
if (maskEntry != null && entitypatch.isLogicalClient()) {
|
||||||
|
interpolatedPose.disableJoint((entry) ->
|
||||||
|
maskEntry.isMasked(
|
||||||
|
this.nextAnimation.get().getProperty(ClientAnimationProperties.LAYER_TYPE).orElse(Layer.LayerType.BASE_LAYER) == Layer.LayerType.BASE_LAYER ? entitypatch.getClientAnimator().currentMotion() : entitypatch.getClientAnimator().currentCompositeMotion()
|
||||||
|
, entry.getKey()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return interpolatedPose;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modifyPose(DynamicAnimation animation, Pose pose, LivingEntityPatch<?> entitypatch, float time, float partialTicks) {
|
||||||
|
this.nextAnimation.get().modifyPose(this, pose, entitypatch, time, partialTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getPlaySpeed(LivingEntityPatch<?> entitypatch, DynamicAnimation animation) {
|
||||||
|
return this.nextAnimation.get().getPlaySpeed(entitypatch, animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNextAnimation(AnimationAccessor<? extends StaticAnimation> animation) {
|
||||||
|
this.nextAnimation = animation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
@Override
|
||||||
|
public Optional<JointMaskEntry> getJointMaskEntry(LivingEntityPatch<?> entitypatch, boolean useCurrentMotion) {
|
||||||
|
return this.nextAnimation.get().getJointMaskEntry(entitypatch, useCurrentMotion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMainFrameAnimation() {
|
||||||
|
return this.nextAnimation.get().isMainFrameAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReboundAnimation() {
|
||||||
|
return this.nextAnimation.get().isReboundAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AssetAccessor<? extends StaticAnimation> getRealAnimation() {
|
||||||
|
return this.nextAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ConcurrentLinkAnimation: Mix " + this.currentAnimation + " and " + this.nextAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimationClip getAnimationClip() {
|
||||||
|
return this.animationClip;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasTransformFor(String joint) {
|
||||||
|
return this.nextAnimation.get().hasTransformFor(joint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLinkAnimation() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConcurrentLinkAnimation get() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation registryName() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPresent() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int id() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimationAccessor<? extends DynamicAnimation> getAccessor() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean inRegistry() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.types;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
|
||||||
|
public class DirectStaticAnimation extends StaticAnimation implements AnimationAccessor<DirectStaticAnimation> {
|
||||||
|
private ResourceLocation registryName;
|
||||||
|
|
||||||
|
public DirectStaticAnimation() {
|
||||||
|
this.accessor = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DirectStaticAnimation(float transitionTime, boolean isRepeat, ResourceLocation registryName, AssetAccessor<? extends Armature> armature) {
|
||||||
|
super(transitionTime, isRepeat, registryName.toString(), armature);
|
||||||
|
|
||||||
|
this.registryName = registryName;
|
||||||
|
this.accessor = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Multilayer, Pov animation Constructor */
|
||||||
|
@ApiStatus.Internal
|
||||||
|
public DirectStaticAnimation(ResourceLocation baseAnimPath, float transitionTime, boolean repeatPlay, String registryName, AssetAccessor<? extends Armature> armature) {
|
||||||
|
super(baseAnimPath, transitionTime, repeatPlay, registryName, armature);
|
||||||
|
|
||||||
|
this.registryName = ResourceLocation.parse(registryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DirectStaticAnimation get() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <A extends DynamicAnimation> AnimationAccessor<A> getAccessor() {
|
||||||
|
return (AnimationAccessor<A>)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation registryName() {
|
||||||
|
return this.registryName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPresent() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int id() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getId() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean inRegistry() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.types;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
|
|
||||||
|
import net.minecraft.client.renderer.MultiBufferSource;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationClip;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationPlayer;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.TransformSheet;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.anim.types.EntityState.StateFactor;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.JointMaskEntry;
|
||||||
|
import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
public abstract class DynamicAnimation {
|
||||||
|
protected final boolean isRepeat;
|
||||||
|
protected final float transitionTime;
|
||||||
|
protected AnimationClip animationClip;
|
||||||
|
|
||||||
|
public DynamicAnimation() {
|
||||||
|
this(TiedUpRigConstants.GENERAL_ANIMATION_TRANSITION_TIME, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DynamicAnimation(float transitionTime, boolean isRepeat) {
|
||||||
|
this.isRepeat = isRepeat;
|
||||||
|
this.transitionTime = transitionTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Pose getRawPose(float time) {
|
||||||
|
return this.getAnimationClip().getPoseInTime(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pose getPoseByTime(LivingEntityPatch<?> entitypatch, float time, float partialTicks) {
|
||||||
|
Pose pose = this.getRawPose(time);
|
||||||
|
this.modifyPose(this, pose, entitypatch, time, partialTicks);
|
||||||
|
|
||||||
|
return pose;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modify the pose both this and link animation. **/
|
||||||
|
public void modifyPose(DynamicAnimation animation, Pose pose, LivingEntityPatch<?> entitypatch, float time, float partialTicks) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void putOnPlayer(AnimationPlayer animationPlayer, LivingEntityPatch<?> entitypatch) {
|
||||||
|
animationPlayer.setPlayAnimation(this.getAccessor());
|
||||||
|
animationPlayer.tick(entitypatch);
|
||||||
|
animationPlayer.begin(this.getAccessor(), entitypatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the animation put on the {@link AnimationPlayer}
|
||||||
|
* @param entitypatch
|
||||||
|
*/
|
||||||
|
public void begin(LivingEntityPatch<?> entitypatch) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called each tick when the animation is played
|
||||||
|
* @param entitypatch
|
||||||
|
*/
|
||||||
|
public void tick(LivingEntityPatch<?> entitypatch) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when both the animation finished or stopped by other animation.
|
||||||
|
* @param entitypatch
|
||||||
|
* @param nextAnimation the next animation to play after the animation ends
|
||||||
|
* @param isEnd whether the animation completed or not
|
||||||
|
*
|
||||||
|
* if @param isEnd true, nextAnimation is null
|
||||||
|
* if @param isEnd false, nextAnimation is not null
|
||||||
|
*/
|
||||||
|
public void end(LivingEntityPatch<?> entitypatch, @Nullable AssetAccessor<? extends DynamicAnimation> nextAnimation, boolean isEnd) {}
|
||||||
|
public void linkTick(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends DynamicAnimation> linkAnimation) {};
|
||||||
|
|
||||||
|
public boolean hasTransformFor(String joint) {
|
||||||
|
return this.getTransfroms().containsKey(joint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public Optional<JointMaskEntry> getJointMaskEntry(LivingEntityPatch<?> entitypatch, boolean useCurrentMotion) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntityState getState(LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
return EntityState.DEFAULT_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TypeFlexibleHashMap<StateFactor<?>> getStatesMap(LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
return new TypeFlexibleHashMap<> (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T getState(StateFactor<T> stateFactor, LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
return stateFactor.defaultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnimationClip getAnimationClip() {
|
||||||
|
return this.animationClip;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, TransformSheet> getTransfroms() {
|
||||||
|
return this.getAnimationClip().getJointTransforms();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getPlaySpeed(LivingEntityPatch<?> entitypatch, DynamicAnimation animation) {
|
||||||
|
return 1.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransformSheet getCoord() {
|
||||||
|
return this.getTransfroms().containsKey("Root") ? this.getTransfroms().get("Root") : TransformSheet.EMPTY_SHEET;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotalTime(float totalTime) {
|
||||||
|
this.getAnimationClip().setClipTime(totalTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getTotalTime() {
|
||||||
|
return this.getAnimationClip().getClipTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getTransitionTime() {
|
||||||
|
return this.transitionTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRepeat() {
|
||||||
|
return this.isRepeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canBePlayedReverse() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceLocation getRegistryName() {
|
||||||
|
return TiedUpRigConstants.identifier("");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <V> Optional<V> getProperty(AnimationProperty<V> propertyType) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBasicAttackAnimation() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMainFrameAnimation() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReboundAnimation() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMetaAnimation() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isClientAnimation() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStaticAnimation() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract <A extends DynamicAnimation> AnimationAccessor<? extends DynamicAnimation> getAccessor();
|
||||||
|
public abstract AssetAccessor<? extends StaticAnimation> getRealAnimation();
|
||||||
|
|
||||||
|
public boolean isLinkAnimation() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean doesHeadRotFollowEntityHead() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public void renderDebugging(PoseStack poseStack, MultiBufferSource buffer, LivingEntityPatch<?> entitypatch, float playTime, float partialTicks) {
|
||||||
|
}
|
||||||
|
}
|
||||||
146
src/main/java/com/tiedup/remake/rig/anim/types/EntityState.java
Normal file
146
src/main/java/com/tiedup/remake/rig/anim/types/EntityState.java
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.types;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import net.minecraft.world.damagesource.DamageSource;
|
||||||
|
import net.minecraftforge.event.entity.ProjectileImpactEvent;
|
||||||
|
import com.tiedup.remake.rig.util.AttackResult;
|
||||||
|
import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap;
|
||||||
|
|
||||||
|
public class EntityState {
|
||||||
|
public static class StateFactor<T> implements TypeFlexibleHashMap.TypeKey<T> {
|
||||||
|
private final String name;
|
||||||
|
private final T defaultValue;
|
||||||
|
|
||||||
|
public StateFactor(String name, T defaultValue) {
|
||||||
|
this.name = name;
|
||||||
|
this.defaultValue = defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toString() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T defaultValue() {
|
||||||
|
return this.defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final EntityState DEFAULT_STATE = new EntityState(new TypeFlexibleHashMap<>(true));
|
||||||
|
|
||||||
|
public static final StateFactor<Boolean> TURNING_LOCKED = new StateFactor<>("turningLocked", false);
|
||||||
|
public static final StateFactor<Boolean> MOVEMENT_LOCKED = new StateFactor<>("movementLocked", false);
|
||||||
|
public static final StateFactor<Boolean> ATTACKING = new StateFactor<>("attacking", false);
|
||||||
|
public static final StateFactor<Boolean> CAN_BASIC_ATTACK = new StateFactor<>("canBasicAttack", true);
|
||||||
|
public static final StateFactor<Boolean> CAN_SKILL_EXECUTION = new StateFactor<>("canExecuteSkill", true);
|
||||||
|
public static final StateFactor<Boolean> CAN_USE_ITEM = new StateFactor<>("canUseItem", true);
|
||||||
|
public static final StateFactor<Boolean> CAN_SWITCH_HAND_ITEM = new StateFactor<>("canSwitchHandItem", true);
|
||||||
|
public static final StateFactor<Boolean> INACTION = new StateFactor<>("takingAction", false);
|
||||||
|
public static final StateFactor<Boolean> KNOCKDOWN = new StateFactor<>("knockdown", false);
|
||||||
|
public static final StateFactor<Boolean> LOCKON_ROTATE = new StateFactor<>("lockonRotate", false);
|
||||||
|
public static final StateFactor<Boolean> UPDATE_LIVING_MOTION = new StateFactor<>("updateLivingMotion", true);
|
||||||
|
public static final StateFactor<Integer> HURT_LEVEL = new StateFactor<>("hurtLevel", 0);
|
||||||
|
public static final StateFactor<Integer> PHASE_LEVEL = new StateFactor<>("phaseLevel", 0);
|
||||||
|
public static final StateFactor<Function<DamageSource, AttackResult.ResultType>> ATTACK_RESULT = new StateFactor<>("attackResultModifier", (damagesource) -> AttackResult.ResultType.SUCCESS);
|
||||||
|
public static final StateFactor<Consumer<ProjectileImpactEvent>> PROJECTILE_IMPACT_RESULT = new StateFactor<>("projectileImpactResult", (event) -> {});
|
||||||
|
|
||||||
|
private final TypeFlexibleHashMap<StateFactor<?>> stateMap;
|
||||||
|
|
||||||
|
public EntityState(TypeFlexibleHashMap<StateFactor<?>> states) {
|
||||||
|
this.stateMap = states;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void setState(StateFactor<T> stateFactor, T val) {
|
||||||
|
this.stateMap.put(stateFactor, (Object)val);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T getState(StateFactor<T> stateFactor) {
|
||||||
|
return this.stateMap.getOrDefault(stateFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TypeFlexibleHashMap<StateFactor<?>> getStateMap() {
|
||||||
|
return this.stateMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean turningLocked() {
|
||||||
|
return this.getState(EntityState.TURNING_LOCKED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean movementLocked() {
|
||||||
|
return this.getState(EntityState.MOVEMENT_LOCKED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean attacking() {
|
||||||
|
return this.getState(EntityState.ATTACKING);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AttackResult.ResultType attackResult(DamageSource damagesource) {
|
||||||
|
return this.getState(EntityState.ATTACK_RESULT).apply(damagesource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProjectileImpactResult(ProjectileImpactEvent event) {
|
||||||
|
this.getState(EntityState.PROJECTILE_IMPACT_RESULT).accept(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canBasicAttack() {
|
||||||
|
return this.getState(EntityState.CAN_BASIC_ATTACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canUseSkill() {
|
||||||
|
return this.getState(EntityState.CAN_SKILL_EXECUTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canUseItem() {
|
||||||
|
return this.canUseSkill() && this.getState(EntityState.CAN_USE_ITEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canSwitchHoldingItem() {
|
||||||
|
return !this.inaction() && this.getState(EntityState.CAN_SWITCH_HAND_ITEM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean inaction() {
|
||||||
|
return this.getState(EntityState.INACTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean updateLivingMotion() {
|
||||||
|
return this.getState(EntityState.UPDATE_LIVING_MOTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hurt() {
|
||||||
|
return this.getState(EntityState.HURT_LEVEL) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int hurtLevel() {
|
||||||
|
return this.getState(EntityState.HURT_LEVEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean knockDown() {
|
||||||
|
return this.getState(EntityState.KNOCKDOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean lockonRotate() {
|
||||||
|
return this.getState(EntityState.LOCKON_ROTATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1: anticipation
|
||||||
|
* 2: attacking
|
||||||
|
* 3: recovery
|
||||||
|
* @return level
|
||||||
|
*/
|
||||||
|
public int getLevel() {
|
||||||
|
return this.getState(EntityState.PHASE_LEVEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.stateMap.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.types;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationClip;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.anim.client.Layer.Priority;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.JointMaskEntry;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigRegistry;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public class LayerOffAnimation extends DynamicAnimation implements AnimationAccessor<LayerOffAnimation> {
|
||||||
|
private AssetAccessor<? extends DynamicAnimation> lastAnimation;
|
||||||
|
private Pose lastPose;
|
||||||
|
private final Priority layerPriority;
|
||||||
|
|
||||||
|
public LayerOffAnimation(Priority layerPriority) {
|
||||||
|
this.layerPriority = layerPriority;
|
||||||
|
this.animationClip = new AnimationClip();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastPose(Pose pose) {
|
||||||
|
this.lastPose = pose;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void end(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends DynamicAnimation> nextAnimation, boolean isEnd) {
|
||||||
|
if (entitypatch.isLogicalClient() && isEnd) {
|
||||||
|
entitypatch.getClientAnimator().baseLayer.disableLayer(this.layerPriority);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pose getPoseByTime(LivingEntityPatch<?> entitypatch, float time, float partialTicks) {
|
||||||
|
Pose lowerLayerPose = entitypatch.getClientAnimator().getComposedLayerPoseBelow(this.layerPriority, Minecraft.getInstance().getFrameTime());
|
||||||
|
Pose interpolatedPose = Pose.interpolatePose(this.lastPose, lowerLayerPose, time / this.getTotalTime());
|
||||||
|
interpolatedPose.disableJoint((joint) -> !this.lastPose.hasTransform(joint.getKey()));
|
||||||
|
|
||||||
|
return interpolatedPose;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<JointMaskEntry> getJointMaskEntry(LivingEntityPatch<?> entitypatch, boolean useCurrentMotion) {
|
||||||
|
return this.lastAnimation.get().getJointMaskEntry(entitypatch, useCurrentMotion);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <V> Optional<V> getProperty(AnimationProperty<V> propertyType) {
|
||||||
|
return this.lastAnimation.get().getProperty(propertyType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastAnimation(AssetAccessor<? extends DynamicAnimation> animation) {
|
||||||
|
this.lastAnimation = animation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean doesHeadRotFollowEntityHead() {
|
||||||
|
return this.lastAnimation.get().doesHeadRotFollowEntityHead();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AssetAccessor<? extends StaticAnimation> getRealAnimation() {
|
||||||
|
return TiedUpRigRegistry.EMPTY_ANIMATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimationClip getAnimationClip() {
|
||||||
|
return this.animationClip;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasTransformFor(String joint) {
|
||||||
|
return this.lastPose.hasTransform(joint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLinkAnimation() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LayerOffAnimation get() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation registryName() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPresent() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int id() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimationAccessor<? extends LayerOffAnimation> getAccessor() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean inRegistry() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.types;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationClip;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
import com.tiedup.remake.rig.anim.Keyframe;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.TransformSheet;
|
||||||
|
import com.tiedup.remake.rig.anim.types.EntityState.StateFactor;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.JointMaskEntry;
|
||||||
|
import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
public class LinkAnimation extends DynamicAnimation implements AnimationAccessor<LinkAnimation> {
|
||||||
|
protected TransformSheet coord;
|
||||||
|
protected AssetAccessor<? extends DynamicAnimation> fromAnimation;
|
||||||
|
protected AssetAccessor<? extends StaticAnimation> toAnimation;
|
||||||
|
protected float nextStartTime;
|
||||||
|
|
||||||
|
public LinkAnimation() {
|
||||||
|
this.animationClip = new AnimationClip();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick(LivingEntityPatch<?> entitypatch) {
|
||||||
|
this.toAnimation.get().linkTick(entitypatch, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void end(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends DynamicAnimation> nextAnimation, boolean isEnd) {
|
||||||
|
if (!isEnd) {
|
||||||
|
this.toAnimation.get().end(entitypatch, nextAnimation, isEnd);
|
||||||
|
} else {
|
||||||
|
if (this.nextStartTime > 0.0F) {
|
||||||
|
entitypatch.getAnimator().getPlayer(this).ifPresent(player -> {
|
||||||
|
player.setElapsedTime(this.nextStartTime);
|
||||||
|
player.markDoNotResetTime();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TypeFlexibleHashMap<StateFactor<?>> getStatesMap(LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
float timeInRealAnimation = Math.max(time - (this.getTotalTime() - this.nextStartTime), 0.0F);
|
||||||
|
TypeFlexibleHashMap<StateFactor<?>> map = this.toAnimation.get().getStatesMap(entitypatch, timeInRealAnimation);
|
||||||
|
|
||||||
|
for (Map.Entry<StateFactor<?>, Object> entry : map.entrySet()) {
|
||||||
|
Object val = this.toAnimation.get().getModifiedLinkState(entry.getKey(), entry.getValue(), entitypatch, time);
|
||||||
|
map.put(entry.getKey(), val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EntityState getState(LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
float timeInRealAnimation = Math.max(time - (this.getTotalTime() - this.nextStartTime), 0.0F);
|
||||||
|
|
||||||
|
EntityState state = this.toAnimation.get().getState(entitypatch, timeInRealAnimation);
|
||||||
|
TypeFlexibleHashMap<StateFactor<?>> map = state.getStateMap();
|
||||||
|
|
||||||
|
for (Map.Entry<StateFactor<?>, Object> entry : map.entrySet()) {
|
||||||
|
Object val = this.toAnimation.get().getModifiedLinkState(entry.getKey(), entry.getValue(), entitypatch, time);
|
||||||
|
map.put(entry.getKey(), val);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <T> T getState(StateFactor<T> stateFactor, LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
float timeInRealAnimation = Math.max(time - (this.getTotalTime() - this.nextStartTime), 0.0F);
|
||||||
|
T state = this.toAnimation.get().getState(stateFactor, entitypatch, timeInRealAnimation);
|
||||||
|
|
||||||
|
return (T)this.toAnimation.get().getModifiedLinkState(stateFactor, state, entitypatch, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Pose getPoseByTime(LivingEntityPatch<?> entitypatch, float time, float partialTicks) {
|
||||||
|
Pose nextStartingPose = this.toAnimation.get().getPoseByTime(entitypatch, this.nextStartTime, partialTicks);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update dest pose
|
||||||
|
*/
|
||||||
|
for (Map.Entry<String, JointTransform> entry : nextStartingPose.getJointTransformData().entrySet()) {
|
||||||
|
if (this.animationClip.hasJointTransform(entry.getKey())) {
|
||||||
|
Keyframe[] keyframe = this.animationClip.getJointTransform(entry.getKey()).getKeyframes();
|
||||||
|
JointTransform jt = keyframe[keyframe.length - 1].transform();
|
||||||
|
JointTransform newJt = nextStartingPose.getJointTransformData().get(entry.getKey());
|
||||||
|
newJt.translation().set(jt.translation());
|
||||||
|
jt.copyFrom(newJt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.getPoseByTime(entitypatch, time, partialTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modifyPose(DynamicAnimation animation, Pose pose, LivingEntityPatch<?> entitypatch, float time, float partialTicks) {
|
||||||
|
// Bad implementation: Add root joint as coord in loading animation
|
||||||
|
if (this.toAnimation.get() instanceof ActionAnimation actionAnimation) {
|
||||||
|
if (!this.getTransfroms().containsKey("Coord")) {
|
||||||
|
actionAnimation.correctRootJoint(this, pose, entitypatch, time, partialTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getPlaySpeed(LivingEntityPatch<?> entitypatch, DynamicAnimation animation) {
|
||||||
|
return this.toAnimation.get().getPlaySpeed(entitypatch, animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setConnectedAnimations(AssetAccessor<? extends DynamicAnimation> from, AssetAccessor<? extends StaticAnimation> to) {
|
||||||
|
this.fromAnimation = from.get().getRealAnimation();
|
||||||
|
this.toAnimation = to;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetAccessor<? extends StaticAnimation> getNextAnimation() {
|
||||||
|
return this.toAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransformSheet getCoord() {
|
||||||
|
if (this.coord != null) {
|
||||||
|
return this.coord;
|
||||||
|
} else if (this.getTransfroms().containsKey("Root")) {
|
||||||
|
return this.getTransfroms().get("Root");
|
||||||
|
}
|
||||||
|
|
||||||
|
return TransformSheet.EMPTY_SHEET;
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public Optional<JointMaskEntry> getJointMaskEntry(LivingEntityPatch<?> entitypatch, boolean useCurrentMotion) {
|
||||||
|
return useCurrentMotion ? this.toAnimation.get().getJointMaskEntry(entitypatch, true) : this.fromAnimation.get().getJointMaskEntry(entitypatch, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMainFrameAnimation() {
|
||||||
|
return this.toAnimation.get().isMainFrameAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReboundAnimation() {
|
||||||
|
return this.toAnimation.get().isReboundAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean doesHeadRotFollowEntityHead() {
|
||||||
|
return this.fromAnimation.get().doesHeadRotFollowEntityHead() && this.toAnimation.get().doesHeadRotFollowEntityHead();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AssetAccessor<? extends StaticAnimation> getRealAnimation() {
|
||||||
|
return this.toAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetAccessor<? extends DynamicAnimation> getFromAnimation() {
|
||||||
|
return this.fromAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimationAccessor<? extends DynamicAnimation> getAccessor() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void copyTo(LinkAnimation dest) {
|
||||||
|
dest.setConnectedAnimations(this.fromAnimation, this.toAnimation);
|
||||||
|
dest.setTotalTime(this.getTotalTime());
|
||||||
|
dest.getAnimationClip().reset();
|
||||||
|
this.getTransfroms().forEach((jointName, transformSheet) -> dest.getAnimationClip().addJointTransform(jointName, transformSheet.copyAll()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadCoord(TransformSheet coord) {
|
||||||
|
this.coord = coord;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getNextStartTime() {
|
||||||
|
return this.nextStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setNextStartTime(float nextStartTime) {
|
||||||
|
this.nextStartTime = nextStartTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetNextStartTime() {
|
||||||
|
this.nextStartTime = 0.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLinkAnimation() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "From " + this.fromAnimation + " to " + this.toAnimation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimationClip getAnimationClip() {
|
||||||
|
return this.animationClip;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LinkAnimation get() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation registryName() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isPresent() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int id() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean inRegistry() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.types;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RIG stub. Upstream EF : MainFrameAnimation ajoute du scheduling combat
|
||||||
|
* via {@code ActionEvent}/{@code PlayerEventListener} (combat tick hooks).
|
||||||
|
* Strippé pour TiedUp — garde juste le type pour satisfaire les
|
||||||
|
* instanceof checks de {@link com.tiedup.remake.rig.asset.JsonAssetLoader}
|
||||||
|
* et la hiérarchie {@link ActionAnimation} / {@link AttackAnimation}.
|
||||||
|
*/
|
||||||
|
public class MainFrameAnimation extends StaticAnimation {
|
||||||
|
|
||||||
|
public MainFrameAnimation(float transitionTime, boolean isRepeat, String registryName, AssetAccessor<? extends Armature> armature) {
|
||||||
|
super(transitionTime, isRepeat, registryName, armature);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MainFrameAnimation(boolean isRepeat, String registryName, AssetAccessor<? extends Armature> armature) {
|
||||||
|
super(isRepeat, registryName, armature);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.types;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||||
|
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||||
|
import com.tiedup.remake.rig.anim.types.EntityState.StateFactor;
|
||||||
|
import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
public class StateSpectrum {
|
||||||
|
private final Set<StatesInTime> timePairs = Sets.newHashSet();
|
||||||
|
|
||||||
|
void readFrom(StateSpectrum.Blueprint blueprint) {
|
||||||
|
this.timePairs.clear();
|
||||||
|
this.timePairs.addAll(blueprint.timePairs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T getSingleState(StateFactor<T> stateFactor, LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
for (StatesInTime state : this.timePairs) {
|
||||||
|
if (state.isIn(entitypatch, time)) {
|
||||||
|
for (Map.Entry<StateFactor<?>, ?> timeEntry : state.getStates(entitypatch)) {
|
||||||
|
if (timeEntry.getKey() == stateFactor) {
|
||||||
|
return (T) timeEntry.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stateFactor.defaultValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TypeFlexibleHashMap<StateFactor<?>> getStateMap(LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
TypeFlexibleHashMap<StateFactor<?>> stateMap = new TypeFlexibleHashMap<>(true);
|
||||||
|
|
||||||
|
for (StatesInTime state : this.timePairs) {
|
||||||
|
if (state.isIn(entitypatch, time)) {
|
||||||
|
for (Map.Entry<StateFactor<?>, ?> timeEntry : state.getStates(entitypatch)) {
|
||||||
|
stateMap.put(timeEntry.getKey(), timeEntry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stateMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract static class StatesInTime {
|
||||||
|
public abstract Set<Map.Entry<StateFactor<?>, Object>> getStates(LivingEntityPatch<?> entitypatch);
|
||||||
|
|
||||||
|
public abstract void removeState(StateFactor<?> state);
|
||||||
|
|
||||||
|
public abstract boolean hasState(StateFactor<?> state);
|
||||||
|
|
||||||
|
public abstract boolean isIn(LivingEntityPatch<?> entitypatch, float time);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class SimpleStatesInTime extends StatesInTime {
|
||||||
|
float start;
|
||||||
|
float end;
|
||||||
|
Map<StateFactor<?>, Object> states = Maps.newHashMap();
|
||||||
|
|
||||||
|
public SimpleStatesInTime(float start, float end) {
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isIn(LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
return this.start <= time && this.end > time;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> StatesInTime addState(StateFactor<T> factor, T val) {
|
||||||
|
this.states.put(factor, val);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Map.Entry<StateFactor<?>, Object>> getStates(LivingEntityPatch<?> entitypatch) {
|
||||||
|
return this.states.entrySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasState(StateFactor<?> state) {
|
||||||
|
return this.states.containsKey(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeState(StateFactor<?> state) {
|
||||||
|
this.states.remove(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("Time: %.2f ~ %.2f, States: %s", this.start, this.end, this.states);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ConditionalStatesInTime extends StatesInTime {
|
||||||
|
float start;
|
||||||
|
float end;
|
||||||
|
Int2ObjectMap<Map<StateFactor<?>, Object>> conditionalStates = new Int2ObjectOpenHashMap<>();
|
||||||
|
Function<LivingEntityPatch<?>, Integer> condition;
|
||||||
|
|
||||||
|
public ConditionalStatesInTime(Function<LivingEntityPatch<?>, Integer> condition, float start, float end) {
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
this.condition = condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> StatesInTime addConditionalState(int metadata, StateFactor<T> factor, T val) {
|
||||||
|
Map<StateFactor<?>, Object> states = this.conditionalStates.computeIfAbsent(metadata, (key) -> Maps.newHashMap());
|
||||||
|
states.put(factor, val);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Override
|
||||||
|
public Set<Map.Entry<StateFactor<?>, Object>> getStates(LivingEntityPatch<?> entitypatch) {
|
||||||
|
return this.conditionalStates.get(this.condition.apply(entitypatch)).entrySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isIn(LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
return this.start <= time && this.end > time;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasState(StateFactor<?> state) {
|
||||||
|
boolean hasState = false;
|
||||||
|
for (Map<StateFactor<?>, Object> states : this.conditionalStates.values()) {
|
||||||
|
hasState |= states.containsKey(state);
|
||||||
|
}
|
||||||
|
return hasState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeState(StateFactor<?> state) {
|
||||||
|
for (Map<StateFactor<?>, Object> states : this.conditionalStates.values()) {
|
||||||
|
states.remove(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(String.format("Time: %.2f ~ %.2f, ", this.start, this.end));
|
||||||
|
int entryCnt = 0;
|
||||||
|
for (Map.Entry<Integer, Map<StateFactor<?>, Object>> entry : this.conditionalStates.entrySet()) {
|
||||||
|
sb.append(String.format("States %d: %s", entry.getKey(), entry.getValue()));
|
||||||
|
entryCnt++;
|
||||||
|
if (entryCnt < this.conditionalStates.size()) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class VariableStatesInTime extends StatesInTime {
|
||||||
|
Function<LivingEntityPatch<?>, Float> variableStart;
|
||||||
|
Function<LivingEntityPatch<?>, Float> variableEnd;
|
||||||
|
Map<StateFactor<?>, Object> states = Maps.newHashMap();
|
||||||
|
|
||||||
|
public VariableStatesInTime(Function<LivingEntityPatch<?>, Float> variableStart, Function<LivingEntityPatch<?>, Float> variableEnd) {
|
||||||
|
this.variableStart = variableStart;
|
||||||
|
this.variableEnd = variableEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isIn(LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
return this.variableStart.apply(entitypatch) <= time && this.variableEnd.apply(entitypatch) > time;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> StatesInTime addState(StateFactor<T> factor, T val) {
|
||||||
|
this.states.put(factor, val);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<Map.Entry<StateFactor<?>, Object>> getStates(LivingEntityPatch<?> entitypatch) {
|
||||||
|
return this.states.entrySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasState(StateFactor<?> state) {
|
||||||
|
return this.states.containsKey(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void removeState(StateFactor<?> state) {
|
||||||
|
this.states.remove(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("States: %s", this.states);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Blueprint {
|
||||||
|
StatesInTime currentState;
|
||||||
|
Set<StatesInTime> timePairs = Sets.newHashSet();
|
||||||
|
|
||||||
|
public Blueprint newTimePair(float start, float end) {
|
||||||
|
this.currentState = new SimpleStatesInTime(start, end);
|
||||||
|
this.timePairs.add(this.currentState);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Blueprint newConditionalTimePair(Function<LivingEntityPatch<?>, Integer> condition, float start, float end) {
|
||||||
|
this.currentState = new ConditionalStatesInTime(condition, start, end);
|
||||||
|
this.timePairs.add(this.currentState);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Blueprint newVariableTimePair(Function<LivingEntityPatch<?>, Float> variableStart, Function<LivingEntityPatch<?>, Float> variableEnd) {
|
||||||
|
this.currentState = new VariableStatesInTime(variableStart, variableEnd);
|
||||||
|
this.timePairs.add(this.currentState);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> Blueprint addState(StateFactor<T> factor, T val) {
|
||||||
|
if (this.currentState instanceof SimpleStatesInTime simpleState) {
|
||||||
|
simpleState.addState(factor, val);
|
||||||
|
}
|
||||||
|
if (this.currentState instanceof VariableStatesInTime variableState) {
|
||||||
|
variableState.addState(factor, val);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> Blueprint addConditionalState(int metadata, StateFactor<T> factor, T val) {
|
||||||
|
if (this.currentState instanceof ConditionalStatesInTime conditionalState) {
|
||||||
|
conditionalState.addConditionalState(metadata, factor, val);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> Blueprint removeState(StateFactor<T> factor) {
|
||||||
|
for (StatesInTime timePair : this.timePairs) {
|
||||||
|
timePair.removeState(factor);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> Blueprint addStateRemoveOld(StateFactor<T> factor, T val) {
|
||||||
|
this.removeState(factor);
|
||||||
|
return this.addState(factor, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> Blueprint addStateIfNotExist(StateFactor<T> factor, T val) {
|
||||||
|
for (StatesInTime timePair : this.timePairs) {
|
||||||
|
if (timePair.hasState(factor)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.addState(factor, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Blueprint clear() {
|
||||||
|
this.currentState = null;
|
||||||
|
this.timePairs.clear();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (StatesInTime state : this.timePairs) {
|
||||||
|
sb.append(state + "\n");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.anim.types;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
|
import com.mojang.blaze3d.vertex.VertexConsumer;
|
||||||
|
|
||||||
|
import io.netty.util.internal.StringUtil;
|
||||||
|
import net.minecraft.client.renderer.MultiBufferSource;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.world.entity.LivingEntity;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationClip;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationManager;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationVariables;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationVariables.IndependentAnimationVariableKey;
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
import com.tiedup.remake.rig.anim.Keyframe;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.anim.TransformSheet;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationEvent;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationEvent.SimpleEvent;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationParameters;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.PlaybackSpeedModifier;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.StaticAnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.anim.types.EntityState.StateFactor;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.asset.JsonAssetLoader;
|
||||||
|
import com.tiedup.remake.rig.anim.client.Layer;
|
||||||
|
import com.tiedup.remake.rig.anim.client.Layer.LayerType;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.JointMaskEntry;
|
||||||
|
import com.tiedup.remake.rig.anim.client.property.TrailInfo;
|
||||||
|
import com.tiedup.remake.rig.exception.AssetLoadingException;
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
import com.tiedup.remake.rig.physics.ik.InverseKinematicsProvider;
|
||||||
|
import com.tiedup.remake.rig.physics.ik.InverseKinematicsSimulatable;
|
||||||
|
import com.tiedup.remake.rig.physics.ik.InverseKinematicsSimulator;
|
||||||
|
import com.tiedup.remake.rig.physics.ik.InverseKinematicsSimulator.BakedInverseKinematicsDefinition;
|
||||||
|
import com.tiedup.remake.rig.physics.ik.InverseKinematicsSimulator.InverseKinematicsObject;
|
||||||
|
import com.tiedup.remake.rig.util.datastruct.TypeFlexibleHashMap;
|
||||||
|
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||||
|
import com.tiedup.remake.rig.math.Vec3f;
|
||||||
|
import com.tiedup.remake.rig.render.TiedUpRenderTypes;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigRegistry;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
import com.tiedup.remake.rig.patch.PlayerPatch;
|
||||||
|
|
||||||
|
public class StaticAnimation extends DynamicAnimation implements InverseKinematicsProvider {
|
||||||
|
public static final IndependentAnimationVariableKey<Boolean> HAD_NO_PHYSICS = AnimationVariables.independent((animator) -> false, true);
|
||||||
|
|
||||||
|
public static String getFileHash(ResourceLocation rl) {
|
||||||
|
String fileHash;
|
||||||
|
|
||||||
|
try {
|
||||||
|
JsonAssetLoader jsonfile = new JsonAssetLoader(AnimationManager.getAnimationResourceManager(), rl);
|
||||||
|
fileHash = jsonfile.getFileHash();
|
||||||
|
} catch (AssetLoadingException e) {
|
||||||
|
fileHash = StringUtil.EMPTY_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final Map<AnimationProperty<?>, Object> properties = Maps.newHashMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* States will bind into animation on {@link AnimationManager#apply}
|
||||||
|
*/
|
||||||
|
protected final StateSpectrum.Blueprint stateSpectrumBlueprint = new StateSpectrum.Blueprint();
|
||||||
|
protected final StateSpectrum stateSpectrum = new StateSpectrum();
|
||||||
|
protected final AssetAccessor<? extends Armature> armature;
|
||||||
|
protected ResourceLocation resourceLocation;
|
||||||
|
protected AnimationAccessor<? extends StaticAnimation> accessor;
|
||||||
|
private final String filehash;
|
||||||
|
|
||||||
|
public StaticAnimation() {
|
||||||
|
super(0.0F, true);
|
||||||
|
|
||||||
|
this.resourceLocation = TiedUpRigConstants.identifier("emtpy");
|
||||||
|
this.armature = null;
|
||||||
|
this.filehash = StringUtil.EMPTY_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StaticAnimation(boolean isRepeat, AnimationAccessor<? extends StaticAnimation> accessor, AssetAccessor<? extends Armature> armature) {
|
||||||
|
this(TiedUpRigConstants.GENERAL_ANIMATION_TRANSITION_TIME, isRepeat, accessor, armature);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StaticAnimation(float transitionTime, boolean isRepeat, AnimationAccessor<? extends StaticAnimation> accessor, AssetAccessor<? extends Armature> armature) {
|
||||||
|
super(transitionTime, isRepeat);
|
||||||
|
|
||||||
|
this.resourceLocation = ResourceLocation.fromNamespaceAndPath(accessor.registryName().getNamespace(), "animmodels/animations/" + accessor.registryName().getPath() + ".json");
|
||||||
|
|
||||||
|
this.armature = armature;
|
||||||
|
this.accessor = accessor;
|
||||||
|
this.filehash = getFileHash(this.resourceLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resourcepack animations — transitionTime par défaut */
|
||||||
|
public StaticAnimation(boolean isRepeat, String path, AssetAccessor<? extends Armature> armature) {
|
||||||
|
this(TiedUpRigConstants.GENERAL_ANIMATION_TRANSITION_TIME, isRepeat, path, armature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resourcepack animations */
|
||||||
|
public StaticAnimation(float transitionTime, boolean isRepeat, String path, AssetAccessor<? extends Armature> armature) {
|
||||||
|
super(transitionTime, isRepeat);
|
||||||
|
|
||||||
|
ResourceLocation registryName = ResourceLocation.parse(path);
|
||||||
|
this.resourceLocation = ResourceLocation.fromNamespaceAndPath(registryName.getNamespace(), "animmodels/animations/" + registryName.getPath() + ".json");
|
||||||
|
this.armature = armature;
|
||||||
|
this.filehash = StringUtil.EMPTY_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Multilayer Constructor */
|
||||||
|
public StaticAnimation(ResourceLocation fileLocation, float transitionTime, boolean isRepeat, String registryName, AssetAccessor<? extends Armature> armature) {
|
||||||
|
super(transitionTime, isRepeat);
|
||||||
|
|
||||||
|
this.resourceLocation = fileLocation;
|
||||||
|
this.armature = armature;
|
||||||
|
this.filehash = StringUtil.EMPTY_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadAnimation() {
|
||||||
|
if (!this.isMetaAnimation()) {
|
||||||
|
if (this.properties.containsKey(StaticAnimationProperty.IK_DEFINITION)) {
|
||||||
|
this.animationClip = AnimationManager.getInstance().loadAnimationClip(this, JsonAssetLoader::loadAllJointsClipForAnimation);
|
||||||
|
|
||||||
|
this.getProperty(StaticAnimationProperty.IK_DEFINITION).ifPresent(ikDefinitions -> {
|
||||||
|
boolean correctY = this.getProperty(ActionAnimationProperty.MOVE_VERTICAL).orElse(false);
|
||||||
|
boolean correctZ = this.isMainFrameAnimation();
|
||||||
|
|
||||||
|
List<BakedInverseKinematicsDefinition> bakedIKDefinitionList = ikDefinitions.stream().map(ikDefinition -> ikDefinition.bake(this.armature, this.animationClip.getJointTransforms(), correctY, correctZ)).toList();
|
||||||
|
this.addProperty(StaticAnimationProperty.BAKED_IK_DEFINITION, bakedIKDefinitionList);
|
||||||
|
|
||||||
|
// Remove the unbaked data
|
||||||
|
this.properties.remove(StaticAnimationProperty.IK_DEFINITION);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.animationClip = AnimationManager.getInstance().loadAnimationClip(this, JsonAssetLoader::loadClipForAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.animationClip.bakeKeyframes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void postInit() {
|
||||||
|
this.stateSpectrum.readFrom(this.stateSpectrumBlueprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimationClip getAnimationClip() {
|
||||||
|
if (this.animationClip == null) {
|
||||||
|
this.loadAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.animationClip;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLinkAnimation(final AssetAccessor<? extends DynamicAnimation> fromAnimation, Pose startPose, boolean isOnSameLayer, float transitionTimeModifier, LivingEntityPatch<?> entitypatch, LinkAnimation dest) {
|
||||||
|
if (!entitypatch.isLogicalClient()) {
|
||||||
|
startPose = TiedUpRigRegistry.EMPTY_ANIMATION.getPoseByTime(entitypatch, 0.0F, 1.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
dest.resetNextStartTime();
|
||||||
|
|
||||||
|
float playTime = this.getPlaySpeed(entitypatch, dest);
|
||||||
|
PlaybackSpeedModifier playSpeedModifier = this.getRealAnimation().get().getProperty(StaticAnimationProperty.PLAY_SPEED_MODIFIER).orElse(null);
|
||||||
|
|
||||||
|
if (playSpeedModifier != null) {
|
||||||
|
playTime = playSpeedModifier.modify(dest, entitypatch, playTime, 0.0F, playTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
playTime = Math.abs(playTime);
|
||||||
|
playTime *= TiedUpRigConstants.A_TICK;
|
||||||
|
|
||||||
|
float linkTime = transitionTimeModifier > 0.0F ? transitionTimeModifier + this.transitionTime : this.transitionTime;
|
||||||
|
float totalTime = playTime * (int)Math.ceil(linkTime / playTime);
|
||||||
|
float nextStartTime = Math.max(0.0F, -transitionTimeModifier);
|
||||||
|
nextStartTime += totalTime - linkTime;
|
||||||
|
|
||||||
|
dest.setNextStartTime(nextStartTime);
|
||||||
|
dest.getAnimationClip().reset();
|
||||||
|
dest.setTotalTime(totalTime);
|
||||||
|
dest.setConnectedAnimations(fromAnimation, this.getAccessor());
|
||||||
|
|
||||||
|
Map<String, JointTransform> data1 = startPose.getJointTransformData();
|
||||||
|
Map<String, JointTransform> data2 = this.getPoseByTime(entitypatch, nextStartTime, 0.0F).getJointTransformData();
|
||||||
|
Set<String> joint1 = new HashSet<> (isOnSameLayer ? data1.keySet() : Set.of());
|
||||||
|
Set<String> joint2 = new HashSet<> (data2.keySet());
|
||||||
|
|
||||||
|
if (entitypatch.isLogicalClient()) {
|
||||||
|
JointMaskEntry entry = fromAnimation.get().getJointMaskEntry(entitypatch, false).orElse(null);
|
||||||
|
JointMaskEntry entry2 = this.getJointMaskEntry(entitypatch, true).orElse(null);
|
||||||
|
|
||||||
|
if (entry != null) {
|
||||||
|
joint1.removeIf((jointName) -> entry.isMasked(fromAnimation.get().getProperty(ClientAnimationProperties.LAYER_TYPE).orElse(Layer.LayerType.BASE_LAYER) == Layer.LayerType.BASE_LAYER ?
|
||||||
|
entitypatch.getClientAnimator().currentMotion() : entitypatch.getClientAnimator().currentCompositeMotion(), jointName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry2 != null) {
|
||||||
|
joint2.removeIf((jointName) -> entry2.isMasked(this.getProperty(ClientAnimationProperties.LAYER_TYPE).orElse(Layer.LayerType.BASE_LAYER) == Layer.LayerType.BASE_LAYER ?
|
||||||
|
entitypatch.getCurrentLivingMotion() : entitypatch.currentCompositeMotion, jointName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
joint1.addAll(joint2);
|
||||||
|
|
||||||
|
if (linkTime != totalTime) {
|
||||||
|
Map<String, JointTransform> firstPose = this.getPoseByTime(entitypatch, 0.0F, 0.0F).getJointTransformData();
|
||||||
|
|
||||||
|
for (String jointName : joint1) {
|
||||||
|
Keyframe[] keyframes = new Keyframe[3];
|
||||||
|
keyframes[0] = new Keyframe(0.0F, data1.get(jointName));
|
||||||
|
keyframes[1] = new Keyframe(linkTime, firstPose.get(jointName));
|
||||||
|
keyframes[2] = new Keyframe(totalTime, data2.get(jointName));
|
||||||
|
TransformSheet sheet = new TransformSheet(keyframes);
|
||||||
|
dest.getAnimationClip().addJointTransform(jointName, sheet);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (String jointName : joint1) {
|
||||||
|
Keyframe[] keyframes = new Keyframe[2];
|
||||||
|
keyframes[0] = new Keyframe(0.0F, data1.get(jointName));
|
||||||
|
keyframes[1] = new Keyframe(totalTime, data2.get(jointName));
|
||||||
|
TransformSheet sheet = new TransformSheet(keyframes);
|
||||||
|
dest.getAnimationClip().addJointTransform(jointName, sheet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void begin(LivingEntityPatch<?> entitypatch) {
|
||||||
|
// Load if null
|
||||||
|
this.getAnimationClip();
|
||||||
|
|
||||||
|
// Please fix this implementation when minecraft supports any mixinable method that returns noPhysics variable
|
||||||
|
this.getProperty(StaticAnimationProperty.NO_PHYSICS).ifPresent(val -> {
|
||||||
|
if (val) {
|
||||||
|
entitypatch.getAnimator().getVariables().put(HAD_NO_PHYSICS, this.getAccessor(), entitypatch.getOriginal().noPhysics);
|
||||||
|
entitypatch.getOriginal().noPhysics = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (entitypatch.isLogicalClient()) {
|
||||||
|
this.getProperty(ClientAnimationProperties.TRAIL_EFFECT).ifPresent(trailInfos -> {
|
||||||
|
int idx = 0;
|
||||||
|
|
||||||
|
for (TrailInfo trailInfo : trailInfos) {
|
||||||
|
double eid = Double.longBitsToDouble((long)entitypatch.getOriginal().getId());
|
||||||
|
double animid = Double.longBitsToDouble((long)this.getId());
|
||||||
|
double jointId = Double.longBitsToDouble((long)this.armature.get().searchJointByName(trailInfo.joint()).getId());
|
||||||
|
double index = Double.longBitsToDouble((long)idx++);
|
||||||
|
|
||||||
|
// RIG : RenderItemBase (combat weapon item render) strippé —
|
||||||
|
// TiedUp n'a pas d'items "actifs" porteurs de trails comme
|
||||||
|
// les weapons EF. Le trailInfo reste tel quel de la définition.
|
||||||
|
|
||||||
|
if (!trailInfo.playable()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
entitypatch.getOriginal().level().addParticle(trailInfo.particle(), eid, 0, animid, jointId, index, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getProperty(StaticAnimationProperty.ON_BEGIN_EVENTS).ifPresent(events -> {
|
||||||
|
for (SimpleEvent<?> event : events) {
|
||||||
|
event.execute(entitypatch, this.getAccessor(), 0.0F, 0.0F);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// RIG : PlayerEventListener (combat animation events) strippé.
|
||||||
|
// Les ON_BEGIN_EVENTS SimpleEvent continuent de fonctionner ci-dessus.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void end(LivingEntityPatch<?> entitypatch, AssetAccessor<? extends DynamicAnimation> nextAnimation, boolean isEnd) {
|
||||||
|
// RIG : ANIMATION_END_EVENT fire strippé (PlayerEventListener combat).
|
||||||
|
|
||||||
|
this.getProperty(StaticAnimationProperty.ON_END_EVENTS).ifPresent((events) -> {
|
||||||
|
for (SimpleEvent<?> event : events) {
|
||||||
|
event.executeWithNewParams(entitypatch, this.getAccessor(), this.getTotalTime(), this.getTotalTime(), event.getParameters() == null ? AnimationParameters.of(isEnd) : AnimationParameters.addParameter(event.getParameters(), isEnd));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.getProperty(StaticAnimationProperty.NO_PHYSICS).ifPresent((val) -> {
|
||||||
|
if (val) {
|
||||||
|
entitypatch.getOriginal().noPhysics = entitypatch.getAnimator().getVariables().getOrDefault(HAD_NO_PHYSICS, this.getAccessor());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
entitypatch.getAnimator().getVariables().removeAll(this.getAccessor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick(LivingEntityPatch<?> entitypatch) {
|
||||||
|
this.getProperty(StaticAnimationProperty.NO_PHYSICS).ifPresent((val) -> {
|
||||||
|
if (val) {
|
||||||
|
entitypatch.getOriginal().noPhysics = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.getProperty(StaticAnimationProperty.TICK_EVENTS).ifPresent((events) -> {
|
||||||
|
entitypatch.getAnimator().getPlayer(this.getAccessor()).ifPresent(player -> {
|
||||||
|
for (AnimationEvent<?, ?> event : events) {
|
||||||
|
float prevElapsed = player.getPrevElapsedTime();
|
||||||
|
float elapsed = player.getElapsedTime();
|
||||||
|
|
||||||
|
event.execute(entitypatch, this.getAccessor(), prevElapsed, elapsed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EntityState getState(LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
return new EntityState(this.getStatesMap(entitypatch, time));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TypeFlexibleHashMap<StateFactor<?>> getStatesMap(LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
return this.stateSpectrum.getStateMap(entitypatch, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T getState(StateFactor<T> stateFactor, LivingEntityPatch<?> entitypatch, float time) {
|
||||||
|
return this.stateSpectrum.getSingleState(stateFactor, entitypatch, time);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<JointMaskEntry> getJointMaskEntry(LivingEntityPatch<?> entitypatch, boolean useCurrentMotion) {
|
||||||
|
return this.getProperty(ClientAnimationProperties.JOINT_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void modifyPose(DynamicAnimation animation, Pose pose, LivingEntityPatch<?> entitypatch, float time, float partialTicks) {
|
||||||
|
entitypatch.poseTick(animation, pose, time, partialTicks);
|
||||||
|
|
||||||
|
this.getProperty(StaticAnimationProperty.POSE_MODIFIER).ifPresent((poseModifier) -> {
|
||||||
|
poseModifier.modify(animation, pose, entitypatch, time, partialTicks);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isStaticAnimation() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean doesHeadRotFollowEntityHead() {
|
||||||
|
return !this.getProperty(StaticAnimationProperty.FIXED_HEAD_ROTATION).orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getId() {
|
||||||
|
return this.accessor.id();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (obj instanceof StaticAnimation staticAnimation) {
|
||||||
|
if (this.accessor != null && staticAnimation.accessor != null) {
|
||||||
|
return this.getId() == staticAnimation.getId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.equals(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceLocation getLocation() {
|
||||||
|
return this.resourceLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResourceLocation getRegistryName() {
|
||||||
|
return this.accessor.registryName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetAccessor<? extends Armature> getArmature() {
|
||||||
|
return this.armature;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getPlaySpeed(LivingEntityPatch<?> entitypatch, DynamicAnimation animation) {
|
||||||
|
return 1.0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransformSheet getCoord() {
|
||||||
|
return this.getProperty(ActionAnimationProperty.COORD).orElse(super.getCoord());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
String classPath = this.getClass().toString();
|
||||||
|
return classPath.substring(classPath.lastIndexOf(".") + 1) + " " + this.getLocation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal use only
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public StaticAnimation addPropertyUnsafe(AnimationProperty<?> propertyType, Object value) {
|
||||||
|
this.properties.put(propertyType, value);
|
||||||
|
this.getSubAnimations().forEach((subAnimation) -> subAnimation.get().addPropertyUnsafe(propertyType, value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <A extends StaticAnimation, V> A addProperty(StaticAnimationProperty<V> propertyType, V value) {
|
||||||
|
this.properties.put(propertyType, value);
|
||||||
|
this.getSubAnimations().forEach((subAnimation) -> subAnimation.get().addProperty(propertyType, value));
|
||||||
|
return (A)this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <V> Optional<V> getProperty(AnimationProperty<V> propertyType) {
|
||||||
|
return (Optional<V>) Optional.ofNullable(this.properties.get(propertyType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public Layer.Priority getPriority() {
|
||||||
|
return this.getProperty(ClientAnimationProperties.PRIORITY).orElse(Layer.Priority.LOWEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public Layer.LayerType getLayerType() {
|
||||||
|
return this.getProperty(ClientAnimationProperties.LAYER_TYPE).orElse(LayerType.BASE_LAYER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getModifiedLinkState(StateFactor<?> factor, Object val, LivingEntityPatch<?> entitypatch, float elapsedTime) {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AssetAccessor<? extends StaticAnimation>> getSubAnimations() {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AnimationAccessor<? extends StaticAnimation> getRealAnimation() {
|
||||||
|
return this.getAccessor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public <A extends DynamicAnimation> AnimationAccessor<A> getAccessor() {
|
||||||
|
return (AnimationAccessor<A>)this.accessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAccessor(AnimationAccessor<? extends StaticAnimation> accessor) {
|
||||||
|
this.accessor = accessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public void renderDebugging(PoseStack poseStack, MultiBufferSource buffer, LivingEntityPatch<?> entitypatch, float playTime, float partialTicks) {
|
||||||
|
// RIG : debug render des targets IK (RenderingTool.drawQuad) strippé.
|
||||||
|
// Pas d'IK en TiedUp. Reactivable Phase 2+ avec un helper drawQuad
|
||||||
|
// simple si on veut debug les joints.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InverseKinematicsObject createSimulationData(InverseKinematicsProvider provider, InverseKinematicsSimulatable simOwner, InverseKinematicsSimulator.InverseKinematicsBuilder simBuilder) {
|
||||||
|
return new InverseKinematicsObject(simBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
269
src/main/java/com/tiedup/remake/rig/armature/Armature.java
Normal file
269
src/main/java/com/tiedup/remake/rig/armature/Armature.java
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.armature;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||||
|
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||||
|
import com.tiedup.remake.rig.armature.Joint;
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
import com.tiedup.remake.rig.anim.Pose;
|
||||||
|
import com.tiedup.remake.rig.asset.JsonAssetLoader;
|
||||||
|
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
|
||||||
|
public class Armature {
|
||||||
|
private final String name;
|
||||||
|
private final Int2ObjectMap<Joint> jointById;
|
||||||
|
private final Map<String, Joint> jointByName;
|
||||||
|
private final Map<String, Joint.HierarchicalJointAccessor> pathIndexMap;
|
||||||
|
private final int jointCount;
|
||||||
|
private final OpenMatrix4f[] poseMatrices;
|
||||||
|
public final Joint rootJoint;
|
||||||
|
|
||||||
|
public Armature(String name, int jointNumber, Joint rootJoint, Map<String, Joint> jointMap) {
|
||||||
|
this.name = name;
|
||||||
|
this.jointCount = jointNumber;
|
||||||
|
this.rootJoint = rootJoint;
|
||||||
|
this.jointByName = jointMap;
|
||||||
|
this.jointById = new Int2ObjectOpenHashMap<>();
|
||||||
|
this.pathIndexMap = Maps.newHashMap();
|
||||||
|
|
||||||
|
this.jointByName.values().forEach((joint) -> {
|
||||||
|
this.jointById.put(joint.getId(), joint);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.poseMatrices = OpenMatrix4f.allocateMatrixArray(this.jointCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Joint getOrLogException(Map<String, Joint> jointMap, String name) {
|
||||||
|
if (!jointMap.containsKey(name)) {
|
||||||
|
if (TiedUpRigConstants.IS_DEV_ENV) {
|
||||||
|
TiedUpRigConstants.LOGGER.debug("Cannot find the joint named " + name + " in " + this.getClass().getCanonicalName());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Joint.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return jointMap.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPose(Pose pose) {
|
||||||
|
this.getPoseTransform(this.rootJoint, new OpenMatrix4f(), pose, this.poseMatrices, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bakeOriginMatrices() {
|
||||||
|
this.rootJoint.initOriginTransform(new OpenMatrix4f());
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenMatrix4f[] getPoseMatrices() {
|
||||||
|
return this.poseMatrices;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param applyOriginTransform if you need a final pose of the animations, give it false.
|
||||||
|
*/
|
||||||
|
public OpenMatrix4f[] getPoseAsTransformMatrix(Pose pose, boolean applyOriginTransform) {
|
||||||
|
OpenMatrix4f[] jointMatrices = new OpenMatrix4f[this.jointCount];
|
||||||
|
this.getPoseTransform(this.rootJoint, new OpenMatrix4f(), pose, jointMatrices, applyOriginTransform);
|
||||||
|
return jointMatrices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getPoseTransform(Joint joint, OpenMatrix4f parentTransform, Pose pose, OpenMatrix4f[] jointMatrices, boolean applyOriginTransform) {
|
||||||
|
OpenMatrix4f result = pose.orElseEmpty(joint.getName()).getAnimationBoundMatrix(joint, parentTransform);
|
||||||
|
jointMatrices[joint.getId()] = result;
|
||||||
|
|
||||||
|
for (Joint joints : joint.getSubJoints()) {
|
||||||
|
this.getPoseTransform(joints, result, pose, jointMatrices, applyOriginTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (applyOriginTransform) {
|
||||||
|
result.mulBack(joint.getToOrigin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inapposite past perfect
|
||||||
|
*/
|
||||||
|
@Deprecated(forRemoval = true, since = "1.21.1")
|
||||||
|
public OpenMatrix4f getBindedTransformFor(Pose pose, Joint joint) {
|
||||||
|
return this.getBoundTransformByJointIndex(pose, this.searchPathIndex(joint.getName()).createAccessTicket(this.rootJoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenMatrix4f getBoundTransformFor(Pose pose, Joint joint) {
|
||||||
|
return this.getBoundTransformByJointIndex(pose, this.searchPathIndex(joint.getName()).createAccessTicket(this.rootJoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenMatrix4f getBoundTransformByJointIndex(Pose pose, Joint.AccessTicket pathIndices) {
|
||||||
|
return this.getBoundJointTransformRecursively(pose, this.rootJoint, new OpenMatrix4f(), pathIndices);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OpenMatrix4f getBoundJointTransformRecursively(Pose pose, Joint joint, OpenMatrix4f parentTransform, Joint.AccessTicket pathIndices) {
|
||||||
|
JointTransform jt = pose.orElseEmpty(joint.getName());
|
||||||
|
OpenMatrix4f result = jt.getAnimationBoundMatrix(joint, parentTransform);
|
||||||
|
|
||||||
|
return pathIndices.hasNext() ? this.getBoundJointTransformRecursively(pose, pathIndices.next(), result, pathIndices) : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasJoint(String name) {
|
||||||
|
return this.jointByName.containsKey(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Joint searchJointById(int id) {
|
||||||
|
return this.jointById.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Joint searchJointByName(String name) {
|
||||||
|
return this.jointByName.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search and record joint path from root to terminal
|
||||||
|
*
|
||||||
|
* @param terminalJointName
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public Joint.HierarchicalJointAccessor searchPathIndex(String terminalJointName) {
|
||||||
|
return this.searchPathIndex(this.rootJoint, terminalJointName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search and record joint path to terminal
|
||||||
|
*
|
||||||
|
* @param start
|
||||||
|
* @param terminalJointName
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public Joint.HierarchicalJointAccessor searchPathIndex(Joint start, String terminalJointName) {
|
||||||
|
String signature = start.getName() + "-" + terminalJointName;
|
||||||
|
|
||||||
|
if (this.pathIndexMap.containsKey(signature)) {
|
||||||
|
return this.pathIndexMap.get(signature);
|
||||||
|
} else {
|
||||||
|
Joint.HierarchicalJointAccessor.Builder pathBuilder = start.searchPath(Joint.HierarchicalJointAccessor.builder(), terminalJointName);
|
||||||
|
Joint.HierarchicalJointAccessor accessor;
|
||||||
|
|
||||||
|
if (pathBuilder == null) {
|
||||||
|
throw new IllegalArgumentException("Failed to get joint path index for " + terminalJointName);
|
||||||
|
} else {
|
||||||
|
accessor = pathBuilder.build();
|
||||||
|
this.pathIndexMap.put(signature, accessor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void gatherAllJointsInPathToTerminal(String terminalJointName, Collection<String> jointsInPath) {
|
||||||
|
if (!this.jointByName.containsKey(terminalJointName)) {
|
||||||
|
throw new NoSuchElementException("No " + terminalJointName + " joint in this armature!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Joint.HierarchicalJointAccessor pathIndices = this.searchPathIndex(terminalJointName);
|
||||||
|
Joint.AccessTicket accessTicket = pathIndices.createAccessTicket(this.rootJoint);
|
||||||
|
|
||||||
|
Joint joint = this.rootJoint;
|
||||||
|
jointsInPath.add(joint.getName());
|
||||||
|
|
||||||
|
while (accessTicket.hasNext()) {
|
||||||
|
jointsInPath.add(accessTicket.next().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getJointNumber() {
|
||||||
|
return this.jointCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Armature deepCopy() {
|
||||||
|
Map<String, Joint> oldToNewJoint = Maps.newHashMap();
|
||||||
|
oldToNewJoint.put("empty", Joint.EMPTY);
|
||||||
|
|
||||||
|
Joint newRoot = this.copyHierarchy(this.rootJoint, oldToNewJoint);
|
||||||
|
newRoot.initOriginTransform(new OpenMatrix4f());
|
||||||
|
Armature newArmature = null;
|
||||||
|
|
||||||
|
// Uses reflection to keep the type of copied armature
|
||||||
|
try {
|
||||||
|
Constructor<? extends Armature> constructor = this.getClass().getConstructor(String.class, int.class, Joint.class, Map.class);
|
||||||
|
newArmature = constructor.newInstance(this.name, this.jointCount, newRoot, oldToNewJoint);
|
||||||
|
} catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
|
||||||
|
throw new IllegalStateException("Armature copy failed! " + e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newArmature;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Joint copyHierarchy(Joint joint, Map<String, Joint> oldToNewJoint) {
|
||||||
|
if (joint == Joint.EMPTY) {
|
||||||
|
return Joint.EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
Joint newJoint = new Joint(joint.getName(), joint.getId(), joint.getLocalTransform());
|
||||||
|
oldToNewJoint.put(joint.getName(), newJoint);
|
||||||
|
|
||||||
|
for (Joint subJoint : joint.getSubJoints()) {
|
||||||
|
newJoint.addSubJoints(this.copyHierarchy(subJoint, oldToNewJoint));
|
||||||
|
}
|
||||||
|
|
||||||
|
return newJoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObject toJsonObject() {
|
||||||
|
JsonObject root = new JsonObject();
|
||||||
|
JsonObject armature = new JsonObject();
|
||||||
|
|
||||||
|
JsonArray jointNamesArray = new JsonArray();
|
||||||
|
JsonArray jointHierarchy = new JsonArray();
|
||||||
|
|
||||||
|
this.jointById.int2ObjectEntrySet().stream().sorted((entry1, entry2) -> Integer.compare(entry1.getIntKey(), entry2.getIntKey())).forEach((entry) -> jointNamesArray.add(entry.getValue().getName()));
|
||||||
|
armature.add("joints", jointNamesArray);
|
||||||
|
armature.add("hierarchy", jointHierarchy);
|
||||||
|
|
||||||
|
exportJoint(jointHierarchy, this.rootJoint, true);
|
||||||
|
|
||||||
|
root.add("armature", armature);
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void exportJoint(JsonArray parent, Joint joint, boolean root) {
|
||||||
|
JsonObject jointJson = new JsonObject();
|
||||||
|
jointJson.addProperty("name", joint.getName());
|
||||||
|
|
||||||
|
JsonArray transformMatrix = new JsonArray();
|
||||||
|
OpenMatrix4f localMatrixInBlender = new OpenMatrix4f(joint.getLocalTransform());
|
||||||
|
|
||||||
|
if (root) {
|
||||||
|
localMatrixInBlender.mulFront(OpenMatrix4f.invert(JsonAssetLoader.BLENDER_TO_MINECRAFT_COORD, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
localMatrixInBlender.transpose();
|
||||||
|
localMatrixInBlender.toList().forEach(transformMatrix::add);
|
||||||
|
jointJson.add("transform", transformMatrix);
|
||||||
|
parent.add(jointJson);
|
||||||
|
|
||||||
|
if (!joint.getSubJoints().isEmpty()) {
|
||||||
|
JsonArray children = new JsonArray();
|
||||||
|
jointJson.add("children", children);
|
||||||
|
joint.getSubJoints().forEach((joint$2) -> exportJoint(children, joint$2, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.armature;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.armature.Joint;
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
import com.tiedup.remake.rig.armature.types.HumanLikeArmature;
|
||||||
|
|
||||||
|
public class HumanoidArmature extends Armature implements HumanLikeArmature {
|
||||||
|
public final Joint thighR;
|
||||||
|
public final Joint legR;
|
||||||
|
public final Joint kneeR;
|
||||||
|
public final Joint thighL;
|
||||||
|
public final Joint legL;
|
||||||
|
public final Joint kneeL;
|
||||||
|
public final Joint torso;
|
||||||
|
public final Joint chest;
|
||||||
|
public final Joint head;
|
||||||
|
public final Joint shoulderR;
|
||||||
|
public final Joint armR;
|
||||||
|
public final Joint handR;
|
||||||
|
public final Joint toolR;
|
||||||
|
public final Joint elbowR;
|
||||||
|
public final Joint shoulderL;
|
||||||
|
public final Joint armL;
|
||||||
|
public final Joint handL;
|
||||||
|
public final Joint toolL;
|
||||||
|
public final Joint elbowL;
|
||||||
|
|
||||||
|
public HumanoidArmature(String name, int jointNumber, Joint rootJoint, Map<String, Joint> jointMap) {
|
||||||
|
super(name, jointNumber, rootJoint, jointMap);
|
||||||
|
|
||||||
|
this.thighR = this.getOrLogException(jointMap, "Thigh_R");
|
||||||
|
this.legR = this.getOrLogException(jointMap, "Leg_R");
|
||||||
|
this.kneeR = this.getOrLogException(jointMap, "Knee_R");
|
||||||
|
this.thighL = this.getOrLogException(jointMap, "Thigh_L");
|
||||||
|
this.legL = this.getOrLogException(jointMap, "Leg_L");
|
||||||
|
this.kneeL = this.getOrLogException(jointMap, "Knee_L");
|
||||||
|
this.torso = this.getOrLogException(jointMap, "Torso");
|
||||||
|
this.chest = this.getOrLogException(jointMap, "Chest");
|
||||||
|
this.head = this.getOrLogException(jointMap, "Head");
|
||||||
|
this.shoulderR = this.getOrLogException(jointMap, "Shoulder_R");
|
||||||
|
this.armR = this.getOrLogException(jointMap, "Arm_R");
|
||||||
|
this.handR = this.getOrLogException(jointMap, "Hand_R");
|
||||||
|
this.toolR = this.getOrLogException(jointMap, "Tool_R");
|
||||||
|
this.elbowR = this.getOrLogException(jointMap, "Elbow_R");
|
||||||
|
this.shoulderL = this.getOrLogException(jointMap, "Shoulder_L");
|
||||||
|
this.armL = this.getOrLogException(jointMap, "Arm_L");
|
||||||
|
this.handL = this.getOrLogException(jointMap, "Hand_L");
|
||||||
|
this.toolL = this.getOrLogException(jointMap, "Tool_L");
|
||||||
|
this.elbowL = this.getOrLogException(jointMap, "Elbow_L");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Joint leftToolJoint() {
|
||||||
|
return this.toolL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Joint rightToolJoint() {
|
||||||
|
return this.toolR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Joint backToolJoint() {
|
||||||
|
return this.chest;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Joint leftHandJoint() {
|
||||||
|
return this.handL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Joint rightHandJoint() {
|
||||||
|
return this.handR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Joint leftArmJoint() {
|
||||||
|
return this.armL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Joint rightArmJoint() {
|
||||||
|
return this.armR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Joint leftLegJoint() {
|
||||||
|
return this.legL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Joint rightLegJoint() {
|
||||||
|
return this.legR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Joint leftThighJoint() {
|
||||||
|
return this.thighL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Joint rightThighJoint() {
|
||||||
|
return this.thighR;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Joint headJoint() {
|
||||||
|
return this.head;
|
||||||
|
}
|
||||||
|
}
|
||||||
278
src/main/java/com/tiedup/remake/rig/armature/Joint.java
Normal file
278
src/main/java/com/tiedup/remake/rig/armature/Joint.java
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.armature;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||||
|
|
||||||
|
public class Joint {
|
||||||
|
public static final Joint EMPTY = new Joint("empty", -1, new OpenMatrix4f());
|
||||||
|
|
||||||
|
private final List<Joint> subJoints = Lists.newArrayList();
|
||||||
|
private final int jointId;
|
||||||
|
private final String jointName;
|
||||||
|
private final OpenMatrix4f localTransform;
|
||||||
|
private final OpenMatrix4f toOrigin = new OpenMatrix4f();
|
||||||
|
|
||||||
|
public Joint(String name, int jointId, OpenMatrix4f localTransform) {
|
||||||
|
this.jointId = jointId;
|
||||||
|
this.jointName = name;
|
||||||
|
this.localTransform = localTransform.unmodifiable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSubJoints(Joint... joints) {
|
||||||
|
for (Joint joint : joints) {
|
||||||
|
if (!this.subJoints.contains(joint)) {
|
||||||
|
this.subJoints.add(joint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeSubJoints(Joint... joints) {
|
||||||
|
for (Joint joint : joints) {
|
||||||
|
this.subJoints.remove(joint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Joint> getAllJoints() {
|
||||||
|
List<Joint> list = Lists.newArrayList();
|
||||||
|
this.getSubJoints(list);
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void iterSubJoints(Consumer<Joint> iterTask) {
|
||||||
|
iterTask.accept(this);
|
||||||
|
|
||||||
|
for (Joint joint : this.subJoints) {
|
||||||
|
joint.iterSubJoints(iterTask);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getSubJoints(List<Joint> list) {
|
||||||
|
list.add(this);
|
||||||
|
|
||||||
|
for (Joint joint : this.subJoints) {
|
||||||
|
joint.getSubJoints(list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initOriginTransform(OpenMatrix4f parentTransform) {
|
||||||
|
OpenMatrix4f modelTransform = OpenMatrix4f.mul(parentTransform, this.localTransform, null);
|
||||||
|
OpenMatrix4f.invert(modelTransform, this.toOrigin);
|
||||||
|
|
||||||
|
for (Joint joint : this.subJoints) {
|
||||||
|
joint.initOriginTransform(modelTransform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenMatrix4f getLocalTransform() {
|
||||||
|
return this.localTransform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenMatrix4f getToOrigin() {
|
||||||
|
return this.toOrigin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Joint> getSubJoints() {
|
||||||
|
return this.subJoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Null if index out of range
|
||||||
|
@Nullable
|
||||||
|
public Joint getSubJoint(int index) {
|
||||||
|
if (index < 0 || this.subJoints.size() <= index) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.subJoints.get(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return this.jointName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getId() {
|
||||||
|
return this.jointId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (o instanceof Joint joint) {
|
||||||
|
return this.jointName.equals(joint.jointName) && this.jointId == joint.jointId;
|
||||||
|
} else {
|
||||||
|
return super.equals(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.jointName.hashCode() ^ this.jointId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the method that memorize path search results. {@link Armature#searchPathIndex(Joint, String)}
|
||||||
|
*
|
||||||
|
* @param builder
|
||||||
|
* @param jointName
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@ApiStatus.Internal
|
||||||
|
public HierarchicalJointAccessor.Builder searchPath(HierarchicalJointAccessor.Builder builder, String jointName) {
|
||||||
|
if (jointName.equals(this.getName())) {
|
||||||
|
return builder;
|
||||||
|
} else {
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for (Joint subJoint : this.subJoints) {
|
||||||
|
HierarchicalJointAccessor.Builder nextBuilder = subJoint.searchPath(builder.append(i), jointName);
|
||||||
|
i++;
|
||||||
|
|
||||||
|
if (nextBuilder != null) {
|
||||||
|
return nextBuilder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("\nid: " + this.jointId);
|
||||||
|
sb.append("\nname: " + this.jointName);
|
||||||
|
sb.append("\nlocal transform: " + this.localTransform);
|
||||||
|
sb.append("\nto origin: " + this.toOrigin);
|
||||||
|
sb.append("\nchildren: [");
|
||||||
|
|
||||||
|
int idx = 0;
|
||||||
|
|
||||||
|
for (Joint joint : this.subJoints) {
|
||||||
|
idx++;
|
||||||
|
sb.append(joint.jointName);
|
||||||
|
|
||||||
|
if (idx != this.subJoints.size()) {
|
||||||
|
sb.append(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append("]\n");
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String printIncludingChildren() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(this.toString());
|
||||||
|
|
||||||
|
for (Joint joint : this.subJoints) {
|
||||||
|
sb.append(joint.printIncludingChildren());
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class HierarchicalJointAccessor {
|
||||||
|
private Queue<Integer> indicesToTerminal;
|
||||||
|
private final String signature;
|
||||||
|
|
||||||
|
private HierarchicalJointAccessor(Builder builder) {
|
||||||
|
this.indicesToTerminal = builder.indicesToTerminal;
|
||||||
|
this.signature = builder.signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccessTicket createAccessTicket(Joint rootJoint) {
|
||||||
|
return new AccessTicket(this.indicesToTerminal, rootJoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (o instanceof HierarchicalJointAccessor accessor) {
|
||||||
|
this.signature.equals(accessor.signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.equals(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.signature.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder(new LinkedList<> (), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private Queue<Integer> indicesToTerminal;
|
||||||
|
private String signature;
|
||||||
|
|
||||||
|
private Builder(Queue<Integer> indicesToTerminal, String signature) {
|
||||||
|
this.indicesToTerminal = indicesToTerminal;
|
||||||
|
this.signature = signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder append(int index) {
|
||||||
|
String signatureNext;
|
||||||
|
|
||||||
|
if (this.indicesToTerminal.isEmpty()) {
|
||||||
|
signatureNext = this.signature + String.valueOf(index);
|
||||||
|
} else {
|
||||||
|
signatureNext = this.signature + "-" + String.valueOf(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
Queue<Integer> nextQueue = new LinkedList<> (this.indicesToTerminal);
|
||||||
|
nextQueue.add(index);
|
||||||
|
|
||||||
|
return new Builder(nextQueue, signatureNext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HierarchicalJointAccessor build() {
|
||||||
|
return new HierarchicalJointAccessor(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AccessTicket implements Iterator<Joint> {
|
||||||
|
Queue<Integer> accecssStack;
|
||||||
|
Joint joint;
|
||||||
|
|
||||||
|
private AccessTicket(Queue<Integer> indicesToTerminal, Joint rootJoint) {
|
||||||
|
this.accecssStack = new LinkedList<> (indicesToTerminal);
|
||||||
|
this.joint = rootJoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasNext() {
|
||||||
|
return !this.accecssStack.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Joint next() {
|
||||||
|
if (this.hasNext()) {
|
||||||
|
int nextIndex = this.accecssStack.poll();
|
||||||
|
this.joint = this.joint.subJoints.get(nextIndex);
|
||||||
|
} else {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.joint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
215
src/main/java/com/tiedup/remake/rig/armature/JointTransform.java
Normal file
215
src/main/java/com/tiedup/remake/rig/armature/JointTransform.java
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.armature;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.joml.Quaternionf;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
|
import net.minecraft.util.Mth;
|
||||||
|
import com.tiedup.remake.rig.math.AnimationTransformEntry;
|
||||||
|
import com.tiedup.remake.rig.math.MathUtils;
|
||||||
|
import com.tiedup.remake.rig.math.MatrixOperation;
|
||||||
|
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||||
|
import com.tiedup.remake.rig.math.Vec3f;
|
||||||
|
|
||||||
|
public class JointTransform {
|
||||||
|
public static final String ANIMATION_TRANSFORM = "animation_transform";
|
||||||
|
public static final String JOINT_LOCAL_TRANSFORM = "joint_local_transform";
|
||||||
|
public static final String PARENT = "parent";
|
||||||
|
public static final String RESULT1 = "front_result";
|
||||||
|
public static final String RESULT2 = "overwrite_rotation";
|
||||||
|
|
||||||
|
public static class TransformEntry {
|
||||||
|
public final MatrixOperation multiplyFunction;
|
||||||
|
public final JointTransform transform;
|
||||||
|
|
||||||
|
public TransformEntry(MatrixOperation multiplyFunction, JointTransform transform) {
|
||||||
|
this.multiplyFunction = multiplyFunction;
|
||||||
|
this.transform = transform;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<String, TransformEntry> entries = Maps.newHashMap();
|
||||||
|
private final Vec3f translation;
|
||||||
|
private final Vec3f scale;
|
||||||
|
private final Quaternionf rotation;
|
||||||
|
|
||||||
|
public JointTransform(Vec3f translation, Quaternionf rotation, Vec3f scale) {
|
||||||
|
this.translation = translation;
|
||||||
|
this.rotation = rotation;
|
||||||
|
this.scale = scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec3f translation() {
|
||||||
|
return this.translation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Quaternionf rotation() {
|
||||||
|
return this.rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vec3f scale() {
|
||||||
|
return this.scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearTransform() {
|
||||||
|
this.translation.set(0.0F, 0.0F, 0.0F);
|
||||||
|
this.rotation.set(0.0F, 0.0F, 0.0F, 1.0F);
|
||||||
|
this.scale.set(1.0F, 1.0F, 1.0F);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointTransform copy() {
|
||||||
|
return JointTransform.empty().copyFrom(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointTransform copyFrom(JointTransform jt) {
|
||||||
|
Vec3f newV = jt.translation();
|
||||||
|
Quaternionf newQ = jt.rotation();
|
||||||
|
Vec3f newS = jt.scale;
|
||||||
|
this.translation.set(newV);
|
||||||
|
this.rotation.set(newQ);
|
||||||
|
this.scale.set(newS);
|
||||||
|
this.entries.putAll(jt.entries);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void jointLocal(JointTransform transform, MatrixOperation multiplyFunction) {
|
||||||
|
this.entries.put(JOINT_LOCAL_TRANSFORM, new TransformEntry(multiplyFunction, this.mergeIfExist(JOINT_LOCAL_TRANSFORM, transform)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void parent(JointTransform transform, MatrixOperation multiplyFunction) {
|
||||||
|
this.entries.put(PARENT, new TransformEntry(multiplyFunction, this.mergeIfExist(PARENT, transform)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void animationTransform(JointTransform transform, MatrixOperation multiplyFunction) {
|
||||||
|
this.entries.put(ANIMATION_TRANSFORM, new TransformEntry(multiplyFunction, this.mergeIfExist(ANIMATION_TRANSFORM, transform)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void frontResult(JointTransform transform, MatrixOperation multiplyFunction) {
|
||||||
|
this.entries.put(RESULT1, new TransformEntry(multiplyFunction, this.mergeIfExist(RESULT1, transform)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void overwriteRotation(JointTransform transform) {
|
||||||
|
this.entries.put(RESULT2, new TransformEntry(OpenMatrix4f::mul, this.mergeIfExist(RESULT2, transform)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public JointTransform mergeIfExist(String entryName, JointTransform transform) {
|
||||||
|
if (this.entries.containsKey(entryName)) {
|
||||||
|
TransformEntry transformEntry = this.entries.get(entryName);
|
||||||
|
return JointTransform.mul(transform, transformEntry.transform, transformEntry.multiplyFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenMatrix4f getAnimationBoundMatrix(Joint joint, OpenMatrix4f parentTransform) {
|
||||||
|
AnimationTransformEntry animationTransformEntry = new AnimationTransformEntry();
|
||||||
|
|
||||||
|
for (Map.Entry<String, TransformEntry> entry : this.entries.entrySet()) {
|
||||||
|
animationTransformEntry.put(entry.getKey(), entry.getValue().transform.toMatrix(), entry.getValue().multiplyFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
animationTransformEntry.put(ANIMATION_TRANSFORM, this.toMatrix(), OpenMatrix4f::mul);
|
||||||
|
animationTransformEntry.put(JOINT_LOCAL_TRANSFORM, joint.getLocalTransform());
|
||||||
|
animationTransformEntry.put(PARENT, parentTransform);
|
||||||
|
|
||||||
|
return animationTransformEntry.getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public OpenMatrix4f toMatrix() {
|
||||||
|
return new OpenMatrix4f().translate(this.translation).mulBack(OpenMatrix4f.fromQuaternion(this.rotation)).scale(this.scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("translation:%s, rotation:%s, scale:%s %d entries ", this.translation, this.rotation, this.scale, this.entries.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointTransform interpolateTransform(JointTransform prev, JointTransform next, float progression, JointTransform dest) {
|
||||||
|
if (dest == null) {
|
||||||
|
dest = JointTransform.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
MathUtils.lerpVector(prev.translation, next.translation, progression, dest.translation);
|
||||||
|
MathUtils.lerpQuaternion(prev.rotation, next.rotation, progression, dest.rotation);
|
||||||
|
MathUtils.lerpVector(prev.scale, next.scale, progression, dest.scale);
|
||||||
|
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointTransform interpolate(JointTransform prev, JointTransform next, float progression) {
|
||||||
|
return interpolate(prev, next, progression, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointTransform interpolate(JointTransform prev, JointTransform next, float progression, JointTransform dest) {
|
||||||
|
if (dest == null) {
|
||||||
|
dest = JointTransform.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev == null || next == null) {
|
||||||
|
dest.clearTransform();
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
|
||||||
|
progression = Mth.clamp(progression, 0.0F, 1.0F);
|
||||||
|
interpolateTransform(prev, next, progression, dest);
|
||||||
|
dest.entries.clear();
|
||||||
|
|
||||||
|
for (Map.Entry<String, TransformEntry> entry : prev.entries.entrySet()) {
|
||||||
|
JointTransform transform = next.entries.containsKey(entry.getKey()) ? next.entries.get(entry.getKey()).transform : JointTransform.empty();
|
||||||
|
dest.entries.put(entry.getKey(), new TransformEntry(entry.getValue().multiplyFunction, interpolateTransform(entry.getValue().transform, transform, progression, null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, TransformEntry> entry : next.entries.entrySet()) {
|
||||||
|
if (!dest.entries.containsKey(entry.getKey())) {
|
||||||
|
dest.entries.put(entry.getKey(), new TransformEntry(entry.getValue().multiplyFunction, interpolateTransform(JointTransform.empty(), entry.getValue().transform, progression, null)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointTransform fromMatrixWithoutScale(OpenMatrix4f matrix) {
|
||||||
|
return new JointTransform(matrix.toTranslationVector(), matrix.toQuaternion(), new Vec3f(1.0F, 1.0F, 1.0F));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointTransform translation(Vec3f vec) {
|
||||||
|
return JointTransform.translationRotation(vec, new Quaternionf(0.0F, 0.0F, 0.0F, 1.0F));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointTransform rotation(Quaternionf quat) {
|
||||||
|
return JointTransform.translationRotation(new Vec3f(0.0F, 0.0F, 0.0F), quat);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointTransform scale(Vec3f vec) {
|
||||||
|
return new JointTransform(new Vec3f(0.0F, 0.0F, 0.0F), new Quaternionf(0.0F, 0.0F, 0.0F, 1.0F), vec);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointTransform fromMatrix(OpenMatrix4f matrix) {
|
||||||
|
return new JointTransform(matrix.toTranslationVector(), matrix.toQuaternion(), matrix.toScaleVector());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointTransform translationRotation(Vec3f vec, Quaternionf quat) {
|
||||||
|
return new JointTransform(vec, quat, new Vec3f(1.0F, 1.0F, 1.0F));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointTransform mul(JointTransform left, JointTransform right, MatrixOperation operation) {
|
||||||
|
return JointTransform.fromMatrix(operation.mul(left.toMatrix(), right.toMatrix(), null));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointTransform fromPrimitives(float locX, float locY, float locZ, float quatX, float quatY, float quatZ, float quatW, float scaX, float scaY, float scaZ) {
|
||||||
|
return new JointTransform(new Vec3f(locX, locY, locZ), new Quaternionf(quatX, quatY, quatZ, quatW), new Vec3f(scaX, scaY, scaZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JointTransform empty() {
|
||||||
|
return new JointTransform(new Vec3f(0.0F, 0.0F, 0.0F), new Quaternionf(0.0F, 0.0F, 0.0F, 1.0F), new Vec3f(1.0F, 1.0F, 1.0F));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.armature.datapack;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.joml.Quaternionf;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
import com.tiedup.remake.rig.armature.Joint;
|
||||||
|
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||||
|
import com.tiedup.remake.rig.math.Vec3f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descriptor immuable d'une armature custom chargee depuis un datapack
|
||||||
|
* ({@code data/<ns>/tiedup/armatures/<name>.json}).
|
||||||
|
*
|
||||||
|
* <h2>But</h2>
|
||||||
|
* <p>Permet a un modder de definir une armature custom (quadruped, centaure,
|
||||||
|
* neko, ...) en JSON sans ecrire de code Java. Cette classe est la
|
||||||
|
* representation parsee du JSON ; elle sait se valider et se convertir en
|
||||||
|
* {@link Armature} runtime via {@link #toRuntimeArmature()}.</p>
|
||||||
|
*
|
||||||
|
* <h2>Format JSON attendu</h2>
|
||||||
|
* <pre>{@code
|
||||||
|
* {
|
||||||
|
* "description": "Four-legged pet creature armature",
|
||||||
|
* "root_joint": "Root",
|
||||||
|
* "joints": {
|
||||||
|
* "Root": {
|
||||||
|
* "id": 0,
|
||||||
|
* "parent": null,
|
||||||
|
* "translation": [0.0, 0.0, 0.0],
|
||||||
|
* "rotation": [0.0, 0.0, 0.0, 1.0],
|
||||||
|
* "children": ["Torso"]
|
||||||
|
* },
|
||||||
|
* "Torso": {
|
||||||
|
* "id": 1,
|
||||||
|
* "parent": "Root",
|
||||||
|
* "translation": [0.0, 12.0, 0.0],
|
||||||
|
* "rotation": [0.0, 0.0, 0.0, 1.0],
|
||||||
|
* "children": []
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* }</pre>
|
||||||
|
*
|
||||||
|
* <h2>Invariants (enforces par {@link #validate()})</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code root_joint} doit exister dans {@code joints}.</li>
|
||||||
|
* <li>Tous les {@code parent} (sauf null) et tous les {@code children}
|
||||||
|
* doivent referencer un joint declare dans la map.</li>
|
||||||
|
* <li>Les {@code id} des joints doivent etre uniques et contigus a partir
|
||||||
|
* de 0 (donc 0..N-1 exactement, ou N = taille de la map).</li>
|
||||||
|
* <li>La hierarchie parent-children doit former un DAG acyclique a racine
|
||||||
|
* unique (le root_joint).</li>
|
||||||
|
* <li>Le nombre total de joints est plafonne a
|
||||||
|
* {@link TiedUpRigConstants#MAX_JOINTS}.</li>
|
||||||
|
* <li>Relation parent-child bidirectionnelle coherente : si A liste B dans
|
||||||
|
* {@code children}, B doit avoir {@code parent = A}.</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Quaternion convention</h2>
|
||||||
|
* <p>Format <b>xyzw</b> (JOML standard, aligne sur {@link Quaternionf}).
|
||||||
|
* Une rotation identity = {@code [0, 0, 0, 1]}.</p>
|
||||||
|
*/
|
||||||
|
public record ArmatureDefinition(
|
||||||
|
ResourceLocation id,
|
||||||
|
String description,
|
||||||
|
String rootJointName,
|
||||||
|
Map<String, JointDefinition> joints
|
||||||
|
) {
|
||||||
|
|
||||||
|
public ArmatureDefinition {
|
||||||
|
// Defensive immutable wrap for the record's invariants. The loader
|
||||||
|
// already passes LinkedHashMap but callers who construct directly in
|
||||||
|
// tests shouldn't be able to mutate the internal state post-hoc.
|
||||||
|
joints = Collections.unmodifiableMap(new LinkedHashMap<>(joints));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Descriptor immuable d'un seul joint.
|
||||||
|
*
|
||||||
|
* @param name nom du joint (cle dans la map parente)
|
||||||
|
* @param jointId id int unique [0..N-1]
|
||||||
|
* @param parentName nom du joint parent, ou null si c'est le root
|
||||||
|
* @param translation translation locale (relative au parent)
|
||||||
|
* @param rotation rotation locale quaternion xyzw
|
||||||
|
* @param childrenNames liste des joints enfants (par nom, ordre preserve)
|
||||||
|
*/
|
||||||
|
public record JointDefinition(
|
||||||
|
String name,
|
||||||
|
int jointId,
|
||||||
|
@Nullable String parentName,
|
||||||
|
Vec3f translation,
|
||||||
|
Quaternionf rotation,
|
||||||
|
List<String> childrenNames
|
||||||
|
) {
|
||||||
|
|
||||||
|
public JointDefinition {
|
||||||
|
Objects.requireNonNull(name, "joint name");
|
||||||
|
Objects.requireNonNull(translation, "translation");
|
||||||
|
Objects.requireNonNull(rotation, "rotation");
|
||||||
|
Objects.requireNonNull(childrenNames, "childrenNames");
|
||||||
|
childrenNames = List.copyOf(childrenNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifie la coherence structurelle du descriptor.
|
||||||
|
*
|
||||||
|
* @return {@link Optional#empty()} si la structure est valide, sinon un
|
||||||
|
* {@link Optional} contenant un message d'erreur humain-lisible
|
||||||
|
* (premiere erreur detectee — la validation s'arrete au premier
|
||||||
|
* probleme pour eviter un deluge de logs).
|
||||||
|
*/
|
||||||
|
public Optional<String> validate() {
|
||||||
|
if (joints.isEmpty()) {
|
||||||
|
return Optional.of("joints map is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (joints.size() > TiedUpRigConstants.MAX_JOINTS) {
|
||||||
|
return Optional.of(
|
||||||
|
"too many joints: " + joints.size() + " > MAX_JOINTS="
|
||||||
|
+ TiedUpRigConstants.MAX_JOINTS
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rootJointName == null || rootJointName.isEmpty()) {
|
||||||
|
return Optional.of("root_joint is null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!joints.containsKey(rootJointName)) {
|
||||||
|
return Optional.of(
|
||||||
|
"root_joint '" + rootJointName + "' is not declared in the joints map"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each joint's internal consistency (name match, id range,
|
||||||
|
// parent exists, children exist, uniqueness).
|
||||||
|
Set<Integer> seenIds = new HashSet<>();
|
||||||
|
int jointCount = joints.size();
|
||||||
|
|
||||||
|
for (Map.Entry<String, JointDefinition> e : joints.entrySet()) {
|
||||||
|
String declaredName = e.getKey();
|
||||||
|
JointDefinition j = e.getValue();
|
||||||
|
|
||||||
|
if (!declaredName.equals(j.name())) {
|
||||||
|
return Optional.of(
|
||||||
|
"joint map key '" + declaredName + "' does not match joint.name '"
|
||||||
|
+ j.name() + "'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int jid = j.jointId();
|
||||||
|
if (jid < 0 || jid >= jointCount) {
|
||||||
|
return Optional.of(
|
||||||
|
"joint '" + declaredName + "' has id " + jid
|
||||||
|
+ " outside the contiguous range [0.."
|
||||||
|
+ (jointCount - 1) + "]"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!seenIds.add(jid)) {
|
||||||
|
return Optional.of(
|
||||||
|
"duplicate joint id " + jid + " on joint '" + declaredName + "'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (j.parentName() != null && !joints.containsKey(j.parentName())) {
|
||||||
|
return Optional.of(
|
||||||
|
"joint '" + declaredName + "' references unknown parent '"
|
||||||
|
+ j.parentName() + "'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String child : j.childrenNames()) {
|
||||||
|
if (!joints.containsKey(child)) {
|
||||||
|
return Optional.of(
|
||||||
|
"joint '" + declaredName + "' references unknown child '"
|
||||||
|
+ child + "'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The above "id in [0..N-1] + unique" implies ids are exactly the set
|
||||||
|
// {0,1,...,N-1} (pigeonhole) — contiguity is therefore guaranteed by
|
||||||
|
// the uniqueness + range checks, no further pass needed.
|
||||||
|
|
||||||
|
// Root must have parent = null; all non-root joints must have a
|
||||||
|
// non-null parent. Verifies that there's exactly one root in the set.
|
||||||
|
JointDefinition rootDef = joints.get(rootJointName);
|
||||||
|
if (rootDef.parentName() != null) {
|
||||||
|
return Optional.of(
|
||||||
|
"root_joint '" + rootJointName + "' must have parent = null, got '"
|
||||||
|
+ rootDef.parentName() + "'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, JointDefinition> e : joints.entrySet()) {
|
||||||
|
if (e.getKey().equals(rootJointName)) continue;
|
||||||
|
if (e.getValue().parentName() == null) {
|
||||||
|
return Optional.of(
|
||||||
|
"joint '" + e.getKey() + "' has no parent but is not the root_joint "
|
||||||
|
+ "'" + rootJointName + "'"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bidirectional coherence : if A lists B in children, B must have
|
||||||
|
// parent=A. Detects orphans + bogus child refs up-front.
|
||||||
|
for (Map.Entry<String, JointDefinition> e : joints.entrySet()) {
|
||||||
|
String parentName = e.getKey();
|
||||||
|
for (String childName : e.getValue().childrenNames()) {
|
||||||
|
JointDefinition child = joints.get(childName);
|
||||||
|
if (!parentName.equals(child.parentName())) {
|
||||||
|
return Optional.of(
|
||||||
|
"joint '" + parentName + "' lists '" + childName
|
||||||
|
+ "' as child, but '" + childName + "'.parent = '"
|
||||||
|
+ child.parentName() + "' (mismatch)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DAG / connectivity check: BFS from the root, every joint must be
|
||||||
|
// reachable exactly once. Detects cycles (would require infinite
|
||||||
|
// reachability — bounded by seen-set, any loop shows up as "child
|
||||||
|
// visited twice"), detached subtrees, and disconnected components.
|
||||||
|
Set<String> visited = new LinkedHashSet<>();
|
||||||
|
Deque<String> queue = new ArrayDeque<>();
|
||||||
|
queue.add(rootJointName);
|
||||||
|
visited.add(rootJointName);
|
||||||
|
|
||||||
|
while (!queue.isEmpty()) {
|
||||||
|
String current = queue.poll();
|
||||||
|
JointDefinition jd = joints.get(current);
|
||||||
|
for (String child : jd.childrenNames()) {
|
||||||
|
if (!visited.add(child)) {
|
||||||
|
return Optional.of(
|
||||||
|
"cycle or duplicate parent detected: joint '" + child
|
||||||
|
+ "' is reachable from more than one path "
|
||||||
|
+ "(parent chain is not a tree)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
queue.add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visited.size() != joints.size()) {
|
||||||
|
List<String> unreachable = new ArrayList<>(joints.keySet());
|
||||||
|
unreachable.removeAll(visited);
|
||||||
|
return Optional.of(
|
||||||
|
"joints not reachable from root '" + rootJointName + "': "
|
||||||
|
+ unreachable
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit ce descriptor en {@link Armature} runtime.
|
||||||
|
*
|
||||||
|
* <p>Pre-condition : {@link #validate()} doit avoir retourne
|
||||||
|
* {@link Optional#empty()}. Si ce n'est pas le cas, cette methode peut
|
||||||
|
* throw {@link IllegalStateException} lors de la construction (ex :
|
||||||
|
* child introuvable). Le loader public
|
||||||
|
* ({@link ArmatureReloadListener}) appelle toujours validate() avant.</p>
|
||||||
|
*
|
||||||
|
* <p>L'armature retournee a ses {@code toOrigin} matrices calcules via
|
||||||
|
* {@link Armature#bakeOriginMatrices()} — prete a etre utilisee par
|
||||||
|
* le renderer sans etape supplementaire.</p>
|
||||||
|
*
|
||||||
|
* @return un {@link Armature} avec la hierarchie + les localTransform
|
||||||
|
* baked depuis (translation, rotation).
|
||||||
|
* @throws IllegalStateException si le descriptor est structurellement
|
||||||
|
* invalide (validate() devrait avoir detecte
|
||||||
|
* ca en amont).
|
||||||
|
*/
|
||||||
|
public Armature toRuntimeArmature() {
|
||||||
|
Map<String, Joint> joints = new LinkedHashMap<>(this.joints.size());
|
||||||
|
|
||||||
|
// Pass 1 : create every Joint with its localTransform (translation +
|
||||||
|
// rotation composed into an OpenMatrix4f). No parent-child wiring yet.
|
||||||
|
for (JointDefinition def : this.joints.values()) {
|
||||||
|
OpenMatrix4f local = OpenMatrix4f.fromQuaternion(def.rotation())
|
||||||
|
.translate(def.translation());
|
||||||
|
joints.put(def.name(), new Joint(def.name(), def.jointId(), local));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass 2 : wire up the hierarchy. Children are added in the order
|
||||||
|
// they appear in the JSON's children array — this determines the
|
||||||
|
// subJoints[i] index used by Joint.HierarchicalJointAccessor, so we
|
||||||
|
// must preserve it verbatim.
|
||||||
|
for (JointDefinition def : this.joints.values()) {
|
||||||
|
Joint parentJoint = joints.get(def.name());
|
||||||
|
for (String childName : def.childrenNames()) {
|
||||||
|
Joint childJoint = joints.get(childName);
|
||||||
|
if (childJoint == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"child joint '" + childName + "' not found while building runtime "
|
||||||
|
+ "armature '" + id + "' — did you call validate() first?"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
parentJoint.addSubJoints(childJoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Joint rootJoint = joints.get(rootJointName);
|
||||||
|
if (rootJoint == null) {
|
||||||
|
// Should never happen if validate() passed.
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"root joint '" + rootJointName + "' missing after pass 1 — "
|
||||||
|
+ "validate() should have caught this"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Armature armature = new Armature(id.toString(), joints.size(), rootJoint, joints);
|
||||||
|
armature.bakeOriginMatrices();
|
||||||
|
return armature;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.armature.datapack;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import org.joml.Quaternionf;
|
||||||
|
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.packs.resources.ResourceManager;
|
||||||
|
import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener;
|
||||||
|
import net.minecraft.util.profiling.ProfilerFiller;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
import com.tiedup.remake.rig.armature.datapack.ArmatureDefinition.JointDefinition;
|
||||||
|
import com.tiedup.remake.rig.math.Vec3f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scanne les fichiers JSON {@code data/<ns>/tiedup/armatures/<name>.json} et
|
||||||
|
* enregistre chaque definition d'armature custom comme un {@link Armature}
|
||||||
|
* runtime queryable via {@link #get(ResourceLocation)}.
|
||||||
|
*
|
||||||
|
* <h2>But</h2>
|
||||||
|
* <p>Permet a un modder / resourcepack-maker de definir des armatures custom
|
||||||
|
* (quadruped, centaure, neko, ...) en JSON — zero code Java. Le format est
|
||||||
|
* decrit dans {@link ArmatureDefinition}.</p>
|
||||||
|
*
|
||||||
|
* <h2>Lookup flow</h2>
|
||||||
|
* <p>{@link com.tiedup.remake.rig.TiedUpArmatures#get(ResourceLocation)} delegue
|
||||||
|
* a cette classe pour tous les IDs qui ne sont pas {@code tiedup:biped} (ni
|
||||||
|
* son alias long-form). Le biped builtin reste hardcode en Java pour
|
||||||
|
* performance + compatibilite backward.</p>
|
||||||
|
*
|
||||||
|
* <h2>Full reload on apply</h2>
|
||||||
|
* <p>A chaque {@code /reload} (server) ou {@code F3+T} (client) le registre
|
||||||
|
* est <b>entierement vide puis repopule</b>. Contrairement a
|
||||||
|
* {@link com.tiedup.remake.rig.anim.LivingMotionReloadListener} qui maintient
|
||||||
|
* un cache JVM-wide pour preserver les ordinals (les enums sont sensibles),
|
||||||
|
* les armatures n'ont pas de contrat d'ordinal stable — une armature peut
|
||||||
|
* etre supprimee / renommee / restructuree par un datapack reload sans casser
|
||||||
|
* un invariant global. Les consumers (renderer, animations) se contentent
|
||||||
|
* de resoudre l'ID a chaque frame, un nouvel {@link Armature} au meme ID est
|
||||||
|
* transparent pour eux.</p>
|
||||||
|
*
|
||||||
|
* <p><b>Attention</b> : les {@link Armature} produites par cette classe ne
|
||||||
|
* sont PAS deep-copied. Si une animation binde une reference a un {@code Joint}
|
||||||
|
* via {@code Armature.searchJointByName} et qu'un reload reconstruit une
|
||||||
|
* nouvelle Armature au meme ID, la reference originale devient orpheline
|
||||||
|
* (la vieille instance existe toujours tant que l'animation la retient). Les
|
||||||
|
* consumers doivent re-resoudre apres reload — c'est deja le pattern de
|
||||||
|
* {@link com.tiedup.remake.rig.util.InstantiateInvoker} (resolution via
|
||||||
|
* {@link com.tiedup.remake.rig.TiedUpArmatures#get(ResourceLocation)}).</p>
|
||||||
|
*
|
||||||
|
* <h2>Side & threading</h2>
|
||||||
|
* <p>Registered server-side via {@code AddReloadListenerEvent} et client-side
|
||||||
|
* via {@code RegisterClientReloadListenersEvent}. Sur server integre, les deux
|
||||||
|
* hooks pointent vers le meme {@link ConcurrentHashMap} — pas de double
|
||||||
|
* registre, pas de race (apply() est appele sequentiellement sur le server
|
||||||
|
* thread ou le client render thread, jamais simultanement).</p>
|
||||||
|
*
|
||||||
|
* <h2>Limitations connues</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li>Les animations Blender-authored qui binds a une armature custom doivent
|
||||||
|
* etre recompilees si la structure de l'armature change (nouveaux
|
||||||
|
* joints, reordering des IDs). Pas de retargeting cross-armature (Phase 5+).</li>
|
||||||
|
* <li>Si un JSON est malforme (structure invalide), il est skip avec un
|
||||||
|
* WARN ; le reste du batch continue.</li>
|
||||||
|
* <li>Les armatures identity (toutes localTransform = identity) rendent un
|
||||||
|
* mesh "effondre a l'origine" — c'est aux auteurs du JSON de fournir
|
||||||
|
* des translations + rotations sensees depuis Blender.</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class ArmatureReloadListener extends SimpleJsonResourceReloadListener {
|
||||||
|
|
||||||
|
/** Dossier scanne : {@code data/<ns>/tiedup/armatures/*.json}. */
|
||||||
|
public static final String DIRECTORY = "tiedup/armatures";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registre runtime des armatures datapack. Re-populated at every reload.
|
||||||
|
* {@link ConcurrentHashMap} protege les reads concurrents depuis les
|
||||||
|
* consumers (rendering) pendant qu'un reload en cours repopule.
|
||||||
|
*/
|
||||||
|
private static final Map<ResourceLocation, Armature> DATAPACK_ARMATURES =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public ArmatureReloadListener() {
|
||||||
|
super(new GsonBuilder().create(), DIRECTORY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resout une armature datapack par son ResourceLocation.
|
||||||
|
*
|
||||||
|
* @param id identifiant namespace:path (ex. {@code mymod:quadruped})
|
||||||
|
* @return l'{@link Armature} enregistree, ou {@code null} si aucun JSON
|
||||||
|
* n'a charge ce ID.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static Armature get(ResourceLocation id) {
|
||||||
|
return DATAPACK_ARMATURES.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nombre d'armatures datapack enregistrees actuellement. */
|
||||||
|
public static int size() {
|
||||||
|
return DATAPACK_ARMATURES.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vue immutable du registre, expose pour debug / tests. */
|
||||||
|
public static Map<ResourceLocation, Armature> view() {
|
||||||
|
return Collections.unmodifiableMap(DATAPACK_ARMATURES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test hook — vide le registre. En prod le clear() est implicite dans
|
||||||
|
* {@link #apply} au debut de chaque reload, pas besoin d'y toucher a la
|
||||||
|
* main.
|
||||||
|
*/
|
||||||
|
public static void clearForTests() {
|
||||||
|
DATAPACK_ARMATURES.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook d'apply direct pour tests — {@link SimpleJsonResourceReloadListener#apply}
|
||||||
|
* est {@code protected}, ce helper l'expose en public pour que les tests
|
||||||
|
* cross-package puissent alimenter le registry sans bootstrap MC.
|
||||||
|
*
|
||||||
|
* <p>Le {@code ResourceManager} et le {@code ProfilerFiller} ne sont pas
|
||||||
|
* lus par notre {@code apply}, on peut passer {@code null} en test.</p>
|
||||||
|
*/
|
||||||
|
public void applyForTests(Map<ResourceLocation, JsonElement> data) {
|
||||||
|
this.apply(data, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void apply(
|
||||||
|
Map<ResourceLocation, JsonElement> objectIn,
|
||||||
|
ResourceManager resourceManager,
|
||||||
|
ProfilerFiller profileFiller
|
||||||
|
) {
|
||||||
|
// Full reset : datapack armatures are ephemeral (no ordinal contract).
|
||||||
|
// A JSON deleted between two reloads must disappear from the registry.
|
||||||
|
DATAPACK_ARMATURES.clear();
|
||||||
|
|
||||||
|
// Stable ordering — if two JSON files are processed in the same reload
|
||||||
|
// and produce the same WARN-log output, TreeMap ensures reproducibility
|
||||||
|
// between JVM boots for debugging.
|
||||||
|
Map<ResourceLocation, JsonElement> sorted = new TreeMap<>(objectIn);
|
||||||
|
|
||||||
|
int loaded = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
|
||||||
|
for (Map.Entry<ResourceLocation, JsonElement> entry : sorted.entrySet()) {
|
||||||
|
ResourceLocation id = entry.getKey();
|
||||||
|
JsonElement element = entry.getValue();
|
||||||
|
|
||||||
|
try {
|
||||||
|
ArmatureDefinition def = parseDefinition(id, element);
|
||||||
|
|
||||||
|
Optional<String> err = def.validate();
|
||||||
|
if (err.isPresent()) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"[ArmatureReloadListener] Invalid armature {}: {}",
|
||||||
|
id, err.get()
|
||||||
|
);
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Armature armature = def.toRuntimeArmature();
|
||||||
|
DATAPACK_ARMATURES.put(id, armature);
|
||||||
|
// INFO (was DEBUG) — datapack-loaded custom armatures are visible
|
||||||
|
// at the default log level, providing a gameday smoke test for
|
||||||
|
// D6 wiring. Volume is bounded by the number of files in
|
||||||
|
// data/<ns>/tiedup/armatures/, currently 0 in vanilla setups.
|
||||||
|
TiedUpRigConstants.LOGGER.info(
|
||||||
|
"[ArmatureReloadListener] Registered armature: {} ({} joints)",
|
||||||
|
id, armature.getJointNumber()
|
||||||
|
);
|
||||||
|
loaded++;
|
||||||
|
} catch (Exception e) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"[ArmatureReloadListener] Failed to parse armature {}: {}",
|
||||||
|
id, e.getMessage()
|
||||||
|
);
|
||||||
|
skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary line — emitted unconditionally to confirm the listener ran.
|
||||||
|
// The builtin BIPED is registered separately in TiedUpArmatures (Java),
|
||||||
|
// not here ; this counter only reflects datapack JSONs from
|
||||||
|
// data/<ns>/tiedup/armatures/.
|
||||||
|
TiedUpRigConstants.LOGGER.info(
|
||||||
|
"[ArmatureReloadListener] Datapack armature reload : {} custom armature(s) "
|
||||||
|
+ "registered ({} skipped, builtin BIPED active)",
|
||||||
|
loaded, skipped
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse un JsonElement en {@link ArmatureDefinition}. Ne valide pas la
|
||||||
|
* coherence structurelle (c'est le role de
|
||||||
|
* {@link ArmatureDefinition#validate()}) — se contente de mapper le JSON
|
||||||
|
* brut sur la structure Java.
|
||||||
|
*
|
||||||
|
* @throws JsonStructureException si le JSON est mal forme (champ manquant,
|
||||||
|
* type incorrect, taille de tableau fausse)
|
||||||
|
*/
|
||||||
|
private static ArmatureDefinition parseDefinition(ResourceLocation id, JsonElement element) {
|
||||||
|
if (!element.isJsonObject()) {
|
||||||
|
throw new JsonStructureException(
|
||||||
|
"top-level JSON is not an object (got " + element.getClass().getSimpleName() + ")"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
JsonObject obj = element.getAsJsonObject();
|
||||||
|
|
||||||
|
String description = readOptionalString(obj, "description", "");
|
||||||
|
|
||||||
|
String rootJointName = readRequiredString(obj, "root_joint");
|
||||||
|
|
||||||
|
if (!obj.has("joints") || !obj.get("joints").isJsonObject()) {
|
||||||
|
throw new JsonStructureException("missing or non-object 'joints'");
|
||||||
|
}
|
||||||
|
JsonObject jointsObj = obj.getAsJsonObject("joints");
|
||||||
|
|
||||||
|
Map<String, JointDefinition> joints = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, JsonElement> e : jointsObj.entrySet()) {
|
||||||
|
String name = e.getKey();
|
||||||
|
if (!e.getValue().isJsonObject()) {
|
||||||
|
throw new JsonStructureException(
|
||||||
|
"joint '" + name + "' is not a JSON object"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
joints.put(name, parseJoint(name, e.getValue().getAsJsonObject()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ArmatureDefinition(id, description, rootJointName, joints);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JointDefinition parseJoint(String name, JsonObject obj) {
|
||||||
|
if (!obj.has("id")) {
|
||||||
|
throw new JsonStructureException("joint '" + name + "' missing 'id'");
|
||||||
|
}
|
||||||
|
int jointId = obj.get("id").getAsInt();
|
||||||
|
|
||||||
|
String parent = null;
|
||||||
|
if (obj.has("parent") && !obj.get("parent").isJsonNull()) {
|
||||||
|
parent = obj.get("parent").getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3f translation = readVec3(obj, "translation", "joint '" + name + "'");
|
||||||
|
Quaternionf rotation = readQuaternion(obj, "rotation", "joint '" + name + "'");
|
||||||
|
|
||||||
|
List<String> children = new ArrayList<>();
|
||||||
|
if (obj.has("children")) {
|
||||||
|
JsonElement childrenEl = obj.get("children");
|
||||||
|
if (!childrenEl.isJsonArray()) {
|
||||||
|
throw new JsonStructureException(
|
||||||
|
"joint '" + name + "'.children is not a JSON array"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (JsonElement c : childrenEl.getAsJsonArray()) {
|
||||||
|
children.add(c.getAsString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JointDefinition(name, jointId, parent, translation, rotation, children);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Vec3f readVec3(JsonObject obj, String key, String context) {
|
||||||
|
if (!obj.has(key)) {
|
||||||
|
throw new JsonStructureException(context + " missing '" + key + "'");
|
||||||
|
}
|
||||||
|
JsonElement el = obj.get(key);
|
||||||
|
if (!el.isJsonArray()) {
|
||||||
|
throw new JsonStructureException(
|
||||||
|
context + "." + key + " is not a JSON array"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
JsonArray arr = el.getAsJsonArray();
|
||||||
|
if (arr.size() != 3) {
|
||||||
|
throw new JsonStructureException(
|
||||||
|
context + "." + key + " must have 3 elements, got " + arr.size()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Vec3f(arr.get(0).getAsFloat(), arr.get(1).getAsFloat(), arr.get(2).getAsFloat());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Quaternionf readQuaternion(JsonObject obj, String key, String context) {
|
||||||
|
if (!obj.has(key)) {
|
||||||
|
throw new JsonStructureException(context + " missing '" + key + "'");
|
||||||
|
}
|
||||||
|
JsonElement el = obj.get(key);
|
||||||
|
if (!el.isJsonArray()) {
|
||||||
|
throw new JsonStructureException(
|
||||||
|
context + "." + key + " is not a JSON array"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
JsonArray arr = el.getAsJsonArray();
|
||||||
|
if (arr.size() != 4) {
|
||||||
|
throw new JsonStructureException(
|
||||||
|
context + "." + key + " must have 4 elements (xyzw), got " + arr.size()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// xyzw convention — JOML default. Identity = [0, 0, 0, 1].
|
||||||
|
return new Quaternionf(
|
||||||
|
arr.get(0).getAsFloat(),
|
||||||
|
arr.get(1).getAsFloat(),
|
||||||
|
arr.get(2).getAsFloat(),
|
||||||
|
arr.get(3).getAsFloat()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readRequiredString(JsonObject obj, String key) {
|
||||||
|
if (!obj.has(key) || obj.get(key).isJsonNull()) {
|
||||||
|
throw new JsonStructureException("missing required string field '" + key + "'");
|
||||||
|
}
|
||||||
|
JsonElement el = obj.get(key);
|
||||||
|
if (!el.isJsonPrimitive() || !el.getAsJsonPrimitive().isString()) {
|
||||||
|
throw new JsonStructureException("field '" + key + "' is not a string");
|
||||||
|
}
|
||||||
|
return el.getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readOptionalString(JsonObject obj, String key, String fallback) {
|
||||||
|
if (!obj.has(key) || obj.get(key).isJsonNull()) return fallback;
|
||||||
|
JsonElement el = obj.get(key);
|
||||||
|
if (!el.isJsonPrimitive() || !el.getAsJsonPrimitive().isString()) return fallback;
|
||||||
|
return el.getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown par le parseur JSON — indique un probleme structurel
|
||||||
|
* dans le fichier (champ manquant, type incorrect). Pas exposee publiquement,
|
||||||
|
* capturee dans {@link #apply} pour log et continuer.
|
||||||
|
*/
|
||||||
|
private static final class JsonStructureException extends RuntimeException {
|
||||||
|
JsonStructureException(String message) {
|
||||||
|
super(Objects.requireNonNull(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.armature.types;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.armature.Joint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is not being used by Epic Fight, but is left to meet various purposes of developers
|
||||||
|
* Also presents developers which joints are necessary when an armature would be Human-like
|
||||||
|
*/
|
||||||
|
public interface HumanLikeArmature extends ToolHolderArmature {
|
||||||
|
public Joint leftHandJoint();
|
||||||
|
public Joint rightHandJoint();
|
||||||
|
public Joint leftArmJoint();
|
||||||
|
public Joint rightArmJoint();
|
||||||
|
public Joint leftLegJoint();
|
||||||
|
public Joint rightLegJoint();
|
||||||
|
public Joint leftThighJoint();
|
||||||
|
public Joint rightThighJoint();
|
||||||
|
public Joint headJoint();
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.armature.types;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.armature.Joint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface pour armatures portant un outil (main gauche/droite + dos).
|
||||||
|
* TiedUp gardera cette convention pour maintenir la compat avec les JSON EF
|
||||||
|
* (rig équivalent : Tool_R, Tool_L, Tool_Back). Dans les faits, dans un
|
||||||
|
* contexte bondage, "outil" = menottes, laisse, cage, etc.
|
||||||
|
*/
|
||||||
|
public interface ToolHolderArmature {
|
||||||
|
Joint leftToolJoint();
|
||||||
|
Joint rightToolJoint();
|
||||||
|
Joint backToolJoint();
|
||||||
|
}
|
||||||
69
src/main/java/com/tiedup/remake/rig/asset/AssetAccessor.java
Normal file
69
src/main/java/com/tiedup/remake/rig/asset/AssetAccessor.java
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.asset;
|
||||||
|
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An accessor class
|
||||||
|
* @param <O> {@link Object} can be any object
|
||||||
|
*/
|
||||||
|
public interface AssetAccessor<O> extends Supplier<O> {
|
||||||
|
O get();
|
||||||
|
|
||||||
|
ResourceLocation registryName();
|
||||||
|
|
||||||
|
default boolean isPresent() {
|
||||||
|
return this.get() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean isEmpty() {
|
||||||
|
return !this.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean inRegistry();
|
||||||
|
|
||||||
|
default boolean checkType(Class<?> cls) {
|
||||||
|
return cls.isAssignableFrom(this.get().getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
default O orElse(O whenNull) {
|
||||||
|
return this.isPresent() ? this.get() : whenNull;
|
||||||
|
}
|
||||||
|
|
||||||
|
default void ifPresent(Consumer<O> action) {
|
||||||
|
if (this.isPresent()) {
|
||||||
|
action.accept(this.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default void ifPresentOrElse(Consumer<O> action, Runnable whenNull) {
|
||||||
|
if (this.isPresent()) {
|
||||||
|
action.accept(this.get());
|
||||||
|
} else {
|
||||||
|
whenNull.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default void doOrThrow(Consumer<O> action) {
|
||||||
|
if (this.isPresent()) {
|
||||||
|
action.accept(this.get());
|
||||||
|
} else {
|
||||||
|
throw new NoSuchElementException("No asset " + this.registryName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default void checkNotNull() {
|
||||||
|
if (!this.isPresent()) {
|
||||||
|
throw new NoSuchElementException("No asset " + this.registryName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
718
src/main/java/com/tiedup/remake/rig/asset/JsonAssetLoader.java
Normal file
718
src/main/java/com/tiedup/remake/rig/asset/JsonAssetLoader.java
Normal file
@@ -0,0 +1,718 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.asset;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.NoSuchElementException;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParseException;
|
||||||
|
import com.google.gson.internal.Streams;
|
||||||
|
import com.google.gson.stream.JsonReader;
|
||||||
|
|
||||||
|
import io.netty.util.internal.StringUtil;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraft.server.packs.resources.Resource;
|
||||||
|
import net.minecraft.server.packs.resources.ResourceManager;
|
||||||
|
import net.minecraft.util.GsonHelper;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
|
import net.minecraftforge.fml.ModList;
|
||||||
|
import net.minecraftforge.fml.loading.FMLEnvironment;
|
||||||
|
import com.tiedup.remake.rig.anim.AnimationClip;
|
||||||
|
import com.tiedup.remake.rig.armature.Joint;
|
||||||
|
import com.tiedup.remake.rig.armature.JointTransform;
|
||||||
|
import com.tiedup.remake.rig.anim.Keyframe;
|
||||||
|
import com.tiedup.remake.rig.anim.TransformSheet;
|
||||||
|
import com.tiedup.remake.rig.anim.property.AnimationProperty.ActionAnimationProperty;
|
||||||
|
import com.tiedup.remake.rig.anim.types.ActionAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.AttackAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.AttackAnimation.Phase;
|
||||||
|
import com.tiedup.remake.rig.anim.types.MainFrameAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.mesh.ClassicMesh;
|
||||||
|
import com.tiedup.remake.rig.mesh.CompositeMesh;
|
||||||
|
import com.tiedup.remake.rig.mesh.Mesh;
|
||||||
|
import com.tiedup.remake.rig.mesh.MeshPartDefinition;
|
||||||
|
import com.tiedup.remake.rig.mesh.Meshes;
|
||||||
|
import com.tiedup.remake.rig.mesh.Meshes.MeshContructor;
|
||||||
|
import com.tiedup.remake.rig.mesh.SkinnedMesh;
|
||||||
|
import com.tiedup.remake.rig.mesh.SoftBodyTranslatable;
|
||||||
|
import com.tiedup.remake.rig.mesh.StaticMesh;
|
||||||
|
import com.tiedup.remake.rig.mesh.VertexBuilder;
|
||||||
|
import com.tiedup.remake.rig.mesh.transformer.VanillaModelTransformer.VanillaMeshPartDefinition;
|
||||||
|
import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObject.ClothPart.ConstraintType;
|
||||||
|
import com.tiedup.remake.rig.exception.AssetLoadingException;
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
import com.tiedup.remake.rig.util.ParseUtil;
|
||||||
|
import com.tiedup.remake.rig.math.MathUtils;
|
||||||
|
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||||
|
import com.tiedup.remake.rig.math.Vec3f;
|
||||||
|
import com.tiedup.remake.rig.math.Vec4f;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
|
||||||
|
public class JsonAssetLoader {
|
||||||
|
public static final OpenMatrix4f BLENDER_TO_MINECRAFT_COORD = OpenMatrix4f.createRotatorDeg(-90.0F, Vec3f.X_AXIS);
|
||||||
|
public static final OpenMatrix4f MINECRAFT_TO_BLENDER_COORD = OpenMatrix4f.invert(BLENDER_TO_MINECRAFT_COORD, null);
|
||||||
|
public static final String UNGROUPED_NAME = "noGroups";
|
||||||
|
public static final String COORD_BONE = "Coord";
|
||||||
|
public static final String ROOT_BONE = "Root";
|
||||||
|
|
||||||
|
private JsonObject rootJson;
|
||||||
|
|
||||||
|
// Used for deciding armature name, other resources are nullable
|
||||||
|
@Nullable
|
||||||
|
private ResourceLocation resourceLocation;
|
||||||
|
private String filehash;
|
||||||
|
|
||||||
|
public JsonAssetLoader(ResourceManager resourceManager, ResourceLocation resourceLocation) throws AssetLoadingException {
|
||||||
|
JsonReader jsonReader = null;
|
||||||
|
this.resourceLocation = resourceLocation;
|
||||||
|
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
if (resourceManager == null) {
|
||||||
|
throw new NoSuchElementException();
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource resource = resourceManager.getResource(resourceLocation).orElseThrow();
|
||||||
|
InputStream inputStream = resource.open();
|
||||||
|
InputStreamReader isr = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
jsonReader = new JsonReader(isr);
|
||||||
|
jsonReader.setLenient(true);
|
||||||
|
this.rootJson = Streams.parse(jsonReader).getAsJsonObject();
|
||||||
|
} catch (NoSuchElementException e) {
|
||||||
|
// In this case, reads the animation data from mod.jar (Especially in a server)
|
||||||
|
Class<?> modClass = ModList.get().getModObjectById(resourceLocation.getNamespace()).orElseThrow(() -> new AssetLoadingException("No modid " + resourceLocation)).getClass();
|
||||||
|
InputStream inputStream = modClass.getResourceAsStream("/assets/" + resourceLocation.getNamespace() + "/" + resourceLocation.getPath());
|
||||||
|
|
||||||
|
if (inputStream == null) {
|
||||||
|
modClass = ModList.get().getModObjectById(TiedUpRigConstants.MODID).get().getClass();
|
||||||
|
inputStream = modClass.getResourceAsStream("/assets/" + resourceLocation.getNamespace() + "/" + resourceLocation.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
//Still null, throws exception.
|
||||||
|
if (inputStream == null) {
|
||||||
|
throw new AssetLoadingException("Can't find resource file: " + resourceLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
|
||||||
|
InputStreamReader reader = new InputStreamReader(bufferedInputStream, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
jsonReader = new JsonReader(reader);
|
||||||
|
jsonReader.setLenient(true);
|
||||||
|
this.rootJson = Streams.parse(jsonReader).getAsJsonObject();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssetLoadingException("Can't read " + resourceLocation.toString() + " because of " + e);
|
||||||
|
} finally {
|
||||||
|
if (jsonReader != null) {
|
||||||
|
try {
|
||||||
|
jsonReader.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filehash = ParseUtil.getBytesSHA256Hash(this.rootJson.toString().getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public JsonAssetLoader(InputStream inputstream, ResourceLocation resourceLocation) throws AssetLoadingException {
|
||||||
|
JsonReader jsonReader = null;
|
||||||
|
this.resourceLocation = resourceLocation;
|
||||||
|
|
||||||
|
jsonReader = new JsonReader(new InputStreamReader(inputstream, StandardCharsets.UTF_8));
|
||||||
|
jsonReader.setLenient(true);
|
||||||
|
this.rootJson = Streams.parse(jsonReader).getAsJsonObject();
|
||||||
|
|
||||||
|
try {
|
||||||
|
jsonReader.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AssetLoadingException("Can't read " + resourceLocation.toString() + ": " + e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filehash = StringUtil.EMPTY_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public JsonAssetLoader(JsonObject rootJson, ResourceLocation rl) {
|
||||||
|
this.rootJson = rootJson;
|
||||||
|
this.resourceLocation = rl;
|
||||||
|
this.filehash = StringUtil.EMPTY_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public static Mesh.RenderProperties getRenderProperties(JsonObject json) {
|
||||||
|
if (!json.has("render_properties")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject properties = json.getAsJsonObject("render_properties");
|
||||||
|
Mesh.RenderProperties.Builder renderProperties = Mesh.RenderProperties.Builder.create();
|
||||||
|
|
||||||
|
if (properties.has("transparent")) {
|
||||||
|
renderProperties.transparency(properties.get("transparent").getAsBoolean());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (properties.has("texture_path")) {
|
||||||
|
renderProperties.customTexturePath(properties.get("texture_path").getAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (properties.has("color")) {
|
||||||
|
JsonArray jsonarray = properties.getAsJsonArray("color");
|
||||||
|
renderProperties.customColor(jsonarray.get(0).getAsFloat(), jsonarray.get(1).getAsFloat(), jsonarray.get(2).getAsFloat());
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderProperties.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public ResourceLocation getParent() {
|
||||||
|
return this.rootJson.has("parent") ? ResourceLocation.parse(this.rootJson.get("parent").getAsString()) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final float DEFAULT_PARTICLE_MASS = 0.16F;
|
||||||
|
private static final float DEFAULT_SELF_COLLISON = 0.05F;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public Map<String, SoftBodyTranslatable.ClothSimulationInfo> loadClothInformation(Float[] positionArray) {
|
||||||
|
JsonObject obj = this.rootJson.getAsJsonObject("vertices");
|
||||||
|
JsonObject clothInfoObj = obj.getAsJsonObject("cloth_info");
|
||||||
|
|
||||||
|
if (clothInfoObj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, SoftBodyTranslatable.ClothSimulationInfo> clothInfo = Maps.newHashMap();
|
||||||
|
|
||||||
|
for (Map.Entry<String, JsonElement> e : clothInfoObj.entrySet()) {
|
||||||
|
JsonObject clothObject = e.getValue().getAsJsonObject();
|
||||||
|
int[] particlesArray = ParseUtil.toIntArrayPrimitive(clothObject.get("particles").getAsJsonObject().get("array").getAsJsonArray());
|
||||||
|
float[] weightsArray = ParseUtil.toFloatArrayPrimitive(clothObject.get("weights").getAsJsonObject().get("array").getAsJsonArray());
|
||||||
|
float particleMass = clothObject.has("particle_mass") ? clothObject.get("particle_mass").getAsFloat() : DEFAULT_PARTICLE_MASS;
|
||||||
|
float selfCollision = clothObject.has("self_collision") ? clothObject.get("self_collision").getAsFloat() : DEFAULT_SELF_COLLISON;
|
||||||
|
|
||||||
|
JsonArray constraintsArray = clothObject.get("constraints").getAsJsonArray();
|
||||||
|
List<int[]> constraintsList = new ArrayList<> (constraintsArray.size());
|
||||||
|
float[] compliances = new float[constraintsArray.size()];
|
||||||
|
ConstraintType[] constraintType = new ConstraintType[constraintsArray.size()];
|
||||||
|
float[] rootDistances = new float[particlesArray.length / 2];
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
|
||||||
|
for (JsonElement element : constraintsArray) {
|
||||||
|
JsonObject asJsonObject = element.getAsJsonObject();
|
||||||
|
|
||||||
|
if (asJsonObject.has("unused") && GsonHelper.getAsBoolean(asJsonObject, "unused")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
constraintType[i] = ConstraintType.valueOf(GsonHelper.getAsString(asJsonObject, "type").toUpperCase(Locale.ROOT));
|
||||||
|
compliances[i] = GsonHelper.getAsFloat(asJsonObject, "compliance");
|
||||||
|
constraintsList.add(ParseUtil.toIntArrayPrimitive(asJsonObject.get("array").getAsJsonArray()));
|
||||||
|
element.getAsJsonObject().get("compliance");
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Vec3> rootParticles = Lists.newArrayList();
|
||||||
|
|
||||||
|
for (int j = 0; j < particlesArray.length / 2; j++) {
|
||||||
|
int weightIndex = particlesArray[j * 2 + 1];
|
||||||
|
float weight = weightsArray[weightIndex];
|
||||||
|
|
||||||
|
if (weight == 0.0F) {
|
||||||
|
int posId = particlesArray[j * 2];
|
||||||
|
rootParticles.add(new Vec3(positionArray[posId * 3 + 0], positionArray[posId * 3 + 1], positionArray[posId * 3 + 2]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int j = 0; j < particlesArray.length / 2; j++) {
|
||||||
|
int posId = particlesArray[j * 2];
|
||||||
|
Vec3 position = new Vec3(positionArray[posId * 3 + 0], positionArray[posId * 3 + 1], positionArray[posId * 3 + 2]);
|
||||||
|
Vec3 nearest = MathUtils.getNearestVector(position, rootParticles);
|
||||||
|
rootDistances[j] = (float)position.distanceTo(nearest);
|
||||||
|
}
|
||||||
|
|
||||||
|
int[] normalOffsetMappingArray = null;
|
||||||
|
|
||||||
|
if (clothObject.has("normal_offsets")) {
|
||||||
|
normalOffsetMappingArray = ParseUtil.toIntArrayPrimitive(clothObject.get("normal_offsets").getAsJsonObject().get("array").getAsJsonArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
SoftBodyTranslatable.ClothSimulationInfo clothSimulInfo = new SoftBodyTranslatable.ClothSimulationInfo(particleMass, selfCollision, constraintsList, constraintType, compliances, particlesArray, weightsArray, rootDistances, normalOffsetMappingArray);
|
||||||
|
clothInfo.put(e.getKey(), clothSimulInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clothInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public <T extends ClassicMesh> T loadClassicMesh(MeshContructor<ClassicMesh.ClassicMeshPart, VertexBuilder, T> constructor) {
|
||||||
|
ResourceLocation parent = this.getParent();
|
||||||
|
|
||||||
|
if (parent != null) {
|
||||||
|
T mesh = Meshes.getOrCreate(parent, (jsonLoader) -> jsonLoader.loadClassicMesh(constructor)).get();
|
||||||
|
return constructor.invoke(null, null, mesh, getRenderProperties(this.rootJson));
|
||||||
|
} else {
|
||||||
|
JsonObject obj = this.rootJson.getAsJsonObject("vertices");
|
||||||
|
JsonObject positions = obj.getAsJsonObject("positions");
|
||||||
|
JsonObject normals = obj.getAsJsonObject("normals");
|
||||||
|
JsonObject uvs = obj.getAsJsonObject("uvs");
|
||||||
|
JsonObject parts = obj.getAsJsonObject("parts");
|
||||||
|
JsonObject indices = obj.getAsJsonObject("indices");
|
||||||
|
Float[] positionArray = ParseUtil.toFloatArray(positions.get("array").getAsJsonArray());
|
||||||
|
|
||||||
|
for (int i = 0; i < positionArray.length / 3; i++) {
|
||||||
|
int k = i * 3;
|
||||||
|
Vec4f posVector = new Vec4f(positionArray[k], positionArray[k+1], positionArray[k+2], 1.0F);
|
||||||
|
OpenMatrix4f.transform(BLENDER_TO_MINECRAFT_COORD, posVector, posVector);
|
||||||
|
positionArray[k] = posVector.x;
|
||||||
|
positionArray[k+1] = posVector.y;
|
||||||
|
positionArray[k+2] = posVector.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
Float[] normalArray = ParseUtil.toFloatArray(normals.get("array").getAsJsonArray());
|
||||||
|
|
||||||
|
for (int i = 0; i < normalArray.length / 3; i++) {
|
||||||
|
int k = i * 3;
|
||||||
|
Vec4f normVector = new Vec4f(normalArray[k], normalArray[k+1], normalArray[k+2], 1.0F);
|
||||||
|
OpenMatrix4f.transform(BLENDER_TO_MINECRAFT_COORD, normVector, normVector);
|
||||||
|
normalArray[k] = normVector.x;
|
||||||
|
normalArray[k+1] = normVector.y;
|
||||||
|
normalArray[k+2] = normVector.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
Float[] uvArray = ParseUtil.toFloatArray(uvs.get("array").getAsJsonArray());
|
||||||
|
|
||||||
|
Map<String, Number[]> arrayMap = Maps.newHashMap();
|
||||||
|
Map<MeshPartDefinition, List<VertexBuilder>> meshMap = Maps.newHashMap();
|
||||||
|
|
||||||
|
arrayMap.put("positions", positionArray);
|
||||||
|
arrayMap.put("normals", normalArray);
|
||||||
|
arrayMap.put("uvs", uvArray);
|
||||||
|
|
||||||
|
if (parts != null) {
|
||||||
|
for (Map.Entry<String, JsonElement> e : parts.entrySet()) {
|
||||||
|
meshMap.put(VanillaMeshPartDefinition.of(e.getKey(), getRenderProperties(e.getValue().getAsJsonObject())), VertexBuilder.create(ParseUtil.toIntArrayPrimitive(e.getValue().getAsJsonObject().get("array").getAsJsonArray())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indices != null) {
|
||||||
|
meshMap.put(VanillaMeshPartDefinition.of(UNGROUPED_NAME), VertexBuilder.create(ParseUtil.toIntArrayPrimitive(indices.get("array").getAsJsonArray())));
|
||||||
|
}
|
||||||
|
|
||||||
|
T mesh = constructor.invoke(arrayMap, meshMap, null, getRenderProperties(this.rootJson));
|
||||||
|
mesh.putSoftBodySimulationInfo(this.loadClothInformation(positionArray));
|
||||||
|
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public <T extends SkinnedMesh> T loadSkinnedMesh(MeshContructor<SkinnedMesh.SkinnedMeshPart, VertexBuilder, T> constructor) {
|
||||||
|
ResourceLocation parent = this.getParent();
|
||||||
|
|
||||||
|
if (parent != null) {
|
||||||
|
T mesh = Meshes.getOrCreate(parent, (jsonLoader) -> jsonLoader.loadSkinnedMesh(constructor)).get();
|
||||||
|
return constructor.invoke(null, null, mesh, getRenderProperties(this.rootJson));
|
||||||
|
} else {
|
||||||
|
JsonObject obj = this.rootJson.getAsJsonObject("vertices");
|
||||||
|
JsonObject positions = obj.getAsJsonObject("positions");
|
||||||
|
JsonObject normals = obj.getAsJsonObject("normals");
|
||||||
|
JsonObject uvs = obj.getAsJsonObject("uvs");
|
||||||
|
JsonObject vdincies = obj.getAsJsonObject("vindices");
|
||||||
|
JsonObject weights = obj.getAsJsonObject("weights");
|
||||||
|
JsonObject vcounts = obj.getAsJsonObject("vcounts");
|
||||||
|
JsonObject parts = obj.getAsJsonObject("parts");
|
||||||
|
JsonObject indices = obj.getAsJsonObject("indices");
|
||||||
|
|
||||||
|
Float[] positionArray = ParseUtil.toFloatArray(positions.get("array").getAsJsonArray());
|
||||||
|
|
||||||
|
for (int i = 0; i < positionArray.length / 3; i++) {
|
||||||
|
int k = i * 3;
|
||||||
|
Vec4f posVector = new Vec4f(positionArray[k], positionArray[k+1], positionArray[k+2], 1.0F);
|
||||||
|
OpenMatrix4f.transform(BLENDER_TO_MINECRAFT_COORD, posVector, posVector);
|
||||||
|
positionArray[k] = posVector.x;
|
||||||
|
positionArray[k+1] = posVector.y;
|
||||||
|
positionArray[k+2] = posVector.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
Float[] normalArray = ParseUtil.toFloatArray(normals.get("array").getAsJsonArray());
|
||||||
|
|
||||||
|
for (int i = 0; i < normalArray.length / 3; i++) {
|
||||||
|
int k = i * 3;
|
||||||
|
Vec4f normVector = new Vec4f(normalArray[k], normalArray[k+1], normalArray[k+2], 1.0F);
|
||||||
|
OpenMatrix4f.transform(BLENDER_TO_MINECRAFT_COORD, normVector, normVector);
|
||||||
|
normalArray[k] = normVector.x;
|
||||||
|
normalArray[k+1] = normVector.y;
|
||||||
|
normalArray[k+2] = normVector.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
Float[] uvArray = ParseUtil.toFloatArray(uvs.get("array").getAsJsonArray());
|
||||||
|
Float[] weightArray = ParseUtil.toFloatArray(weights.get("array").getAsJsonArray());
|
||||||
|
Integer[] affectingJointCounts = ParseUtil.toIntArray(vcounts.get("array").getAsJsonArray());
|
||||||
|
Integer[] affectingJointIndices = ParseUtil.toIntArray(vdincies.get("array").getAsJsonArray());
|
||||||
|
|
||||||
|
Map<String, Number[]> arrayMap = Maps.newHashMap();
|
||||||
|
Map<MeshPartDefinition, List<VertexBuilder>> meshMap = Maps.newHashMap();
|
||||||
|
arrayMap.put("positions", positionArray);
|
||||||
|
arrayMap.put("normals", normalArray);
|
||||||
|
arrayMap.put("uvs", uvArray);
|
||||||
|
arrayMap.put("weights", weightArray);
|
||||||
|
arrayMap.put("vcounts", affectingJointCounts);
|
||||||
|
arrayMap.put("vindices", affectingJointIndices);
|
||||||
|
|
||||||
|
if (parts != null) {
|
||||||
|
for (Map.Entry<String, JsonElement> e : parts.entrySet()) {
|
||||||
|
meshMap.put(VanillaMeshPartDefinition.of(e.getKey(), getRenderProperties(e.getValue().getAsJsonObject())), VertexBuilder.create(ParseUtil.toIntArrayPrimitive(e.getValue().getAsJsonObject().get("array").getAsJsonArray())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indices != null) {
|
||||||
|
meshMap.put(VanillaMeshPartDefinition.of(UNGROUPED_NAME), VertexBuilder.create(ParseUtil.toIntArrayPrimitive(indices.get("array").getAsJsonArray())));
|
||||||
|
}
|
||||||
|
|
||||||
|
T mesh = constructor.invoke(arrayMap, meshMap, null, getRenderProperties(this.rootJson));
|
||||||
|
mesh.putSoftBodySimulationInfo(this.loadClothInformation(positionArray));
|
||||||
|
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public CompositeMesh loadCompositeMesh() throws AssetLoadingException {
|
||||||
|
if (!this.rootJson.has("meshes")) {
|
||||||
|
throw new AssetLoadingException("Composite mesh loading exception: lower meshes undefined");
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonAssetLoader clothLoader = new JsonAssetLoader(this.rootJson.get("meshes").getAsJsonObject().get("cloth").getAsJsonObject(), null);
|
||||||
|
JsonAssetLoader staticLoader = new JsonAssetLoader(this.rootJson.get("meshes").getAsJsonObject().get("static").getAsJsonObject(), null);
|
||||||
|
SoftBodyTranslatable softBodyMesh = (SoftBodyTranslatable)clothLoader.loadMesh(false);
|
||||||
|
StaticMesh<?> staticMesh = (StaticMesh<?>)staticLoader.loadMesh(false);
|
||||||
|
|
||||||
|
if (!softBodyMesh.canStartSoftBodySimulation()) {
|
||||||
|
throw new AssetLoadingException("Composite mesh loading exception: soft mesh doesn't have cloth info");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CompositeMesh(staticMesh, softBodyMesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
public Mesh loadMesh() throws AssetLoadingException {
|
||||||
|
return this.loadMesh(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
private Mesh loadMesh(boolean allowCompositeMesh) throws AssetLoadingException {
|
||||||
|
if (!this.rootJson.has("mesh_loader")) {
|
||||||
|
throw new AssetLoadingException("Mesh loading exception: No mesh loader provided!");
|
||||||
|
}
|
||||||
|
|
||||||
|
String loader = this.rootJson.get("mesh_loader").getAsString();
|
||||||
|
|
||||||
|
switch (loader) {
|
||||||
|
case "classic_mesh" -> {
|
||||||
|
return this.loadClassicMesh(ClassicMesh::new);
|
||||||
|
}
|
||||||
|
case "skinned_mesh" -> {
|
||||||
|
return this.loadSkinnedMesh(SkinnedMesh::new);
|
||||||
|
}
|
||||||
|
case "composite_mesh" -> {
|
||||||
|
if (!allowCompositeMesh) {
|
||||||
|
throw new AssetLoadingException("Can't have a composite mesh inside another composite mesh");
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.loadCompositeMesh();
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
throw new AssetLoadingException("Mesh loading exception: Unsupported mesh loader: " + loader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnimationClip loadClipForAnimation(StaticAnimation animation) {
|
||||||
|
if (this.rootJson == null) {
|
||||||
|
throw new AssetLoadingException("Can't find animation in path: " + animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animation.getArmature() == null) {
|
||||||
|
TiedUpRigConstants.LOGGER.error("Animation " + animation + " doesn't have an armature.");
|
||||||
|
}
|
||||||
|
|
||||||
|
TransformFormat format = getAsTransformFormatOrDefault(this.rootJson, "format");
|
||||||
|
JsonArray array = this.rootJson.get("animation").getAsJsonArray();
|
||||||
|
boolean action = animation instanceof MainFrameAnimation;
|
||||||
|
boolean attack = animation instanceof AttackAnimation;
|
||||||
|
boolean noTransformData = !action && !attack && FMLEnvironment.dist == Dist.DEDICATED_SERVER;
|
||||||
|
boolean root = true;
|
||||||
|
Armature armature = animation.getArmature().get();
|
||||||
|
Set<String> allowedJoints = Sets.newLinkedHashSet();
|
||||||
|
|
||||||
|
if (attack) {
|
||||||
|
for (Phase phase : ((AttackAnimation)animation).phases) {
|
||||||
|
for (AttackAnimation.JointColliderPair colliderInfo : phase.getColliders()) {
|
||||||
|
armature.gatherAllJointsInPathToTerminal(colliderInfo.getFirst().getName(), allowedJoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (action) {
|
||||||
|
allowedJoints.add(ROOT_BONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimationClip clip = new AnimationClip();
|
||||||
|
|
||||||
|
for (JsonElement element : array) {
|
||||||
|
JsonObject jObject = element.getAsJsonObject();
|
||||||
|
String name = jObject.get("name").getAsString();
|
||||||
|
|
||||||
|
if (attack && FMLEnvironment.dist == Dist.DEDICATED_SERVER && !allowedJoints.contains(name)) {
|
||||||
|
if (name.equals(COORD_BONE)) {
|
||||||
|
root = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Joint joint = armature.searchJointByName(name);
|
||||||
|
|
||||||
|
if (joint == null) {
|
||||||
|
if (name.equals(COORD_BONE)) {
|
||||||
|
TransformSheet sheet = getTransformSheet(jObject, new OpenMatrix4f(), true, format);
|
||||||
|
|
||||||
|
if (action) {
|
||||||
|
((ActionAnimation)animation).addProperty(ActionAnimationProperty.COORD, sheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
root = false;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
TiedUpRigConstants.LOGGER.debug("[EpicFightMod] No joint named " + name + " in " + animation);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TransformSheet sheet = getTransformSheet(jObject, OpenMatrix4f.invert(joint.getLocalTransform(), null), root, format);
|
||||||
|
|
||||||
|
if (!noTransformData) {
|
||||||
|
clip.addJointTransform(name, sheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
float maxFrameTime = sheet.maxFrameTime();
|
||||||
|
|
||||||
|
if (clip.getClipTime() < maxFrameTime) {
|
||||||
|
clip.setClipTime(maxFrameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
root = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnimationClip loadAllJointsClipForAnimation(StaticAnimation animation) {
|
||||||
|
TransformFormat format = getAsTransformFormatOrDefault(this.rootJson, "format");
|
||||||
|
JsonArray array = this.rootJson.get("animation").getAsJsonArray();
|
||||||
|
boolean root = true;
|
||||||
|
|
||||||
|
if (animation.getArmature() == null) {
|
||||||
|
TiedUpRigConstants.LOGGER.error("Animation " + animation + " doesn't have an armature.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Armature armature = animation.getArmature().get();
|
||||||
|
AnimationClip clip = new AnimationClip();
|
||||||
|
|
||||||
|
for (JsonElement element : array) {
|
||||||
|
JsonObject jObject = element.getAsJsonObject();
|
||||||
|
String name = jObject.get("name").getAsString();
|
||||||
|
Joint joint = armature.searchJointByName(name);
|
||||||
|
|
||||||
|
if (joint == null) {
|
||||||
|
if (TiedUpRigConstants.IS_DEV_ENV) {
|
||||||
|
TiedUpRigConstants.LOGGER.debug(animation.getRegistryName() + ": No joint named " + name + " in armature");
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransformSheet sheet = getTransformSheet(jObject, OpenMatrix4f.invert(joint.getLocalTransform(), null), root, format);
|
||||||
|
clip.addJointTransform(name, sheet);
|
||||||
|
float maxFrameTime = sheet.maxFrameTime();
|
||||||
|
|
||||||
|
if (clip.getClipTime() < maxFrameTime) {
|
||||||
|
clip.setClipTime(maxFrameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
root = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObject getRootJson() {
|
||||||
|
return this.rootJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFileHash() {
|
||||||
|
return this.filehash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TransformFormat getAsTransformFormatOrDefault(JsonObject jsonObject, String propertyName) {
|
||||||
|
return jsonObject.has(propertyName) ? ParseUtil.enumValueOfOrNull(TransformFormat.class, GsonHelper.getAsString(jsonObject, propertyName)) : TransformFormat.MATRIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AnimationClip loadAnimationClip(Armature armature) {
|
||||||
|
TransformFormat format = getAsTransformFormatOrDefault(this.rootJson, "format");
|
||||||
|
JsonArray array = this.rootJson.get("animation").getAsJsonArray();
|
||||||
|
AnimationClip clip = new AnimationClip();
|
||||||
|
boolean root = true;
|
||||||
|
|
||||||
|
for (JsonElement element : array) {
|
||||||
|
JsonObject jObject = element.getAsJsonObject();
|
||||||
|
String name = jObject.get("name").getAsString();
|
||||||
|
Joint joint = armature.searchJointByName(name);
|
||||||
|
|
||||||
|
if (joint == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransformSheet sheet = getTransformSheet(element.getAsJsonObject(), OpenMatrix4f.invert(joint.getLocalTransform(), null), root, format);
|
||||||
|
clip.addJointTransform(name, sheet);
|
||||||
|
float maxFrameTime = sheet.maxFrameTime();
|
||||||
|
|
||||||
|
if (clip.getClipTime() < maxFrameTime) {
|
||||||
|
clip.setClipTime(maxFrameTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
root = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param jObject
|
||||||
|
* @param invLocalTransform nullable if transformFormat == {@link TransformFormat#ATTRIBUTES}
|
||||||
|
* @param rootCorrection no matter what the value is if transformFormat == {@link TransformFormat#ATTRIBUTES}
|
||||||
|
* @param transformFormat
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static TransformSheet getTransformSheet(JsonObject jObject, @Nullable OpenMatrix4f invLocalTransform, boolean rootCorrection, TransformFormat transformFormat) throws AssetLoadingException, JsonParseException {
|
||||||
|
JsonArray timeArray = jObject.getAsJsonArray("time");
|
||||||
|
JsonArray transformArray = jObject.getAsJsonArray("transform");
|
||||||
|
|
||||||
|
if (timeArray.size() != transformArray.size()) {
|
||||||
|
throw new AssetLoadingException(
|
||||||
|
"Can't read transform sheet: the size of timestamp and transform array is different."
|
||||||
|
+ "timestamp array size: " + timeArray.size() + ", transform array size: " + transformArray.size()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int timesCount = timeArray.size();
|
||||||
|
List<Keyframe> keyframeList = Lists.newArrayList();
|
||||||
|
|
||||||
|
for (int i = 0; i < timesCount; i++) {
|
||||||
|
float timeStamp = timeArray.get(i).getAsFloat();
|
||||||
|
|
||||||
|
if (timeStamp < 0.0F) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// WORKAROUND: The case when transform format is wrongly specified!
|
||||||
|
if (transformFormat == TransformFormat.ATTRIBUTES && transformArray.get(i).isJsonArray()) {
|
||||||
|
transformFormat = TransformFormat.MATRIX;
|
||||||
|
} else if (transformFormat == TransformFormat.MATRIX && transformArray.get(i).isJsonObject()) {
|
||||||
|
transformFormat = TransformFormat.ATTRIBUTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (transformFormat) {
|
||||||
|
case MATRIX -> {
|
||||||
|
JsonArray matrixArray = transformArray.get(i).getAsJsonArray();
|
||||||
|
float[] matrixElements = new float[16];
|
||||||
|
|
||||||
|
for (int j = 0; j < 16; j++) {
|
||||||
|
matrixElements[j] = matrixArray.get(j).getAsFloat();
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenMatrix4f matrix = OpenMatrix4f.load(null, matrixElements);
|
||||||
|
matrix.transpose();
|
||||||
|
|
||||||
|
if (rootCorrection) {
|
||||||
|
matrix.mulFront(BLENDER_TO_MINECRAFT_COORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
matrix.mulFront(invLocalTransform);
|
||||||
|
|
||||||
|
JointTransform transform = JointTransform.fromMatrix(matrix);
|
||||||
|
transform.rotation().normalize();
|
||||||
|
keyframeList.add(new Keyframe(timeStamp, transform));
|
||||||
|
}
|
||||||
|
case ATTRIBUTES -> {
|
||||||
|
JsonObject transformObject = transformArray.get(i).getAsJsonObject();
|
||||||
|
JsonArray locArray = transformObject.get("loc").getAsJsonArray();
|
||||||
|
JsonArray rotArray = transformObject.get("rot").getAsJsonArray();
|
||||||
|
JsonArray scaArray = transformObject.get("sca").getAsJsonArray();
|
||||||
|
JointTransform transform
|
||||||
|
= JointTransform.fromPrimitives(
|
||||||
|
locArray.get(0).getAsFloat()
|
||||||
|
, locArray.get(1).getAsFloat()
|
||||||
|
, locArray.get(2).getAsFloat()
|
||||||
|
, -rotArray.get(1).getAsFloat()
|
||||||
|
, -rotArray.get(2).getAsFloat()
|
||||||
|
, -rotArray.get(3).getAsFloat()
|
||||||
|
, rotArray.get(0).getAsFloat()
|
||||||
|
, scaArray.get(0).getAsFloat()
|
||||||
|
, scaArray.get(1).getAsFloat()
|
||||||
|
, scaArray.get(2).getAsFloat()
|
||||||
|
);
|
||||||
|
|
||||||
|
keyframeList.add(new Keyframe(timeStamp, transform));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TransformSheet sheet = new TransformSheet(keyframeList);
|
||||||
|
|
||||||
|
return sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines how the transform is expressed in json
|
||||||
|
*
|
||||||
|
* {@link TransformFormat#MATRIX} be like,
|
||||||
|
* [0, 1, 2, ..., 15]
|
||||||
|
*
|
||||||
|
* {@link TransformFormat#ATTRIBUTES} be like,
|
||||||
|
* {
|
||||||
|
* "loc": [0, 0, 0],
|
||||||
|
* "rot": [0, 0, 0, 1],
|
||||||
|
* "sca": [1, 1, 1],
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public enum TransformFormat {
|
||||||
|
MATRIX, ATTRIBUTES
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.bridge;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table d'alias runtime pour mapper les noms de joints des GLB legacy TiedUp
|
||||||
|
* (riggés via PlayerAnimator / bendy-lib avec noms type {@code leftUpperArm})
|
||||||
|
* vers le skeleton biped Epic Fight utilisé par RIG ({@code Arm_L}, etc.).
|
||||||
|
*
|
||||||
|
* <p>Voir {@code docs/plans/rig/ARCHITECTURE.md §6.3} pour la source de vérité
|
||||||
|
* du mapping. Tout joint inconnu après lookup doit être loggé WARN par le
|
||||||
|
* caller et fallback sur {@code Root}.</p>
|
||||||
|
*
|
||||||
|
* <p><b>Cas spécial "body/torso"</b> — le GLB legacy a souvent un unique joint
|
||||||
|
* couvrant l'ensemble du torse. On le mappe sur {@code Chest} par défaut
|
||||||
|
* (meilleur fit pour les items bondage majoritairement attachés au haut du
|
||||||
|
* corps : harnais, menottes de poitrine, collier). Si un item a besoin
|
||||||
|
* d'attachement à {@code Torso} (ceinture), le modeler devra renommer son
|
||||||
|
* joint en {@code waist} explicitement.</p>
|
||||||
|
*/
|
||||||
|
public final class GlbJointAliasTable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mapping direct PlayerAnimator → biped EF. Les clés sont la forme
|
||||||
|
* lowercase EXACTE des noms exportés par les GLB legacy.
|
||||||
|
*/
|
||||||
|
private static final Map<String, String> ALIAS = ImmutableMap.<String, String>builder()
|
||||||
|
// Torso region
|
||||||
|
.put("body", "Chest")
|
||||||
|
.put("torso", "Chest")
|
||||||
|
.put("chest", "Chest")
|
||||||
|
.put("waist", "Torso")
|
||||||
|
.put("hip", "Torso")
|
||||||
|
|
||||||
|
// Head
|
||||||
|
.put("head", "Head")
|
||||||
|
|
||||||
|
// Arms left
|
||||||
|
.put("leftshoulder", "Shoulder_L")
|
||||||
|
.put("leftupperarm", "Arm_L")
|
||||||
|
.put("leftarm", "Arm_L")
|
||||||
|
.put("leftlowerarm", "Elbow_L")
|
||||||
|
.put("leftforearm", "Elbow_L")
|
||||||
|
.put("leftelbow", "Elbow_L")
|
||||||
|
.put("lefthand", "Hand_L")
|
||||||
|
|
||||||
|
// Arms right
|
||||||
|
.put("rightshoulder", "Shoulder_R")
|
||||||
|
.put("rightupperarm", "Arm_R")
|
||||||
|
.put("rightarm", "Arm_R")
|
||||||
|
.put("rightlowerarm", "Elbow_R")
|
||||||
|
.put("rightforearm", "Elbow_R")
|
||||||
|
.put("rightelbow", "Elbow_R")
|
||||||
|
.put("righthand", "Hand_R")
|
||||||
|
|
||||||
|
// Legs left
|
||||||
|
.put("leftupperleg", "Thigh_L")
|
||||||
|
.put("leftleg", "Thigh_L")
|
||||||
|
.put("leftlowerleg", "Knee_L")
|
||||||
|
.put("leftknee", "Knee_L")
|
||||||
|
.put("leftfoot", "Leg_L")
|
||||||
|
|
||||||
|
// Legs right
|
||||||
|
.put("rightupperleg", "Thigh_R")
|
||||||
|
.put("rightleg", "Thigh_R")
|
||||||
|
.put("rightlowerleg", "Knee_R")
|
||||||
|
.put("rightknee", "Knee_R")
|
||||||
|
.put("rightfoot", "Leg_R")
|
||||||
|
|
||||||
|
// Root fallback (déjà nommé Root dans GLB modernes)
|
||||||
|
.put("root", "Root")
|
||||||
|
.put("armature", "Root")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
private GlbJointAliasTable() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traduit un nom de joint GLB legacy vers le nom biped EF équivalent.
|
||||||
|
* Case-insensitive. Les noms déjà au format biped EF (ex: {@code Arm_L}) sont
|
||||||
|
* retournés tels quels après vérification.
|
||||||
|
*
|
||||||
|
* @param gltfJointName nom tel qu'exporté dans le GLB (jointNames[])
|
||||||
|
* @return nom biped EF (ex: {@code Arm_L}), ou null si inconnu
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static String mapGltfJointName(String gltfJointName) {
|
||||||
|
if (gltfJointName == null || gltfJointName.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct hit sur le biped EF (GLB moderne déjà bien rigged).
|
||||||
|
if (isBipedJointName(gltfJointName)) {
|
||||||
|
return gltfJointName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ALIAS.get(gltfJointName.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un nom est déjà au format biped EF. Utilisé pour court-circuiter
|
||||||
|
* l'alias lookup sur les GLB modernes.
|
||||||
|
*/
|
||||||
|
public static boolean isBipedJointName(String name) {
|
||||||
|
// Heuristique : les noms biped EF sont en PascalCase avec suffixe _R/_L,
|
||||||
|
// ou parmi {Root, Torso, Chest, Head}.
|
||||||
|
return switch (name) {
|
||||||
|
case "Root", "Torso", "Chest", "Head" -> true;
|
||||||
|
default -> name.endsWith("_R") || name.endsWith("_L");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.bridge;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||||
|
import it.unimi.dsi.fastutil.ints.IntList;
|
||||||
|
|
||||||
|
import com.tiedup.remake.client.gltf.GltfData;
|
||||||
|
import com.tiedup.remake.client.gltf.GltfData.Primitive;
|
||||||
|
import com.tiedup.remake.rig.TiedUpRigConstants;
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
import com.tiedup.remake.rig.armature.Joint;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.math.Vec2f;
|
||||||
|
import com.tiedup.remake.rig.math.Vec3f;
|
||||||
|
import com.tiedup.remake.rig.mesh.MeshPartDefinition;
|
||||||
|
import com.tiedup.remake.rig.mesh.SingleGroupVertexBuilder;
|
||||||
|
import com.tiedup.remake.rig.mesh.SkinnedMesh;
|
||||||
|
import com.tiedup.remake.rig.mesh.transformer.VanillaModelTransformer.VanillaMeshPartDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pont Phase 1 : convertit un {@link GltfData} (format GLB legacy TiedUp
|
||||||
|
* riggé 11-joints PlayerAnimator) en {@link SkinnedMesh} Epic Fight
|
||||||
|
* (biped ~20 joints).
|
||||||
|
*
|
||||||
|
* <p>Algorithme (voir {@code docs/plans/rig/MIGRATION.md §1.2.1}) :</p>
|
||||||
|
* <ol>
|
||||||
|
* <li>Pré-calculer le mapping {@code gltfJointIdx → bipedJointId} via
|
||||||
|
* {@link GlbJointAliasTable} + {@link Armature#searchJointByName}.</li>
|
||||||
|
* <li>Pour chaque vertex :
|
||||||
|
* <ul>
|
||||||
|
* <li>Position / normal / UV depuis {@link GltfData}</li>
|
||||||
|
* <li>Retenir les 3 joints de plus fort poids parmi les 4 glTF</li>
|
||||||
|
* <li>Renormaliser les poids retenus pour sommer à 1.0</li>
|
||||||
|
* <li>Construire le {@link SingleGroupVertexBuilder}</li>
|
||||||
|
* </ul>
|
||||||
|
* </li>
|
||||||
|
* <li>Grouper les indices par {@link Primitive} en autant de
|
||||||
|
* {@link MeshPartDefinition}.</li>
|
||||||
|
* <li>{@link SingleGroupVertexBuilder#loadVertexInformation(List, Map)}
|
||||||
|
* construit le {@link SkinnedMesh}.</li>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <p>Les animations éventuellement embarquées dans le GLB sont <b>ignorées</b> —
|
||||||
|
* les animations passent par JSON EF natif via {@code JsonAssetLoader}.</p>
|
||||||
|
*/
|
||||||
|
public final class GltfToSkinnedMesh {
|
||||||
|
|
||||||
|
private static final float WEIGHT_EPSILON = 1.0e-4F;
|
||||||
|
|
||||||
|
private GltfToSkinnedMesh() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit un GLB parsé en {@link SkinnedMesh} utilisable par le pipeline
|
||||||
|
* de rendu RIG.
|
||||||
|
*
|
||||||
|
* @param data données GLB parsées par {@code GlbParser.parse(...)}
|
||||||
|
* @param armature armature biped EF cible (doit être déjà chargée)
|
||||||
|
* @return SkinnedMesh prêt à être rendu
|
||||||
|
* @throws IllegalStateException si {@code armature} est null
|
||||||
|
*/
|
||||||
|
public static SkinnedMesh convert(GltfData data, AssetAccessor<? extends Armature> armature) {
|
||||||
|
if (armature == null || armature.get() == null) {
|
||||||
|
throw new IllegalStateException(
|
||||||
|
"Armature not loaded — GltfToSkinnedMesh.convert() called before resource reload completed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Armature arm = armature.get();
|
||||||
|
int[] jointIdMap = buildJointIdMap(data.jointNames(), arm);
|
||||||
|
|
||||||
|
int vertexCount = data.vertexCount();
|
||||||
|
float[] positions = data.positions();
|
||||||
|
float[] normals = data.normals();
|
||||||
|
float[] texCoords = data.texCoords();
|
||||||
|
int[] joints = data.joints();
|
||||||
|
float[] weights = data.weights();
|
||||||
|
|
||||||
|
List<SingleGroupVertexBuilder> vertices = new ArrayList<>(vertexCount);
|
||||||
|
for (int i = 0; i < vertexCount; i++) {
|
||||||
|
vertices.add(buildVertex(i, positions, normals, texCoords, joints, weights, jointIdMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<MeshPartDefinition, IntList> partIndices = buildPartIndices(data.primitives());
|
||||||
|
|
||||||
|
return SingleGroupVertexBuilder.loadVertexInformation(vertices, partIndices);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit le mapping {@code gltfJointIdx → bipedJointId} une seule fois
|
||||||
|
* avant la boucle vertex. Les noms inconnus retombent sur la racine
|
||||||
|
* {@code Root} (id 0) avec un log WARN.
|
||||||
|
*/
|
||||||
|
private static int[] buildJointIdMap(String[] gltfJointNames, Armature arm) {
|
||||||
|
int[] map = new int[gltfJointNames.length];
|
||||||
|
int unknownCount = 0;
|
||||||
|
int aliasedCount = 0;
|
||||||
|
int rootId = arm.rootJoint != null ? arm.rootJoint.getId() : 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < gltfJointNames.length; i++) {
|
||||||
|
String gltfName = gltfJointNames[i];
|
||||||
|
String bipedName = GlbJointAliasTable.mapGltfJointName(gltfName);
|
||||||
|
|
||||||
|
if (bipedName == null) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"GltfToSkinnedMesh: unknown joint '{}' — fallback to Root",
|
||||||
|
gltfName
|
||||||
|
);
|
||||||
|
map[i] = rootId;
|
||||||
|
unknownCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Joint joint = arm.searchJointByName(bipedName);
|
||||||
|
if (joint == null) {
|
||||||
|
TiedUpRigConstants.LOGGER.warn(
|
||||||
|
"GltfToSkinnedMesh: biped joint '{}' (aliased from '{}') not found in armature — fallback to Root",
|
||||||
|
bipedName, gltfName
|
||||||
|
);
|
||||||
|
map[i] = rootId;
|
||||||
|
unknownCount++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
map[i] = joint.getId();
|
||||||
|
if (!gltfName.equals(bipedName)) {
|
||||||
|
aliasedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TiedUpRigConstants.LOGGER.info(
|
||||||
|
"GltfToSkinnedMesh: {} joints mapped ({} via alias, {} unknown→Root)",
|
||||||
|
gltfJointNames.length, aliasedCount, unknownCount
|
||||||
|
);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit un vertex individuel : position/normal/UV depuis les arrays
|
||||||
|
* flattened, puis sélection des 3 plus forts poids (drop du 4e) + renormalisation.
|
||||||
|
*/
|
||||||
|
private static SingleGroupVertexBuilder buildVertex(
|
||||||
|
int i,
|
||||||
|
float[] positions, float[] normals, float[] texCoords,
|
||||||
|
int[] joints, float[] weights,
|
||||||
|
int[] jointIdMap) {
|
||||||
|
|
||||||
|
SingleGroupVertexBuilder vb = new SingleGroupVertexBuilder();
|
||||||
|
vb.setPosition(new Vec3f(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]));
|
||||||
|
vb.setNormal(new Vec3f(normals[i * 3], normals[i * 3 + 1], normals[i * 3 + 2]));
|
||||||
|
vb.setTextureCoordinate(new Vec2f(texCoords[i * 2], texCoords[i * 2 + 1]));
|
||||||
|
|
||||||
|
// Récupère les 4 joints/poids glTF, sélectionne les 3 plus forts.
|
||||||
|
int[] rawJoints = new int[4];
|
||||||
|
float[] rawWeights = new float[4];
|
||||||
|
for (int k = 0; k < 4; k++) {
|
||||||
|
rawJoints[k] = joints[i * 4 + k];
|
||||||
|
rawWeights[k] = weights[i * 4 + k];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trouve l'index du plus faible poids (à drop).
|
||||||
|
int minIdx = 0;
|
||||||
|
for (int k = 1; k < 4; k++) {
|
||||||
|
if (rawWeights[k] < rawWeights[minIdx]) {
|
||||||
|
minIdx = k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build les 3 retenus + compte effective.
|
||||||
|
float w0 = 0, w1 = 0, w2 = 0;
|
||||||
|
int id0 = 0, id1 = 0, id2 = 0;
|
||||||
|
int effectiveCount = 0;
|
||||||
|
int slot = 0;
|
||||||
|
for (int k = 0; k < 4; k++) {
|
||||||
|
if (k == minIdx) continue;
|
||||||
|
float w = rawWeights[k];
|
||||||
|
int id = jointIdMap[rawJoints[k]];
|
||||||
|
switch (slot) {
|
||||||
|
case 0 -> { w0 = w; id0 = id; }
|
||||||
|
case 1 -> { w1 = w; id1 = id; }
|
||||||
|
case 2 -> { w2 = w; id2 = id; }
|
||||||
|
}
|
||||||
|
if (w > WEIGHT_EPSILON) effectiveCount++;
|
||||||
|
slot++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renormalise les 3 poids pour qu'ils somment à 1.0.
|
||||||
|
float sum = w0 + w1 + w2;
|
||||||
|
if (sum > WEIGHT_EPSILON) {
|
||||||
|
float inv = 1.0F / sum;
|
||||||
|
w0 *= inv; w1 *= inv; w2 *= inv;
|
||||||
|
} else {
|
||||||
|
// Vertex sans skinning (tout-zéro ou bugué) — attache au Root avec poids 1.
|
||||||
|
w0 = 1.0F; w1 = 0; w2 = 0;
|
||||||
|
id0 = 0; id1 = 0; id2 = 0;
|
||||||
|
effectiveCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
vb.setEffectiveJointIDs(new Vec3f(id0, id1, id2));
|
||||||
|
vb.setEffectiveJointWeights(new Vec3f(w0, w1, w2));
|
||||||
|
vb.setEffectiveJointNumber(Math.max(1, effectiveCount));
|
||||||
|
return vb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groupe les indices par primitive (= material dans Blender) → une
|
||||||
|
* {@link VanillaMeshPartDefinition} par primitive. Le partName est pris sur
|
||||||
|
* le {@code materialName} si défini, sinon un nom synthétique
|
||||||
|
* {@code "part_N"}.
|
||||||
|
*/
|
||||||
|
private static Map<MeshPartDefinition, IntList> buildPartIndices(List<Primitive> primitives) {
|
||||||
|
Map<MeshPartDefinition, IntList> partIndices = new HashMap<>();
|
||||||
|
int fallbackCounter = 0;
|
||||||
|
|
||||||
|
for (Primitive prim : primitives) {
|
||||||
|
String partName = prim.materialName();
|
||||||
|
if (partName == null || partName.isEmpty()) {
|
||||||
|
partName = "part_" + fallbackCounter++;
|
||||||
|
}
|
||||||
|
|
||||||
|
MeshPartDefinition partDef = VanillaMeshPartDefinition.of(partName);
|
||||||
|
IntList indexList = new IntArrayList(prim.indices().length);
|
||||||
|
for (int idx : prim.indices()) {
|
||||||
|
indexList.add(idx);
|
||||||
|
}
|
||||||
|
partIndices.put(partDef, indexList);
|
||||||
|
}
|
||||||
|
|
||||||
|
return partIndices;
|
||||||
|
}
|
||||||
|
}
|
||||||
135
src/main/java/com/tiedup/remake/rig/cloth/AbstractSimulator.java
Normal file
135
src/main/java/com/tiedup/remake/rig/cloth/AbstractSimulator.java
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.cloth;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.physics.PhysicsSimulator;
|
||||||
|
import com.tiedup.remake.rig.physics.SimulationObject;
|
||||||
|
import com.tiedup.remake.rig.physics.SimulationObject.SimulationObjectBuilder;
|
||||||
|
import com.tiedup.remake.rig.physics.SimulationProvider;
|
||||||
|
|
||||||
|
public abstract class AbstractSimulator<KEY, B extends SimulationObjectBuilder, PV extends SimulationProvider<O, SO, B, PV>, O, SO extends SimulationObject<B, PV, O>> implements PhysicsSimulator<KEY, B, PV, O, SO> {
|
||||||
|
protected Map<KEY, ObjectWrapper> simulationObjects = Maps.newHashMap();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void tick(O simObject) {
|
||||||
|
this.simulationObjects.values().removeIf((keyWrapper) -> {
|
||||||
|
if (keyWrapper.isRunning()) {
|
||||||
|
if (!keyWrapper.runWhen.getAsBoolean()) {
|
||||||
|
keyWrapper.stopRunning();
|
||||||
|
|
||||||
|
if (!keyWrapper.permanent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (keyWrapper.runWhen.getAsBoolean()) {
|
||||||
|
keyWrapper.startRunning(simObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a simulation object and run. Remove when @Param until returns false
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void runUntil(KEY key, PV provider, B builder, BooleanSupplier until) {
|
||||||
|
this.simulationObjects.put(key, new ObjectWrapper(provider, until, false, builder));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an undeleted simulation object. Run simulation when @Param when returns true
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void runWhen(KEY key, PV provider, B builder, BooleanSupplier when) {
|
||||||
|
this.simulationObjects.put(key, new ObjectWrapper(provider, when, true, builder));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop simulation
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void stop(KEY key) {
|
||||||
|
this.simulationObjects.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restart with the same condition but with another provider
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void restart(KEY key) {
|
||||||
|
ObjectWrapper kwrap = this.simulationObjects.get(key);
|
||||||
|
|
||||||
|
if (kwrap != null) {
|
||||||
|
this.stop(key);
|
||||||
|
this.simulationObjects.put(key, new ObjectWrapper(kwrap.provider, kwrap.runWhen, kwrap.permanent, kwrap.builder));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRunning(KEY key) {
|
||||||
|
return this.simulationObjects.containsKey(key) ? this.simulationObjects.get(key).isRunning() : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<SO> getRunningObject(KEY key) {
|
||||||
|
if (!this.simulationObjects.containsKey(key)) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.ofNullable(this.simulationObjects.get(key).simulationObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Pair<KEY, SO>> getAllRunningObjects() {
|
||||||
|
return this.simulationObjects.entrySet().stream().filter((entry) -> entry.getValue().isRunning()).map((entry) -> Pair.of(entry.getKey(), entry.getValue().simulationObject)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected class ObjectWrapper {
|
||||||
|
final PV provider;
|
||||||
|
final B builder;
|
||||||
|
final BooleanSupplier runWhen;
|
||||||
|
final boolean permanent;
|
||||||
|
|
||||||
|
SO simulationObject;
|
||||||
|
boolean isRunning;
|
||||||
|
|
||||||
|
ObjectWrapper(PV key, BooleanSupplier runWhen, boolean permanent, B builder) {
|
||||||
|
this.provider = key;
|
||||||
|
this.runWhen = runWhen;
|
||||||
|
this.permanent = permanent;
|
||||||
|
this.builder = builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startRunning(O simObject) {
|
||||||
|
this.simulationObject = this.provider.createSimulationData(this.provider, simObject, this.builder);
|
||||||
|
|
||||||
|
if (this.simulationObject != null) {
|
||||||
|
this.isRunning = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopRunning() {
|
||||||
|
this.isRunning = false;
|
||||||
|
this.simulationObject = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRunning() {
|
||||||
|
return this.isRunning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.cloth;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.mojang.datafixers.util.Pair;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 0: Root,
|
||||||
|
* 1: Thigh_R,
|
||||||
|
* 2: "Leg_R",
|
||||||
|
* 3: "Knee_R",
|
||||||
|
* 4: "Thigh_L",
|
||||||
|
* 5: "Leg_L",
|
||||||
|
* 6: "Knee_L",
|
||||||
|
* 7: "Torso",
|
||||||
|
* 8: "Chest",
|
||||||
|
* 9: "Head",
|
||||||
|
* 10: "Shoulder_R",
|
||||||
|
* 11: "Arm_R",
|
||||||
|
* 12: "Hand_R",
|
||||||
|
* 13: "Tool_R",
|
||||||
|
* 14: "Elbow_R",
|
||||||
|
* 15: "Shoulder_L",
|
||||||
|
* 16: "Arm_L",
|
||||||
|
* 17: "Hand_L",
|
||||||
|
* 18: "Tool_L",
|
||||||
|
* 19: "Elbow_L"
|
||||||
|
**/
|
||||||
|
|
||||||
|
public class ClothColliderPresets {
|
||||||
|
public static final List<Pair<Function<ClothSimulatable, OpenMatrix4f>, ClothSimulator.ClothOBBCollider>> BIPED_SLIM = ImmutableList.<Pair<Function<ClothSimulatable, OpenMatrix4f>, ClothSimulator.ClothOBBCollider>>builder()
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[1], new ClothSimulator.ClothOBBCollider(0.125D, 0.24D, 0.125D, 0.0D, 0.22D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[2], new ClothSimulator.ClothOBBCollider(0.125D, 0.1875D, 0.125D, 0.0D, 0.1875D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[4], new ClothSimulator.ClothOBBCollider(0.125D, 0.24D, 0.125D, 0.0D, 0.22D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[5], new ClothSimulator.ClothOBBCollider(0.125D, 0.1875D, 0.125D, 0.0D, 0.1875D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[7], new ClothSimulator.ClothOBBCollider(0.25D, 0.25D, 0.13D, 0.0D, 0.125D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[8], new ClothSimulator.ClothOBBCollider(0.25D, 0.25D, 0.13D, 0.0D, 0.3D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[9], new ClothSimulator.ClothOBBCollider(0.25D, 0.25D, 0.25D, 0.0D, 0.2D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[11], new ClothSimulator.ClothOBBCollider(0.12D, 0.24D, 0.125D, -0.05D, 0.14D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[12], new ClothSimulator.ClothOBBCollider(0.12D, 0.1875D, 0.125D, -0.05D, 0.14D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[16], new ClothSimulator.ClothOBBCollider(0.12D, 0.24D, 0.125D, 0.05D, 0.14D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[17], new ClothSimulator.ClothOBBCollider(0.12D, 0.1875D, 0.125D, 0.05D, 0.14D, 0.0D)))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
public static final List<Pair<Function<ClothSimulatable, OpenMatrix4f>, ClothSimulator.ClothOBBCollider>> BIPED = ImmutableList.<Pair<Function<ClothSimulatable, OpenMatrix4f>, ClothSimulator.ClothOBBCollider>>builder()
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[1], new ClothSimulator.ClothOBBCollider(0.125D, 0.24D, 0.125D, 0.0D, 0.22D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[2], new ClothSimulator.ClothOBBCollider(0.125D, 0.1875D, 0.125D, 0.0D, 0.1875D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[4], new ClothSimulator.ClothOBBCollider(0.125D, 0.24D, 0.125D, 0.0D, 0.22D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[5], new ClothSimulator.ClothOBBCollider(0.125D, 0.1875D, 0.125D, 0.0D, 0.1875D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[7], new ClothSimulator.ClothOBBCollider(0.25D, 0.25D, 0.13D, 0.0D, 0.125D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[8], new ClothSimulator.ClothOBBCollider(0.25D, 0.25D, 0.13D, 0.0D, 0.3D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[9], new ClothSimulator.ClothOBBCollider(0.25D, 0.25D, 0.25D, 0.0D, 0.2D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[11], new ClothSimulator.ClothOBBCollider(0.13D, 0.24D, 0.13D, -0.0D, 0.14D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[12], new ClothSimulator.ClothOBBCollider(0.13D, 0.1875D, 0.13D, -0.0D, 0.14D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[16], new ClothSimulator.ClothOBBCollider(0.13D, 0.24D, 0.13D, 0.0D, 0.14D, 0.0D)))
|
||||||
|
.add(Pair.of((simObject) -> simObject.getArmature().getPoseMatrices()[17], new ClothSimulator.ClothOBBCollider(0.13D, 0.1875D, 0.13D, 0.0D, 0.14D, 0.0D)))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.cloth;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
import com.tiedup.remake.rig.anim.Animator;
|
||||||
|
import com.tiedup.remake.rig.armature.Armature;
|
||||||
|
import com.tiedup.remake.rig.physics.SimulatableObject;
|
||||||
|
|
||||||
|
public interface ClothSimulatable extends SimulatableObject {
|
||||||
|
@Nullable
|
||||||
|
Armature getArmature();
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
Animator getSimulatableAnimator();
|
||||||
|
|
||||||
|
boolean invalid();
|
||||||
|
public Vec3 getObjectVelocity();
|
||||||
|
public float getYRot();
|
||||||
|
public float getYRotO();
|
||||||
|
|
||||||
|
// Cloth object requires providing location info for 2 steps before for accurate continuous collide detection.
|
||||||
|
public Vec3 getAccurateCloakLocation(float partialFrame);
|
||||||
|
public Vec3 getAccuratePartialLocation(float partialFrame);
|
||||||
|
public float getAccurateYRot(float partialFrame);
|
||||||
|
public float getYRotDelta(float partialFrame);
|
||||||
|
public float getScale();
|
||||||
|
public float getGravity();
|
||||||
|
|
||||||
|
ClothSimulator getClothSimulator();
|
||||||
|
}
|
||||||
1931
src/main/java/com/tiedup/remake/rig/cloth/ClothSimulator.java
Normal file
1931
src/main/java/com/tiedup/remake/rig/cloth/ClothSimulator.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.collider;
|
||||||
|
|
||||||
|
import net.minecraft.world.phys.AABB;
|
||||||
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||||
|
import com.tiedup.remake.rig.math.Vec3f;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OBB géométrique forké depuis EF, strippé de la logique combat
|
||||||
|
* (isCollide(Entity), drawInternal, updateAndSelectCollideEntity).
|
||||||
|
* Ne garde que les données + transform() utilisés par ClothOBBCollider.
|
||||||
|
*/
|
||||||
|
public class OBBCollider {
|
||||||
|
protected final Vec3 modelCenter;
|
||||||
|
protected final AABB outerAABB;
|
||||||
|
protected Vec3 worldCenter;
|
||||||
|
protected final Vec3[] modelVertices;
|
||||||
|
protected final Vec3[] modelNormals;
|
||||||
|
protected Vec3[] rotatedVertices;
|
||||||
|
protected Vec3[] rotatedNormals;
|
||||||
|
protected Vec3f scale = new Vec3f(1.0F, 1.0F, 1.0F);
|
||||||
|
|
||||||
|
public OBBCollider(double vertexX, double vertexY, double vertexZ, double centerX, double centerY, double centerZ) {
|
||||||
|
this(getInitialAABB(vertexX, vertexY, vertexZ, centerX, centerY, centerZ), vertexX, vertexY, vertexZ, centerX, centerY, centerZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OBBCollider(AABB outerAABB, double vertexX, double vertexY, double vertexZ, double centerX, double centerY, double centerZ) {
|
||||||
|
this.modelCenter = new Vec3(centerX, centerY, centerZ);
|
||||||
|
this.outerAABB = outerAABB;
|
||||||
|
this.worldCenter = new Vec3(0.0D, 0.0D, 0.0D);
|
||||||
|
|
||||||
|
this.modelVertices = new Vec3[4];
|
||||||
|
this.modelNormals = new Vec3[3];
|
||||||
|
this.rotatedVertices = new Vec3[4];
|
||||||
|
this.rotatedNormals = new Vec3[3];
|
||||||
|
this.modelVertices[0] = new Vec3(vertexX, vertexY, -vertexZ);
|
||||||
|
this.modelVertices[1] = new Vec3(vertexX, vertexY, vertexZ);
|
||||||
|
this.modelVertices[2] = new Vec3(-vertexX, vertexY, vertexZ);
|
||||||
|
this.modelVertices[3] = new Vec3(-vertexX, vertexY, -vertexZ);
|
||||||
|
this.modelNormals[0] = new Vec3(1, 0, 0);
|
||||||
|
this.modelNormals[1] = new Vec3(0, 1, 0);
|
||||||
|
this.modelNormals[2] = new Vec3(0, 0, 1);
|
||||||
|
this.rotatedVertices[0] = new Vec3(0.0D, 0.0D, 0.0D);
|
||||||
|
this.rotatedVertices[1] = new Vec3(0.0D, 0.0D, 0.0D);
|
||||||
|
this.rotatedVertices[2] = new Vec3(0.0D, 0.0D, 0.0D);
|
||||||
|
this.rotatedVertices[3] = new Vec3(0.0D, 0.0D, 0.0D);
|
||||||
|
this.rotatedNormals[0] = new Vec3(0.0D, 0.0D, 0.0D);
|
||||||
|
this.rotatedNormals[1] = new Vec3(0.0D, 0.0D, 0.0D);
|
||||||
|
this.rotatedNormals[2] = new Vec3(0.0D, 0.0D, 0.0D);
|
||||||
|
}
|
||||||
|
|
||||||
|
static AABB getInitialAABB(double posX, double posY, double posZ, double center_x, double center_y, double center_z) {
|
||||||
|
double xLength = Math.abs(posX) + Math.abs(center_x);
|
||||||
|
double yLength = Math.abs(posY) + Math.abs(center_y);
|
||||||
|
double zLength = Math.abs(posZ) + Math.abs(center_z);
|
||||||
|
double maxLength = Math.max(xLength, Math.max(yLength, zLength));
|
||||||
|
return new AABB(maxLength, maxLength, maxLength, -maxLength, -maxLength, -maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void transform(OpenMatrix4f modelMatrix) {
|
||||||
|
OpenMatrix4f noTranslation = modelMatrix.removeTranslation();
|
||||||
|
|
||||||
|
for (int i = 0; i < this.modelVertices.length; i++) {
|
||||||
|
this.rotatedVertices[i] = OpenMatrix4f.transform(noTranslation, this.modelVertices[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < this.modelNormals.length; i++) {
|
||||||
|
this.rotatedNormals[i] = OpenMatrix4f.transform(noTranslation, this.modelNormals[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scale = noTranslation.toScaleVector();
|
||||||
|
this.worldCenter = OpenMatrix4f.transform(modelMatrix, this.modelCenter);
|
||||||
|
}
|
||||||
|
}
|
||||||
357
src/main/java/com/tiedup/remake/rig/debug/RigDebugOverlay.java
Normal file
357
src/main/java/com/tiedup/remake/rig/debug/RigDebugOverlay.java
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.debug;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.gui.Font;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.client.player.LocalPlayer;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
|
import net.minecraftforge.client.event.RenderGuiOverlayEvent;
|
||||||
|
import net.minecraftforge.client.gui.overlay.VanillaGuiOverlay;
|
||||||
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||||
|
import net.minecraftforge.fml.common.Mod;
|
||||||
|
|
||||||
|
import com.tiedup.remake.core.TiedUpMod;
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotion;
|
||||||
|
import com.tiedup.remake.rig.anim.client.ClientAnimator;
|
||||||
|
import com.tiedup.remake.rig.anim.client.Layer;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.patch.PlayerPatch;
|
||||||
|
import com.tiedup.remake.rig.patch.TiedUpCapabilities;
|
||||||
|
import com.tiedup.remake.v2.BodyRegionV2;
|
||||||
|
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
|
||||||
|
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
|
||||||
|
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* P3-19 — debug overlay textuel inspiré de F3+B (vanilla debug hitboxes).
|
||||||
|
*
|
||||||
|
* <p>Affiche en temps réel l'état du pipeline RIG du {@link LocalPlayer} :</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code currentLivingMotion} + {@code currentCompositeMotion}</li>
|
||||||
|
* <li>Nombre d'items bondage équipés (data-driven)</li>
|
||||||
|
* <li>Nombre de bindings {@code livingAnimations} dans l'animator</li>
|
||||||
|
* <li>Liste des layers actifs (base + composite) avec priority + anim courante</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p><b>Objectif dev</b> : distinguer pipeline broken vs pipeline OK mais anim
|
||||||
|
* identity invisible (cas placeholder JSON pré-Phase 4 où aucune anim Blender
|
||||||
|
* co-authored n'est encore bound). Sans ce feedback visuel, impossible de
|
||||||
|
* vérifier gameday que {@code addLivingAnimation(WALK_BOUND, ...)} a bien
|
||||||
|
* pushé le binding.</p>
|
||||||
|
*
|
||||||
|
* <h2>Usage</h2>
|
||||||
|
* <p>Toggle via la keybind {@code key.tiedup.rig_debug} (default F6, configurable
|
||||||
|
* dans le menu Controls, catégorie TiedUp!). L'overlay ne render que lorsque
|
||||||
|
* {@link #DEBUG_OVERLAY_ENABLED} est {@code true}.</p>
|
||||||
|
*
|
||||||
|
* <h2>Testabilité</h2>
|
||||||
|
* <p>{@link #buildOverlayLines(Player)} est package-private et pur — testable
|
||||||
|
* unit avec un player null ou un player sans patch. Le rendu effectif
|
||||||
|
* ({@link #onRenderOverlay}) nécessite MC runtime (GuiGraphics, Font,
|
||||||
|
* Minecraft.getInstance) et n'est validable qu'en gameday.</p>
|
||||||
|
*
|
||||||
|
* <h2>Perf</h2>
|
||||||
|
* <p>Coût négligeable quand {@link #DEBUG_OVERLAY_ENABLED} = false (early
|
||||||
|
* return). Quand actif, le build lit la capability + itère les layers composite
|
||||||
|
* (5 priorities max) → O(1) par frame.</p>
|
||||||
|
*/
|
||||||
|
@OnlyIn(Dist.CLIENT)
|
||||||
|
@Mod.EventBusSubscriber(
|
||||||
|
modid = TiedUpMod.MOD_ID,
|
||||||
|
bus = Mod.EventBusSubscriber.Bus.FORGE,
|
||||||
|
value = Dist.CLIENT
|
||||||
|
)
|
||||||
|
public final class RigDebugOverlay {
|
||||||
|
|
||||||
|
/** Toggle global — flipped par la keybind dans {@code ModKeybindings#onClientTick}. */
|
||||||
|
private static boolean DEBUG_OVERLAY_ENABLED = false;
|
||||||
|
|
||||||
|
/** Padding gauche/haut de l'overlay en pixels guiScaled. */
|
||||||
|
private static final int MARGIN = 4;
|
||||||
|
|
||||||
|
/** Hauteur d'une ligne en pixels (vanilla font = 9, +3 pour l'espacement). */
|
||||||
|
private static final int LINE_HEIGHT = 12;
|
||||||
|
|
||||||
|
/** Couleur par défaut (blanc), drop shadow activé dans drawString. */
|
||||||
|
private static final int COLOR_DEFAULT = 0xFFFFFFFF;
|
||||||
|
|
||||||
|
private RigDebugOverlay() {
|
||||||
|
// utility class — enregistrée via @EventBusSubscriber
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== TOGGLE STATE ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flip l'état de l'overlay. Appelé depuis le handler de keybind dans
|
||||||
|
* {@code ModKeybindings}.
|
||||||
|
*
|
||||||
|
* @return le nouvel état (après toggle)
|
||||||
|
*/
|
||||||
|
public static boolean toggle() {
|
||||||
|
DEBUG_OVERLAY_ENABLED = !DEBUG_OVERLAY_ENABLED;
|
||||||
|
return DEBUG_OVERLAY_ENABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lecture de l'état courant (utilisé en tests). */
|
||||||
|
public static boolean isEnabled() {
|
||||||
|
return DEBUG_OVERLAY_ENABLED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset state — uniquement pour les tests. */
|
||||||
|
static void resetEnabledForTesting() {
|
||||||
|
DEBUG_OVERLAY_ENABLED = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== RENDER EVENT ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook render — s'attache au post-hotbar overlay (non-critical placement
|
||||||
|
* qui ne perturbe pas les HUD vanilla de gameplay).
|
||||||
|
*/
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onRenderOverlay(RenderGuiOverlayEvent.Post event) {
|
||||||
|
if (!DEBUG_OVERLAY_ENABLED) return;
|
||||||
|
|
||||||
|
// On attache sur le HOTBAR overlay pour ne render qu'une fois par frame
|
||||||
|
// (choisi arbitrairement — n'importe quel overlay post-render ferait
|
||||||
|
// l'affaire, HOTBAR est présent dans 100% des contextes gameplay).
|
||||||
|
if (event.getOverlay() != VanillaGuiOverlay.HOTBAR.type()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
LocalPlayer player = mc.player;
|
||||||
|
if (player == null) return;
|
||||||
|
|
||||||
|
GuiGraphics graphics = event.getGuiGraphics();
|
||||||
|
Font font = mc.font;
|
||||||
|
if (font == null) return;
|
||||||
|
|
||||||
|
List<String> lines = buildOverlayLines(player);
|
||||||
|
renderLines(graphics, font, lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render effectif des lignes en haut-gauche. Extrait pour pouvoir être
|
||||||
|
* appelé depuis un contexte non-event (future devtool).
|
||||||
|
*/
|
||||||
|
private static void renderLines(GuiGraphics graphics, Font font, List<String> lines) {
|
||||||
|
int y = MARGIN;
|
||||||
|
for (String line : lines) {
|
||||||
|
graphics.drawString(font, line, MARGIN, y, COLOR_DEFAULT, true);
|
||||||
|
y += LINE_HEIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PURE LOGIC (testable) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit la liste de lignes à afficher. Pur (à l'exception de la lecture
|
||||||
|
* de la capability) — testable unit avec un player null.
|
||||||
|
*
|
||||||
|
* <p>Contrat null-safety :</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code player == null} → liste avec header + "player: null"</li>
|
||||||
|
* <li>patch absent → header + "patch: null"</li>
|
||||||
|
* <li>animator absent (server side / pas ClientAnimator) → header
|
||||||
|
* + "animator: null"</li>
|
||||||
|
* <li>nominal : header + motion + items + bindings + layers</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @return liste non-null, jamais vide (header toujours présent)
|
||||||
|
*/
|
||||||
|
static List<String> buildOverlayLines(Player player) {
|
||||||
|
List<String> lines = new ArrayList<>();
|
||||||
|
lines.add("§l[TiedUp RIG Debug]§r");
|
||||||
|
|
||||||
|
if (player == null) {
|
||||||
|
lines.add("§cplayer: null§r");
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerPatch<?> patch = TiedUpCapabilities.getPlayerPatch(player);
|
||||||
|
if (patch == null) {
|
||||||
|
lines.add("§cpatch: null§r");
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClientAnimator animator = patch.getClientAnimator();
|
||||||
|
if (animator == null) {
|
||||||
|
lines.add("§canimator: null§r");
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendMotionLines(lines, patch);
|
||||||
|
appendItemCountLine(lines, player);
|
||||||
|
appendBindingsCountLine(lines, animator);
|
||||||
|
appendLayerLines(lines, animator);
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append les lignes {@code motion:} et {@code composite:} pour le patch
|
||||||
|
* fourni. Extrait pour lisibilité du {@link #buildOverlayLines} pipeline.
|
||||||
|
*/
|
||||||
|
private static void appendMotionLines(List<String> lines, PlayerPatch<?> patch) {
|
||||||
|
LivingMotion motion = patch.currentLivingMotion;
|
||||||
|
LivingMotion composite = patch.currentCompositeMotion;
|
||||||
|
lines.add(String.format("motion: §a%s§r", motionName(motion)));
|
||||||
|
lines.add(String.format("composite: §a%s§r", motionName(composite)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compte les items data-driven équipés. Un armbinder occupant 3 régions
|
||||||
|
* compte pour 1 — la dédup identity est déjà garantie upstream par
|
||||||
|
* {@link com.tiedup.remake.v2.bondage.capability.V2BondageEquipment#getAllEquipped()}
|
||||||
|
* via {@code IdentityHashMap}. Pas de {@code .distinct()} ici : serait
|
||||||
|
* du dead defensive code.
|
||||||
|
*/
|
||||||
|
private static void appendItemCountLine(List<String> lines, Player player) {
|
||||||
|
Map<BodyRegionV2, ItemStack> equipped = V2EquipmentHelper.getAllEquipped(player);
|
||||||
|
long count = equipped.values().stream()
|
||||||
|
.filter(s -> s != null && !s.isEmpty())
|
||||||
|
.filter(s -> DataDrivenItemRegistry.get(s) != null)
|
||||||
|
.count();
|
||||||
|
lines.add(String.format("items: §e%d§r", count));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compte les bindings {@code livingAnimations} actifs dans l'animator.
|
||||||
|
* Utilise l'API publique {@link com.tiedup.remake.rig.anim.Animator#getLivingAnimations()}
|
||||||
|
* qui retourne une {@code ImmutableMap.copyOf} — safe à lire sans
|
||||||
|
* modification.
|
||||||
|
*/
|
||||||
|
private static void appendBindingsCountLine(List<String> lines, ClientAnimator animator) {
|
||||||
|
int bindings = animator.getLivingAnimations().size();
|
||||||
|
lines.add(String.format("bindings: §e%d§r", bindings));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append une ligne header {@code layers:} puis une ligne par layer actif
|
||||||
|
* (base + composite non-off).
|
||||||
|
*
|
||||||
|
* <p>Format ligne layer : {@code " [BASE|priority] anim_name"} — avec
|
||||||
|
* indentation 2 espaces pour visual grouping sous le header.</p>
|
||||||
|
*/
|
||||||
|
private static void appendLayerLines(List<String> lines, ClientAnimator animator) {
|
||||||
|
lines.add("§7layers:§r");
|
||||||
|
|
||||||
|
// Collect layer descriptions via iterAllLayers. On ne peut pas early-
|
||||||
|
// return depuis un Consumer, on collecte tout puis filtre.
|
||||||
|
List<String> layerLines = new ArrayList<>();
|
||||||
|
Consumer<Layer> layerCollector = layer -> {
|
||||||
|
if (layer.isOff() && !isBaseLayer(layer)) {
|
||||||
|
// Skip composite layers off — le base est toujours rendu,
|
||||||
|
// même s'il tourne en IDLE.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
layerLines.add(describeLayer(layer));
|
||||||
|
};
|
||||||
|
|
||||||
|
animator.iterAllLayers(layerCollector);
|
||||||
|
|
||||||
|
if (layerLines.isEmpty()) {
|
||||||
|
lines.add(" §8(none)§r");
|
||||||
|
} else {
|
||||||
|
lines.addAll(layerLines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format une ligne de description pour un {@link Layer}. Public-ish
|
||||||
|
* (package-private) pour testabilité.
|
||||||
|
*
|
||||||
|
* <p>Format :</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Base layer : {@code " [BASE/PRIORITY] anim_registry_name"}</li>
|
||||||
|
* <li>Composite layer : {@code " [COMP/PRIORITY] anim_registry_name"}</li>
|
||||||
|
* <li>Anim null/empty : {@code "(empty)"}</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
static String describeLayer(Layer layer) {
|
||||||
|
String kind = isBaseLayer(layer) ? "BASE" : "COMP";
|
||||||
|
String priority = layerPriorityName(layer);
|
||||||
|
String animName = currentAnimationName(layer);
|
||||||
|
return String.format(" §7[%s/%s]§r §b%s§r", kind, priority, animName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le nom d'une {@link LivingMotion}. Pour un enum classique
|
||||||
|
* (LivingMotions, TiedUpLivingMotions), {@code toString()} renvoie
|
||||||
|
* {@code name()}. Pour null, retourne {@code "null"}.
|
||||||
|
*/
|
||||||
|
static String motionName(LivingMotion motion) {
|
||||||
|
return motion != null ? motion.toString() : "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le nom du registre de l'animation courante jouée par le layer,
|
||||||
|
* ou {@code "(empty)"} si le player n'a pas d'anim (layer off / fraichement
|
||||||
|
* initialisé).
|
||||||
|
*/
|
||||||
|
private static String currentAnimationName(Layer layer) {
|
||||||
|
if (layer.animationPlayer == null || layer.animationPlayer.isEmpty()) {
|
||||||
|
return "(empty)";
|
||||||
|
}
|
||||||
|
AssetAccessor<? extends com.tiedup.remake.rig.anim.types.DynamicAnimation> anim =
|
||||||
|
layer.animationPlayer.getAnimation();
|
||||||
|
if (anim == null) return "(null)";
|
||||||
|
try {
|
||||||
|
var rl = anim.registryName();
|
||||||
|
return rl != null ? rl.toString() : "(unnamed)";
|
||||||
|
} catch (Exception e) {
|
||||||
|
// registryName() peut throw pour EMPTY_ANIMATION / LinkAnimation
|
||||||
|
// / LayerOffAnimation ; on fallback sur le class name.
|
||||||
|
return anim.getClass().getSimpleName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne le nom de la priority d'un layer. Pour la base layer, on lit
|
||||||
|
* {@link Layer.BaseLayer#getBaseLayerPriority()}. Pour les composite,
|
||||||
|
* on lit le champ priority (protected — accessed via public describeLayer
|
||||||
|
* helper). Priority peut être null sur la base layer si elle n'a pas encore
|
||||||
|
* été initialisée (ne devrait jamais arriver après postInit, mais défensif).
|
||||||
|
*/
|
||||||
|
private static String layerPriorityName(Layer layer) {
|
||||||
|
if (layer instanceof Layer.BaseLayer baseLayer) {
|
||||||
|
Layer.Priority p = baseLayer.getBaseLayerPriority();
|
||||||
|
return p != null ? p.name() : "?";
|
||||||
|
}
|
||||||
|
// Pour un Layer composite, priority est protected — on passe par
|
||||||
|
// toString() en fallback et on parse. Plus simple : on regarde dans
|
||||||
|
// quelle entry du map il se trouve. Mais on n'a pas accès au map.
|
||||||
|
// Compromis pragmatique : le toString() de Layer inclut déjà la priority.
|
||||||
|
// Exemple : " Composite Layer(HIGH) : ..." → on extrait HIGH.
|
||||||
|
String toString = layer.toString();
|
||||||
|
int open = toString.indexOf('(');
|
||||||
|
int close = toString.indexOf(')');
|
||||||
|
if (open >= 0 && close > open) {
|
||||||
|
return toString.substring(open + 1, close);
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teste si un {@link Layer} est la base layer. On ne peut pas utiliser
|
||||||
|
* {@code instanceof BaseLayer} car certains tests fournissent des mocks ;
|
||||||
|
* pragmatique : instanceof + null-check.
|
||||||
|
*/
|
||||||
|
private static boolean isBaseLayer(Layer layer) {
|
||||||
|
return layer instanceof Layer.BaseLayer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Derived from Epic Fight (https://github.com/Epic-Fight/epicfight)
|
||||||
|
* by the Epic Fight Team, licensed under GPLv3.
|
||||||
|
* Modifications © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.event;
|
||||||
|
|
||||||
|
import net.minecraftforge.eventbus.api.Event;
|
||||||
|
import com.tiedup.remake.rig.anim.Animator;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
|
||||||
|
public class InitAnimatorEvent extends Event {
|
||||||
|
private final LivingEntityPatch<?> entitypatch;
|
||||||
|
private final Animator animator;
|
||||||
|
|
||||||
|
public InitAnimatorEvent(LivingEntityPatch<?> entitypatch, Animator animator) {
|
||||||
|
this.entitypatch = entitypatch;
|
||||||
|
this.animator = animator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LivingEntityPatch<?> getEntityPatch() {
|
||||||
|
return this.entitypatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Animator getAnimator() {
|
||||||
|
return this.animator;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user