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_output.log
|
||||
logs/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
docs/
|
||||
docs.tar.gz
|
||||
|
||||
102
LICENSE
102
LICENSE
@@ -1,66 +1,21 @@
|
||||
# TiedUp! Remake - License
|
||||
|
||||
**Effective Date:** January 2025
|
||||
**Applies to:** All versions of TiedUp! Remake (past, present, and future)
|
||||
**Effective Date:** April 2026 (license change from GPL-3.0 + Commons-Clause to GPL-3.0 pure)
|
||||
**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
|
||||
|
||||
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:**
|
||||
- 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
|
||||
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/`).
|
||||
|
||||
---
|
||||
|
||||
## Full License Terms
|
||||
## GNU General Public License v3.0-or-later
|
||||
|
||||
### Part 1: Commons Clause Restriction
|
||||
|
||||
"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
|
||||
Copyright (C) 2024-2026 TiedUp! Remake Contributors
|
||||
|
||||
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
|
||||
@@ -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
|
||||
their respective owners:
|
||||
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:
|
||||
|
||||
- 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)
|
||||
- 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
|
||||
|
||||
2. **Minecraft Assets**
|
||||
@@ -95,28 +60,15 @@ their respective owners:
|
||||
- Subject to Minecraft EULA: https://www.minecraft.net/en-us/eula
|
||||
|
||||
3. **Third-Party Libraries**
|
||||
- PlayerAnimator: Subject to its own license (dev.kosmx.player-anim)
|
||||
- Forge: Subject to Forge license (MinecraftForge)
|
||||
- Other dependencies: Subject to their respective licenses
|
||||
- Forge: subject to Forge license (MinecraftForge)
|
||||
- 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/`)
|
||||
is fully covered by this GPL-3.0 + Commons Clause license.
|
||||
Code written for this project (files in `src/main/java/com/tiedup/remake/`) is fully covered by GPL-3.0-or-later.
|
||||
|
||||
---
|
||||
|
||||
### Part 4: Derivative Works
|
||||
|
||||
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
|
||||
## Disclaimer
|
||||
|
||||
THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
@@ -131,9 +83,9 @@ SOFTWARE.
|
||||
## SPDX Identifier
|
||||
|
||||
```
|
||||
SPDX-License-Identifier: GPL-3.0-only WITH Commons-Clause-1.0
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
```
|
||||
|
||||
## 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.
|
||||
// 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.
|
||||
// accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg')
|
||||
accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg')
|
||||
|
||||
// Default run configurations.
|
||||
// These can be tweaked, removed, or duplicated as needed.
|
||||
@@ -105,6 +105,7 @@ minecraft {
|
||||
// Mixin config arg
|
||||
args '-mixin.config=tiedup.mixins.json'
|
||||
args '-mixin.config=tiedup-compat.mixins.json'
|
||||
args '-mixin.config=tiedup-rig.mixins.json'
|
||||
}
|
||||
|
||||
server {
|
||||
@@ -118,6 +119,7 @@ minecraft {
|
||||
// Mixin config arg
|
||||
args '-mixin.config=tiedup.mixins.json'
|
||||
args '-mixin.config=tiedup-compat.mixins.json'
|
||||
args '-mixin.config=tiedup-rig.mixins.json'
|
||||
}
|
||||
|
||||
// Additional client instances for multiplayer testing
|
||||
|
||||
@@ -49,7 +49,7 @@ mod_id=tiedup
|
||||
# The human-readable display name for the mod.
|
||||
mod_name=TiedUp
|
||||
# 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/
|
||||
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.
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
/**
|
||||
* 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 */
|
||||
private static boolean lastForceSeatState = false;
|
||||
|
||||
@@ -149,7 +161,8 @@ public class ModKeybindings {
|
||||
event.register(BOUNTY_KEY);
|
||||
event.register(FORCE_SEAT_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) ====================
|
||||
@@ -212,6 +225,19 @@ public class ModKeybindings {
|
||||
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();
|
||||
if (mc.player == null || mc.level == null) {
|
||||
return;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package com.tiedup.remake.client.animation;
|
||||
|
||||
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.KeyframeAnimationPlayer;
|
||||
import dev.kosmx.playerAnim.api.layered.ModifierLayer;
|
||||
import dev.kosmx.playerAnim.core.data.KeyframeAnimation;
|
||||
import dev.kosmx.playerAnim.impl.IAnimatedPlayer;
|
||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
|
||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationFactory;
|
||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
|
||||
import java.util.Map;
|
||||
@@ -15,7 +13,6 @@ import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
@@ -39,92 +36,42 @@ public class BondageAnimationManager {
|
||||
|
||||
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 =
|
||||
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 =
|
||||
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) */
|
||||
private static final int CONTEXT_LAYER_PRIORITY = 40;
|
||||
/** Priority for item animation layer (higher = overrides context layer) */
|
||||
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.
|
||||
* 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() {
|
||||
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(
|
||||
"BondageAnimationManager: Factories registered — context (pri {}), item (pri {}), furniture (pri {})",
|
||||
CONTEXT_LAYER_PRIORITY,
|
||||
ITEM_LAYER_PRIORITY,
|
||||
FURNITURE_LAYER_PRIORITY
|
||||
"BondageAnimationManager: NPC-only pipeline (Phase 2.8 RIG cleanup). " +
|
||||
"Players handled by RigAnimationTickHandler; all player call sites no-op."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -151,6 +98,11 @@ public class BondageAnimationManager {
|
||||
* <p>If the animation layer is not available (e.g., remote player not fully
|
||||
* 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 animId Full ResourceLocation of the animation
|
||||
* @return true if animation started successfully, false if layer not available
|
||||
@@ -163,6 +115,12 @@ public class BondageAnimationManager {
|
||||
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);
|
||||
if (anim == null) {
|
||||
// Try fallback: remove _sneak_ suffix if present
|
||||
@@ -199,7 +157,7 @@ public class BondageAnimationManager {
|
||||
}
|
||||
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());
|
||||
|
||||
LOGGER.debug(
|
||||
@@ -208,24 +166,12 @@ public class BondageAnimationManager {
|
||||
entity.getUUID()
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
// Layer not available - queue for retry if it's a player
|
||||
if (entity instanceof AbstractClientPlayer) {
|
||||
PendingAnimationManager.queueForRetry(
|
||||
entity.getUUID(),
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -246,6 +192,12 @@ public class BondageAnimationManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Phase 2.8 : player path is dead.
|
||||
if (entity instanceof Player player) {
|
||||
logPlayerCallOnce(player, "playDirect");
|
||||
return false;
|
||||
}
|
||||
|
||||
ModifierLayer<IAnimation> layer = getOrCreateLayer(entity);
|
||||
if (layer != null) {
|
||||
IAnimation current = layer.getAnimation();
|
||||
@@ -273,6 +225,11 @@ public class BondageAnimationManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase 2.8 : player path is dead — no layer to clear.
|
||||
if (entity instanceof Player) {
|
||||
return;
|
||||
}
|
||||
|
||||
ModifierLayer<IAnimation> layer = getLayer(entity);
|
||||
if (layer != null) {
|
||||
layer.setAnimation(null);
|
||||
@@ -284,56 +241,36 @@ public class BondageAnimationManager {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// Players: try PlayerAnimationAccess first, then cache
|
||||
if (entity instanceof AbstractClientPlayer player) {
|
||||
ModifierLayer<IAnimation> factoryLayer = getPlayerLayer(player);
|
||||
if (factoryLayer != null) {
|
||||
return factoryLayer;
|
||||
if (entity instanceof Player) {
|
||||
return null;
|
||||
}
|
||||
// Check cache (for remote players using fallback)
|
||||
return npcLayers.get(entity.getUUID());
|
||||
}
|
||||
|
||||
// NPCs: use cache
|
||||
return npcLayers.get(entity.getUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
LivingEntity entity
|
||||
) {
|
||||
// Phase 2.8 : strip player path entirely (no partially-alive fallback).
|
||||
if (entity instanceof Player) {
|
||||
return null;
|
||||
}
|
||||
|
||||
UUID uuid = entity.getUUID();
|
||||
|
||||
// Players: try factory-based access first, fallback to direct stack access
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// NPCs implementing IAnimatedPlayer: create/cache layer
|
||||
if (entity instanceof IAnimatedPlayer animated) {
|
||||
return npcLayers.computeIfAbsent(uuid, k -> {
|
||||
@@ -353,87 +290,49 @@ public class BondageAnimationManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Per-player dedup set so we log the factory-access failure at most once per UUID. */
|
||||
private static final java.util.Set<UUID> layerFailureLogged =
|
||||
/** Per-player-UUID dedup so stale call sites log at most once per session. */
|
||||
private static final java.util.Set<UUID> playerCallLogged =
|
||||
java.util.concurrent.ConcurrentHashMap.newKeySet();
|
||||
|
||||
/**
|
||||
* Get the animation layer for a player from PlayerAnimationAccess.
|
||||
*
|
||||
* <p>Throws during the factory-race window for remote players (the factory
|
||||
* hasn't yet initialized their associated data). This is the expected path
|
||||
* 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>
|
||||
* Log once per player UUID that a stale call site is invoking this manager.
|
||||
* Used by the player no-op short-circuits ({@link #playAnimation},
|
||||
* {@link #playDirect}) to surface call sites that should be migrated to the
|
||||
* RIG pipeline (tracked in V3_REWORK_BACKLOG).
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static ModifierLayer<IAnimation> getPlayerLayer(
|
||||
AbstractClientPlayer player
|
||||
) {
|
||||
try {
|
||||
return (ModifierLayer<
|
||||
IAnimation
|
||||
>) PlayerAnimationAccess.getPlayerAssociatedData(player).get(
|
||||
FACTORY_ID
|
||||
private static void logPlayerCallOnce(Player player, String op) {
|
||||
if (playerCallLogged.add(player.getUUID())) {
|
||||
LOGGER.warn(
|
||||
"BondageAnimationManager.{} called on player {} — no-op " +
|
||||
"(RIG owns player anims since Phase 2.7). " +
|
||||
"Migrate call site to RigAnimationTickHandler (V3 rework).",
|
||||
op,
|
||||
player.getName().getString()
|
||||
);
|
||||
} 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.
|
||||
* Returns null if the layer is not yet initialized.
|
||||
*
|
||||
* <p>Public method for PendingAnimationManager to access.
|
||||
* Checks both the factory-based layer and the NPC cache fallback.
|
||||
* <p>Phase 2.8 : always returns {@code null}. The player pipeline is
|
||||
* 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
|
||||
* @return The animation layer, or null if not available
|
||||
* @param player The player (unused)
|
||||
* @return always null in Phase 2.8+
|
||||
*/
|
||||
@javax.annotation.Nullable
|
||||
public static ModifierLayer<IAnimation> getPlayerLayerSafe(
|
||||
AbstractClientPlayer player
|
||||
) {
|
||||
// Try factory first
|
||||
ModifierLayer<IAnimation> factoryLayer = getPlayerLayer(player);
|
||||
if (factoryLayer != null) {
|
||||
return factoryLayer;
|
||||
}
|
||||
|
||||
// Check NPC cache (for remote players using fallback path)
|
||||
return npcLayers.get(player.getUUID());
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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.
|
||||
* Uses CONTEXT_LAYER_PRIORITY, below the item layer at ITEM_LAYER_PRIORITY.
|
||||
@@ -471,13 +370,14 @@ public class BondageAnimationManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
ModifierLayer<IAnimation> layer;
|
||||
if (entity instanceof AbstractClientPlayer player) {
|
||||
layer = getPlayerContextLayer(player);
|
||||
} else {
|
||||
layer = getOrCreateNpcContextLayer(entity);
|
||||
// Phase 2.8 : player context layer is dead (sit/kneel/sneak visuals
|
||||
// will be re-expressed as RIG StaticAnimations — cf. V3-REW-14).
|
||||
if (entity instanceof Player player) {
|
||||
logPlayerCallOnce(player, "playContext");
|
||||
return false;
|
||||
}
|
||||
|
||||
ModifierLayer<IAnimation> layer = getOrCreateNpcContextLayer(entity);
|
||||
if (layer != null) {
|
||||
layer.setAnimation(new KeyframeAnimationPlayer(anim));
|
||||
return true;
|
||||
@@ -495,13 +395,12 @@ public class BondageAnimationManager {
|
||||
return;
|
||||
}
|
||||
|
||||
ModifierLayer<IAnimation> layer;
|
||||
if (entity instanceof AbstractClientPlayer player) {
|
||||
layer = getPlayerContextLayer(player);
|
||||
} else {
|
||||
layer = npcContextLayers.get(entity.getUUID());
|
||||
// Phase 2.8 : player path is dead — no layer to clear.
|
||||
if (entity instanceof Player) {
|
||||
return;
|
||||
}
|
||||
|
||||
ModifierLayer<IAnimation> layer = npcContextLayers.get(entity.getUUID());
|
||||
if (layer != null) {
|
||||
layer.setAnimation(null);
|
||||
}
|
||||
@@ -533,194 +432,46 @@ public class BondageAnimationManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
ModifierLayer<IAnimation> layer = getOrCreateFurnitureLayer(player);
|
||||
if (layer != null) {
|
||||
layer.setAnimation(new KeyframeAnimationPlayer(animation));
|
||||
// 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()
|
||||
);
|
||||
// Phase 2.8 : player furniture seat pose is dead (will be ported to
|
||||
// RIG StaticAnimations — cf. V3_REWORK_BACKLOG furniture seat entry).
|
||||
logPlayerCallOnce(player, "playFurniture");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static void stopFurniture(Player player) {
|
||||
if (player == null || !player.level().isClientSide()) {
|
||||
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()
|
||||
);
|
||||
// Phase 2.8 : dead path. Retained signature for backward-compat.
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @return true if the furniture layer has an active animation
|
||||
* @return always false in Phase 2.8+
|
||||
*/
|
||||
public static boolean hasFurnitureAnimation(Player player) {
|
||||
if (player == null || !player.level().isClientSide()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ModifierLayer<IAnimation> layer = getFurnitureLayer(player);
|
||||
return layer != null && layer.getAnimation() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the furniture ModifierLayer for a player (READ-ONLY).
|
||||
* 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.
|
||||
* Safety tick for furniture animations.
|
||||
*
|
||||
* <p>For the local player (factory-registered), returns the factory layer.
|
||||
* For remote players, creates a new layer on first call and caches it in
|
||||
* {@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>Phase 2.8 : no-op — the player furniture layer is dead, nothing to
|
||||
* guard. Kept as an empty stub in case older call sites remain.</p>
|
||||
*
|
||||
* <p>If a player has an active furniture animation but is NOT riding an
|
||||
* {@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
|
||||
* @param player the player to check (unused)
|
||||
*/
|
||||
public static void tickFurnitureSafety(Player player) {
|
||||
if (player == null || !player.level().isClientSide()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Phase 2.8 : dead path. Retained signature for backward-compat.
|
||||
}
|
||||
|
||||
// FALLBACK ANIMATION HANDLING
|
||||
@@ -789,8 +540,9 @@ public class BondageAnimationManager {
|
||||
* @param entityId UUID of the removed entity
|
||||
*/
|
||||
/** All NPC layer caches, for bulk cleanup operations. */
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
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) {
|
||||
for (Map<UUID, ModifierLayer<IAnimation>> cache : ALL_NPC_CACHES) {
|
||||
@@ -799,8 +551,7 @@ public class BondageAnimationManager {
|
||||
layer.setAnimation(null);
|
||||
}
|
||||
}
|
||||
furnitureGraceTicks.remove(entityId);
|
||||
layerFailureLogged.remove(entityId);
|
||||
playerCallLogged.remove(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.clear();
|
||||
}
|
||||
furnitureGraceTicks.clear();
|
||||
layerFailureLogged.clear();
|
||||
playerCallLogged.clear();
|
||||
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.
|
||||
* 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) {
|
||||
float[] state = dogPoseState.get(playerUuid);
|
||||
return state != null ? state[IDX_DELTA] : 0f;
|
||||
@@ -61,7 +68,14 @@ public class DogPoseRenderHandler {
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
float[] state = dogPoseState.get(playerUuid);
|
||||
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.tiedup.remake.client.animation.AnimationStateRegistry;
|
||||
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.LeashProxyClientHandler;
|
||||
import com.tiedup.remake.client.gltf.GltfAnimationApplier;
|
||||
import com.tiedup.remake.client.state.ClothesClientCache;
|
||||
import com.tiedup.remake.client.state.MovementStyleClientState;
|
||||
import com.tiedup.remake.client.state.PetBedClientState;
|
||||
import com.tiedup.remake.util.HumanChairHelper;
|
||||
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.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
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.OnlyIn;
|
||||
import net.minecraftforge.event.TickEvent;
|
||||
@@ -35,16 +23,29 @@ import net.minecraftforge.fml.common.Mod;
|
||||
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>
|
||||
* <li>Tracks tied/struggling/sneaking state for players</li>
|
||||
* <li>Plays animations via BondageAnimationManager when state changes</li>
|
||||
* <li>Handles cleanup on logout/world unload</li>
|
||||
* <li>Nettoyage périodique de {@code ClothesClientCache} (cache remote
|
||||
* players, hygiène mémoire indépendante du pipeline de rendu)</li>
|
||||
* <li>Cleanup logout / world unload (caches V2 encore utilisés par les
|
||||
* NPCs ticked par {@link NpcAnimationTickHandler})</li>
|
||||
* </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(
|
||||
modid = "tiedup",
|
||||
@@ -83,8 +84,20 @@ public class AnimationTickHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Client tick event - called every tick on the client.
|
||||
* Updates animations for all players when their bondage state changes.
|
||||
* Client tick event — called every tick on the client.
|
||||
*
|
||||
* <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
|
||||
public static void onClientTick(TickEvent.ClientTickEvent event) {
|
||||
@@ -97,193 +110,17 @@ public class AnimationTickHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process pending animations first (retry failed animations for remote players)
|
||||
PendingAnimationManager.processPending(mc.level);
|
||||
|
||||
// Periodic cleanup of stale cache entries (every 60 seconds = 1200 ticks)
|
||||
// Periodic cleanup of stale clothes cache entries (every 60 seconds = 1200 ticks).
|
||||
// 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.
|
||||
if (++cleanupTickCounter >= 1200) {
|
||||
cleanupTickCounter = 0;
|
||||
ClothesClientCache.cleanupStale();
|
||||
}
|
||||
|
||||
// Then update all player animations
|
||||
for (Player player : mc.level.players()) {
|
||||
if (player instanceof AbstractClientPlayer clientPlayer) {
|
||||
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);
|
||||
// Le tick per-player V2 (updatePlayerAnimation, tickFurnitureSafety,
|
||||
// cold-cache furniture retry) est délégué à RigAnimationTickHandler
|
||||
// Phase 2.7+. Rien à faire ici.
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,22 +15,14 @@ import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
* <li>Head yaw: convert to zRot (roll) since yRot axis is sideways</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Architecture: Players vs NPCs</h2>
|
||||
* <pre>
|
||||
* ┌─────────────────────────────────────────────────────────────────┐
|
||||
* │ PLAYERS │
|
||||
* ├─────────────────────────────────────────────────────────────────┤
|
||||
* │ 1. PlayerArmHideEventHandler.onRenderPlayerPre() │
|
||||
* │ - 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() │
|
||||
* └─────────────────────────────────────────────────────────────────┘
|
||||
* <h2>Architecture — NPCs only (Phase 2.8 RIG cleanup)</h2>
|
||||
* <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},
|
||||
* 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
|
||||
* {@code docs/plans/rig/V3_REWORK_BACKLOG.md}).</p>
|
||||
*
|
||||
* <pre>
|
||||
* ┌─────────────────────────────────────────────────────────────────┐
|
||||
* │ NPCs │
|
||||
* ├─────────────────────────────────────────────────────────────────┤
|
||||
@@ -48,25 +40,13 @@ import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
* └─────────────────────────────────────────────────────────────────┘
|
||||
* </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>
|
||||
* <p>Used by:
|
||||
* <ul>
|
||||
* <li>MixinPlayerModel - for player head compensation</li>
|
||||
* <li>DamselModel - for NPC head compensation</li>
|
||||
* </ul>
|
||||
*
|
||||
* @see RotationSmoother for Y rotation smoothing
|
||||
* @see com.tiedup.remake.mixin.client.MixinPlayerModel
|
||||
* @see com.tiedup.remake.client.model.DamselModel
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
@@ -130,7 +110,14 @@ public final class DogPoseHelper {
|
||||
* @param headPitch Player's up/down look angle in degrees
|
||||
* @param headYaw Head yaw relative to body 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(
|
||||
ModelPart head,
|
||||
ModelPart hat,
|
||||
|
||||
@@ -137,6 +137,27 @@ public class TiedUpMod {
|
||||
|
||||
// Register dispenser behaviors (must be on main thread)
|
||||
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
|
||||
event.enqueueWork(() -> {
|
||||
// Initialize unified BondageAnimationManager
|
||||
com.tiedup.remake.client.animation.BondageAnimationManager.init();
|
||||
LOGGER.info("BondageAnimationManager initialized");
|
||||
// RIG Phase 2 — override client dispatch PLAYER → Local/Client/ServerPlayerPatch
|
||||
com.tiedup.remake.rig.patch.EntityPatchProvider.registerEntityPatchesClient();
|
||||
|
||||
// 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
|
||||
com.tiedup.remake.client.renderer.obj.ObjModelRegistry.init();
|
||||
@@ -589,6 +615,39 @@ public class TiedUpMod {
|
||||
LOGGER.info(
|
||||
"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.trader.PacketBuyCaptive;
|
||||
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.PacketV2LockToggle;
|
||||
import com.tiedup.remake.v2.bondage.network.PacketV2SelfEquip;
|
||||
@@ -592,6 +593,14 @@ public class ModNetwork {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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