36 Commits

Author SHA1 Message Date
notevil
1fa291563c Audit-10 : add rig/ test coverage (37 new tests)
Added:
- TransformSheetTest (10 tests) : binary search getInterpolationInfo edge
  cases (empty, negative-wrap, exact boundary, multi-keyframe segment),
  maxFrameTime sentinel, copyAll/copy subrange
- PoseTest (10 tests) : interpolatePose merge + lerp correctness at 0/0.5/1,
  orElseEmpty fallback, load() all three LoadOperation modes, disableAllJoints
- LivingMotionIsSameTest (7 tests) : IDLE==INACTION symmetry, uniqueness of
  universalOrdinal across all LivingMotions values
- TimePairListTest (7 tests) : odd-arg rejection, empty list, inclusive begin /
  exclusive end boundary, multi-pair gap
- RigAnimationTickHandlerTest (2 tests) : resetLoggedErrors idempotency

Skipped (MC runtime dep):
- TiedUpCapabilityEventsTest : AttachCapabilitiesEvent + live ForgeEventBus
- EntityPatchProviderInvalidateTest : LazyOptional is a Forge runtime class
- LivingEntityPatch.onConstructed : requires real LivingEntity hierarchy
- RigAnimationTickHandler.tickPlayer/maybePlayIdle : require
  TiedUpCapabilities.getEntityPatch + ClientAnimator + LivingEntityPatch

Bug flagged (no fix) :
- TransformSheet.getFirstFrame() calls copy(0,2) without guarding size >= 2;
  a single-keyframe sheet would throw ArrayIndexOutOfBoundsException
2026-04-23 09:11:32 +02:00
notevil
05cc07a97d Audit-9 review : drop MovementAnimation from phase0 extract script
Reviewer flagged scripts/rig-extract-phase0.sh:49 as stale after Audit-9
deleted rig/anim/types/MovementAnimation.java. The script is one-shot
tooling, but keep it coherent with the actual fork.
2026-04-23 06:04:38 +02:00
notevil
d3a3b400aa Audit-9 : strip Vec2i + exceptions + HitEntityList + MovementAnimation, document datapack-surface keeps
Deletes (5 files, 132 lines) :
- rig/anim/types/MovementAnimation.java (zero callers outside self-decl)
- rig/exception/DatapackException.java (dead)
- rig/exception/ShaderParsingException.java (dead)
- rig/math/Vec2i.java (dead)
- rig/util/HitEntityList.java (only consumer was HIT_PRIORITY, retiré)

Modifs :
- rig/anim/property/AnimationProperty.java : retire la constante
  AttackPhaseProperty<Priority> HIT_PRIORITY et l'import HitEntityList.Priority.

Docs (3 Javadocs retenues pour surface datapack / Phase 3) :
- CapabilityItem : précise l'usage Phase 3 (data-driven item anims,
  ON_ITEM_CHANGE_EVENT + HumanoidMobPatch.modifyLivingMotionByCurrentItem).
- AnimationProperty.ON_ITEM_CHANGE_EVENT : documenté comme hook datapack
  pour réagir au changement d'item porté.
- MoveCoordFunctions : doc class-level clarifie que les 8 constantes
  MoveCoord* sont consommées par réflection (StaticFieldArgument) côté
  datapack EF tiers — ne pas purger individuellement.

Compile GREEN, 20/20 rig tests GREEN.
2026-04-23 06:02:36 +02:00
notevil
687b810a0e Audit-8 : strip anim core dead methods (-222 LOC)
TransformSheet (-53 LOC, 4 methods): getStartTransform,
getInterpolatedRotation, getCorrectedModelCoord, extendsZCoord.
Skipped: extend (live internal caller in transformToWorldCoordOriginAsDest),
getInterpolationInfo (live internal callers in getInterpolatedTranslation /
getInterpolatedTransform).

ClientAnimator (-44 LOC, 6 methods): getAllLayers, iterVisibleLayers,
isAiming, getOwner, getJumpAnimation, getPriorityFor. Also removed now-unused
java.util.Collection import. Skipped: getCompositeLivingMotion (live callers
in tick/forceResetBeforeAction/resetCompositeMotion), applyBindModifier (live
internal callers in getPose/getComposedLayerPoseBelow/recursion), compareMotion
(live caller in tick), compareCompositeMotion (live caller in tick), iterAllLayers
(live callers in setSoftPause/renderDebuggingInfoForAllLayers), getLivingMotion
(live internal callers in tick/forceResetBeforeAction).

StaticAnimation (-125 LOC, 17 methods): getFileHash instance, idBetween,
in(StaticAnimation[]), in(AnimationAccessor[]), setResourceLocation,
invalidate, isInvalid, removeProperty, addEvents (both overloads),
newTimePair, newConditionalTimePair, addState, removeState,
addConditionalState, addStateRemoveOld, addStateIfNotExist. Also removed
now-unused imports (Collection, Function, Stream).
Skipped @Override/contract methods: getState, getStatesMap, modifyPose,
doesHeadRotFollowEntityHead, getId, equals, getRegistryName, getCoord,
getAccessor (all @Override DynamicAnimation/Object — removing breaks
polymorphism contract). Skipped methods with live callers: getFileHash static
(called in ctor line 116), addProperty (15+ external callers in registry and
AnimationSubFileReader), getModifiedLinkState (3 callers in LinkAnimation),
getSubAnimations (callers in AnimationManager and 5 internal self-calls),
setAccessor (called by AnimationManager.apply line 352), loadAnimation
(called by kept getAnimationClip), createSimulationData (implements
InverseKinematicsProvider interface contract — hot-path risk skip).

20/20 rig tests GREEN.
2026-04-23 05:53:01 +02:00
notevil
06ec7c7c5f Audit-7 : strip ParseUtil + TiedUpRenderTypes dead utilities (-682 LOC)
ParseUtil.java (-232 LOC) :
  Removed 18 zero-caller helpers inherited from Epic Fight :
  toAttributeModifier, nullOrToString, nullOrApply, nvl,
  snakeToSpacedCamel, compareNullables, nullParam, getRegistryName,
  getOrSupply, isParsableAllowingMinus, isParsable, valueOfOmittingType,
  parseOrGet, mapEntryToPair, remove, convertToJsonObject,
  parseCharacterToNumber, parseTagOrThrow.
  Kept toVector3f/toVector3d (unused but reserved for forked callers
  type TrailInfo), toLowerCase, toUpperCase, getBytesSHA256Hash,
  enumValueOfOrNull, orElse, getOrDefaultTag, and the array helpers
  actually wired into JsonAssetLoader / SkinnedMesh / StaticMesh.
  Pruned imports : AttributeModifier, IForgeRegistry, TagParser, Pair,
  JsonOps, ByteTag, CommandSyntaxException, Collectors, Nullable, UUID,
  Function, ArrayList, Collection, Set, Map.

TiedUpRenderTypes.java (-450 LOC) :
  Removed 16 zero-caller render types :
  entityUIColor, entityUITexture, debugCollider, guiTriangle,
  entityAfterimageStencil/Translucent/White, itemAfterimageStencil/
  Translucent/White, blockHighlight, coloredGlintWorldRendertype
  (both overloads), freeUnusedWorldRenderTypes, clearWorldRenderTypes,
  addRenderType(String,ResourceLocation,RenderType), makeTriangulated.
  Also dropped the private fields feeding them : ENTITY_UI_COLORED,
  ENTITY_UI_TEXTURE, OBB, GUI_TRIANGLE, ENTITY_AFTERIMAGE_WHITE,
  ITEM_AFTERIMAGE_WHITE, ENTITY_PARTICLE, ITEM_PARTICLE,
  ENTITY_PARTICLE_STENCIL, ITEM_PARTICLE_STENCIL, BLOCK_HIGHLIGHT,
  WORLD_RENDERTYPES_COLORED_GLINT, plus the newly orphaned
  PARTICLE_SHADER / ShaderColorStateShard / MutableCompositeState
  (only ever used by coloredGlint).
  Kept getTriangulated + replaceTexture (SkinnedMesh / MeshPart / Mesh),
  plus their backing TRIANGULATED_OUTLINE / TRIANGULATED_RENDER_TYPES /
  TRIANGLED_RENDERTYPES_BY_NAME_TEXTURE infra.

debugQuads : DELETED. Only StaticAnimation.renderDebugging /
DynamicAnimation.renderDebugging reference the debug path, and both
were already stripped to empty stubs in Phase 2 ('RIG : debug render
des targets IK strippé. Pas d'IK en TiedUp').

overlayModel : DELETED. PatchedLivingEntityRenderer.getOverlayCoord
explicitly comments out the modifyOverlay path ('RIG Phase 2.5 :
EntityDecorations.modifyOverlay strippé — utilise les defaults
vanilla'), so OVERLAY_MODEL has no reachable caller.

Compile GREEN. 20/20 rig tests GREEN.
2026-04-23 05:38:00 +02:00
notevil
8530671a49 Audit-6 : strip dead math utilities (-300 LOC)
Confirmed-dead method cleanup in rig/math/ following AUDIT_2026-04-22
essentiality cross-check. All removals verified via grep across src/
showing zero live callers.

- MathUtils.java (-242 LOC) : bezierCurve (both overloads), rotWrap,
  lerpDegree, greatest/least (all 6 numeric overloads), translateStack,
  rotateStack, scaleStack (+ their OPEN_MATRIX_BUFFER field),
  lerpMojangVector, setQuaternion, mulQuaternion, getLeastAngleVector
  (idx variant kept, used by ClothSimulator), packColor, unpackColor,
  normalIntValue, wrapClamp, worldToScreenCoord. Unused imports
  (Vector4i, Vector4f, Axis, Camera, Vec2) stripped.

- OpenMatrix4f.java (-56 LOC net) : 3-arg add, mulAsOrigin, createScale,
  ofScale. mulAsOriginInverse (task-KEEP) inlined to no longer depend
  on mulAsOrigin.

- Vec3f.java (-2 LOC net) : toMojangVector, fromMojangVector.

ValueModifier.java NOT deleted despite task instruction : grep found
live references in rig/anim/property/AnimationProperty.java
(4 AttackPhaseProperty constants + import). Task explicitly says
"STOP and report" in this case. The 4 constants have no downstream
readers and could be purged in a follow-up, but that is outside this
task's scope.

20/20 rig tests GREEN (--rerun-tasks verified), ./gradlew compileJava
GREEN after each file group.
2026-04-23 05:26:34 +02:00
notevil
03c28e3332 Audit-3 : align H-04/H-05 signatures vs EF upstream
Deux signatures avaient dérivé du fork EF pendant Phase 0. Les réaligner
maintenant évite du frottement quand on re-porte des fixes EF upstream.

H-04 — ActionAnimation.correctRootJoint
  Avant : correctRootJoint(LinkAnimation linkAnimation, ...)
  Après : correctRootJoint(DynamicAnimation animation, ...)
  LinkAnimation extends DynamicAnimation → widening safe, le call-site
  dans LinkAnimation.modifyPose (ligne 118) passe toujours `this` qui
  type-matche DynamicAnimation sans édition.

H-05 — AttackAnimation.phases
  Avant : public final List<Phase> phases = Collections.emptyList();
  Après : public final Phase[] phases = new Phase[0];
  Le seul caller (JsonAssetLoader.java:477) est un for-each, donc
  array-compatible sans édition. Aucun .size()/.get()/.stream() nulle
  part. Imports java.util.{List,Collections} retirés.

Compile GREEN. 20/20 tests rig GREEN.
2026-04-23 05:03:35 +02:00
notevil
4152f9fc71 Audit-2 : strip EF extension chain + AnimationConfig dead fields (-334 LOC)
H-02 (EF extension chain DEAD) :
- Delete 5 unreferenced EF-derived files :
  - event/EntityPatchRegistryEvent (ModLoader event for third-party patch
    registration — no third-party EF mods to host).
  - event/RegisterResourceLayersEvent (extra renderer layers registration
    event — no consumers).
  - render/layer/LayerUtil (+ empty package) — sole LayerProvider interface
    only used by the deleted RegisterResourceLayersEvent.
  - render/item/RenderItemBase (+ empty package) — abstract weapon renderer
    stub, TiedUp does not host weapon trails.
  - asset/SelfAccessor — AssetAccessor variant never created.
- PatchedRenderersEvent : drop inner classes RegisterItemRenderer (tied to
  RenderItemBase) and Modify (never posted). Keep Add, used by
  TiedUpRenderEngine.
- JsonAssetLoader : remove loadArmature(ArmatureContructor) + its private
  getJoint helper. Armatures are loaded via the GLB path in Phase 1 — this
  EF JSON route has no callers.
- TiedUpRigRegistry : drop inner interface ArmatureContructor (EF-style
  typo preserved) now that loadArmature is gone.
- EntityPatchProvider : drop CUSTOM_CAPABILITIES map + putCustomEntityPatch
  + clearCustom. No third-party extension surface needed ; inline the
  provider lookup to CAPABILITIES only.

H-03 (TiedUpAnimationConfig dead fields) :
- Remove enableOriginalModel, enableAnimatedFirstPersonModel,
  enableCosmetics, enablePovAction, autoSwitchCamera, preferenceWork,
  combatPreferredItems, miningPreferredItems. None had callers — they were
  inherited from EF ClientConfig without a TiedUp consumer.
- Keep activateComputeShader — read at SkinnedMesh.java:249 to toggle the
  GPU skinning path.

Compile green, 20/20 rig tests still green.
2026-04-23 04:55:07 +02:00
notevil
647894508d Audit-1 review : fix stale '6 ticks' comment + track NONE_MASK null
Reviewer flagged two MEDIUM cosmetic items :
- SMELL-001 : TiedUpAnimationRegistry:97 comment still said '6 ticks' —
  updated to '0.15s = 3 ticks' to match the new constant.
- SMELL-002 : JointMaskReloadListener.getNoneMask() can still return
  null if a resource pack omits tiedup:none. Call sites tolerate null
  today; logged in PHASE0_DEGRADATIONS for a Phase 3 fallback JSON +
  warn log.
2026-04-23 04:46:22 +02:00
notevil
f4aae9adb7 Audit-1 : fix transition time + JointMaskReloadListener
C-01 : GENERAL_ANIMATION_TRANSITION_TIME was int 6 while all consumers
expect a float transitionTime in seconds — 6s gave inter-animation
transitions of 120 ticks. Change to float 0.15F (3 ticks).

C-02 : JointMaskReloadListener was never registered, leaving
AnimationSubFileReader callers at lines 170/182/184 to receive null
from getNoneMask()/getJointMaskEntry() with no data loaded. Register
it in V2ClientSetup at LOW priority so it fires after the GLB cache
clear and alongside the other bondage client reload listeners.

M-03 : ASSETS_NEEDED.md JSON example already corrected in the earlier
doc-keeper pass (Torso=7, Chest=8, etc.).

Drive-by: add logs/ to .gitignore — the runClient logs were
accidentally tracked.
2026-04-23 04:41:48 +02:00
notevil
f80dc68c0b Phase 2.8 review fixes : strip player fallback + backlog V3-REW-12-14 + QA edge cases
- BondageAnimationManager : strip total du path joueur, tous les call sites
  'player' retournent null/false avec WARN once-per-UUID. getOrCreateLayer
  court-circuite sur Player direct. Retire dead code factory/furniture
  (FACTORY_ID, FURNITURE_*, npcFurnitureLayers, getPlayerLayer, etc.).
  Javadoc init() reflete la semantique NPC-only.
- DogPoseHelper.applyHeadCompensationClamped : @Deprecated(since=2.8)
  pointant vers V3-REW-07 (dead apres retrait MixinPlayerModel).
- DogPoseRenderHandler.getAppliedRotationDelta + isDogPoseMoving :
  @Deprecated(since=2.8) meme raison.
- Docs (gitignored) : V3_REWORK_BACKLOG.md ajoute V3-REW-12/13/14 (pet bed
  body-lock, human chair yaw clamp, context layer sit/kneel/sneak), tableau
  recap 14 -> 17 items. PHASE2_QA.md ajoute sec 2.5 edge cases + corrige
  le grep pattern 4 -> >=4 lignes. PHASE0_DEGRADATIONS.md ajoute la section
  Phase 2.8 findings.

Compile GREEN. 20/20 tests rig GREEN. Net LOC src : -239 (strip dead code
+ guards player).

Call sites player no-op (attendu par design) :
- PacketSyncPetBedState.playAnimation -> V3-REW-12
- PacketPlayTestAnimation.playAnimation (debug) -> V3-REW-14
- FurnitureClientAnimator.playFurniture -> furniture seat rework V3
2026-04-23 01:10:02 +02:00
notevil
5a39fb0c1c Phase 2.8 : V2 player rendering cleanup + QA checklist doc
Cut wholesale du pipeline V2 player-anim suite au bascule RIG (pas d'opt-in,
pas de rollback — accepté par le mainteneur).

- rm MixinPlayerModel : le renderer RIG patched ne passe plus par
  PlayerModel.setupAnim, donc l'injection @TAIL devenait dead code.
  Les features dog pose head compensation seront ré-exprimées en
  StaticAnimation pose_dog.json (V3-REW-07).
- Strip tiedup.mixins.json : retiré client/MixinPlayerModel, restent
  MixinCamera + MixinLivingEntitySleeping.
- BondageAnimationManager.init() : retiré les 3 PlayerAnimationFactory
  registrations (context / item / furniture), le path joueur n'en
  dépend plus. Factory IDs conservés car les getPlayerLayer*() sont
  tolérantes au null retour via try/catch existants — et restent
  utilisées par le cache fallback remote. Les NPCs continuent
  d'utiliser cette classe via l'accès direct animation stack
  (IAnimatedPlayer.getAnimationStack().addAnimLayer), inchangé.
- TiedUpMod.onClientSetup : suppression de l'appel BondageAnimationManager.init()
  (la méthode est maintenant un log no-op, conservée pour la signature
  publique + doc du changement).
- AnimationTickHandler.onClientTick : retrait de la boucle
  mc.level.players() + updatePlayerAnimation + tickFurnitureSafety +
  cold-cache furniture retry. Les joueurs sont ticked par
  RigAnimationTickHandler (Phase 2.7). Conservé : le cleanup
  périodique ClothesClientCache (hygiène mémoire indépendante), le
  hook onPlayerLogout (cleanup per-UUID des caches NPC restants), et
  le hook onWorldUnload (caches V2 encore utilisés par NpcAnimationTickHandler).
  Imports unused strippés.
- DogPoseHelper : mis à jour la javadoc pour refléter le retrait
  du path player (NPCs only désormais).

Compile GREEN. 20/20 tests rig GREEN.

QA runtime : cf. docs/plans/rig/PHASE2_QA.md (non commit — fichier
working doc sous docs/plans/ gitignored par convention repo).

Net LOC : -276.
2026-04-23 00:42:10 +02:00
notevil
b494b60d60 Phase 2.7 review fixes : resetLoggedErrors wiring + IDLE self-heal + JSON cleanup + tests 2026-04-23 00:27:26 +02:00
notevil
08808dbcc1 Phase 2.7 : animation registry + tick handler + placeholder assets
TiedUpAnimationRegistry.CONTEXT_STAND_IDLE instancié au FMLCommonSetupEvent
via DirectStaticAnimation + armature BIPED. Placeholder JSON 2-keyframes
identity (joueur statique) à assets/tiedup/animmodels/animations/ jusqu'à
ce qu'un export Blender authored remplace. biped.json de même (hiérarchie
identity) placé dans assets/tiedup/armatures/ — parse via JsonAssetLoader
mais pas encore chargé au runtime (l'armature reste procédurale côté Java).

RigAnimationTickHandler tick chaque player visible côté client (phase END) :
- patch.getAnimator().tick() → avance les layers EF
- trigger playAnimation(CONTEXT_STAND_IDLE, 0.2f) quand motion=IDLE et anim
  courante ≠ CONTEXT_STAND_IDLE (idempotent)
- try/catch per-entity avec dedup des erreurs par UUID (pattern
  TiedUpRenderEngine.loggedRenderErrors)
- skip si level null / paused

PlayerPatch.initAnimator bind désormais IDLE → CONTEXT_STAND_IDLE quand le
registry est prêt (fallback EMPTY_ANIMATION si patch construit avant setup).

Voir docs/plans/rig/ASSETS_NEEDED.md pour la spec des assets authored
définitifs (anim idle swing respiration 3 keyframes + offsets biped
anatomiques).
2026-04-23 00:07:32 +02:00
notevil
73264db3c6 Phase 2.6 review fixes : level guard + thread-safety + priority revert + V3-REW-11 track
RISK-001 : early-return si entity.level() == null dans onRenderLiving
  (NPE guard sur detach race despawn / dim teleport). Aligne sur EF.

RISK-004 : clear loggedRenderErrors en tête de onAddLayers() — reset
  du set anti-spam sur reload resources (F3+T), borne aussi la taille
  en session longue.

RISK-005 : remplace HashSet par ConcurrentHashMap.newKeySet() pour
  loggedRenderErrors — render thread + loader thread + test runner
  muteront le set.

RISK-002 : retire priority = EventPriority.HIGH sur onRenderLiving,
  revert au défaut NORMAL aligné EF upstream. Aucun cas concret ne
  justifie HIGH aujourd'hui ; on réévaluera si conflit d'interception
  apparaît Phase 3+. Import EventPriority supprimé.

RISK-003 : tracké V3-REW-11 dans V3_REWORK_BACKLOG.md (inventory GUI
  special-case EF pas porté — rotation/scale/pose preview inventaire).

Docs locaux (gitignored) mis à jour :
  - PHASE0_DEGRADATIONS.md : nouvelle section Phase 2.6 findings
  - V3_REWORK_BACKLOG.md : V3-REW-11 ajouté section MAJEURE

Tests : compileJava GREEN, 18/18 rig tests GREEN.
2026-04-22 23:41:07 +02:00
notevil
987efde86b Phase 2.6 : TiedUpRenderEngine dispatch (RenderLivingEvent hook)
Câble le dispatch renderer RIG via deux subscribers auto-registered (split
MOD bus pour AddLayers, FORGE bus pour RenderLivingEvent.Pre) — pas besoin
de wiring explicite dans TiedUpMod.

onAddLayers (MOD bus) : construit la map entityRendererProvider avec
EntityType.PLAYER → TiedUpPlayerRenderer (Phase 2) ; poste
PatchedRenderersEvent.Add pour extensions tierces.

onRenderLiving (FORGE bus, priority HIGH) : filtre strict instanceof
Player || AbstractTiedUpNpc (protège MCA villagers cf. V3-REW-10) ;
vérifie patch.overrideRender() ; dispatche vers PatchedEntityRenderer et
cancel l'event. Try/catch robuste : log WARN une seule fois par UUID sur
exception, fallback vanilla (event non-canceled).

3 tests unitaires (pure-logic, sans MC runtime) : null-safety du filtre
et idempotence du reset. Le dispatch complet sera validé Phase 2.8
runClient smoke test.

Le biped armature étant identity (Phase 2.4 stub), le hook rendra le
player effondré à l'origine dès qu'il s'active — attendu, warn déjà en
place depuis Phase 2.5.
2026-04-22 23:28:45 +02:00
notevil
d129983eb7 Phase 2.5 review fixes : P0 biped warn + CME guard + double-draw prevention
P0-BUG-001 : LOGGER.warn dans TiedUpArmatures.Holder static init quand les
joints biped sont en identity (Phase 2.4 stub). Sans ça, Phase 2.6 renderer
afficherait un mesh effondré sans signal dev. Warn apparaît une seule fois
(class-init lock JVM).

P0-BUG-002 : Lists.newArrayList(renderer.layers) copie défensive dans
PatchedLivingEntityRenderer.renderLayer. Mods tiers (Wildfire, SkinLayers3D,
Cosmetic Armor) peuvent muter renderer.layers runtime via AddLayers event
-> CME garantie sans copie. TiedUp va au-dela d'EF upstream.

P1-RISK-001 : Pose.orElseEmpty(name) utilise Map.getOrDefault qui retourne
un JointTransform.empty() detache non stocke dans la map -> toute mutation
.frontResult est perdue si le joint est absent. En pratique l'Animator peuple
toujours Head via getComposedLayerPose, mais defensif : on recupere l'instance,
mute, puis pose.putJointData(Head, mutatedTransform). Meme bug latent en EF
upstream.

P1-RISK-003 : PlayerItemInHandLayer extends ItemInHandLayer mais le dispatch
patchedLayers.containsKey(layer.getClass()) est strict -> layer player-specifique
passe au travers. Ajout du mapping explicite dans TiedUpPlayerRenderer
constructor avec le meme stub PatchedItemInHandLayer. Preventif Phase 3.

P2-RISK-004 : @SuppressWarnings scope resserre des 2 methodes aux 2 call sites
dispatch raw PatchedLayer qui en ont reellement besoin. Commentaire pointant
vers l'invariant runtime addPatchedLayer.

Tests : 15/15 rig tests GREEN, compile GREEN.
Doc : docs/plans/rig/PHASE0_DEGRADATIONS.md section Phase 2.5 findings ajoutee
(fichier gitignore, changements locaux pour future session).
2026-04-22 23:16:35 +02:00
notevil
8dff4c0e03 WIP Phase 2.5 : fork render pipeline (PatchedEntityRenderer family)
Fork EF client renderer patched/entity/ → rig/render/, imports rewrités.

Nouveaux fichiers :
 - render/PatchedEntityRenderer.java   (~96 LOC, était stub 16 LOC)  — base abstraite,
   mulPoseStack + setArmaturePose + nameplate via mixin invoker
 - render/PatchedLivingEntityRenderer.java  (~230 LOC, EF 294 → stripped)  — hot path
   render, strippé du JSON customLayers loading + EntityDecorations (pas impl. côté
   LivingEntityPatch TiedUp)
 - render/PHumanoidRenderer.java       (~95 LOC, EF 53 → adapté)  — intermediate
   biped, baby head scale, enregistre PatchedArmorLayer + PatchedItemInHandLayer
 - render/TiedUpPlayerRenderer.java    (~95 LOC, EF PPlayerRenderer 60 → adapté)  —
   dispatch slim/default (Meshes.ALEX vs BIPED), propage modelParts visible flags,
   skip cape/bee/arrow layers (V3-REW-08/09)
 - render/PatchedLayer.java            (~55 LOC)  — base no-op layer
 - render/PatchedArmorLayer.java       (~45 LOC)  — stub no-op (V3-REW-04)
 - render/PatchedItemInHandLayer.java  (~45 LOC)  — stub no-op (Phase 3)
 - mixin/client/MixinEntityRenderer.java       (~35 LOC)  — invoker shouldShowName +
   renderNameTag (EF verbatim)
 - mixin/client/MixinLivingEntityRenderer.java (~30 LOC)  — invoker isBodyVisible,
   getRenderType, getBob

Modifs :
 - resources/tiedup-rig.mixins.json : enregistre les 2 nouveaux mixins client

Dépendances stubbées :
 - PatchedArmorLayer / PatchedItemInHandLayer no-op — "mangent" le layer vanilla
   équivalent pour éviter double-rendu, mais ne dessinent rien. À implémenter
   Phase 3 (armor : V3-REW-04 ; item-in-hand : tool joints GLB).
 - EntityDecorations (color/light/overlay modifiers) strippées du render path —
   le patch TiedUp n'expose pas encore cette API. Hurt/death overlay natifs
   conservés. À rework si feature premium glow/tint NPC.

TODOs Phase 2.6 / Phase 3 :
 - animated_layers/*.json datapack-driven loading (strippé du constructor) si
   besoin futur
 - PlayerItemInHandLayer (version player-spécifique, distincte de ItemInHandLayer
   générique) pas encore mangée côté player → layer vanilla continue de draw
 - ElytraLayer / CustomHeadLayer pas mangés (cohérent "on mange juste les layers
   qui entrent en conflit avec le mesh skinné")

Surprise pendant le fork :
 - ItemInHandLayer vanilla MC 1.20.1 exige M extends EntityModel<E> & ArmedModel,
   pas HeadedModel comme PatchedItemInHandLayer EF 1.20.1 pouvait le suggérer
   via ses anciennes versions. Alignement forcé sur ArmedModel.
 - EF utilise 2 mixins @Invoker (MixinEntityRenderer / MixinLivingEntityRenderer)
   pour accéder aux méthodes protected de vanilla — forkés verbatim dans
   rig/mixin/client/.

Tests :
 - ./gradlew compileJava : BUILD SUCCESSFUL
 - ./gradlew test --tests "com.tiedup.remake.rig.*" : 15/15 GREEN
   (TiedUpArmaturesTest 4 + GlbJointAliasTableTest 4 + GltfToSkinnedMeshTest 6 =
   14 rig + 1 utilitaire)
2026-04-22 22:39:41 +02:00
notevil
39f6177595 Phase 2.4 review fixes : P0-BUG-001 joint order + P0-BUG-002 singleton + tests
P0-BUG-001 — Ordre IDs joints Elbow/Hand/Tool ≠ EF
  TiedUpArmatures.buildBiped() assignait Arm_R=11, Elbow_R=12, Hand_R=13,
  Tool_R=14 (idem gauche 16/17/18/19) alors que VanillaModelTransformer.RIGHT_ARM
  encode upperJoint=11, lowerJoint=12, middleJoint=14 — donc Arm_R=11, Hand_R=12,
  Elbow_R=14. Résultat : lowerJoint=17 (SimpleTransformer qui attache les
  vertices à Hand) pointait en fait vers Elbow_L → bras tordus au rendu.
  Fix : réassigner les IDs dans buildBiped() pour matcher le layout EF
  (Arm_R=11, Hand_R=12, Tool_R=13, Elbow_R=14 ; symétrique 16-19).
  VanillaModelTransformer non touché (source de vérité EF).

P0-BUG-002 — Singleton BIPED non thread-safe
  if (BIPED_INSTANCE == null) BIPED_INSTANCE = buildBiped() est un double-init
  race. En SP intégré (client + server threads sur la même JVM), deux threads
  peuvent entrer simultanément dans le if null et créer deux HumanoidArmature
  distincts — pose matrices incohérentes selon les call sites.
  Fix : Holder idiome (static inner class). Le class-init lock JVM garantit
  (JLS §12.4.1) qu'une seule init, visible à tous les threads, sans
  synchronized ni volatile. Zero overhead après init.

P1 — Nouveau TiedUpArmaturesTest (4 tests)
  - bipedHas20Joints : BIPED.get().getJointNumber() == 20
  - searchBipedJointByNameReturnsNonNull : vérifie les 20 noms EF
  - jointIdsMatchEfLayout : verrou P0-BUG-001 (id=12→Hand_R, id=14→Elbow_R,
    etc.) — aurait attrapé le bug en review initiale
  - bipedSingleton : BIPED.get() == BIPED.get() (verrou P0-BUG-002)

P2 backlog tracé dans docs/plans/rig/PHASE0_DEGRADATIONS.md :
  biped collapsed visuellement jusqu'à Phase 2.7, PlayerPatch.yBodyRot sans
  lerp, ridingEntity non géré, isFirstPersonHidden nommage ambigu,
  ServerPlayerPatch hérite méthodes client-only sans @OnlyIn.

Tests : 15 GREEN (11 bridge pré-existants + 4 nouveaux biped).
Compile clean.
2026-04-22 22:27:01 +02:00
notevil
79fc470aa0 Phase 2.4 : étoffer PlayerPatch stubs + biped armature procédurale
PlayerPatch : getArmature→TiedUpArmatures.BIPED, overrideRender→true,
getModelMatrix avec PLAYER_SCALE=15/16, initAnimator bind IDLE→EMPTY.
ServerPlayerPatch : ré-override overrideRender→false (le serveur ne rend pas).
ClientPlayerPatch : stub isFirstPersonHidden() pour Phase 3 (V3-REW-01).
LocalPlayerPatch : override render→false si on est soi-même en first-person
(laisse rendre les bras vanilla), true sinon.

TiedUpArmatures.BIPED : HumanoidArmature procédurale 20 joints identity
(Root → Thigh/Leg/Knee_R/L, Torso → Chest → Head + Shoulder/Arm/Elbow/
Hand/Tool_R/L). Phase 2.7 remplacera par JSON Blender-authored.

Fixe P2-RISK-01 (InitAnimatorEvent listeners NPE si getArmature()=null).

Tests : 11 bridge tests GREEN, full suite GREEN, compile clean.
2026-04-22 22:12:44 +02:00
notevil
ccec6bd87e Phase 2.3 review fixes : P2-BUG-01/02/03 + RISK-02
Résout les findings P0 remontés par la review post-Phase 2.3 (@4780c96).

P2-BUG-01 — Pipeline RIG dormant au runtime
  EntityPatchProvider.registerEntityPatches() et registerEntityPatchesClient()
  étaient définis mais JAMAIS appelés depuis TiedUpMod setups → CAPABILITIES
  map vide → aucun patch ne se crée, pipeline entier dormant.
  Fix :
  - commonSetup : event.enqueueWork(EntityPatchProvider::registerEntityPatches)
  - ClientModEvents.onClientSetup : event.enqueueWork(
    EntityPatchProvider::registerEntityPatchesClient) avant BondageAnimationManager.init

P2-BUG-02 — Memory leak LazyOptional non invalidée
  EntityPatchProvider.optional jamais invalidated → chaque respawn/dim change
  fuit patch + animator + armature.
  Fix :
  - EntityPatchProvider.invalidate() public méthode qui appelle optional.invalidate()
  - TiedUpCapabilityEvents.attachEntityCapability : event.addListener(provider::invalidate)
    après addCapability. Pattern aligné sur V2BondageEquipmentProvider existant.

P2-BUG-03 — NPE latent HumanoidModelBaker.bakeArmor
  Ligne 88 retournait entityMesh.getHumanoidArmorModel(slot).get() mais le
  getter retourne null (Phase 0 strip S-03 : Meshes.HELMET/CHESTPLATE/LEGGINS/BOOTS
  strippés). Null-check + fallback null + commentaire pointant vers V3-REW-04.

P2-RISK-02 — @OnlyIn(Dist.CLIENT) sur transformers
  VanillaModelTransformer, HumanoidModelTransformer, HumanoidModelBaker
  importent HumanoidModel/PoseStack (client-only). Risque NoClassDefFoundError
  si code serveur touche HumanoidModelBaker.VANILLA_TRANSFORMER static field.
  Fix : @OnlyIn(Dist.CLIENT) sur les 3 classes.

P2-RISK-01/03 + SMELL-01/02/03 tracés dans docs/plans/rig/PHASE0_DEGRADATIONS.md
Phase 2.1-2.3 findings section.

Compile BUILD SUCCESSFUL + 11 tests bridge GREEN maintenus.
2026-04-22 21:42:22 +02:00
notevil
faad0ced0f Phase 2.3 : capability system RIG (EntityPatchProvider + events)
5 classes ajoutées dans rig/patch/ :

- TiedUpCapabilities.java
  Holder du Capability<EntityPatch> CAPABILITY_ENTITY (CapabilityToken
  auto-register) + helpers getEntityPatch / getPlayerPatch /
  getPlayerPatchAsOptional. Simplifié de EF (pas de ITEM/PROJECTILE/SKILL
  caps, combat only).

- EntityPatchProvider.java
  ICapabilityProvider + Map<EntityType, Function<Entity, Supplier<EntityPatch<?>>>>.
  registerEntityPatches() pour commonSetup (EntityType.PLAYER seul Phase 2),
  registerEntityPatchesClient() pour clientSetup (dispatch LocalPlayerPatch vs
  ClientPlayerPatch<RemotePlayer> vs ServerPlayerPatch). CUSTOM_CAPABILITIES
  pour extensions futures. Pas de GlobalMobPatch combat fallback.
  IMPORTANT : n'enregistre PAS EntityType.VILLAGER (MCA conflict V3-REW-10).

- TiedUpCapabilityEvents.java
  @Mod.EventBusSubscriber sur AttachCapabilitiesEvent<Entity>. Check oldPatch
  pour éviter double-attach, construit provider, appelle onConstructed eager
  (D-01 pattern EF), addCapability. Priority NORMAL (order d'attachement
  ne matière pas, c'est les runtime cross-cap reads qui importent et ceux-là
  sont déjà lazy dans onConstructed).

3 stubs PlayerPatch subclasses (placeholders Phase 2.4) :

- ServerPlayerPatch : overrideRender=false, getArmature=null stub, updateMotion no-op
- ClientPlayerPatch<T extends AbstractClientPlayer> : overrideRender=true, @OnlyIn CLIENT
- LocalPlayerPatch extends ClientPlayerPatch<LocalPlayer> : vide pour l'instant

Ces stubs satisfont le compile de EntityPatchProvider.registerEntityPatchesClient().
Le getArmature() null est non-bloquant Phase 2.3 mais devra être fixé Phase 2.4
pour le vrai rendering (lien avec TiedUpRigRegistry.BIPED à créer Phase 2.7).

Compile BUILD SUCCESSFUL + 11 tests bridge GREEN maintenus.
2026-04-22 21:23:01 +02:00
notevil
3aec681436 Phase 2.2 : fork mesh/transformer/ (vanilla PlayerModel → SkinnedMesh)
COPY verbatim EF + rewrite imports :
- rig/mesh/transformer/VanillaModelTransformer.java (~700 LOC)
  Transformer principal : bake PlayerModel vanilla / HumanoidModel → SkinnedMesh biped
  EF. Utilise les PartTransformer (HEAD/ARM/LEG/CHEST) avec AABB cover area.
- rig/mesh/transformer/HumanoidModelTransformer.java (~70 LOC)
  Base abstract commune aux transformers humanoïdes.
- rig/mesh/transformer/HumanoidModelBaker.java (~115 LOC)
  Entry point bake() + export JSON + registry MODEL_TRANSFORMERS.

L'ancienne stub de VanillaMeshPartDefinition (record 55 LOC) est remplacée par
la vraie record dans le fork — API identique (of(partName), of(partName, path,
invertedParentTransform, root)).

Ajouté mixin accessor :
- rig/mixin/client/MixinAgeableListModel.java (@Invoker pour headParts/bodyParts
  sur AgeableListModel).
- src/main/resources/tiedup-rig.mixins.json (nouveau mixin config, package
  com.tiedup.remake.rig.mixin).
- build.gradle : args '-mixin.config=tiedup-rig.mixins.json' dans client+server
  run configs.
- META-INF/mods.toml : [[mixins]] config="tiedup-rig.mixins.json"

Logger EpicFightMod.LOGGER → TiedUpRigConstants.LOGGER dans HumanoidModelBaker.
Packages correctement rewrités par scripts/rig-rewrite-imports.sh. Compile
BUILD SUCCESSFUL maintenu.
2026-04-22 20:59:32 +02:00
notevil
4a587b7478 Phase 2.1 : D-01 fix — LivingEntityPatch animator field + eager init
Close le trap CRITIQUE tracé dans docs/plans/rig/PHASE0_DEGRADATIONS.md D-01
(getAnimator()=null → NPE garanti sur premier appel à MoveCoordFunctions:202,263).

Pattern EF-style conforme LivingEntityPatch EF:146-156 :
- protected Animator animator field
- Override onConstructed(T) : super + factory apply + initAnimator + postInit
- initAnimator(Animator) hook no-op pour subclasses (bind LivingMotion→Anim)
- getAnimator() retourne le field (non-null après onConstructed)
- getClientAnimator() : cast conditionnel instanceof ClientAnimator

Factory TiedUpRigConstants.ANIMATOR_PROVIDER (déjà en place, pattern lazy
method-ref client/server split) fournit la bonne instance selon Dist.

Compile + tests GREEN maintenus (11 tests bridge).
2026-04-22 20:31:00 +02:00
notevil
4d90a87b48 Phase 1 polish : SMELL-001, DOC-001, TEST-001 fixes
Résout les 3 items remontés par la review globale pré-Phase 2 :

SMELL-001 — TiedUpRigConstants.ANIMATOR_PROVIDER
  Le ternaire retournait ServerAnimator::getAnimator dans les 2 branches
  alors que ClientAnimator est maintenant forké (présent dans rig/anim/client/).
  Switch vers ClientAnimator::getAnimator côté client (pattern lazy method-ref
  préserve la non-chargement sur serveur dédié).

DOC-001 — AnimationManager:211
  Commentaire ambigu "SkillManager.reloadAllSkillsAnimations() strippé"
  clarifié : préciser que l'appel upstream EF venait de yesman.epicfight.skill.*
  et que le combat system est hors scope TiedUp.

TEST-001 — GltfToSkinnedMeshTest coverage gaps
  Tests précédents utilisaient [1,0,0,0] → drop trivial, renorm no-op.
  Ajoute 3 tests :
  - convertDropsLowestWeightAndRenormalizes : poids [0.5, 0.3, 0.15, 0.05]
    force le drop du plus faible (0.05) + renorm des 3 restants.
  - convertHandlesZeroWeightVertex : weights tous-zéro → fallback Root w=1.
  - convertFallsBackToRootForUnknownJointName : joint GLB inconnu ("TentacleJoint42")
    → log WARN + fallback Root id=0 sans crash.

11 tests bridge GREEN (5 alias + 6 convert). Compile BUILD SUCCESSFUL.
2026-04-22 19:58:51 +02:00
notevil
29c4fddb90 Phase 1.4 : tests unitaires bridge GLB→SkinnedMesh
8 tests GREEN :

GlbJointAliasTableTest (5) :
- mapLegacyPlayerAnimatorNames : body→Chest, leftUpperArm→Arm_L,
  leftLowerArm→Elbow_L, leftUpperLeg→Thigh_L, leftLowerLeg→Knee_L, etc.
- isCaseInsensitive : BODY/LeftUpperArm/leftupperarm tous remappés
- bypassBipedNames : Arm_L, Elbow_R, Head, Chest, Torso, Root non transformés
- unknownReturnsNull : null pour nom inconnu / vide / null
- isBipedJointNameDetection : _R/_L suffix + Root/Torso/Chest/Head

GltfToSkinnedMeshTest (3) :
- convertSyntheticGltfDoesNotThrow : 3 vertices + armature biped minimale (4
  joints manuels Root→Chest→{Arm_L,Arm_R}) → SkinnedMesh non null
- convertSyntheticGltfHasExpectedParts : partName dérivé du materialName de la
  primitive glTF
- convertThrowsOnNullArmature : IllegalStateException si armature null

Fixture : buildMinimalArmature() construit une hiérarchie 4 joints via Joint()
+ addSubJoints() + Armature(name, count, root, jointMap).bakeOriginMatrices().
buildSyntheticGltf() produit un triangle 3-vertices avec jointNames
(body, leftUpperArm, rightUpperArm) pour tester le mapping PlayerAnimator→EF.
2026-04-22 19:08:05 +02:00
notevil
94fcece05a Phase 1.1-1.3 : bridge GLB → SkinnedMesh
Premier jalon Phase 1 : conversion d'un GltfData (format legacy 11-joints
PlayerAnimator) vers SkinnedMesh Epic Fight (biped ~20 joints).

Files :
- rig/bridge/GlbJointAliasTable.java : table mapping statique PlayerAnimator
  → biped EF (body/torso→Chest, leftUpperArm→Arm_L, leftLowerArm→Elbow_L, etc).
  Fallback Root pour inconnus. Bypass si nom déjà biped (Root/Torso/Chest/Head
  ou suffixe _R/_L).
- rig/bridge/GltfToSkinnedMesh.java : convert(GltfData, AssetAccessor<Armature>)
  → SkinnedMesh. Pré-calcule jointIdMap, boucle vertices (pos/normal/uv + drop 4th
  joint à plus faible poids + renormalise 3 restants), groupe indices par
  primitive (material) en VanillaMeshPartDefinition.

Note : animations GLB ignorées (scope Phase 4 JSON EF authored).

Compile BUILD SUCCESSFUL maintenu.
2026-04-22 14:28:37 +02:00
notevil
4a615368df Phase 0 audit findings : fixes D-07 + D-08 post-verification
Cross-check de l'audit Phase 0 contre les sources EF (4 agents) a remonté :

D-08 RÉFUTÉ partiellement :
  - EF LivingEntityPatch.getTarget() = getLastHurtMob() — identique à notre stub.
  - MAIS MobPatch.getTarget() override avec Mob.getTarget() manquait chez nous.
  - Fix : override ajouté, ref commentée à EF MobPatch.java:171-174.
  - Sans ça, MoveCoordFunctions.MOB_ATTACK_TARGET_LOOK aurait la mauvaise
    sémantique (dernier mob qui m'a frappé vs cible AI courante) → NPC ne
    tourne pas vers sa cible pendant attack anim.

D-07 VALIDÉ :
  - correctRootJoint zero-out X/Z de la Root en espace monde pour éviter
    sliding visuel pendant LinkAnimation vers ActionAnimation.
  - Safe Phase 1 (idle/walk = StaticAnimation, pas ActionAnimation).
  - Critical Phase 2+ dès qu'une vraie ActionAnimation bondage est jouée.
  - Fix : dev assertion LOGGER.warn en IS_DEV_ENV pointant vers
    PHASE0_DEGRADATIONS.md D-07. Empêche découverte tardive.

Autres findings post-vérification (traçés en doc gitignorée) :
  D-01 getAnimator()=null : fix Phase 2 (pas Phase 1) — field protected EF-style
  D-02 sync() stripped : FAUX-POSITIF partiel — BEGINNING_LOCATION non affecté
       (IndependentVariableKey non-synced), seul DESTINATION en MP dédié
  D-03 InstantiateInvoker throws : swallowed par try/catch, silent no-op
  D-04 Patch suppression : doc EXTRACTION.md §3.12/§10 corrigée (Option A)
  D-05 reloadAllSkillsAnimations : était déjà dans SkillManager (commentaire OK)
  D-06 playAnimationAt : ARCHITECTURE.md §5.5.1 pseudocode (signature fantôme)
       → notes ajoutées pointant vers D-06
  D-09 AnimationBegin/EndEvent : listeners EF uniquement skill system interne,
       ON_BEGIN/END_EVENTS data-driven continuent de fonctionner
  D-10 AT 127 lignes : ~50% utile (GUI TiedUp existant), ne pas fix maintenant

IK stack (S-05 pas dans les docs) : section R12 ajoutée à ARCHITECTURE.md §11.

Compile BUILD SUCCESSFUL maintenu (0 errors).
2026-04-22 03:39:44 +02:00
notevil
1cef57a472 Phase 0 : compile SUCCESS (464 -> 0 errors)
Core data model du rig EF extractible compile désormais cleanly.

Changements clé :

1. AccessTransformer wiring (-80 errors)
   - Copie EF accesstransformer.cfg dans resources/META-INF/
   - Uncomment accessTransformer = file(...) dans build.gradle
   - Débloque l'héritage des package-private RenderType.CompositeState +
     RenderType.CompositeRenderType + RenderType.OutlineProperty nécessaires
     à TiedUpRenderTypes.

2. Stubs compat rendering Phase 2
   - PatchedEntityRenderer<E,T,M,R> : type param 4 pour PrepareModelEvent
   - RenderItemBase : type marker pour PatchedRenderersEvent.RegisterItemRenderer
   - LayerUtil + LayerProvider : interface fonctionnelle 5-params pour RegisterResourceLayersEvent
   - PlayerPatch<T extends Player> : extends LivingEntityPatch
   - ToolHolderArmature interface : leftTool/rightTool/backToolJoint()

3. Stubs compat combat Phase 2+
   - AttackResult + ResultType enum : utilisé comme type pour StateFactor ATTACK_RESULT
   - TrailInfo record : stubbé avec playable=false → particle trail jamais émis
   - AttackAnimation.Phase.hand = InteractionHand.MAIN_HAND
   - AttackAnimation.JointColliderPair : stub pour instanceof check
   - AttackAnimation.getPhaseByTime(float) : retourne Phase neutre
   - ActionAnimation.correctRootJoint() : no-op Phase 0
   - ActionAnimation.BEGINNING_LOCATION + INITIAL_LOOK_VEC_DOT re-exposés comme AnimationVariables

4. Physics types alignés
   - InverseKinematicsProvider extends SimulationProvider<...>
   - InverseKinematicsSimulator implements PhysicsSimulator<Joint, ...>
   - InverseKinematicsObject implements SimulationObject<...>
   - InverseKinematicsBuilder extends SimulationObject.SimulationObjectBuilder
   - ik.bake() signature : (Object, Object, boolean, boolean) conforme StaticAnimation usage

5. Mesh/compute stubs
   - ComputeShaderSetup.TOTAL_POSES + TOTAL_NORMALS : OpenMatrix4f[MAX_JOINTS] pool
   - ComputeShaderSetup.MeshPartBuffer inner class + destroyBuffers()
   - ComputeShaderProvider.supportComputeShader() = false
   - VanillaModelTransformer.VanillaMeshPartDefinition record minimal
   - HumanoidMesh.getHumanoidArmorModel() : return null (armor rendering Phase 2)

6. Fixes typage / API
   - TiedUpRenderTypes.prefix("x").toString() x15 : ResourceLocation -> String
   - AnimationManager Logger : log4j -> slf4j
   - TiedUpRigConstants.logAndStacktraceIfDevSide 4-arg overload + Throwable instead of RuntimeException
   - LivingEntityPatch.getReach(InteractionHand) overload
   - StaticAnimation(boolean, String, AssetAccessor) 3-arg overload

Result : compileJava -> BUILD SUCCESSFUL
Prochain jalon : runClient + verify rig se charge sans crash.
2026-04-22 03:16:14 +02:00
notevil
bdbd429bdf WIP: fork patch/collider/codec stubs, 464->135 compile errors
Phase 0 compile progress (70% reduction). Core data model compile :

Refs yesman.epicfight strippées (hors 4 javadocs) :
- AnimationProperty : combat properties EXTRA_DAMAGE, STUN_TYPE, PARTICLE
- ClientAnimator : playAnimationAt(..., AnimatorControlPacket.Layer, Priority)
- ClothSimulator : OBBCollider -> fork geometry-only dans rig/collider/
- InstantiateInvoker : Collider, ColliderPreset, Armatures, DatapackEditScreen
- MoveCoordFunctions : GrapplingAttackAnimation
- SimulationTypes : InverseKinematicsSimulator (path rewrite)

Stubs patch/ :
- EntityPatch<T> abstract — getOriginal, isLogicalClient, getMatrix, getAngleTo
- LivingEntityPatch<T> abstract — getAnimator, getArmature, getTarget, getYRot*
- MobPatch<T extends Mob> — instanceof check only
- item/CapabilityItem — type marker

Forks utilitaires :
- rig/collider/OBBCollider — geometry only (strip Entity collision, drawInternal)
- anim/types/StateSpectrum — identique EF, imports rewrités
- util/PacketBufferCodec — StreamCodec backport
- util/TimePairList — identique EF
- util/HitEntityList — shell pour Priority enum uniquement
- util/ExtendableEnum + ExtendableEnumManager — register/assign enum

Fix package declarations :
- armature/Joint.java + JointTransform.java : package rig.anim -> rig.armature
- Imports JointTransform ajoutés dans anim/{Pose,Keyframe,TransformSheet}

Residu 135 errors = cluster rendering (Phase 2) :
- render/TiedUpRenderTypes (17) : CompositeState package-private MC
- event/PatchedRenderersEvent (11) : missing PatchedEntityRenderer
- mesh/SkinnedMesh (13) : VanillaMeshPartDefinition, compute shader fields
- asset/JsonAssetLoader (6), anim/LivingMotion (5)
2026-04-22 02:45:18 +02:00
notevil
f0d8408384 WIP: stub ClientConfig + gameasset registries, strip Meshes mobs
Nouveaux stubs core :
- TiedUpAnimationConfig     — remplace yesman.epicfight.config.ClientConfig.
                              Flags animation/rendu, no-op pour combat.
- TiedUpRigRegistry         — remplace gameasset.Animations.EMPTY_ANIMATION +
                              gameasset.Armatures.ArmatureContructor.

Fichiers forkés additionnels (dépendances transitives découvertes) :
- anim/types/DirectStaticAnimation.java   (EMPTY_ANIMATION est un DirectStaticAnimation)
- event/InitAnimatorEvent.java            (postInit() forge event)
- event/EntityPatchRegistryEvent.java     (mod bus event pour register patches)

Strip combat :
- Meshes.java : retiré les 11 mob meshes (CreeperMesh, DragonMesh, VexMesh,
                WitherMesh, etc.) + armor + particle + cape. Garde BIPED
                et ALEX / BIPED_OLD_TEX / BIPED_OUTLAYER (variants joueur).
- Animator.playDeathAnimation : Animations.BIPED_DEATH (ARR asset) →
                                EMPTY_ANIMATION fallback.
- AnimationManager.apply : Armatures.reload() stripped (no-op, à rebrancher
                           Phase 2 sur TiedUpArmatures).
- ClientPlayerPatch.entityPairing : body entier strippé (combat skills
                                    Technician/Adrenaline/Emergency Escape).

sed global : ClientConfig.* → TiedUpAnimationConfig.*
sed global : Animations.EMPTY_ANIMATION → TiedUpRigRegistry.EMPTY_ANIMATION
sed global : Armatures.ArmatureContructor → TiedUpRigRegistry.ArmatureContructor

Résidus yesman.epicfight : 86 → 74 (-12)
Reste : physics (16) + network (13) + world combat (10) + particle (3) +
collider (2) + client misc (2) + skill (2). Tous combat-entangled,
demandent strip méthode par méthode.
2026-04-22 00:53:42 +02:00
notevil
324e7fb984 WIP: create TiedUpRigConstants, replace EpicFightMod/SharedConstants refs
- Nouveau TiedUpRigConstants.java : centralise MODID/LOGGER/identifier/prefix,
  constantes runtime (IS_DEV_ENV, A_TICK, GENERAL_ANIMATION_TRANSITION_TIME,
  MAX_JOINTS), factory ANIMATOR_PROVIDER (client/server split) + helpers
  stacktraceIfDevSide/logAndStacktraceIfDevSide.

- sed global : EpicFightMod.* → TiedUpRigConstants.*
- sed global : EpicFightSharedConstants.* → TiedUpRigConstants.*
- sed global : EpicFightRenderTypes → TiedUpRenderTypes (class rename upstream)
- Fix package declarations : Armature.java + TiedUpRenderTypes.java

Résidus yesman.epicfight : 115 → 86 (-29)
Reste : gameasset/physics/network/world/config/skill (combat deps à strip) +
combat mode refs dans patch/LocalPlayerPatch + ClientPlayerPatch (Phase 2).
2026-04-22 00:33:39 +02:00
notevil
cbf61906e0 WIP: initial epic fight core extraction (Phase 0)
83 files forkés d'Epic Fight (~18k LOC). Base non-compilable en l'état.

Contenu extrait :
- math/ — OpenMatrix4f, Vec3f/4f/2f, QuaternionUtils, MathUtils, ...
- armature/ — Armature, Joint, JointTransform, HumanoidArmature
- anim/ — Animator, ServerAnimator, ClientAnimator, LivingMotion, ...
- anim/types/ — StaticAnimation, DynamicAnimation, MovementAnimation, LinkAnimation,
                ConcurrentLinkAnimation, LayerOffAnimation, EntityState
- anim/client/ — Layer, ClientAnimator, JointMask
- mesh/ — SkinnedMesh, SingleGroupVertexBuilder, Mesh, HumanoidMesh, ...
- cloth/ — AbstractSimulator, ClothSimulator (dépendance transitive de StaticMesh)
- asset/ — JsonAssetLoader, AssetAccessor
- patch/ — EntityPatch, LivingEntityPatch, PlayerPatch, ClientPlayerPatch
- util/ — ParseUtil, TypeFlexibleHashMap
- exception/ — AssetLoadingException
- event/ — PatchedRenderersEvent, PrepareModelEvent, RegisterResourceLayersEvent
- render/ — TiedUpRenderTypes

Headers GPLv3 + attribution injectés sur tous les .java.
Package declarations fixées sur Armature.java et TiedUpRenderTypes.java.

115 imports résiduels à résoudre manuellement :
- yesman.epicfight.main (EpicFightMod, EpicFightSharedConstants) — 30
- yesman.epicfight.gameasset (Animations, Armatures, EpicFightSounds) — 12
- yesman.epicfight.api.physics + physics.ik (combat physics) — 16
- yesman.epicfight.network.* (combat packets) — 13
- yesman.epicfight.world.* (combat entity logic) — 10
- yesman.epicfight.config.ClientConfig — 3
- yesman.epicfight.skill, .client.gui, .particle, .collider — divers combat/UI

Stratégie fix (2-3 sem manuel) : strip usage combat, stubs pour refs
core (EpicFightMod → TiedUpMod, SharedConstants → TiedUpRigConstants,
ClientConfig → TiedUpAnimationConfig).
2026-04-22 00:26:29 +02:00
notevil
b141e137e7 add rig extraction scripts
scripts/rig-rewrite-imports.sh  — package rewrites yesman.epicfight.* → com.tiedup.remake.rig.*
scripts/rig-headers.sh          — GPLv3 + attribution Epic Fight header injection
scripts/rig-extract-phase0.sh   — master script Phase 0 (copy + rewrite + headers)

Cf. docs/plans/rig/EXTRACTION.md §9 (docs restent locales).
2026-04-22 00:26:09 +02:00
notevil
b0b719b3dd relicense to GPL-3.0-or-later
Drop Commons-Clause and monetization restrictions to enable
incorporating third-party GPLv3 code in upcoming rig system work.

Prior versions (0.1.0–0.5.x) remain under GPL-3.0 WITH Commons-Clause.
From 0.6.0-ALPHA, GPL-3.0-or-later pure.
2026-04-22 00:13:50 +02:00
5d0c7c6c69 Merge pull request 'Polish V2 subsystem: lockpick kinds, package boundaries, client extractions' (#22) from refactor/v2-polish into develop
Reviewed-on: #22
2026-04-19 00:06:54 +00:00
162 changed files with 22200 additions and 761 deletions

1
.gitignore vendored
View File

@@ -40,6 +40,7 @@ package-lock.json
# Build logs
build_output.log
logs/
# OS files
.DS_Store

102
LICENSE
View File

@@ -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.

View File

@@ -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

View File

@@ -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
View 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
View 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
View 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

View File

@@ -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(
@@ -209,23 +167,11 @@ public class BondageAnimationManager {
);
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()
);
}
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;
}
// Check cache (for remote players using fallback)
return npcLayers.get(entity.getUUID());
if (entity instanceof Player) {
return null;
}
// 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
) {
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;
});
}
// Phase 2.8 : strip player path entirely (no partially-alive fallback).
if (entity instanceof Player) {
return null;
}
UUID uuid = entity.getUUID();
// 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;
return false;
}
/**
* 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");
}
}

View File

@@ -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;

View File

@@ -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.
}
/**

View File

@@ -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,

View File

@@ -137,6 +137,14 @@ public class TiedUpMod {
// Register dispenser behaviors (must be on main thread)
event.enqueueWork(DispenserBehaviors::register);
// RIG Phase 2 — dispatcher EntityType → EntityPatch (PLAYER Phase 2, NPCs Phase 5)
event.enqueueWork(com.tiedup.remake.rig.patch.EntityPatchProvider::registerEntityPatches);
// RIG Phase 2.7 — registre des StaticAnimation (CONTEXT_STAND_IDLE).
// Placeholder JSON procédural jusqu'à ce que les assets Blender arrivent
// (cf. docs/plans/rig/ASSETS_NEEDED.md).
event.enqueueWork(com.tiedup.remake.rig.TiedUpAnimationRegistry::initStaticAnimations);
}
/**
@@ -185,9 +193,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();

View File

@@ -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
);
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,156 @@
/*
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
*/
package com.tiedup.remake.rig;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import com.tiedup.remake.rig.anim.client.Layer;
import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties;
import com.tiedup.remake.rig.anim.types.DirectStaticAnimation;
import com.tiedup.remake.rig.anim.types.StaticAnimation;
/**
* Phase 2.7 — registry central des {@link StaticAnimation} TiedUp. Expose les
* accessors statiques (ex. {@link #CONTEXT_STAND_IDLE}) utilisés par les
* patches + tick handler pour jouer les animations idle / walk / etc.
*
* <h2>Placeholder assets</h2>
* <p>Les JSON associés 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>
*
* <h2>Lifecycle</h2>
* <ul>
* <li>{@link #initStaticAnimations()} appelé au {@code FMLCommonSetupEvent}
* (via {@code event.enqueueWork(...)}). Crée les instances
* {@link DirectStaticAnimation} — pas de chargement JSON ici, juste les
* métadonnées (registry name, armature, repeat flag).</li>
* <li>Le chargement effectif du JSON ({@code JsonAssetLoader}) est lazy :
* à la première lecture de {@link StaticAnimation#getAnimationClip()},
* donc typiquement à la première frame où l'animation est jouée.</li>
* <li>Si l'asset JSON est absent / corrompu, {@code StaticAnimation.loadAnimation}
* relance une {@code AssetLoadingException}. Le tick handler
* ({@link com.tiedup.remake.rig.tick.RigAnimationTickHandler}) attrape ces
* throwables pour éviter un crash complet.</li>
* </ul>
*
* <h2>Dist</h2>
* <p>Les animations tournent côté client (l'{@code Animator} est créé via
* {@code ClientAnimator::getAnimator} côté physical client), donc le registry
* est indépendant du side pour le bootstrap mais toute la lecture JSON passe
* par {@code Minecraft.getInstance().getResourceManager()} côté client. Les
* champs sont accessibles côté serveur (validation / logs), seul
* {@link StaticAnimation#loadAnimation()} est client-heavy (et protégé par
* la dispatch server/client du {@code AnimationManager.getAnimationResourceManager()}).</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}). */
public static final ResourceLocation CONTEXT_STAND_IDLE_ID =
ResourceLocation.fromNamespaceAndPath(TiedUpRigConstants.MODID, "context_stand_idle");
/**
* Anim idle par défaut — joue quand aucune motion active. Placeholder 2
* keyframes identity (joueur sans mouvement visible) jusqu'à ce qu'un
* asset authored Blender arrive.
*
* <p><b>Attention init order</b> : ce field est {@code null} tant que
* {@link #initStaticAnimations()} n'a pas tourné. Les sites qui
* référencent ce field doivent être gardés par un null-check, ou être
* appelés post-setup (tick handler, patch init, etc.).</p>
*
* <p>Utilise {@link DirectStaticAnimation} (vs un hand-written
* {@code StaticAnimation}) pour hériter du pattern accessor=self +
* registryName() utilisés dans {@link TiedUpRigRegistry#EMPTY_ANIMATION}.</p>
*/
public static DirectStaticAnimation CONTEXT_STAND_IDLE;
/**
* Construit les {@link StaticAnimation} TiedUp. À appeler exactement une
* fois par game, en {@code FMLCommonSetupEvent.enqueueWork(...)}.
*
* <p>Pas de chargement JSON ici — juste l'instanciation des accessors.
* La première lecture de {@code getAnimationClip()} déclenchera le load
* via {@link com.tiedup.remake.rig.asset.JsonAssetLoader}.</p>
*
* <p>Idempotent (re-appel sans effet visible) mais pas thread-safe. Ne
* devrait jamais être appelé hors du mod bus.</p>
*/
public static void initStaticAnimations() {
if (CONTEXT_STAND_IDLE != null) {
// Déjà init (hot-reload setup, test double-init). Log debug seulement.
TiedUpRigConstants.LOGGER.debug(
"TiedUpAnimationRegistry.initStaticAnimations: déjà initialisé, skip."
);
return;
}
try {
// transitionTime=GENERAL (0.15s = 3 ticks) + isRepeat=true + registryName +
// armature=BIPED. L'ordre match le ctor DirectStaticAnimation(float, boolean, ResourceLocation, AssetAccessor).
CONTEXT_STAND_IDLE = new DirectStaticAnimation(
TiedUpRigConstants.GENERAL_ANIMATION_TRANSITION_TIME,
/* isRepeat */ true,
CONTEXT_STAND_IDLE_ID,
TiedUpArmatures.BIPED
);
// Layer BASE + priority LOWEST — idle default, écrasable par toute
// autre anim. Sans ces props la default du StaticAnimation (LOWEST /
// BASE_LAYER) s'applique déjà — on les set explicitement pour la doc.
setLowestBaseLayer(CONTEXT_STAND_IDLE);
TiedUpRigConstants.LOGGER.info(
"TiedUpAnimationRegistry: CONTEXT_STAND_IDLE registered ({})",
CONTEXT_STAND_IDLE_ID
);
} catch (Throwable t) {
// Fallback : log + laisse CONTEXT_STAND_IDLE null. Le tick handler
// verra null et skippera silencieusement. Évite de tout casser si
// un asset placeholder est malformé en dev.
TiedUpRigConstants.LOGGER.error(
"TiedUpAnimationRegistry: init échoué pour CONTEXT_STAND_IDLE — "
+ "animation idle désactivée. Voir docs/plans/rig/ASSETS_NEEDED.md §2.",
t
);
}
}
/**
* Helper — set LayerType=BASE_LAYER + Priority=LOWEST sur une anim.
* Évite de forcer les callers à importer {@link ClientAnimationProperties}
* + {@link Layer}.
*
* <p>{@code @OnlyIn(CLIENT)} indirect : les properties
* {@code LAYER_TYPE}/{@code PRIORITY} sont client-only mais leur écriture
* via {@code addProperty} ne déclenche pas de class-load de
* {@code net.minecraft.client.*}. Le tag d'{@code @OnlyIn} serait
* incorrect ici (le method serait appelé depuis commonSetup). La safety
* réelle est assurée par le fait que les properties sont juste stockées
* dans la map et lues plus tard sur client uniquement (via
* {@code getLayerType()}/{@code getPriority()} tagués
* {@code @OnlyIn(CLIENT)}).</p>
*/
private static void setLowestBaseLayer(StaticAnimation anim) {
anim.addProperty(ClientAnimationProperties.LAYER_TYPE, Layer.LayerType.BASE_LAYER);
anim.addProperty(ClientAnimationProperties.PRIORITY, Layer.Priority.LOWEST);
}
/**
* Helper — vrai ssi la static anim de référence a fini l'init (tick
* handler l'utilise en early-return quand Phase 2.7 assets sont absents
* en dev test).
*/
@OnlyIn(Dist.CLIENT)
public static boolean isReady() {
return CONTEXT_STAND_IDLE != null;
}
}

View File

@@ -0,0 +1,230 @@
/*
* 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.HumanoidArmature;
import com.tiedup.remake.rig.armature.Joint;
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");
/**
* 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;
}
}

View 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);
}
}
}

View 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;
}
};
}

View 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;
}
}

View File

@@ -0,0 +1,436 @@
/*
* 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.
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;
}
}
}

View 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;
}
}

View File

@@ -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,
}
}

View 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;
}
}
}

View 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();
}
}

View 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());
}
}

View 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;
}
}

View 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;
}
}

View 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
}
}

View 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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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));
}

View 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View 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();
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.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)
*/
public static final StaticAnimationProperty<Layer.LayerType> LAYER_TYPE = new StaticAnimationProperty<Layer.LayerType> ();
/**
* Priority of composite layer.
*/
public static final StaticAnimationProperty<Layer.Priority> PRIORITY = new StaticAnimationProperty<Layer.Priority> ();
/**
* Joint mask for composite layer.
*/
public static final StaticAnimationProperty<JointMaskEntry> JOINT_MASK = new StaticAnimationProperty<JointMaskEntry> ();
/**
* 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> ();
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -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);
}
}

View File

@@ -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!");
}
}

View File

@@ -0,0 +1,375 @@
/*
* 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.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.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();
@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.
*/
public static final StaticAnimationProperty<List<AnimationEvent<?, ?>>> TICK_EVENTS = new StaticAnimationProperty<List<AnimationEvent<?, ?>>> ();
/**
* Events that are fired when the animation starts.
*/
public static final StaticAnimationProperty<List<SimpleEvent<?>>> ON_BEGIN_EVENTS = new StaticAnimationProperty<List<SimpleEvent<?>>> ();
/**
* Events that are fired when the animation ends.
*/
public static final StaticAnimationProperty<List<SimpleEvent<?>>> ON_END_EVENTS = new StaticAnimationProperty<List<SimpleEvent<?>>> ();
/**
* An event triggered when entity changes an item in hand.
* Retenu comme hook datapack : un event écrit en JSON par un datapack tiers peut s'abonner
* au changement d'item porté pour déclencher une réaction (voir Phase 3 data-driven anims).
*/
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.
*/
public static final StaticAnimationProperty<PlaybackSpeedModifier> PLAY_SPEED_MODIFIER = new StaticAnimationProperty<PlaybackSpeedModifier> ();
/**
* You can modify the playback speed of the animation.
*/
public static final StaticAnimationProperty<PlaybackTimeModifier> ELAPSED_TIME_MODIFIER = new StaticAnimationProperty<PlaybackTimeModifier> ();
/**
* This property will be called both in client and server when modifying the pose
*/
public static final StaticAnimationProperty<PoseModifier> POSE_MODIFIER = new StaticAnimationProperty<PoseModifier> ();
/**
* Fix the head rotation to the player's body rotation
*/
public static final StaticAnimationProperty<Boolean> FIXED_HEAD_ROTATION = new StaticAnimationProperty<Boolean> ();
/**
* Defines static animations as link animation when the animation is followed by a specific animation
*/
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
*/
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
*/
public static final StaticAnimationProperty<List<InverseKinematicsDefinition>> IK_DEFINITION = new StaticAnimationProperty<List<InverseKinematicsDefinition>> ();
/**
* This property automatically baked when animation is loaded
*/
public static final StaticAnimationProperty<List<BakedInverseKinematicsDefinition>> BAKED_IK_DEFINITION = new StaticAnimationProperty<List<BakedInverseKinematicsDefinition>> ();
/**
* This property reset the entity's living motion
*/
public static final StaticAnimationProperty<LivingMotion> RESET_LIVING_MOTION = new StaticAnimationProperty<LivingMotion> ();
}
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.
*/
public static final ActionAnimationProperty<TimePairList> NO_GRAVITY_TIME = new ActionAnimationProperty<TimePairList> ();
/**
* Coord of action animation
*/
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.
*/
public static final ActionAnimationProperty<TimePairList> MOVE_TIME = new ActionAnimationProperty<TimePairList> ();
/**
* Set the dynamic coordinates of {@link ActionAnimation}. Called before creation of {@link LinkAnimation}.
*/
public static final ActionAnimationProperty<MoveCoordSetter> COORD_SET_BEGIN = new ActionAnimationProperty<MoveCoordSetter> ();
/**
* Set the dynamic coordinates of {@link ActionAnimation}.
*/
public static final ActionAnimationProperty<MoveCoordSetter> COORD_SET_TICK = new ActionAnimationProperty<MoveCoordSetter> ();
/**
* Set the coordinates of action animation.
*/
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}}
*/
public static final ActionAnimationProperty<TimePairList> COORD_UPDATE_TIME = new ActionAnimationProperty<TimePairList> ();
/**
* 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}
*/
public static final ActionAnimationProperty<DestLocationProvider> DEST_LOCATION_PROVIDER = new ActionAnimationProperty<DestLocationProvider> ();
/**
* Provide y rotation of entity {@link MoveCoordFunctions}
*/
public static final ActionAnimationProperty<YRotProvider> ENTITY_YROT_PROVIDER = new ActionAnimationProperty<YRotProvider> ();
/**
* Provide y rotation of tracing coord {@link MoveCoordFunctions}
*/
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}
*/
public static final ActionAnimationProperty<Integer> COORD_START_KEYFRAME_INDEX = new ActionAnimationProperty<Integer> ();
/**
* Decides the index of destination key frame for coord transform, See also with {@link MoveCoordFunctions#TRACE_ORIGIN_AS_DESTINATION}
*/
public static final ActionAnimationProperty<Integer> COORD_DEST_KEYFRAME_INDEX = new ActionAnimationProperty<Integer> ();
/**
* 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);
}
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);
}
}

View File

@@ -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());
});
}
}
};
}

View File

@@ -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."
);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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) {
}
}

View 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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}

View 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));
}
}
}

View File

@@ -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;
}
}

View 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;
}
}
}

View 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));
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View 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());
}
}
}

View 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
}
}

View File

@@ -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");
};
}
}

View File

@@ -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;
}
}

View 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;
}
}
}

View File

@@ -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();
}

View File

@@ -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();
}

File diff suppressed because it is too large Load Diff

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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.event;
import java.util.Map;
import java.util.function.Function;
import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.world.entity.EntityType;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.event.IModBusEvent;
import com.tiedup.remake.rig.render.PatchedEntityRenderer;
@SuppressWarnings("rawtypes")
public abstract class PatchedRenderersEvent extends Event implements IModBusEvent {
public static class Add extends PatchedRenderersEvent {
private final Map<EntityType<?>, Function<EntityType<?>, PatchedEntityRenderer>> entityRendererProvider;
private final EntityRendererProvider.Context context;
public Add(Map<EntityType<?>, Function<EntityType<?>, PatchedEntityRenderer>> entityRendererProvider, EntityRendererProvider.Context context) {
this.entityRendererProvider = entityRendererProvider;
this.context = context;
}
public void addPatchedEntityRenderer(EntityType<?> entityType, Function<EntityType<?>, PatchedEntityRenderer> provider) {
this.entityRendererProvider.put(entityType, provider);
}
public EntityRendererProvider.Context getContext() {
return this.context;
}
}
}

View File

@@ -0,0 +1,64 @@
/*
* 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 com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraftforge.eventbus.api.Event;
import com.tiedup.remake.rig.mesh.SkinnedMesh;
import com.tiedup.remake.rig.render.PatchedEntityRenderer;
import com.tiedup.remake.rig.patch.LivingEntityPatch;
public class PrepareModelEvent extends Event {
private final SkinnedMesh mesh;
private final LivingEntityPatch<?> entitypatch;
private final MultiBufferSource buffer;
private final PoseStack poseStack;
private final int packedLight;
private final float partialTicks;
private final PatchedEntityRenderer<?, ?, ?, ?> renderer;
public PrepareModelEvent(PatchedEntityRenderer<?, ?, ?, ?> renderer, SkinnedMesh mesh, LivingEntityPatch<?> entitypatch, MultiBufferSource buffer, PoseStack poseStack, int packedLight, float partialTicks) {
this.renderer = renderer;
this.mesh = mesh;
this.entitypatch = entitypatch;
this.buffer = buffer;
this.poseStack = poseStack;
this.packedLight = packedLight;
this.partialTicks = partialTicks;
}
public SkinnedMesh getMesh() {
return this.mesh;
}
public LivingEntityPatch<?> getEntityPatch() {
return this.entitypatch;
}
public MultiBufferSource getBuffer() {
return this.buffer;
}
public PoseStack getPoseStack() {
return this.poseStack;
}
public int getPackedLight() {
return this.packedLight;
}
public float getPartialTicks() {
return this.partialTicks;
}
public PatchedEntityRenderer<?, ?, ?, ?> getRenderer() {
return this.renderer;
}
}

View File

@@ -0,0 +1,26 @@
/*
* 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.exception;
public class AnimationInvokeException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 1L;
public AnimationInvokeException(String message) {
super(message);
}
public AnimationInvokeException(String message, Throwable cause) {
super(message, cause);
}
public AnimationInvokeException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,22 @@
/*
* 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.exception;
public class AssetLoadingException extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 1L;
public AssetLoadingException(String message) {
super(message);
}
public AssetLoadingException(String message, Throwable ex) {
super(message, ex);
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.math;
import java.util.Map;
import com.google.common.collect.Maps;
import com.mojang.datafixers.util.Pair;
import com.tiedup.remake.rig.armature.JointTransform;
public class AnimationTransformEntry {
private static final String[] BINDING_PRIORITY = {JointTransform.PARENT, JointTransform.JOINT_LOCAL_TRANSFORM, JointTransform.ANIMATION_TRANSFORM, JointTransform.RESULT1, JointTransform.RESULT2};
private final Map<String, Pair<OpenMatrix4f, MatrixOperation>> matrices = Maps.newHashMap();
public void put(String entryPosition, OpenMatrix4f matrix) {
this.put(entryPosition, matrix, OpenMatrix4f::mul);
}
public void put(String entryPosition, OpenMatrix4f matrix, MatrixOperation operation) {
if (this.matrices.containsKey(entryPosition)) {
Pair<OpenMatrix4f, MatrixOperation> appliedTransform = this.matrices.get(entryPosition);
OpenMatrix4f result = appliedTransform.getSecond().mul(appliedTransform.getFirst(), matrix, null);
this.matrices.put(entryPosition, Pair.of(result, operation));
} else {
this.matrices.put(entryPosition, Pair.of(new OpenMatrix4f(matrix), operation));
}
}
public OpenMatrix4f getResult() {
OpenMatrix4f result = new OpenMatrix4f();
for (String entryName : BINDING_PRIORITY) {
if (this.matrices.containsKey(entryName)) {
Pair<OpenMatrix4f, MatrixOperation> pair = this.matrices.get(entryName);
pair.getSecond().mul(result, pair.getFirst(), result);
}
}
return result;
}
}

View File

@@ -0,0 +1,357 @@
/*
* 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.math;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.joml.Math;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
public class MathUtils {
public static final Vec3 XP = new Vec3(1.0D, 0.0D, 0.0D);
public static final Vec3 XN = new Vec3(-1.0D, 0.0D, 0.0D);
public static final Vec3 YP = new Vec3(0.0D, 1.0D, 0.0D);
public static final Vec3 YN = new Vec3(0.0D, -1.0D, 0.0D);
public static final Vec3 ZP = new Vec3(0.0D, 0.0D, 1.0D);
public static final Vec3 ZN = new Vec3(0.0D, 0.0D, -1.0D);
public static OpenMatrix4f getModelMatrixIntegral(float xPosO, float xPos, float yPosO, float yPos, float zPosO, float zPos, float xRotO, float xRot, float yRotO, float yRot, float partialTick, float scaleX, float scaleY, float scaleZ) {
OpenMatrix4f modelMatrix = new OpenMatrix4f();
Vec3f translation = new Vec3f(-(xPosO + (xPos - xPosO) * partialTick), ((yPosO + (yPos - yPosO) * partialTick)), -(zPosO + (zPos - zPosO) * partialTick));
float partialXRot = Mth.rotLerp(partialTick, xRotO, xRot);
float partialYRot = Mth.rotLerp(partialTick, yRotO, yRot);
modelMatrix.translate(translation).rotateDeg(-partialYRot, Vec3f.Y_AXIS).rotateDeg(-partialXRot, Vec3f.X_AXIS).scale(scaleX, scaleY, scaleZ);
return modelMatrix;
}
public static int getSign(double value) {
return value > 0.0D ? 1 : -1;
}
public static Vec3 getVectorForRotation(float pitch, float yaw) {
float f = pitch * (float) Math.PI / 180F;
float f1 = -yaw * (float) Math.PI / 180F;
float f2 = Mth.cos(f1);
float f3 = Mth.sin(f1);
float f4 = Mth.cos(f);
float f5 = Mth.sin(f);
return new Vec3(f3 * f4, -f5, f2 * f4);
}
public static float lerpBetween(float f1, float f2, float zero2one) {
float f = 0;
for (f = f2 - f1; f < -180.0F; f += 360.0F) {
}
while (f >= 180.0F) {
f -= 360.0F;
}
return f1 + zero2one * f;
}
public static float rotlerp(float from, float to, float limit) {
float f = Mth.wrapDegrees(to - from);
if (f > limit) {
f = limit;
}
if (f < -limit) {
f = -limit;
}
float f1 = from + f;
while (f1 >= 180.0F) {
f1 -= 360.0F;
}
while (f1 <= -180.0F) {
f1 += 360.0F;
}
return f1;
}
public static float wrapRadian(float pValue) {
float maxRot = (float)Math.PI * 2.0F;
float f = pValue % maxRot;
if (f >= Math.PI) {
f -= maxRot;
}
if (f < -Math.PI) {
f += maxRot;
}
return f;
}
public static float findNearestRotation(float src, float rotation) {
float diff = Math.abs(src - rotation);
float idealRotation = rotation;
int sign = Mth.sign(src - rotation);
if (sign == 0) {
return rotation;
}
while (true) {
float next = idealRotation + sign * 360.0F;
if (Math.abs(src - next) > diff) {
return idealRotation;
}
idealRotation = next;
diff = Math.abs(src - next);
}
}
public static Vec3 getNearestVector(Vec3 from, Vec3... vectors) {
double minLength = 1000000.0D;
int index = 0;
for (int i = 0; i < vectors.length; i++) {
if (vectors[i] == null) {
continue;
}
double distSqr = from.distanceToSqr(vectors[i]);
if (distSqr < minLength) {
minLength = distSqr;
index = i;
}
}
return vectors[index];
}
public static Vec3 getNearestVector(Vec3 from, List<Vec3> vectors) {
return getNearestVector(from, vectors.toArray(new Vec3[0]));
}
private static final Matrix4f MATRIX4F = new Matrix4f();
private static final Matrix3f MATRIX3F = new Matrix3f();
public static void mulStack(PoseStack poseStack, OpenMatrix4f mat) {
OpenMatrix4f.exportToMojangMatrix(mat, MATRIX4F);
MATRIX3F.set(MATRIX4F);
poseStack.mulPoseMatrix(MATRIX4F);
poseStack.last().normal().mul(MATRIX3F);
}
public static double getAngleBetween(Vec3f a, Vec3f b) {
Vec3f normA = Vec3f.normalize(a, null);
Vec3f normB = Vec3f.normalize(b, null);
double cos = (normA.x * normB.x + normA.y * normB.y + normA.z * normB.z);
return Math.toDegrees(Math.acos(cos));
}
public static double getAngleBetween(Vec3 a, Vec3 b) {
Vec3 normA = a.normalize();
Vec3 normB = b.normalize();
double cos = (normA.x * normB.x + normA.y * normB.y + normA.z * normB.z);
return Math.toDegrees(Math.safeAcos(cos));
}
public static float getAngleBetween(Quaternionf a, Quaternionf b) {
float dot = a.w * b.w + a.x * b.x + a.y * b.y + a.z * b.z;
return 2.0F * (Math.safeAcos(MathUtils.getSign(dot) * b.w) - Math.safeAcos(a.w));
}
public static double getXRotOfVector(Vec3 vec) {
Vec3 normalized = vec.normalize();
return -(Math.atan2(normalized.y, (float)Math.sqrt(normalized.x * normalized.x + normalized.z * normalized.z)) * (180D / Math.PI));
}
public static double getYRotOfVector(Vec3 vec) {
Vec3 normalized = vec.normalize();
return Math.atan2(normalized.z, normalized.x) * (180D / Math.PI) - 90.0F;
}
private static Quaternionf getQuaternionFromMatrix(OpenMatrix4f mat) {
Quaternionf quat = new Quaternionf(0, 0, 0, 1);
quat.setFromUnnormalized(OpenMatrix4f.exportToMojangMatrix(mat.transpose(null)));
return quat;
}
public static Vec3f lerpVector(Vec3f start, Vec3f end, float delta) {
return lerpVector(start, end, delta, new Vec3f());
}
public static Vec3f lerpVector(Vec3f start, Vec3f end, float delta, Vec3f dest) {
if (dest == null) {
dest = new Vec3f();
}
dest.x = start.x + (end.x - start.x) * delta;
dest.y = start.y + (end.y - start.y) * delta;
dest.z = start.z + (end.z - start.z) * delta;
return dest;
}
public static Vec3 lerpVector(Vec3 start, Vec3 end, float delta) {
return new Vec3(start.x + (end.x - start.x) * delta, start.y + (end.y - start.y) * delta, start.z + (end.z - start.z) * delta);
}
public static Vec3 projectVector(Vec3 from, Vec3 to) {
double dot = to.dot(from);
double normalScale = 1.0D / ((to.x * to.x) + (to.y * to.y) + (to.z * to.z));
return new Vec3(dot * to.x * normalScale, dot * to.y * normalScale, dot * to.z * normalScale);
}
public static Vec3f projectVector(Vec3f from, Vec3f to, Vec3f dest) {
if (dest == null) {
dest = new Vec3f();
}
float dot = Vec3f.dot(to, from);
float normalScale = 1.0F / ((to.x * to.x) + (to.y * to.y) + (to.z * to.z));
dest.x = dot * to.x * normalScale;
dest.y = dot * to.y * normalScale;
dest.z = dot * to.z * normalScale;
return dest;
}
public static Quaternionf lerpQuaternion(Quaternionf from, Quaternionf to, float delta) {
return lerpQuaternion(from, to, delta, null);
}
public static Quaternionf lerpQuaternion(Quaternionf from, Quaternionf to, float delta, Quaternionf dest) {
if (dest == null) {
dest = new Quaternionf();
}
float fromX = from.x();
float fromY = from.y();
float fromZ = from.z();
float fromW = from.w();
float toX = to.x();
float toY = to.y();
float toZ = to.z();
float toW = to.w();
float resultX;
float resultY;
float resultZ;
float resultW;
float dot = fromW * toW + fromX * toX + fromY * toY + fromZ * toZ;
float blendI = 1.0F - delta;
if (dot < 0.0F) {
resultW = blendI * fromW + delta * -toW;
resultX = blendI * fromX + delta * -toX;
resultY = blendI * fromY + delta * -toY;
resultZ = blendI * fromZ + delta * -toZ;
} else {
resultW = blendI * fromW + delta * toW;
resultX = blendI * fromX + delta * toX;
resultY = blendI * fromY + delta * toY;
resultZ = blendI * fromZ + delta * toZ;
}
dest.set(resultX, resultY, resultZ, resultW);
dest.normalize();
return dest;
}
private static Vector3f getScaleVectorFromMatrix(OpenMatrix4f mat) {
Vec3f a = new Vec3f(mat.m00, mat.m10, mat.m20);
Vec3f b = new Vec3f(mat.m01, mat.m11, mat.m21);
Vec3f c = new Vec3f(mat.m02, mat.m12, mat.m22);
return new Vector3f(a.length(), b.length(), c.length());
}
public static <T> Set<Set<T>> getSubset(Collection<T> collection) {
Set<Set<T>> subsets = new HashSet<> ();
List<T> asList = new ArrayList<> (collection);
createSubset(0, asList, new HashSet<> (), subsets);
return subsets;
}
private static <T> void createSubset(int idx, List<T> elements, Set<T> parent, Set<Set<T>> subsets) {
for (int i = idx; i < elements.size(); i++) {
Set<T> subset = new HashSet<> (parent);
subset.add(elements.get(i));
subsets.add(subset);
createSubset(i + 1, elements, subset, subsets);
}
}
public static int getLeastAngleVectorIdx(Vec3f src, Vec3f... candidates) {
int leastVectorIdx = -1;
int current = 0;
float maxDot = -10000.0F;
for (Vec3f normzlizedVec : Stream.of(candidates).map((vec) -> vec.normalize()).collect(Collectors.toList())) {
float dot = Vec3f.dot(src, normzlizedVec);
if (maxDot < dot) {
maxDot = dot;
leastVectorIdx = current;
}
current++;
}
return leastVectorIdx;
}
public static boolean canBeSeen(Entity target, Entity watcher, double maxDistance) {
if (target.level() != watcher.level()) {
return false;
}
double sqr = maxDistance * maxDistance;
Level level = target.level();
Vec3 vec1 = watcher.getEyePosition();
double height = target.getBoundingBox().maxY - target.getBoundingBox().minY;
Vec3 vec2 = target.position().add(0.0D, height * 0.15D, 0.0D);
Vec3 vec3 = target.position().add(0.0D, height * 0.5D, 0.0D);
Vec3 vec4 = target.position().add(0.0D, height * 0.95D, 0.0D);
return vec1.distanceToSqr(vec2) < sqr && level.clip(new ClipContext(vec1, vec2, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, watcher)).getType() == HitResult.Type.MISS ||
vec1.distanceToSqr(vec3) < sqr && level.clip(new ClipContext(vec1, vec3, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, watcher)).getType() == HitResult.Type.MISS ||
vec1.distanceToSqr(vec4) < sqr && level.clip(new ClipContext(vec1, vec4, ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, watcher)).getType() == HitResult.Type.MISS;
}
private MathUtils() {}
}

View File

@@ -0,0 +1,12 @@
/*
* 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.math;
@FunctionalInterface
public interface MatrixOperation {
OpenMatrix4f mul(OpenMatrix4f left, OpenMatrix4f right, OpenMatrix4f dest);
}

View File

@@ -0,0 +1,903 @@
/*
* 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.math;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.List;
import javax.annotation.Nullable;
import org.joml.Math;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
import com.google.common.collect.Lists;
import net.minecraft.world.phys.Vec3;
import com.tiedup.remake.rig.armature.JointTransform;
public class OpenMatrix4f {
private static final FloatBuffer MATRIX_TRANSFORMER = ByteBuffer.allocateDirect(16 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
private static final Vec3f VECTOR_STORAGE = new Vec3f();
private static final Vec4f VEC4_STORAGE = new Vec4f();
public static final OpenMatrix4f IDENTITY = new OpenMatrix4f();
/*
* m00 m01 m02 m03
* m10 m11 m12 m13
* m20 m21 m22 m23
* m30 m31 m32 m33
*/
public float m00, m01, m02, m03, m10, m11, m12, m13, m20, m21, m22, m23, m30, m31, m32, m33;
private final boolean immutable;
public OpenMatrix4f() {
this.setIdentity();
this.immutable = false;
}
public OpenMatrix4f(final OpenMatrix4f src) {
this(src, false);
}
public OpenMatrix4f(final OpenMatrix4f src, boolean immutable) {
load(src);
this.immutable = immutable;
}
public OpenMatrix4f(final JointTransform jointTransform) {
load(OpenMatrix4f.fromQuaternion(jointTransform.rotation()).translate(jointTransform.translation()).scale(jointTransform.scale()));
this.immutable = false;
}
public OpenMatrix4f(
float m00, float m01, float m02, float m03
, float m10, float m11, float m12, float m13
, float m20, float m21, float m22, float m23
, float m30, float m31, float m32, float m33
) {
this.m00 = m00;
this.m01 = m01;
this.m02 = m02;
this.m03 = m03;
this.m10 = m10;
this.m11 = m11;
this.m12 = m12;
this.m13 = m13;
this.m20 = m20;
this.m21 = m21;
this.m22 = m22;
this.m23 = m23;
this.m30 = m30;
this.m31 = m31;
this.m32 = m32;
this.m33 = m33;
this.immutable = false;
}
public OpenMatrix4f setIdentity() {
return setIdentity(this);
}
/**
* Set the given matrix to be the identity matrix.
* @param m The matrix to set to the identity
* @return m
*/
public static OpenMatrix4f setIdentity(OpenMatrix4f m) {
if (m.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
m.m00 = 1.0f;
m.m01 = 0.0f;
m.m02 = 0.0f;
m.m03 = 0.0f;
m.m10 = 0.0f;
m.m11 = 1.0f;
m.m12 = 0.0f;
m.m13 = 0.0f;
m.m20 = 0.0f;
m.m21 = 0.0f;
m.m22 = 1.0f;
m.m23 = 0.0f;
m.m30 = 0.0f;
m.m31 = 0.0f;
m.m32 = 0.0f;
m.m33 = 1.0f;
return m;
}
public OpenMatrix4f load(OpenMatrix4f src) {
return load(src, this);
}
public static OpenMatrix4f load(OpenMatrix4f src, @Nullable OpenMatrix4f dest) {
if (dest == null) {
dest = new OpenMatrix4f();
} else if (dest.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
dest.m00 = src.m00;
dest.m01 = src.m01;
dest.m02 = src.m02;
dest.m03 = src.m03;
dest.m10 = src.m10;
dest.m11 = src.m11;
dest.m12 = src.m12;
dest.m13 = src.m13;
dest.m20 = src.m20;
dest.m21 = src.m21;
dest.m22 = src.m22;
dest.m23 = src.m23;
dest.m30 = src.m30;
dest.m31 = src.m31;
dest.m32 = src.m32;
dest.m33 = src.m33;
return dest;
}
public static OpenMatrix4f load(@Nullable OpenMatrix4f dest, float[] elements) {
if (dest == null) {
dest = new OpenMatrix4f();
} else if (dest.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
dest.m00 = elements[0];
dest.m01 = elements[1];
dest.m02 = elements[2];
dest.m03 = elements[3];
dest.m10 = elements[4];
dest.m11 = elements[5];
dest.m12 = elements[6];
dest.m13 = elements[7];
dest.m20 = elements[8];
dest.m21 = elements[9];
dest.m22 = elements[10];
dest.m23 = elements[11];
dest.m30 = elements[12];
dest.m31 = elements[13];
dest.m32 = elements[14];
dest.m33 = elements[15];
return dest;
}
public static OpenMatrix4f load(@Nullable OpenMatrix4f dest, FloatBuffer buf) {
if (dest == null) {
dest = new OpenMatrix4f();
} else if (dest.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
buf.position(0);
dest.m00 = buf.get();
dest.m01 = buf.get();
dest.m02 = buf.get();
dest.m03 = buf.get();
dest.m10 = buf.get();
dest.m11 = buf.get();
dest.m12 = buf.get();
dest.m13 = buf.get();
dest.m20 = buf.get();
dest.m21 = buf.get();
dest.m22 = buf.get();
dest.m23 = buf.get();
dest.m30 = buf.get();
dest.m31 = buf.get();
dest.m32 = buf.get();
dest.m33 = buf.get();
return dest;
}
public OpenMatrix4f load(FloatBuffer buf) {
return OpenMatrix4f.load(this, buf);
}
public OpenMatrix4f store(FloatBuffer buf) {
buf.put(m00);
buf.put(m01);
buf.put(m02);
buf.put(m03);
buf.put(m10);
buf.put(m11);
buf.put(m12);
buf.put(m13);
buf.put(m20);
buf.put(m21);
buf.put(m22);
buf.put(m23);
buf.put(m30);
buf.put(m31);
buf.put(m32);
buf.put(m33);
return this;
}
public List<Float> toList() {
List<Float> elements = Lists.newArrayList();
elements.add(0, m00);
elements.add(1, m01);
elements.add(2, m02);
elements.add(3, m03);
elements.add(4, m10);
elements.add(5, m11);
elements.add(6, m12);
elements.add(7, m13);
elements.add(8, m20);
elements.add(9, m21);
elements.add(10, m22);
elements.add(11, m23);
elements.add(12, m30);
elements.add(13, m31);
elements.add(14, m32);
elements.add(15, m33);
return elements;
}
public OpenMatrix4f unmodifiable() {
return new OpenMatrix4f(this, true);
}
public OpenMatrix4f mulFront(OpenMatrix4f mulTransform) {
return OpenMatrix4f.mul(mulTransform, this, this);
}
public OpenMatrix4f mulBack(OpenMatrix4f mulTransform) {
return OpenMatrix4f.mul(this, mulTransform, this);
}
public static OpenMatrix4f mul(OpenMatrix4f left, OpenMatrix4f right, @Nullable OpenMatrix4f dest) {
if (dest == null) {
dest = new OpenMatrix4f();
} else if (dest.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
float m00 = left.m00 * right.m00 + left.m10 * right.m01 + left.m20 * right.m02 + left.m30 * right.m03;
float m01 = left.m01 * right.m00 + left.m11 * right.m01 + left.m21 * right.m02 + left.m31 * right.m03;
float m02 = left.m02 * right.m00 + left.m12 * right.m01 + left.m22 * right.m02 + left.m32 * right.m03;
float m03 = left.m03 * right.m00 + left.m13 * right.m01 + left.m23 * right.m02 + left.m33 * right.m03;
float m10 = left.m00 * right.m10 + left.m10 * right.m11 + left.m20 * right.m12 + left.m30 * right.m13;
float m11 = left.m01 * right.m10 + left.m11 * right.m11 + left.m21 * right.m12 + left.m31 * right.m13;
float m12 = left.m02 * right.m10 + left.m12 * right.m11 + left.m22 * right.m12 + left.m32 * right.m13;
float m13 = left.m03 * right.m10 + left.m13 * right.m11 + left.m23 * right.m12 + left.m33 * right.m13;
float m20 = left.m00 * right.m20 + left.m10 * right.m21 + left.m20 * right.m22 + left.m30 * right.m23;
float m21 = left.m01 * right.m20 + left.m11 * right.m21 + left.m21 * right.m22 + left.m31 * right.m23;
float m22 = left.m02 * right.m20 + left.m12 * right.m21 + left.m22 * right.m22 + left.m32 * right.m23;
float m23 = left.m03 * right.m20 + left.m13 * right.m21 + left.m23 * right.m22 + left.m33 * right.m23;
float m30 = left.m00 * right.m30 + left.m10 * right.m31 + left.m20 * right.m32 + left.m30 * right.m33;
float m31 = left.m01 * right.m30 + left.m11 * right.m31 + left.m21 * right.m32 + left.m31 * right.m33;
float m32 = left.m02 * right.m30 + left.m12 * right.m31 + left.m22 * right.m32 + left.m32 * right.m33;
float m33 = left.m03 * right.m30 + left.m13 * right.m31 + left.m23 * right.m32 + left.m33 * right.m33;
dest.m00 = m00;
dest.m01 = m01;
dest.m02 = m02;
dest.m03 = m03;
dest.m10 = m10;
dest.m11 = m11;
dest.m12 = m12;
dest.m13 = m13;
dest.m20 = m20;
dest.m21 = m21;
dest.m22 = m22;
dest.m23 = m23;
dest.m30 = m30;
dest.m31 = m31;
dest.m32 = m32;
dest.m33 = m33;
return dest;
}
public static OpenMatrix4f mulMatrices(OpenMatrix4f... srcs) {
OpenMatrix4f result = new OpenMatrix4f();
for (OpenMatrix4f src : srcs) {
result.mulBack(src);
}
return result;
}
public static OpenMatrix4f mulAsOriginInverse(OpenMatrix4f left, OpenMatrix4f right, OpenMatrix4f dest) {
float x = left.m30;
float y = left.m31;
float z = left.m32;
OpenMatrix4f result = mul(right, left, dest);
result.m30 = x;
result.m31 = y;
result.m32 = z;
return result;
}
public static Vec4f transform(OpenMatrix4f matrix, Vec4f src, @Nullable Vec4f dest) {
if (dest == null) {
dest = new Vec4f();
}
float x = matrix.m00 * src.x + matrix.m10 * src.y + matrix.m20 * src.z + matrix.m30 * src.w;
float y = matrix.m01 * src.x + matrix.m11 * src.y + matrix.m21 * src.z + matrix.m31 * src.w;
float z = matrix.m02 * src.x + matrix.m12 * src.y + matrix.m22 * src.z + matrix.m32 * src.w;
float w = matrix.m03 * src.x + matrix.m13 * src.y + matrix.m23 * src.z + matrix.m33 * src.w;
dest.x = x;
dest.y = y;
dest.z = z;
dest.w = w;
return dest;
}
public static Vec3 transform(OpenMatrix4f matrix, Vec3 src) {
double x = matrix.m00 * src.x + matrix.m10 * src.y + matrix.m20 * src.z + matrix.m30;
double y = matrix.m01 * src.x + matrix.m11 * src.y + matrix.m21 * src.z + matrix.m31;
double z = matrix.m02 * src.x + matrix.m12 * src.y + matrix.m22 * src.z + matrix.m32;
return new Vec3(x, y ,z);
}
public static Vec3f transform3v(OpenMatrix4f matrix, Vec3f src, @Nullable Vec3f dest) {
if (dest == null) {
dest = new Vec3f();
}
VEC4_STORAGE.set(src.x, src.y, src.z, 1.0F);
Vec4f result = transform(matrix, VEC4_STORAGE, null);
dest.x = result.x;
dest.y = result.y;
dest.z = result.z;
return dest;
}
public OpenMatrix4f transpose() {
return transpose(this);
}
public OpenMatrix4f transpose(OpenMatrix4f dest) {
return transpose(this, dest);
}
public static OpenMatrix4f transpose(OpenMatrix4f src, @Nullable OpenMatrix4f dest) {
if (dest == null) {
dest = new OpenMatrix4f();
} else if (dest.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
float m00 = src.m00;
float m01 = src.m10;
float m02 = src.m20;
float m03 = src.m30;
float m10 = src.m01;
float m11 = src.m11;
float m12 = src.m21;
float m13 = src.m31;
float m20 = src.m02;
float m21 = src.m12;
float m22 = src.m22;
float m23 = src.m32;
float m30 = src.m03;
float m31 = src.m13;
float m32 = src.m23;
float m33 = src.m33;
dest.m00 = m00;
dest.m01 = m01;
dest.m02 = m02;
dest.m03 = m03;
dest.m10 = m10;
dest.m11 = m11;
dest.m12 = m12;
dest.m13 = m13;
dest.m20 = m20;
dest.m21 = m21;
dest.m22 = m22;
dest.m23 = m23;
dest.m30 = m30;
dest.m31 = m31;
dest.m32 = m32;
dest.m33 = m33;
return dest;
}
public float determinant() {
float f = m00 * ((m11 * m22 * m33 + m12 * m23 * m31 + m13 * m21 * m32) - m13 * m22 * m31 - m11 * m23 * m32 - m12 * m21 * m33);
f -= m01 * ((m10 * m22 * m33 + m12 * m23 * m30 + m13 * m20 * m32) - m13 * m22 * m30 - m10 * m23 * m32 - m12 * m20 * m33);
f += m02 * ((m10 * m21 * m33 + m11 * m23 * m30 + m13 * m20 * m31) - m13 * m21 * m30 - m10 * m23 * m31 - m11 * m20 * m33);
f -= m03 * ((m10 * m21 * m32 + m11 * m22 * m30 + m12 * m20 * m31) - m12 * m21 * m30 - m10 * m22 * m31 - m11 * m20 * m32);
return f;
}
private static float determinant3x3(float t00, float t01, float t02, float t10, float t11, float t12, float t20, float t21, float t22) {
return t00 * (t11 * t22 - t12 * t21) + t01 * (t12 * t20 - t10 * t22) + t02 * (t10 * t21 - t11 * t20);
}
public OpenMatrix4f invert() {
return OpenMatrix4f.invert(this, this);
}
public static OpenMatrix4f invert(OpenMatrix4f src, @Nullable OpenMatrix4f dest) {
float determinant = src.determinant();
if (determinant != 0.0F) {
if (dest == null) {
dest = new OpenMatrix4f();
} else if (dest.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
float determinant_inv = 1.0F / determinant;
float t00 = determinant3x3(src.m11, src.m12, src.m13, src.m21, src.m22, src.m23, src.m31, src.m32, src.m33);
float t01 = -determinant3x3(src.m10, src.m12, src.m13, src.m20, src.m22, src.m23, src.m30, src.m32, src.m33);
float t02 = determinant3x3(src.m10, src.m11, src.m13, src.m20, src.m21, src.m23, src.m30, src.m31, src.m33);
float t03 = -determinant3x3(src.m10, src.m11, src.m12, src.m20, src.m21, src.m22, src.m30, src.m31, src.m32);
float t10 = -determinant3x3(src.m01, src.m02, src.m03, src.m21, src.m22, src.m23, src.m31, src.m32, src.m33);
float t11 = determinant3x3(src.m00, src.m02, src.m03, src.m20, src.m22, src.m23, src.m30, src.m32, src.m33);
float t12 = -determinant3x3(src.m00, src.m01, src.m03, src.m20, src.m21, src.m23, src.m30, src.m31, src.m33);
float t13 = determinant3x3(src.m00, src.m01, src.m02, src.m20, src.m21, src.m22, src.m30, src.m31, src.m32);
float t20 = determinant3x3(src.m01, src.m02, src.m03, src.m11, src.m12, src.m13, src.m31, src.m32, src.m33);
float t21 = -determinant3x3(src.m00, src.m02, src.m03, src.m10, src.m12, src.m13, src.m30, src.m32, src.m33);
float t22 = determinant3x3(src.m00, src.m01, src.m03, src.m10, src.m11, src.m13, src.m30, src.m31, src.m33);
float t23 = -determinant3x3(src.m00, src.m01, src.m02, src.m10, src.m11, src.m12, src.m30, src.m31, src.m32);
float t30 = -determinant3x3(src.m01, src.m02, src.m03, src.m11, src.m12, src.m13, src.m21, src.m22, src.m23);
float t31 = determinant3x3(src.m00, src.m02, src.m03, src.m10, src.m12, src.m13, src.m20, src.m22, src.m23);
float t32 = -determinant3x3(src.m00, src.m01, src.m03, src.m10, src.m11, src.m13, src.m20, src.m21, src.m23);
float t33 = determinant3x3(src.m00, src.m01, src.m02, src.m10, src.m11, src.m12, src.m20, src.m21, src.m22);
dest.m00 = t00 * determinant_inv;
dest.m11 = t11 * determinant_inv;
dest.m22 = t22 * determinant_inv;
dest.m33 = t33 * determinant_inv;
dest.m01 = t10 * determinant_inv;
dest.m10 = t01 * determinant_inv;
dest.m20 = t02 * determinant_inv;
dest.m02 = t20 * determinant_inv;
dest.m12 = t21 * determinant_inv;
dest.m21 = t12 * determinant_inv;
dest.m03 = t30 * determinant_inv;
dest.m30 = t03 * determinant_inv;
dest.m13 = t31 * determinant_inv;
dest.m31 = t13 * determinant_inv;
dest.m32 = t23 * determinant_inv;
dest.m23 = t32 * determinant_inv;
return dest;
} else {
dest.setIdentity();
return dest;
}
}
public OpenMatrix4f translate(float x, float y, float z) {
VECTOR_STORAGE.set(x, y, z);
return translate(VECTOR_STORAGE, this);
}
public OpenMatrix4f translate(Vec3f vec) {
return translate(vec, this);
}
public OpenMatrix4f translate(Vec3f vec, OpenMatrix4f dest) {
return translate(vec, this, dest);
}
public static OpenMatrix4f translate(Vec3f vec, OpenMatrix4f src, @Nullable OpenMatrix4f dest) {
if (dest == null) {
dest = new OpenMatrix4f();
} else if (dest.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
dest.m30 += src.m00 * vec.x + src.m10 * vec.y + src.m20 * vec.z;
dest.m31 += src.m01 * vec.x + src.m11 * vec.y + src.m21 * vec.z;
dest.m32 += src.m02 * vec.x + src.m12 * vec.y + src.m22 * vec.z;
dest.m33 += src.m03 * vec.x + src.m13 * vec.y + src.m23 * vec.z;
return dest;
}
public static OpenMatrix4f createTranslation(float x, float y, float z) {
return ofTranslation(x, y, z, null);
}
public static OpenMatrix4f ofTranslation(float x, float y, float z, OpenMatrix4f dest) {
if (dest == null) {
dest = new OpenMatrix4f();
} else if (dest.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
dest.setIdentity();
dest.m30 = x;
dest.m31 = y;
dest.m32 = z;
return dest;
}
public OpenMatrix4f rotateDeg(float angle, Vec3f axis) {
return rotate((float)Math.toRadians(angle), axis);
}
public OpenMatrix4f rotate(float angle, Vec3f axis) {
return rotate(angle, axis, this);
}
public OpenMatrix4f rotate(float angle, Vec3f axis, OpenMatrix4f dest) {
return rotate(angle, axis, this, dest);
}
public static OpenMatrix4f createRotatorDeg(float degree, Vec3f axis) {
return rotate((float)Math.toRadians(degree), axis, new OpenMatrix4f(), null);
}
public static OpenMatrix4f ofRotationDegree(float degree, Vec3f axis, @Nullable OpenMatrix4f dest) {
dest.setIdentity();
return rotate((float)Math.toRadians(degree), axis, dest, dest);
}
public static OpenMatrix4f rotate(float angle, Vec3f axis, OpenMatrix4f src, @Nullable OpenMatrix4f dest) {
if (dest == null) {
dest = new OpenMatrix4f();
} else if (dest.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
float c = (float) Math.cos(angle);
float s = (float) Math.sin(angle);
float oneminusc = 1.0f - c;
float xy = axis.x * axis.y;
float yz = axis.y * axis.z;
float xz = axis.x * axis.z;
float xs = axis.x * s;
float ys = axis.y * s;
float zs = axis.z * s;
float f00 = axis.x * axis.x * oneminusc+c;
float f01 = xy * oneminusc + zs;
float f02 = xz * oneminusc - ys;
// n[3] not used
float f10 = xy * oneminusc - zs;
float f11 = axis.y * axis.y * oneminusc+c;
float f12 = yz * oneminusc + xs;
// n[7] not used
float f20 = xz * oneminusc + ys;
float f21 = yz * oneminusc - xs;
float f22 = axis.z * axis.z * oneminusc+c;
float t00 = src.m00 * f00 + src.m10 * f01 + src.m20 * f02;
float t01 = src.m01 * f00 + src.m11 * f01 + src.m21 * f02;
float t02 = src.m02 * f00 + src.m12 * f01 + src.m22 * f02;
float t03 = src.m03 * f00 + src.m13 * f01 + src.m23 * f02;
float t10 = src.m00 * f10 + src.m10 * f11 + src.m20 * f12;
float t11 = src.m01 * f10 + src.m11 * f11 + src.m21 * f12;
float t12 = src.m02 * f10 + src.m12 * f11 + src.m22 * f12;
float t13 = src.m03 * f10 + src.m13 * f11 + src.m23 * f12;
dest.m20 = src.m00 * f20 + src.m10 * f21 + src.m20 * f22;
dest.m21 = src.m01 * f20 + src.m11 * f21 + src.m21 * f22;
dest.m22 = src.m02 * f20 + src.m12 * f21 + src.m22 * f22;
dest.m23 = src.m03 * f20 + src.m13 * f21 + src.m23 * f22;
dest.m00 = t00;
dest.m01 = t01;
dest.m02 = t02;
dest.m03 = t03;
dest.m10 = t10;
dest.m11 = t11;
dest.m12 = t12;
dest.m13 = t13;
return dest;
}
public Vec3f toTranslationVector() {
return toTranslationVector(this);
}
public Vec3f toTranslationVector(Vec3f dest) {
return toTranslationVector(this, dest);
}
public static Vec3f toTranslationVector(OpenMatrix4f matrix) {
return toTranslationVector(matrix, null);
}
public static Vec3f toTranslationVector(OpenMatrix4f matrix, Vec3f dest) {
if (dest == null) {
dest = new Vec3f();
}
dest.x = matrix.m30;
dest.y = matrix.m31;
dest.z = matrix.m32;
return dest;
}
public Quaternionf toQuaternion() {
return OpenMatrix4f.toQuaternion(this);
}
public Quaternionf toQuaternion(Quaternionf dest) {
return OpenMatrix4f.toQuaternion(this, dest);
}
public static Quaternionf toQuaternion(OpenMatrix4f matrix) {
return toQuaternion(matrix, new Quaternionf());
}
private static final OpenMatrix4f MATRIX_STORAGE = new OpenMatrix4f();
public static Quaternionf toQuaternion(OpenMatrix4f matrix, Quaternionf dest) {
if (dest == null) {
dest = new Quaternionf();
}
OpenMatrix4f.load(matrix, MATRIX_STORAGE);
float w, x, y, z;
MATRIX_STORAGE.transpose();
float lenX = MATRIX_STORAGE.m00 * MATRIX_STORAGE.m00 + MATRIX_STORAGE.m01 * MATRIX_STORAGE.m01 + MATRIX_STORAGE.m02 * MATRIX_STORAGE.m02;
float lenY = MATRIX_STORAGE.m10 * MATRIX_STORAGE.m10 + MATRIX_STORAGE.m11 * MATRIX_STORAGE.m11 + MATRIX_STORAGE.m12 * MATRIX_STORAGE.m12;
float lenZ = MATRIX_STORAGE.m20 * MATRIX_STORAGE.m20 + MATRIX_STORAGE.m21 * MATRIX_STORAGE.m21 + MATRIX_STORAGE.m22 * MATRIX_STORAGE.m22;
if (lenX == 0.0F || lenY == 0.0F || lenZ == 0.0F) {
return new Quaternionf(0.0F, 0.0F, 0.0F, 1.0F);
}
lenX = Math.invsqrt(lenX);
lenY = Math.invsqrt(lenY);
lenZ = Math.invsqrt(lenZ);
MATRIX_STORAGE.m00 *= lenX; MATRIX_STORAGE.m01 *= lenX; MATRIX_STORAGE.m02 *= lenX;
MATRIX_STORAGE.m10 *= lenY; MATRIX_STORAGE.m11 *= lenY; MATRIX_STORAGE.m12 *= lenY;
MATRIX_STORAGE.m20 *= lenZ; MATRIX_STORAGE.m21 *= lenZ; MATRIX_STORAGE.m22 *= lenZ;
float t;
float tr = MATRIX_STORAGE.m00 + MATRIX_STORAGE.m11 + MATRIX_STORAGE.m22;
if (tr >= 0.0F) {
t = (float)Math.sqrt(tr + 1.0F);
w = t * 0.5F;
t = 0.5F / t;
x = (MATRIX_STORAGE.m12 - MATRIX_STORAGE.m21) * t;
y = (MATRIX_STORAGE.m20 - MATRIX_STORAGE.m02) * t;
z = (MATRIX_STORAGE.m01 - MATRIX_STORAGE.m10) * t;
} else {
if (MATRIX_STORAGE.m00 >= MATRIX_STORAGE.m11 && MATRIX_STORAGE.m00 >= MATRIX_STORAGE.m22) {
t = (float)Math.sqrt(MATRIX_STORAGE.m00 - (MATRIX_STORAGE.m11 + MATRIX_STORAGE.m22) + 1.0);
x = t * 0.5F;
t = 0.5F / t;
y = (MATRIX_STORAGE.m10 + MATRIX_STORAGE.m01) * t;
z = (MATRIX_STORAGE.m02 + MATRIX_STORAGE.m20) * t;
w = (MATRIX_STORAGE.m12 - MATRIX_STORAGE.m21) * t;
} else if (MATRIX_STORAGE.m11 > MATRIX_STORAGE.m22) {
t = (float)Math.sqrt(MATRIX_STORAGE.m11 - (MATRIX_STORAGE.m22 + MATRIX_STORAGE.m00) + 1.0F);
y = t * 0.5F;
t = 0.5F / t;
z = (MATRIX_STORAGE.m21 + MATRIX_STORAGE.m12) * t;
x = (MATRIX_STORAGE.m10 + MATRIX_STORAGE.m01) * t;
w = (MATRIX_STORAGE.m20 - MATRIX_STORAGE.m02) * t;
} else {
t = (float)Math.sqrt(MATRIX_STORAGE.m22 - (MATRIX_STORAGE.m00 + MATRIX_STORAGE.m11) + 1.0F);
z = t * 0.5F;
t = 0.5F / t;
x = (MATRIX_STORAGE.m02 + MATRIX_STORAGE.m20) * t;
y = (MATRIX_STORAGE.m21 + MATRIX_STORAGE.m12) * t;
w = (MATRIX_STORAGE.m01 - MATRIX_STORAGE.m10) * t;
}
}
dest.x = x;
dest.y = y;
dest.z = z;
dest.w = w;
return dest;
}
public static OpenMatrix4f fromQuaternion(Quaternionf quaternion) {
return fromQuaternion(quaternion, null);
}
public static OpenMatrix4f fromQuaternion(Quaternionf quaternion, OpenMatrix4f dest) {
if (dest == null) {
dest = new OpenMatrix4f();
} else if (dest.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
float x = quaternion.x();
float y = quaternion.y();
float z = quaternion.z();
float w = quaternion.w();
float xy = x * y;
float xz = x * z;
float xw = x * w;
float yz = y * z;
float yw = y * w;
float zw = z * w;
float xSquared = 2F * x * x;
float ySquared = 2F * y * y;
float zSquared = 2F * z * z;
dest.m00 = 1.0F - ySquared - zSquared;
dest.m01 = 2.0F * (xy - zw);
dest.m02 = 2.0F * (xz + yw);
dest.m10 = 2.0F * (xy + zw);
dest.m11 = 1.0F - xSquared - zSquared;
dest.m12 = 2.0F * (yz - xw);
dest.m20 = 2.0F * (xz - yw);
dest.m21 = 2.0F * (yz + xw);
dest.m22 = 1.0F - xSquared - ySquared;
return dest;
}
public OpenMatrix4f scale(float x, float y, float z) {
VECTOR_STORAGE.set(x, y, z);
return this.scale(VECTOR_STORAGE);
}
public OpenMatrix4f scale(Vec3f vec) {
return scale(vec, this, this);
}
public static OpenMatrix4f scale(Vec3f vec, OpenMatrix4f src, @Nullable OpenMatrix4f dest) {
if (dest == null) {
dest = new OpenMatrix4f();
} else if (dest.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
dest.m00 = src.m00 * vec.x;
dest.m01 = src.m01 * vec.x;
dest.m02 = src.m02 * vec.x;
dest.m03 = src.m03 * vec.x;
dest.m10 = src.m10 * vec.y;
dest.m11 = src.m11 * vec.y;
dest.m12 = src.m12 * vec.y;
dest.m13 = src.m13 * vec.y;
dest.m20 = src.m20 * vec.z;
dest.m21 = src.m21 * vec.z;
dest.m22 = src.m22 * vec.z;
dest.m23 = src.m23 * vec.z;
return dest;
}
public Vec3f toScaleVector() {
return toScaleVector(null);
}
public Vec3f toScaleVector(Vec3f dest) {
if (dest == null) {
dest = new Vec3f();
}
VECTOR_STORAGE.set(this.m00, this.m01, this.m02);
dest.x = VECTOR_STORAGE.length();
VECTOR_STORAGE.set(this.m10, this.m11, this.m12);
dest.y = VECTOR_STORAGE.length();
VECTOR_STORAGE.set(this.m20, this.m21, this.m22);
dest.z = VECTOR_STORAGE.length();
return dest;
}
public OpenMatrix4f removeTranslation() {
return removeTranslation(this, null);
}
public static OpenMatrix4f removeTranslation(OpenMatrix4f src, @Nullable OpenMatrix4f dest) {
if (dest == null) {
dest = new OpenMatrix4f();
} else if (dest.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
dest.load(src);
dest.m30 = 0.0F;
dest.m31 = 0.0F;
dest.m32 = 0.0F;
return dest;
}
public OpenMatrix4f removeScale() {
return removeScale(this, null);
}
public static OpenMatrix4f removeScale(OpenMatrix4f src, @Nullable OpenMatrix4f dest) {
if (dest == null) {
dest = new OpenMatrix4f();
} else if (dest.immutable) {
throw new UnsupportedOperationException("Can't modify immutable matrix");
}
VECTOR_STORAGE.set(src.m00, src.m01, src.m02);
float xScale = VECTOR_STORAGE.length();
VECTOR_STORAGE.set(src.m10, src.m11, src.m12);
float yScale = VECTOR_STORAGE.length();
VECTOR_STORAGE.set(src.m20, src.m21, src.m22);
float zScale = VECTOR_STORAGE.length();
dest.load(src);
dest.scale(1.0F / xScale, 1.0F / yScale, 1.0F / zScale);
return dest;
}
@Override
public String toString() {
return "\n" +
String.format("%.4f", m00) + " " + String.format("%.4f", m01) + " " + String.format("%.4f", m02) + " " + String.format("%.4f", m03) + "\n" +
String.format("%.4f", m10) + " " + String.format("%.4f", m11) + " " + String.format("%.4f", m12) + " " + String.format("%.4f", m13) + "\n" +
String.format("%.4f", m20) + " " + String.format("%.4f", m21) + " " + String.format("%.4f", m22) + " " + String.format("%.4f", m23) + "\n" +
String.format("%.4f", m30) + " " + String.format("%.4f", m31) + " " + String.format("%.4f", m32) + " " + String.format("%.4f", m33) + "\n"
;
}
public static Matrix4f exportToMojangMatrix(OpenMatrix4f src) {
return exportToMojangMatrix(src, null);
}
public static Matrix4f exportToMojangMatrix(OpenMatrix4f src, Matrix4f dest) {
if (dest == null) {
dest = new Matrix4f();
}
MATRIX_TRANSFORMER.position(0);
src.store(MATRIX_TRANSFORMER);
MATRIX_TRANSFORMER.position(0);
return dest.set(MATRIX_TRANSFORMER);
}
public static OpenMatrix4f importFromMojangMatrix(Matrix4f src) {
MATRIX_TRANSFORMER.position(0);
src.get(MATRIX_TRANSFORMER);
return OpenMatrix4f.load(null, MATRIX_TRANSFORMER);
}
public static OpenMatrix4f[] allocateMatrixArray(int size) {
OpenMatrix4f[] matrixArray = new OpenMatrix4f[size];
for (int i = 0; i < size; i++) {
matrixArray[i] = new OpenMatrix4f();
}
return matrixArray;
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.math;
import org.joml.Quaternionf;
import org.joml.Vector3f;
public class QuaternionUtils {
public static Axis XN = new Axis(-1.0F, 0.0F, 0.0F);
public static Axis XP = new Axis(1.0F, 0.0F, 0.0F);
public static Axis YN = new Axis(0.0F, -1.0F, 0.0F);
public static Axis YP = new Axis(0.0F, 1.0F, 0.0F);
public static Axis ZN = new Axis(0.0F, 0.0F, -1.0F);
public static Axis ZP = new Axis(0.0F, 0.0F, 1.0F);
public static Quaternionf rotationDegrees(Vector3f axis, float degress) {
float angle = degress * (float) Math.PI / 180;
return rotation(axis, angle);
}
public static Quaternionf rotation(Vector3f axis, float angle) {
Quaternionf quat = new Quaternionf();
quat.setAngleAxis(angle, axis.x, axis.y, axis.z);
return quat;
}
public static class Axis {
private final Vector3f axis;
public Axis(float x, float y, float z) {
this.axis = new Vector3f(x, y, z);
}
public Quaternionf rotation(float angle) {
return QuaternionUtils.rotation(axis, angle);
}
public Quaternionf rotationDegrees(float degrees) {
return QuaternionUtils.rotationDegrees(axis, degrees);
}
}
}

View File

@@ -0,0 +1,139 @@
/*
* 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.math;
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
public interface ValueModifier {
public static final Codec<Unified> CODEC =
RecordCodecBuilder.create(instance -> instance.group(
Codec.FLOAT.fieldOf("adder").forGetter(Unified::adder),
Codec.FLOAT.fieldOf("multiplier").forGetter(Unified::multiplier),
Codec.FLOAT.fieldOf("setter").forGetter(Unified::setter)
).apply(instance, Unified::new)
);
public void attach(ResultCalculator calculator);
public static ValueModifier adder(float value) {
return new Adder(value);
}
public static ValueModifier multiplier(float value) {
return new Multiplier(value);
}
public static ValueModifier setter(float arg) {
return new Setter(arg);
}
public static record Adder(float adder) implements ValueModifier {
@Override
public void attach(ResultCalculator calculator) {
calculator.add += this.adder;
}
}
public static record Multiplier(float multiplier) implements ValueModifier {
@Override
public void attach(ResultCalculator calculator) {
calculator.multiply *= this.multiplier;
}
}
public static record Setter(float setter) implements ValueModifier {
@Override
public void attach(ResultCalculator calculator) {
if (Float.isNaN(calculator.set)) {
calculator.set = this.setter;
} else if (!Float.isNaN(this.setter)) {
calculator.set = Math.min(calculator.set, this.setter);
}
}
}
public static record Unified(float adder, float multiplier, float setter) implements ValueModifier {
@Override
public void attach(ResultCalculator calculator) {
if (Float.isNaN(calculator.set)) {
calculator.set = this.setter;
} else if (!Float.isNaN(this.setter)) {
calculator.set = Math.min(calculator.set, this.setter);
}
calculator.add += this.adder;
calculator.multiply *= this.multiplier;
}
}
public static ResultCalculator calculator() {
return new ResultCalculator();
}
public static class ResultCalculator implements ValueModifier {
private float set = Float.NaN;
private float add = 0.0F;
private float multiply = 1.0F;
public ResultCalculator attach(ValueModifier valueModifier) {
valueModifier.attach(this);
return this;
}
@Override
public void attach(ResultCalculator calculator) {
if (Float.isNaN(calculator.set)) {
calculator.set = this.set;
} else if (!Float.isNaN(this.set)) {
calculator.set = Math.min(calculator.set, this.set);
}
calculator.add += this.add;
calculator.multiply *= this.multiply;
}
public ValueModifier toValueModifier() {
if (Float.isNaN(this.set)) {
if (Float.compare(this.add, 0.0F) == 0 && Float.compare(this.multiply, 1.0F) != 0) {
return new Multiplier(this.multiply);
} else if (Float.compare(this.add, 0.0F) != 0 && Float.compare(this.multiply, 1.0F) == 0) {
return new Adder(this.add);
}
} else if (Float.compare(this.add, 0.0F) == 0 && Float.compare(this.multiply, 1.0F) == 0) {
return new Setter(this.set);
}
return new Unified(this.set, this.add, this.multiply);
}
public void set(float f) {
this.set = f;
}
public void add(float f) {
this.add += add;
}
public void multiply(float f) {
this.multiply *= f;
}
public float getResult(float baseValue) {
float result = baseValue;
if (!Float.isNaN(this.set)) {
result = this.set;
}
result += this.add;
result *= this.multiply;
return result;
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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.math;
public class Vec2f {
public float x;
public float y;
public Vec2f() {
this.x = 0;
this.y = 0;
}
public Vec2f(float x, float y) {
this.x = x;
this.y = y;
}
public Vec2f scale(float f) {
this.x *= f;
this.y *= f;
return this;
}
@Override
public String toString() {
return "Vec2f[" + this.x + ", " + this.y + ", " + "]";
}
}

View File

@@ -0,0 +1,465 @@
/*
* 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.math;
import java.util.Collection;
import java.util.List;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import net.minecraft.world.phys.Vec3;
import com.tiedup.remake.rig.TiedUpRigConstants;
public class Vec3f extends Vec2f {
public static final Vec3f X_AXIS = new Vec3f(1.0F, 0.0F, 0.0F);
public static final Vec3f Y_AXIS = new Vec3f(0.0F, 1.0F, 0.0F);
public static final Vec3f Z_AXIS = new Vec3f(0.0F, 0.0F, 1.0F);
public static final Vec3f M_X_AXIS = new Vec3f(-1.0F, 0.0F, 0.0F);
public static final Vec3f M_Y_AXIS = new Vec3f(0.0F, -1.0F, 0.0F);
public static final Vec3f M_Z_AXIS = new Vec3f(0.0F, 0.0F, -1.0F);
public static final Vec3f ZERO = new Vec3f(0.0F, 0.0F, 0.0F);
public float z;
public Vec3f() {
super();
this.z = 0;
}
public Vec3f(float x, float y, float z) {
super(x, y);
this.z = z;
}
public Vec3f(double x, double y, double z) {
this((float)x, (float)y, (float)z);
}
public Vec3f(Vec3 mojangVec) {
this((float)mojangVec.x, (float)mojangVec.y, (float)mojangVec.z);
}
public Vec3f set(float x, float y, float z) {
this.x = x;
this.y = y;
this.z = z;
return this;
}
public Vec3f set(Vec3 vec3f) {
this.x = (float)vec3f.x;
this.y = (float)vec3f.y;
this.z = (float)vec3f.z;
return this;
}
public Vec3f set(Vec3f vec3f) {
this.x = vec3f.x;
this.y = vec3f.y;
this.z = vec3f.z;
return this;
}
public Vec3f add(float x, float y, float z) {
this.x += x;
this.y += y;
this.z += z;
return this;
}
public Vec3f add(Vec3f vec) {
return this.add(vec.x, vec.y, vec.z);
}
public Vec3f add(Vec3 vec) {
return this.add((float)vec.x, (float)vec.y, (float)vec.z);
}
public Vec3f sub(float x, float y, float z) {
this.x -= x;
this.y -= y;
this.z -= z;
return this;
}
public Vec3f sub(Vec3f vec) {
return this.sub(vec.x, vec.y, vec.z);
}
public static Vec3f add(Vec3f left, Vec3f right, Vec3f dest) {
if (dest == null) {
return new Vec3f(left.x + right.x, left.y + right.y, left.z + right.z);
} else {
dest.set(left.x + right.x, left.y + right.y, left.z + right.z);
return dest;
}
}
public static Vec3f sub(Vec3f left, Vec3f right, Vec3f dest) {
if (dest == null) {
return new Vec3f(left.x - right.x, left.y - right.y, left.z - right.z);
} else {
dest.set(left.x - right.x, left.y - right.y, left.z - right.z);
return dest;
}
}
public Vec3f multiply(Vec3f vec) {
return multiply(this, this, vec.x, vec.y, vec.z);
}
public Vec3f multiply(float x, float y, float z) {
return multiply(this, this, x, y, z);
}
public static Vec3f multiply(Vec3f src, Vec3f dest, float x, float y, float z) {
if (dest == null) {
dest = new Vec3f();
}
dest.x = src.x * x;
dest.y = src.y * y;
dest.z = src.z * z;
return dest;
}
@Override
public Vec3f scale(float f) {
return scale(this, this, f);
}
public static Vec3f scale(Vec3f src, Vec3f dest, float f) {
if (dest == null) {
dest = new Vec3f();
}
dest.x = src.x * f;
dest.y = src.y * f;
dest.z = src.z * f;
return dest;
}
public Vec3f copy() {
return new Vec3f(this.x, this.y, this.z);
}
public float length() {
return (float) Math.sqrt(this.lengthSqr());
}
public float lengthSqr() {
return this.x * this.x + this.y * this.y + this.z * this.z;
}
public float distance(Vec3f opponent) {
return (float)Math.sqrt(this.distanceSqr(opponent));
}
public float distanceSqr(Vec3f opponent) {
return (float)(Math.pow(this.x - opponent.x, 2) + Math.pow(this.y - opponent.y, 2) + Math.pow(this.z - opponent.z, 2));
}
public float horizontalDistance() {
return (float)Math.sqrt(this.x * this.x + this.z * this.z);
}
public float horizontalDistanceSqr() {
return this.x * this.x + this.z * this.z;
}
public void rotate(float degree, Vec3f axis) {
rotate(degree, axis, this, this);
}
public void invalidate() {
this.x = Float.NaN;
this.y = Float.NaN;
this.z = Float.NaN;
}
public boolean validateValues() {
return Float.isFinite(this.x) && Float.isFinite(this.y) && Float.isFinite(this.z);
}
public static Vec3f rotate(float degree, Vec3f axis, Vec3f src, Vec3f dest) {
if (dest == null) {
dest = new Vec3f();
}
return OpenMatrix4f.transform3v(OpenMatrix4f.createRotatorDeg(degree, axis), src, dest);
}
private static final Vector3f SRC = new Vector3f();
private static final Vector3f TRANSFORM_RESULT = new Vector3f();
public static Vec3f rotate(Quaternionf rot, Vec3f src, Vec3f dest) {
if (dest == null) {
dest = new Vec3f();
}
SRC.set(src.x, src.y, src.z);
rot.transform(SRC, TRANSFORM_RESULT);
dest.set(TRANSFORM_RESULT.x, TRANSFORM_RESULT.y, TRANSFORM_RESULT.z);
return dest;
}
public static float dot(Vec3f left, Vec3f right) {
return left.x * right.x + left.y * right.y + left.z * right.z;
}
public static Vec3f cross(Vec3f left, Vec3f right, Vec3f dest) {
if (dest == null) {
dest = new Vec3f();
}
dest.set(left.y * right.z - left.z * right.y, right.x * left.z - right.z * left.x, left.x * right.y - left.y * right.x);
return dest;
}
public static float getAngleBetween(Vec3f a, Vec3f b) {
return (float) Math.acos(Math.min(1.0F, Vec3f.dot(a, b) / (a.length() * b.length())));
}
public static Quaternionf getRotatorBetween(Vec3f a, Vec3f b, Quaternionf dest) {
if (dest == null) {
dest = new Quaternionf();
}
Vec3f axis = Vec3f.cross(a, b, null).normalize();
float dotDivLength = Vec3f.dot(a, b) / (a.length() * b.length());
if (!Float.isFinite(dotDivLength)) {
TiedUpRigConstants.LOGGER.info("Warning : given vector's length is zero");
(new IllegalArgumentException()).printStackTrace();
dotDivLength = 1.0F;
}
float radian = (float)Math.acos(Math.min(1.0F, dotDivLength));
dest.setAngleAxis(radian, axis.x, axis.y, axis.z);
return dest;
}
public static Vec3f interpolate(Vec3f from, Vec3f to, float interpolation, Vec3f dest) {
if (dest == null) {
dest = new Vec3f();
}
dest.x = from.x + (to.x - from.x) * interpolation;
dest.y = from.y + (to.y - from.y) * interpolation;
dest.z = from.z + (to.z - from.z) * interpolation;
return dest;
}
public Vec3f normalize() {
return normalize(this, this);
}
public static Vec3f normalize(Vec3f src, Vec3f dest) {
if (dest == null) {
dest = new Vec3f();
}
float norm = (float) Math.sqrt(src.x * src.x + src.y * src.y + src.z * src.z);
if (norm > 1E-5F) {
dest.x = src.x / norm;
dest.y = src.y / norm;
dest.z = src.z / norm;
} else {
dest.x = 0;
dest.y = 0;
dest.z = 0;
}
return dest;
}
@Override
public String toString() {
return "[" + this.x + ", " + this.y + ", " + this.z + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (o instanceof Vec3f vec3f) {
return Float.compare(this.x, vec3f.x) == 0 && Float.compare(this.y, vec3f.y) == 0 && Float.compare(this.z, vec3f.z) == 0;
}
return false;
}
@Override
public int hashCode() {
int j = Float.floatToIntBits(this.x);
int i = (int) (j ^ j >>> 32);
j = Float.floatToIntBits(this.y);
i = 31 * i + (int) (j ^ j >>> 32);
j = Float.floatToIntBits(this.z);
return 31 * i + (int) (j ^ j >>> 32);
}
public static Vec3f average(Collection<Vec3f> vectors, Vec3f dest) {
if (dest == null) {
dest = new Vec3f();
}
dest.set(0.0F, 0.0F, 0.0F);
for (Vec3f v : vectors) {
dest.add(v);
}
dest.scale(1.0F / vectors.size());
return dest;
}
public static Vec3f average(Vec3f dest, Vec3f... vectors) {
if (dest == null) {
dest = new Vec3f();
}
dest.set(0.0F, 0.0F, 0.0F);
for (Vec3f v : vectors) {
dest.add(v);
}
dest.scale(vectors.length);
return dest;
}
public static int getNearest(Vec3f from, List<Vec3f> vectors) {
float minLength = Float.MAX_VALUE;
int index = -1;
for (int i = 0; i < vectors.size(); i++) {
if (vectors.get(i) == null) {
continue;
}
if (!vectors.get(i).validateValues()) {
continue;
}
float distSqr = from.distanceSqr(vectors.get(i));
if (distSqr < minLength) {
minLength = distSqr;
index = i;
}
}
return index;
}
public static int getNearest(Vec3f from, Vec3f... vectors) {
float minLength = Float.MAX_VALUE;
int index = -1;
for (int i = 0; i < vectors.length; i++) {
if (vectors[i] == null) {
continue;
}
if (!vectors[i].validateValues()) {
continue;
}
float distSqr = from.distanceSqr(vectors[i]);
if (distSqr < minLength) {
minLength = distSqr;
index = i;
}
}
return index;
}
public static int getMostSimilar(Vec3f start, Vec3f end, Vec3f... vectors) {
Vec3f.sub(end, start, BASIS_DIRECTION);
float maxDot = Float.MIN_VALUE;
int index = -1;
for (int i = 0; i < vectors.length; i++) {
if (vectors[i] == null) {
continue;
}
if (!vectors[i].validateValues()) {
continue;
}
Vec3f.sub(vectors[i], start, COMPARISION);
float dot = Vec3f.dot(BASIS_DIRECTION, COMPARISION) / BASIS_DIRECTION.length() * COMPARISION.length();
if (dot > maxDot) {
maxDot = dot;
index = i;
}
}
return index;
}
private static final Vec3f BASIS_DIRECTION = new Vec3f();
private static final Vec3f COMPARISION = new Vec3f();
public static int getMostSimilar(Vec3f start, Vec3f end, List<Vec3f> vectors) {
Vec3f.sub(end, start, BASIS_DIRECTION);
float maxDot = Float.MIN_VALUE;
int index = -1;
for (int i = 0; i < vectors.size(); i++) {
if (vectors.get(i) == null) {
continue;
}
if (!vectors.get(i).validateValues()) {
continue;
}
Vec3f.sub(vectors.get(i), start, COMPARISION);
float dot = Vec3f.dot(BASIS_DIRECTION, COMPARISION) / BASIS_DIRECTION.length() * COMPARISION.length();
if (dot > maxDot) {
maxDot = dot;
index = i;
}
}
return index;
}
public Vec3 toDoubleVector() {
return new Vec3(this.x, this.y, this.z);
}
public static Vec3f fromDoubleVector(Vec3 vec3) {
return new Vec3f((float)vec3.x(), (float)vec3.y(), (float)vec3.z());
}
private static final OpenMatrix4f DEST = new OpenMatrix4f();
public Vec3f rotateDegree(Vec3f axis, float degree) {
OpenMatrix4f.ofRotationDegree(degree, axis, DEST);
OpenMatrix4f.transform3v(DEST, this, this);
return this;
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.math;
public class Vec4f extends Vec3f {
public float w;
public Vec4f() {
super();
this.w = 0;
}
public Vec4f(float x, float y, float z, float w) {
super(x, y, z);
this.w = w;
}
public Vec4f(Vec3f vec3f) {
super(vec3f.x, vec3f.y, vec3f.z);
this.w = 1.0F;
}
public void set(float x, float y, float z, float w) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
public void set(Vec4f vec4f) {
super.set(vec4f);
this.w = vec4f.w;
}
public Vec4f add(float x, float y, float z, float w) {
this.x += x;
this.y += y;
this.z += z;
this.w += w;
return this;
}
public static Vec4f add(Vec4f left, Vec4f right, Vec4f dest) {
if (dest == null) {
dest = new Vec4f();
}
dest.x = left.x + right.x;
dest.y = left.y + right.y;
dest.z = left.z + right.z;
dest.w = left.w + right.w;
return dest;
}
@Override
public Vec4f scale(float f) {
super.scale(f);
this.w *= f;
return this;
}
public Vec4f transform(OpenMatrix4f matrix) {
return OpenMatrix4f.transform(matrix, this, this);
}
@Override
public String toString() {
return "Vec4f[" + this.x + ", " + this.y + ", " + this.z + ", " + this.w + "]";
}
}

View File

@@ -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.mesh;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import com.google.common.collect.Maps;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.tiedup.remake.rig.mesh.ClassicMesh.ClassicMeshPart;
import com.tiedup.remake.rig.armature.Armature;
import com.tiedup.remake.rig.math.OpenMatrix4f;
import com.tiedup.remake.rig.TiedUpRigConstants;
public class ClassicMesh extends StaticMesh<ClassicMeshPart> {
public ClassicMesh(Map<String, Number[]> arrayMap, Map<MeshPartDefinition, List<VertexBuilder>> partBuilders, ClassicMesh parent, RenderProperties properties) {
super(arrayMap, partBuilders, parent, properties);
}
@Override
protected Map<String, ClassicMeshPart> createModelPart(Map<MeshPartDefinition, List<VertexBuilder>> partBuilders) {
Map<String, ClassicMeshPart> parts = Maps.newHashMap();
partBuilders.forEach((partDefinition, vertexBuilder) -> {
parts.put(partDefinition.partName(), new ClassicMeshPart(vertexBuilder, partDefinition.renderProperties(), partDefinition.getModelPartAnimationProvider()));
});
return parts;
}
@Override
protected ClassicMeshPart getOrLogException(Map<String, ClassicMeshPart> parts, String name) {
if (!parts.containsKey(name)) {
TiedUpRigConstants.LOGGER.debug("Can not find the mesh part named " + name + " in " + this.getClass().getCanonicalName());
return null;
}
return parts.get(name);
}
@Override
public void draw(PoseStack poseStack, VertexConsumer vertexConsumer, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay) {
for (ClassicMeshPart part : this.parts.values()) {
part.draw(poseStack, vertexConsumer, drawingFunction, packedLight, r, g, b, a, overlay);
}
}
@Override
public void drawPosed(PoseStack poseStack, VertexConsumer vertexConsumer, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay, Armature armature, OpenMatrix4f[] poses) {
this.draw(poseStack, vertexConsumer, drawingFunction, packedLight, r, g, b, a, overlay);
}
public class ClassicMeshPart extends MeshPart {
public ClassicMeshPart(List<VertexBuilder> verticies, @Nullable Mesh.RenderProperties renderProperties, @Nullable Supplier<OpenMatrix4f> vanillaPartTracer) {
super(verticies, renderProperties, vanillaPartTracer);
}
protected static final Vector4f POSITION = new Vector4f();
protected static final Vector3f NORMAL = new Vector3f();
@Override
public void draw(PoseStack poseStack, VertexConsumer bufferbuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay) {
if (this.isHidden()) {
return;
}
Vector4f color = this.getColor(r, g, b, a);
poseStack.pushPose();
OpenMatrix4f transform = this.getVanillaPartTransform();
if (transform != null) {
poseStack.mulPoseMatrix(OpenMatrix4f.exportToMojangMatrix(transform));
}
Matrix4f matrix4f = poseStack.last().pose();
Matrix3f matrix3f = poseStack.last().normal();
for (VertexBuilder vi : this.getVertices()) {
getVertexPosition(vi.position, POSITION);
getVertexNormal(vi.normal, NORMAL);
POSITION.mul(matrix4f);
NORMAL.mul(matrix3f);
drawingFunction.draw(bufferbuilder, POSITION.x(), POSITION.y(), POSITION.z(), NORMAL.x(), NORMAL.y(), NORMAL.z(), packedLight, color.x, color.y, color.z, color.w, uvs[vi.uv * 2], uvs[vi.uv * 2 + 1], overlay);
}
poseStack.popPose();
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* 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.mesh;
import java.util.Map;
import javax.annotation.Nullable;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import com.tiedup.remake.rig.cloth.ClothSimulatable;
import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObject;
import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObjectBuilder;
import com.tiedup.remake.rig.armature.Armature;
import com.tiedup.remake.rig.math.OpenMatrix4f;
public class CompositeMesh implements Mesh, SoftBodyTranslatable {
private final StaticMesh<?> staticMesh;
private final SoftBodyTranslatable softBodyMesh;
public CompositeMesh(StaticMesh<?> staticMesh, SoftBodyTranslatable softBodyMesh) {
this.staticMesh = staticMesh;
this.softBodyMesh = softBodyMesh;
}
@Override
public void initialize() {
this.staticMesh.initialize();
}
@Override
public void draw(PoseStack poseStack, VertexConsumer bufferBuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay) {
this.staticMesh.draw(poseStack, bufferBuilder, drawingFunction, packedLight, r, g, b, a, overlay);
this.softBodyMesh.getOriginalMesh().draw(poseStack, bufferBuilder, drawingFunction, packedLight, r, g, b, a, overlay);
}
@Override
public void drawPosed(PoseStack poseStack, VertexConsumer bufferBuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay, Armature armature, OpenMatrix4f[] poses) {
this.staticMesh.drawPosed(poseStack, bufferBuilder, drawingFunction, packedLight, r, g, b, a, overlay, armature, poses);
this.softBodyMesh.getOriginalMesh().drawPosed(poseStack, bufferBuilder, drawingFunction, packedLight, r, g, b, a, overlay, armature, poses);
}
@Override
public boolean canStartSoftBodySimulation() {
return this.softBodyMesh.canStartSoftBodySimulation();
}
@Override
public ClothObject createSimulationData(@Nullable SoftBodyTranslatable provider, ClothSimulatable simOwner, ClothObjectBuilder simBuilder) {
return this.softBodyMesh.createSimulationData(this, simOwner, simBuilder);
}
@Nullable
public StaticMesh<?> getStaticMesh() {
return this.staticMesh;
}
@Override
public StaticMesh<?> getOriginalMesh() {
return (StaticMesh<?>)this.softBodyMesh;
}
@Override
public void putSoftBodySimulationInfo(Map<String, ClothSimulationInfo> sofyBodySimulationInfo) {
this.softBodyMesh.putSoftBodySimulationInfo(sofyBodySimulationInfo);
}
@Override
public Map<String, ClothSimulationInfo> getSoftBodySimulationInfo() {
return this.softBodyMesh.getSoftBodySimulationInfo();
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.mesh;
import java.util.List;
import java.util.Map;
import net.minecraft.world.entity.EquipmentSlot;
import com.tiedup.remake.rig.asset.AssetAccessor;
import com.tiedup.remake.rig.mesh.MeshPartDefinition;
import com.tiedup.remake.rig.mesh.Meshes;
import com.tiedup.remake.rig.mesh.SkinnedMesh;
import com.tiedup.remake.rig.mesh.VertexBuilder;
public class HumanoidMesh extends SkinnedMesh {
public final SkinnedMeshPart head;
public final SkinnedMeshPart torso;
public final SkinnedMeshPart leftArm;
public final SkinnedMeshPart rightArm;
public final SkinnedMeshPart leftLeg;
public final SkinnedMeshPart rightLeg;
public final SkinnedMeshPart hat;
public final SkinnedMeshPart jacket;
public final SkinnedMeshPart leftSleeve;
public final SkinnedMeshPart rightSleeve;
public final SkinnedMeshPart leftPants;
public final SkinnedMeshPart rightPants;
public HumanoidMesh(Map<String, Number[]> arrayMap, Map<MeshPartDefinition, List<VertexBuilder>> parts, SkinnedMesh parent, RenderProperties properties) {
super(arrayMap, parts, parent, properties);
this.head = this.getOrLogException(this.parts, "head");
this.torso = this.getOrLogException(this.parts, "torso");
this.leftArm = this.getOrLogException(this.parts, "leftArm");
this.rightArm = this.getOrLogException(this.parts, "rightArm");
this.leftLeg = this.getOrLogException(this.parts, "leftLeg");
this.rightLeg = this.getOrLogException(this.parts, "rightLeg");
this.hat = this.getOrLogException(this.parts, "hat");
this.jacket = this.getOrLogException(this.parts, "jacket");
this.leftSleeve = this.getOrLogException(this.parts, "leftSleeve");
this.rightSleeve = this.getOrLogException(this.parts, "rightSleeve");
this.leftPants = this.getOrLogException(this.parts, "leftPants");
this.rightPants = this.getOrLogException(this.parts, "rightPants");
}
public AssetAccessor<? extends SkinnedMesh> getHumanoidArmorModel(EquipmentSlot slot) {
// RIG : Meshes.{HELMET,CHESTPLATE,LEGGINS,BOOTS} strippés en Phase 0 (armor rendering
// hors scope bondage V1). Re-implém Phase 2 si besoin de rendre armures vanilla
// sur le rig — pour l'instant retour null = armor rendering off.
return null;
}
}

View File

@@ -0,0 +1,214 @@
/*
* 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.mesh;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import javax.annotation.Nullable;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import org.lwjgl.system.MemoryStack;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.core.Vec3i;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.client.extensions.IForgeVertexConsumer;
import net.minecraftforge.client.model.IQuadTransformer;
import com.tiedup.remake.rig.armature.Armature;
import com.tiedup.remake.rig.math.OpenMatrix4f;
import com.tiedup.remake.rig.math.Vec3f;
import com.tiedup.remake.rig.render.TiedUpRenderTypes;
public interface Mesh {
void initialize();
/* Draw wihtout mesh deformation */
void draw(PoseStack poseStack, VertexConsumer vertexConsumer, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay);
/* Draw with mesh deformation */
void drawPosed(PoseStack poseStack, VertexConsumer vertexConsumer, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay, @Nullable Armature armature, OpenMatrix4f[] poses);
/* Universal method */
default void draw(PoseStack poseStack, MultiBufferSource bufferSources, RenderType renderType, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay, @Nullable Armature armature, OpenMatrix4f[] poses) {
this.drawPosed(poseStack, bufferSources.getBuffer(TiedUpRenderTypes.getTriangulated(renderType)), drawingFunction, packedLight, r, g, b, a, overlay, armature, poses);
}
public static record RenderProperties(ResourceLocation customTexturePath, Vec3f customColor, boolean isTransparent) {
public static class Builder {
protected String customTexturePath;
protected Vec3f customColor = new Vec3f();
protected boolean isTransparent;
public RenderProperties.Builder customTexturePath(String path) {
this.customTexturePath = path;
return this;
}
public RenderProperties.Builder transparency(boolean isTransparent) {
this.isTransparent = isTransparent;
return this;
}
public RenderProperties.Builder customColor(float r, float g, float b) {
this.customColor.x = r;
this.customColor.y = g;
this.customColor.z = b;
return this;
}
public RenderProperties build() {
return new RenderProperties(this.customTexturePath == null ? null : ResourceLocation.parse(this.customTexturePath), this.customColor, this.isTransparent);
}
public static RenderProperties.Builder create() {
return new RenderProperties.Builder();
}
}
}
@FunctionalInterface
public interface DrawingFunction {
public static final DrawingFunction NEW_ENTITY = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> {
builder.vertex(posX, posY, posZ, r, g, b, a, u, v, overlay, packedLight, normX, normY, normZ);
};
public static final DrawingFunction POSITION_TEX = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> {
builder.vertex(posX, posY, posZ);
builder.uv(u, v);
builder.endVertex();
};
public static final DrawingFunction POSITION_TEX_COLOR_NORMAL = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> {
builder.vertex(posX, posY, posZ);
builder.uv(u, v);
builder.color(r, g, b, a);
builder.normal(normX, normY, normZ);
builder.endVertex();
};
public static final DrawingFunction POSITION_TEX_COLOR_LIGHTMAP = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> {
builder.vertex(posX, posY, posZ);
builder.uv(u, v);
builder.color(r, g, b, a);
builder.uv2(packedLight);
builder.endVertex();
};
public static final DrawingFunction POSITION_COLOR_LIGHTMAP = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> {
builder.vertex(posX, posY, posZ);
builder.color(r, g, b, a);
builder.uv2(packedLight);
builder.endVertex();
};
public static final DrawingFunction POSITION_COLOR_NORMAL = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> {
builder.vertex(posX, posY, posZ);
builder.color(r, g, b, a);
builder.normal(normX, normY, normZ);
builder.endVertex();
};
public static final DrawingFunction POSITION_COLOR_TEX_LIGHTMAP = (builder, posX, posY, posZ, normX, normY, normZ, packedLight, r, g, b, a, u, v, overlay) -> {
builder.vertex(posX, posY, posZ);
builder.color(r, g, b, a);
builder.uv(u, v);
builder.uv2(packedLight);
builder.endVertex();
};
public void draw(VertexConsumer vertexConsumer, float posX, float posY, float posZ, float normX, float normY, float normZ, int packedLight, float r, float g, float b, float a, float u, float v, int overlay);
default void putBulkData(PoseStack.Pose pose, BakedQuad bakedQuad, VertexConsumer vertexConsumer, float red, float green, float blue, float alpha, int packedLight, int packedOverlay, boolean readExistingColor) {
putBulkDataWithDrawingFunction(this, vertexConsumer, pose, bakedQuad, new float[] { 1.0F, 1.0F, 1.0F, 1.0F }, red, green, blue, alpha, new int[] { packedLight, packedLight, packedLight, packedLight }, packedOverlay, readExistingColor);
}
static void putBulkDataWithDrawingFunction(DrawingFunction drawingFunction, VertexConsumer builder, PoseStack.Pose pPoseEntry, BakedQuad pQuad, float[] pColorMuls, float pRed, float pGreen, float pBlue, float alpha, int[] pCombinedLights, int pCombinedOverlay, boolean pMulColor) {
float[] afloat = new float[] { pColorMuls[0], pColorMuls[1], pColorMuls[2], pColorMuls[3] };
int[] aint1 = pQuad.getVertices();
Vec3i vec3i = pQuad.getDirection().getNormal();
Matrix4f matrix4f = pPoseEntry.pose();
Vector3f vector3f = pPoseEntry.normal().transform(new Vector3f((float) vec3i.getX(), (float) vec3i.getY(), (float) vec3i.getZ()));
int j = aint1.length / 8;
try (MemoryStack memorystack = MemoryStack.stackPush()) {
ByteBuffer bytebuffer = memorystack.malloc(DefaultVertexFormat.BLOCK.getVertexSize());
IntBuffer intbuffer = bytebuffer.asIntBuffer();
for (int k = 0; k < j; ++k) {
intbuffer.clear();
intbuffer.put(aint1, k * 8, 8);
float f = bytebuffer.getFloat(0);
float f1 = bytebuffer.getFloat(4);
float f2 = bytebuffer.getFloat(8);
float f3;
float f4;
float f5;
if (pMulColor) {
float f6 = (float) (bytebuffer.get(12) & 255) / 255.0F;
float f7 = (float) (bytebuffer.get(13) & 255) / 255.0F;
float f8 = (float) (bytebuffer.get(14) & 255) / 255.0F;
f3 = f6 * afloat[k] * pRed;
f4 = f7 * afloat[k] * pGreen;
f5 = f8 * afloat[k] * pBlue;
} else {
f3 = afloat[k] * pRed;
f4 = afloat[k] * pGreen;
f5 = afloat[k] * pBlue;
}
int l = applyBakedLighting(pCombinedLights[k], bytebuffer);
float f9 = bytebuffer.getFloat(16);
float f10 = bytebuffer.getFloat(20);
Vector4f vector4f = matrix4f.transform(new Vector4f(f, f1, f2, 1.0F));
applyBakedNormals(vector3f, bytebuffer, pPoseEntry.normal());
float vertexAlpha = pMulColor ? alpha * (float) (bytebuffer.get(15) & 255) / 255.0F : alpha;
drawingFunction.draw(builder, vector4f.x(), vector4f.y(), vector4f.z(), vector3f.x(), vector3f.y(), vector3f.z(), l, f3, f4, f5, vertexAlpha, f9, f10, pCombinedOverlay);
}
}
}
/**
* Code copy from {@link IForgeVertexConsumer#applyBakedLighting}
*/
static int applyBakedLighting(int packedLight, ByteBuffer data) {
int bl = packedLight & 0xFFFF;
int sl = (packedLight >> 16) & 0xFFFF;
int offset = IQuadTransformer.UV2 * 4; // int offset for vertex 0 * 4 bytes per int
int blBaked = Short.toUnsignedInt(data.getShort(offset));
int slBaked = Short.toUnsignedInt(data.getShort(offset + 2));
bl = Math.max(bl, blBaked);
sl = Math.max(sl, slBaked);
return bl | (sl << 16);
}
/**
* Code copy from {@link IForgeVertexConsumer#applyBakedNormals}
*/
static void applyBakedNormals(Vector3f generated, ByteBuffer data, Matrix3f normalTransform) {
byte nx = data.get(28);
byte ny = data.get(29);
byte nz = data.get(30);
if (nx != 0 || ny != 0 || nz != 0)
{
generated.set(nx / 127f, ny / 127f, nz / 127f);
generated.mul(normalTransform);
}
}
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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.mesh;
import java.util.List;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.joml.Vector4f;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import com.tiedup.remake.rig.math.OpenMatrix4f;
import com.tiedup.remake.rig.render.TiedUpRenderTypes;
public abstract class MeshPart {
protected final List<VertexBuilder> verticies;
protected final Mesh.RenderProperties renderProperties;
protected final Supplier<OpenMatrix4f> vanillaPartTracer;
protected boolean isHidden;
public MeshPart(List<VertexBuilder> vertices, @Nullable Mesh.RenderProperties renderProperties, @Nullable Supplier<OpenMatrix4f> vanillaPartTracer) {
this.verticies = vertices;
this.renderProperties = renderProperties;
this.vanillaPartTracer = vanillaPartTracer;
}
public abstract void draw(PoseStack poseStack, VertexConsumer bufferbuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay);
public void setHidden(boolean hidden) {
this.isHidden = hidden;
}
public boolean isHidden() {
return this.isHidden;
}
public List<VertexBuilder> getVertices() {
return this.verticies;
}
public OpenMatrix4f getVanillaPartTransform() {
if (this.vanillaPartTracer == null) {
return null;
}
return this.vanillaPartTracer.get();
}
public VertexConsumer getBufferBuilder(RenderType renderType, MultiBufferSource bufferSource) {
if (this.renderProperties.customTexturePath() != null) {
return bufferSource.getBuffer(TiedUpRenderTypes.replaceTexture(this.renderProperties.customTexturePath(), renderType));
}
return bufferSource.getBuffer(renderType);
}
protected static final Vector4f COLOR = new Vector4f();
public Vector4f getColor(float r, float g, float b, float a) {
if (this.renderProperties != null && this.renderProperties.customColor() != null) {
COLOR.set(
this.renderProperties.customColor().x
, this.renderProperties.customColor().y
, this.renderProperties.customColor().z
, a
);
return COLOR;
} else {
COLOR.set(
r
, g
, b
, a
);
return COLOR;
}
}
}

View File

@@ -0,0 +1,17 @@
/*
* 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.mesh;
import java.util.function.Supplier;
import com.tiedup.remake.rig.math.OpenMatrix4f;
public interface MeshPartDefinition {
String partName();
Mesh.RenderProperties renderProperties();
Supplier<OpenMatrix4f> getModelPartAnimationProvider();
}

View File

@@ -0,0 +1,185 @@
/*
* 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.mesh;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import com.google.common.collect.Maps;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.util.profiling.ProfilerFiller;
import com.tiedup.remake.rig.asset.AssetAccessor;
import com.tiedup.remake.rig.asset.JsonAssetLoader;
import com.tiedup.remake.rig.mesh.Mesh.RenderProperties;
import com.tiedup.remake.rig.cloth.ClothSimulatable;
import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObject;
import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObjectBuilder;
import com.tiedup.remake.rig.TiedUpRigConstants;
public class Meshes implements PreparableReloadListener {
private static final Map<ResourceLocation, MeshAccessor<? extends Mesh>> ACCESSORS = Maps.newHashMap();
private static final Map<MeshAccessor<? extends Mesh>, Mesh> MESHES = Maps.newHashMap();
private static ResourceManager resourceManager = null;
//For resource reloader
public static final Meshes INSTANCE = new Meshes();
// RIG : on garde uniquement les meshes humanoïdes (joueur + NPCs bondage).
// Les mob meshes combat EF (Creeper/Dragon/Spider/etc.) et armor/particle/cape
// ont été retirés du fork — hors scope bondage.
public static final MeshAccessor<HumanoidMesh> ALEX = MeshAccessor.create(TiedUpRigConstants.MODID, "entity/biped_slim_arm", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(HumanoidMesh::new));
public static final MeshAccessor<HumanoidMesh> BIPED = MeshAccessor.create(TiedUpRigConstants.MODID, "entity/biped", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(HumanoidMesh::new));
public static final MeshAccessor<HumanoidMesh> BIPED_OLD_TEX = MeshAccessor.create(TiedUpRigConstants.MODID, "entity/biped_old_texture", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(HumanoidMesh::new));
public static final MeshAccessor<HumanoidMesh> BIPED_OUTLAYER = MeshAccessor.create(TiedUpRigConstants.MODID, "entity/biped_outlayer", (jsonModelLoader) -> jsonModelLoader.loadSkinnedMesh(HumanoidMesh::new));
public static void reload(ResourceManager resourceManager) {
Meshes.resourceManager = resourceManager;
ACCESSORS.entrySet().removeIf(entry -> !entry.getValue().inRegistry);
MESHES.values().forEach((mesh) -> {
if (mesh instanceof SkinnedMesh skinnedMesh) {
skinnedMesh.destroy();
}
});
MESHES.clear();
}
@SuppressWarnings("unchecked")
@Nullable
public static <M extends Mesh> AssetAccessor<M> get(ResourceLocation id) {
return (AssetAccessor<M>) ACCESSORS.get(id);
}
@SuppressWarnings("unchecked")
public static <M extends Mesh> AssetAccessor<M> getOrCreate(ResourceLocation id, Function<JsonAssetLoader, M> jsonLoader) {
return ACCESSORS.containsKey(id) ? (AssetAccessor<M>)ACCESSORS.get(id) : MeshAccessor.create(id, jsonLoader, false);
}
@SuppressWarnings("unchecked")
public static <M extends Mesh> Set<AssetAccessor<M>> entry(Class<? extends Mesh> filter) {
return ACCESSORS.values().stream().filter((accessor) -> filter.isAssignableFrom(accessor.get().getClass())).map((accessor) -> (AssetAccessor<M>)accessor).collect(Collectors.toSet());
}
public static ResourceLocation wrapLocation(ResourceLocation rl) {
return rl.getPath().matches("animmodels/.*\\.json") ? rl : ResourceLocation.fromNamespaceAndPath(rl.getNamespace(), "animmodels/" + rl.getPath() + ".json");
}
@Override
public CompletableFuture<Void> reload(PreparableReloadListener.PreparationBarrier stage, ResourceManager resourceManager, ProfilerFiller preparationsProfiler, ProfilerFiller reloadProfiler, Executor backgroundExecutor, Executor gameExecutor) {
return CompletableFuture.runAsync(() -> {
Meshes.reload(resourceManager);
}, gameExecutor).thenCompose(stage::wait);
}
@FunctionalInterface
public interface MeshContructor<P extends MeshPart, V extends VertexBuilder, M extends StaticMesh<P>> {
M invoke(Map<String, Number[]> arrayMap, Map<MeshPartDefinition, List<V>> parts, M parent, RenderProperties properties);
}
public static record MeshAccessor<M extends Mesh> (ResourceLocation registryName, Function<JsonAssetLoader, M> jsonLoader, boolean inRegistry) implements AssetAccessor<M>, SoftBodyTranslatable {
public static <M extends Mesh> MeshAccessor<M> create(String namespaceId, String path, Function<JsonAssetLoader, M> jsonLoader) {
return create(ResourceLocation.fromNamespaceAndPath(namespaceId, path), jsonLoader, true);
}
private static <M extends Mesh> MeshAccessor<M> create(ResourceLocation id, Function<JsonAssetLoader, M> jsonLoader, boolean inRegistry) {
MeshAccessor<M> accessor = new MeshAccessor<M> (id, jsonLoader, inRegistry);
ACCESSORS.put(id, accessor);
return accessor;
}
@SuppressWarnings("unchecked")
@Override
public M get() {
if (!MESHES.containsKey(this)) {
JsonAssetLoader jsonModelLoader = new JsonAssetLoader(resourceManager, wrapLocation(this.registryName));
MESHES.put(this, this.jsonLoader.apply(jsonModelLoader));
}
return (M)MESHES.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 MeshAccessor 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;
}
}
@Override
public boolean canStartSoftBodySimulation() {
Mesh mesh = this.get();
if (mesh instanceof StaticMesh<?> staticMesh) {
return staticMesh.canStartSoftBodySimulation();
} else if (mesh instanceof CompositeMesh compositeMesh) {
return compositeMesh.canStartSoftBodySimulation();
}
return false;
}
@Override
public ClothObject createSimulationData(SoftBodyTranslatable provider, ClothSimulatable simOwner, ClothObjectBuilder simBuilder) {
Mesh mesh = this.get();
if (mesh instanceof StaticMesh<?> staticMesh) {
return staticMesh.createSimulationData(provider, simOwner, simBuilder);
} else if (mesh instanceof CompositeMesh compositeMesh) {
return compositeMesh.createSimulationData(provider, simOwner, simBuilder);
}
return null;
}
@Override
public void putSoftBodySimulationInfo(Map<String, ClothSimulationInfo> sofyBodySimulationInfo) {
Mesh mesh = this.get();
if (mesh instanceof SoftBodyTranslatable softBodyTranslatable) {
softBodyTranslatable.putSoftBodySimulationInfo(sofyBodySimulationInfo);
}
}
@Override
public Map<String, ClothSimulationInfo> getSoftBodySimulationInfo() {
Mesh mesh = this.get();
if (mesh instanceof SoftBodyTranslatable softBodyTranslatable) {
return softBodyTranslatable.getSoftBodySimulationInfo();
} else {
return null;
}
}
}
}

View File

@@ -0,0 +1,157 @@
/*
* 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.mesh;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Maps;
import it.unimi.dsi.fastutil.floats.FloatArrayList;
import it.unimi.dsi.fastutil.floats.FloatList;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import com.tiedup.remake.rig.math.Vec2f;
import com.tiedup.remake.rig.math.Vec3f;
public class SingleGroupVertexBuilder {
private Vec3f position;
private Vec3f normal;
private Vec2f textureCoordinate;
private Vec3f effectiveJointIDs;
private Vec3f effectiveJointWeights;
private int effectiveJointNumber;
public SingleGroupVertexBuilder() {
this.position = null;
this.normal = null;
this.textureCoordinate = null;
}
public SingleGroupVertexBuilder(SingleGroupVertexBuilder vertex) {
this.position = vertex.position;
this.effectiveJointIDs = vertex.effectiveJointIDs;
this.effectiveJointWeights = vertex.effectiveJointWeights;
this.effectiveJointNumber = vertex.effectiveJointNumber;
}
public SingleGroupVertexBuilder setPosition(Vec3f position) {
this.position = position;
return this;
}
public SingleGroupVertexBuilder setNormal(Vec3f vector) {
this.normal = vector;
return this;
}
public SingleGroupVertexBuilder setTextureCoordinate(Vec2f vector) {
this.textureCoordinate = vector;
return this;
}
public SingleGroupVertexBuilder setEffectiveJointIDs(Vec3f effectiveJointIDs) {
this.effectiveJointIDs = effectiveJointIDs;
return this;
}
public SingleGroupVertexBuilder setEffectiveJointWeights(Vec3f effectiveJointWeights) {
this.effectiveJointWeights = effectiveJointWeights;
return this;
}
public SingleGroupVertexBuilder setEffectiveJointNumber(int count) {
this.effectiveJointNumber = count;
return this;
}
public State compareTextureCoordinateAndNormal(Vec3f normal, Vec2f textureCoord) {
if (this.textureCoordinate == null) {
return State.EMPTY;
} else if (this.textureCoordinate.equals(textureCoord) && this.normal.equals(normal)) {
return State.EQUAL;
} else {
return State.DIFFERENT;
}
}
public static SkinnedMesh loadVertexInformation(List<SingleGroupVertexBuilder> vertices, Map<MeshPartDefinition, IntList> indices) {
FloatList positions = new FloatArrayList();
FloatList normals = new FloatArrayList();
FloatList texCoords = new FloatArrayList();
IntList animationIndices = new IntArrayList();
FloatList jointWeights = new FloatArrayList();
IntList affectCountList = new IntArrayList();
for (int i = 0; i < vertices.size(); i++) {
SingleGroupVertexBuilder vertex = vertices.get(i);
Vec3f position = vertex.position;
Vec3f normal = vertex.normal;
Vec2f texCoord = vertex.textureCoordinate;
positions.add(position.x);
positions.add(position.y);
positions.add(position.z);
normals.add(normal.x);
normals.add(normal.y);
normals.add(normal.z);
texCoords.add(texCoord.x);
texCoords.add(texCoord.y);
Vec3f effectIDs = vertex.effectiveJointIDs;
Vec3f weights = vertex.effectiveJointWeights;
int count = Math.min(vertex.effectiveJointNumber, 3);
affectCountList.add(count);
for (int j = 0; j < count; j++) {
switch (j) {
case 0:
animationIndices.add((int) effectIDs.x);
jointWeights.add(weights.x);
animationIndices.add(jointWeights.size() - 1);
break;
case 1:
animationIndices.add((int) effectIDs.y);
jointWeights.add(weights.y);
animationIndices.add(jointWeights.size() - 1);
break;
case 2:
animationIndices.add((int) effectIDs.z);
jointWeights.add(weights.z);
animationIndices.add(jointWeights.size() - 1);
break;
default:
}
}
}
Float[] positionList = positions.toArray(new Float[0]);
Float[] normalList = normals.toArray(new Float[0]);
Float[] texCoordList = texCoords.toArray(new Float[0]);
Integer[] affectingJointIndices = animationIndices.toArray(new Integer[0]);
Float[] jointWeightList = jointWeights.toArray(new Float[0]);
Integer[] affectJointCounts = affectCountList.toArray(new Integer[0]);
Map<String, Number[]> arrayMap = Maps.newHashMap();
Map<MeshPartDefinition, List<VertexBuilder>> meshDefinitions = Maps.newHashMap();
arrayMap.put("positions", positionList);
arrayMap.put("normals", normalList);
arrayMap.put("uvs", texCoordList);
arrayMap.put("weights", jointWeightList);
arrayMap.put("vcounts", affectJointCounts);
arrayMap.put("vindices", affectingJointIndices);
for (Map.Entry<MeshPartDefinition, IntList> e : indices.entrySet()) {
meshDefinitions.put(e.getKey(), VertexBuilder.create(e.getValue().toIntArray()));
}
return new SkinnedMesh(arrayMap, meshDefinitions, null, null);
}
public enum State {
EMPTY, EQUAL, DIFFERENT
}
}

View File

@@ -0,0 +1,397 @@
/*
* 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.mesh;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import javax.annotation.Nullable;
import org.joml.Matrix3f;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import org.joml.Vector4f;
import com.google.common.collect.Maps;
import com.google.gson.JsonObject;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import com.tiedup.remake.rig.asset.JsonAssetLoader;
import com.tiedup.remake.rig.mesh.SkinnedMesh.SkinnedMeshPart;
import com.tiedup.remake.rig.armature.Armature;
import com.tiedup.remake.rig.util.ParseUtil;
import com.tiedup.remake.rig.math.OpenMatrix4f;
import com.tiedup.remake.rig.math.Vec4f;
import com.tiedup.remake.rig.render.TiedUpRenderTypes;
import com.tiedup.remake.rig.render.compute.ComputeShaderSetup;
import com.tiedup.remake.rig.render.compute.ComputeShaderProvider;
import com.tiedup.remake.rig.TiedUpAnimationConfig;
import com.tiedup.remake.rig.TiedUpRigConstants;
public class SkinnedMesh extends StaticMesh<SkinnedMeshPart> {
protected final float[] weights;
protected final int[] affectingJointCounts;
protected final int[][] affectingWeightIndices;
protected final int[][] affectingJointIndices;
private final int maxJointCount;
@Nullable
private ComputeShaderSetup computerShaderSetup;
public SkinnedMesh(@Nullable Map<String, Number[]> arrayMap, @Nullable Map<MeshPartDefinition, List<VertexBuilder>> partBuilders, @Nullable SkinnedMesh parent, RenderProperties properties) {
super(arrayMap, partBuilders, parent, properties);
this.weights = parent == null ? ParseUtil.unwrapFloatWrapperArray(arrayMap.get("weights")) : parent.weights;
this.affectingJointCounts = parent == null ? ParseUtil.unwrapIntWrapperArray(arrayMap.get("vcounts")) : parent.affectingJointCounts;
if (parent != null) {
this.affectingJointIndices = parent.affectingJointIndices;
this.affectingWeightIndices = parent.affectingWeightIndices;
} else {
int[] vindices = ParseUtil.unwrapIntWrapperArray(arrayMap.get("vindices"));
this.affectingJointIndices = new int[this.affectingJointCounts.length][];
this.affectingWeightIndices = new int[this.affectingJointCounts.length][];
int idx = 0;
for (int i = 0; i < this.affectingJointCounts.length; i++) {
int count = this.affectingJointCounts[i];
int[] jointId = new int[count];
int[] weights = new int[count];
for (int j = 0; j < count; j++) {
jointId[j] = vindices[idx * 2];
weights[j] = vindices[idx * 2 + 1];
idx++;
}
this.affectingJointIndices[i] = jointId;
this.affectingWeightIndices[i] = weights;
}
}
int maxJointId = 0;
for (int[] i : this.affectingJointIndices) {
for (int j : i) {
if (maxJointId < j) {
maxJointId = j;
}
}
}
this.maxJointCount = maxJointId;
if (ComputeShaderProvider.supportComputeShader()) {
if (RenderSystem.isOnRenderThread()) {
this.computerShaderSetup = ComputeShaderProvider.getComputeShaderSetup(this);
} else {
RenderSystem.recordRenderCall(() -> {
this.computerShaderSetup = ComputeShaderProvider.getComputeShaderSetup(this);
});
}
}
}
public void destroy() {
if (RenderSystem.isOnRenderThread()) {
if (this.computerShaderSetup != null) {
this.computerShaderSetup.destroyBuffers();
}
} else {
RenderSystem.recordRenderCall(() -> {
if (this.computerShaderSetup != null) {
this.computerShaderSetup.destroyBuffers();
}
});
}
}
@Override
protected Map<String, SkinnedMeshPart> createModelPart(Map<MeshPartDefinition, List<VertexBuilder>> partBuilders) {
Map<String, SkinnedMeshPart> parts = Maps.newHashMap();
partBuilders.forEach((partDefinition, vertexBuilder) -> {
parts.put(partDefinition.partName(), new SkinnedMeshPart(vertexBuilder, partDefinition.renderProperties(), partDefinition.getModelPartAnimationProvider()));
});
return parts;
}
@Override
protected SkinnedMeshPart getOrLogException(Map<String, SkinnedMeshPart> parts, String name) {
if (!parts.containsKey(name)) {
if (TiedUpRigConstants.IS_DEV_ENV) {
TiedUpRigConstants.LOGGER.debug("Cannot find the mesh part named " + name + " in " + this.getClass().getCanonicalName());
}
return null;
}
return parts.get(name);
}
private static final Vec4f TRANSFORM = new Vec4f();
private static final Vec4f POS = new Vec4f();
private static final Vec4f TOTAL_POS = new Vec4f();
@Override
public void getVertexPosition(int positionIndex, Vector4f dest, @Nullable OpenMatrix4f[] poses) {
int index = positionIndex * 3;
POS.set(this.positions[index], this.positions[index + 1], this.positions[index + 2], 1.0F);
TOTAL_POS.set(0.0F, 0.0F, 0.0F, 0.0F);
for (int i = 0; i < this.affectingJointCounts[positionIndex]; i++) {
int jointIndex = this.affectingJointIndices[positionIndex][i];
int weightIndex = this.affectingWeightIndices[positionIndex][i];
float weight = this.weights[weightIndex];
Vec4f.add(OpenMatrix4f.transform(poses[jointIndex], POS, TRANSFORM).scale(weight), TOTAL_POS, TOTAL_POS);
}
dest.set(TOTAL_POS.x, TOTAL_POS.y, TOTAL_POS.z, 1.0F);
}
private static final Vec4f NORM = new Vec4f();
private static final Vec4f TOTAL_NORM = new Vec4f();
@Override
public void getVertexNormal(int positionIndex, int normalIndex, Vector3f dest, @Nullable OpenMatrix4f[] poses) {
int index = normalIndex * 3;
NORM.set(this.normals[index], this.normals[index + 1], this.normals[index + 2], 1.0F);
TOTAL_NORM.set(0.0F, 0.0F, 0.0F, 0.0F);
for (int i = 0; i < this.affectingJointCounts[positionIndex]; i++) {
int jointIndex = this.affectingJointIndices[positionIndex][i];
int weightIndex = this.affectingWeightIndices[positionIndex][i];
float weight = this.weights[weightIndex];
Vec4f.add(OpenMatrix4f.transform(poses[jointIndex], NORM, TRANSFORM).scale(weight), TOTAL_NORM, TOTAL_NORM);
}
dest.set(TOTAL_NORM.x, TOTAL_NORM.y, TOTAL_NORM.z);
}
/**
* Draws the model without applying animation
*/
@Override
public void draw(PoseStack poseStack, VertexConsumer bufferbuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay) {
for (SkinnedMeshPart part : this.parts.values()) {
part.draw(poseStack, bufferbuilder, drawingFunction, packedLight, r, g, b, a, overlay);
}
}
protected static final Vector4f POSITION = new Vector4f();
protected static final Vector3f NORMAL = new Vector3f();
/**
* Draws the model to vanilla buffer
*/
@Override
public void drawPosed(PoseStack poseStack, VertexConsumer bufferbuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay, @Nullable Armature armature, OpenMatrix4f[] poses) {
Matrix4f pose = poseStack.last().pose();
Matrix3f normal = poseStack.last().normal();
for (SkinnedMeshPart part : this.parts.values()) {
if (!part.isHidden()) {
OpenMatrix4f transform = part.getVanillaPartTransform();
for (int i = 0; i < poses.length; i++) {
ComputeShaderSetup.TOTAL_POSES[i].load(poses[i]);
if (armature != null) {
ComputeShaderSetup.TOTAL_POSES[i].mulBack(armature.searchJointById(i).getToOrigin());
}
if (transform != null) {
ComputeShaderSetup.TOTAL_POSES[i].mulBack(transform);
}
ComputeShaderSetup.TOTAL_NORMALS[i] = ComputeShaderSetup.TOTAL_POSES[i].removeTranslation();
}
for (VertexBuilder vi : part.getVertices()) {
this.getVertexPosition(vi.position, POSITION, ComputeShaderSetup.TOTAL_POSES);
this.getVertexNormal(vi.position, vi.normal, NORMAL, ComputeShaderSetup.TOTAL_NORMALS);
POSITION.mul(pose);
NORMAL.mul(normal);
drawingFunction.draw(bufferbuilder, POSITION.x, POSITION.y, POSITION.z, NORMAL.x, NORMAL.y, NORMAL.z, packedLight, r, g, b, a, this.uvs[vi.uv * 2], this.uvs[vi.uv * 2 + 1], overlay);
}
}
}
}
/**
* Draws the model depending on animation shader option
* @param armature give this parameter as null if @param poses already bound origin translation
* @param poses
*/
public void draw(PoseStack poseStack, MultiBufferSource bufferSources, RenderType renderType, int packedLight, float r, float g, float b, float a, int overlay, @Nullable Armature armature, OpenMatrix4f[] poses) {
this.draw(poseStack, bufferSources, renderType, Mesh.DrawingFunction.NEW_ENTITY, packedLight, r, g, b, a, overlay, armature, poses);
}
@Override
public void draw(PoseStack poseStack, MultiBufferSource bufferSources, RenderType renderType, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay, @Nullable Armature armature, OpenMatrix4f[] poses) {
if (TiedUpAnimationConfig.activateComputeShader && this.computerShaderSetup != null) {
this.computerShaderSetup.drawWithShader(this, poseStack, bufferSources, TiedUpRenderTypes.getTriangulated(renderType), packedLight, r, g, b, a, overlay, armature, poses);
} else {
this.drawPosed(poseStack, bufferSources.getBuffer(TiedUpRenderTypes.getTriangulated(renderType)), drawingFunction, packedLight, r, g, b, a, overlay, armature, poses);
}
}
public int getMaxJointCount() {
return this.maxJointCount;
}
public float[] weights() {
return this.weights;
}
public int[] affectingJointCounts() {
return this.affectingJointCounts;
}
public int[][] affectingWeightIndices() {
return this.affectingWeightIndices;
}
public int[][] affectingJointIndices() {
return this.affectingJointIndices;
}
public class SkinnedMeshPart extends MeshPart {
private ComputeShaderSetup.MeshPartBuffer partVBO;
public SkinnedMeshPart(List<VertexBuilder> animatedMeshPartList, @Nullable Mesh.RenderProperties renderProperties, @Nullable Supplier<OpenMatrix4f> vanillaPartTracer) {
super(animatedMeshPartList, renderProperties, vanillaPartTracer);
}
public void initVBO(ComputeShaderSetup.MeshPartBuffer partVBO) {
this.partVBO = partVBO;
}
public ComputeShaderSetup.MeshPartBuffer getPartVBO() {
return this.partVBO;
}
@Override
public void draw(PoseStack poseStack, VertexConsumer bufferBuilder, Mesh.DrawingFunction drawingFunction, int packedLight, float r, float g, float b, float a, int overlay) {
if (this.isHidden()) {
return;
}
Vector4f color = this.getColor(r, g, b, a);
Matrix4f pose = poseStack.last().pose();
Matrix3f normal = poseStack.last().normal();
for (VertexBuilder vi : this.getVertices()) {
getVertexPosition(vi.position, POSITION);
getVertexNormal(vi.normal, NORMAL);
POSITION.mul(pose);
NORMAL.mul(normal);
drawingFunction.draw(bufferBuilder, POSITION.x(), POSITION.y(), POSITION.z(), NORMAL.x(), NORMAL.y(), NORMAL.z(), packedLight, color.x, color.y, color.z, color.w, uvs[vi.uv * 2], uvs[vi.uv * 2 + 1], overlay);
}
}
}
/**
* Export this model as Json format
*/
public JsonObject toJsonObject() {
JsonObject root = new JsonObject();
JsonObject vertices = new JsonObject();
float[] positions = this.positions.clone();
float[] normals = this.normals.clone();
for (int i = 0; i < positions.length / 3; i++) {
int k = i * 3;
Vec4f posVector = new Vec4f(positions[k], positions[k+1], positions[k+2], 1.0F);
posVector.transform(JsonAssetLoader.MINECRAFT_TO_BLENDER_COORD);
positions[k] = posVector.x;
positions[k+1] = posVector.y;
positions[k+2] = posVector.z;
}
for (int i = 0; i < normals.length / 3; i++) {
int k = i * 3;
Vec4f normVector = new Vec4f(normals[k], normals[k+1], normals[k+2], 1.0F);
normVector.transform(JsonAssetLoader.MINECRAFT_TO_BLENDER_COORD);
normals[k] = normVector.x;
normals[k+1] = normVector.y;
normals[k+2] = normVector.z;
}
IntList affectingJointAndWeightIndices = new IntArrayList();
for (int i = 0; i < this.affectingJointCounts.length; i++) {
for (int j = 0; j < this.affectingJointCounts[j]; j++) {
affectingJointAndWeightIndices.add(this.affectingJointIndices[i][j]);
affectingJointAndWeightIndices.add(this.affectingWeightIndices[i][j]);
}
}
vertices.add("positions", ParseUtil.farrayToJsonObject(positions, 3));
vertices.add("uvs", ParseUtil.farrayToJsonObject(this.uvs, 2));
vertices.add("normals", ParseUtil.farrayToJsonObject(normals, 3));
vertices.add("vcounts", ParseUtil.iarrayToJsonObject(this.affectingJointCounts, 1));
vertices.add("weights", ParseUtil.farrayToJsonObject(this.weights, 1));
vertices.add("vindices", ParseUtil.iarrayToJsonObject(affectingJointAndWeightIndices.toIntArray(), 1));
if (!this.parts.isEmpty()) {
JsonObject parts = new JsonObject();
for (Map.Entry<String, SkinnedMeshPart> partEntry : this.parts.entrySet()) {
IntList indicesArray = new IntArrayList();
for (VertexBuilder vertexIndicator : partEntry.getValue().getVertices()) {
indicesArray.add(vertexIndicator.position);
indicesArray.add(vertexIndicator.uv);
indicesArray.add(vertexIndicator.normal);
}
parts.add(partEntry.getKey(), ParseUtil.iarrayToJsonObject(indicesArray.toIntArray(), 3));
}
vertices.add("parts", parts);
} else {
int i = 0;
int[] indices = new int[this.vertexCount * 3];
for (SkinnedMeshPart part : this.parts.values()) {
for (VertexBuilder vertexIndicator : part.getVertices()) {
indices[i * 3] = vertexIndicator.position;
indices[i * 3 + 1] = vertexIndicator.uv;
indices[i * 3 + 2] = vertexIndicator.normal;
i++;
}
}
vertices.add("indices", ParseUtil.iarrayToJsonObject(indices, 3));
}
root.add("vertices", vertices);
if (this.renderProperties != null) {
JsonObject renderProperties = new JsonObject();
renderProperties.addProperty("texture_path", this.renderProperties.customTexturePath().toString());
renderProperties.addProperty("transparent", this.renderProperties.isTransparent());
root.add("render_properties", renderProperties);
}
return root;
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.mesh;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Lists;
import com.tiedup.remake.rig.mesh.Meshes.MeshAccessor;
import com.tiedup.remake.rig.cloth.ClothSimulatable;
import com.tiedup.remake.rig.cloth.ClothSimulator;
import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObject.ClothPart.ConstraintType;
import com.tiedup.remake.rig.physics.SimulationProvider;
public interface SoftBodyTranslatable extends SimulationProvider<ClothSimulatable, ClothSimulator.ClothObject, ClothSimulator.ClothObjectBuilder, SoftBodyTranslatable> {
public static final List<ClothSimulatable> TRACKING_SIMULATION_SUBJECTS = Lists.newArrayList();
default boolean canStartSoftBodySimulation() {
return this.getSoftBodySimulationInfo() != null;
}
void putSoftBodySimulationInfo(Map<String, ClothSimulationInfo> sofyBodySimulationInfo);
Map<String, ClothSimulationInfo> getSoftBodySimulationInfo();
default StaticMesh<?> getOriginalMesh() {
if (this instanceof MeshAccessor<?> meshAccessor) {
return (StaticMesh<?>)meshAccessor.get();
} else {
return (StaticMesh<?>)this;
}
}
public static record ClothSimulationInfo(float particleMass, float selfCollision, List<int[]> constraints, ConstraintType[] constraintTypes, float[] compliances, int[] particles, float[] weights, float[] rootDistance, int[] normalOffsetMapping) {
}
}

View File

@@ -0,0 +1,150 @@
/*
* 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.mesh;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import org.joml.Vector3f;
import org.joml.Vector4f;
import com.google.common.collect.ImmutableList;
import net.minecraft.world.phys.Vec3;
import com.tiedup.remake.rig.cloth.ClothSimulatable;
import com.tiedup.remake.rig.cloth.ClothSimulator;
import com.tiedup.remake.rig.cloth.ClothSimulator.ClothObject;
import com.tiedup.remake.rig.util.ParseUtil;
import com.tiedup.remake.rig.math.OpenMatrix4f;
public abstract class StaticMesh<P extends MeshPart> implements Mesh, SoftBodyTranslatable {
protected final float[] positions;
protected final float[] normals;
protected final float[] uvs;
protected final int vertexCount;
protected final Mesh.RenderProperties renderProperties;
protected final Map<String, P> parts;
protected final List<Vec3> normalList;
private Map<String, ClothSimulationInfo> softBodySimulationInfo;
/**
* @param arrayMap Null if parent is not null
* @param partBuilders Null if parent is not null
* @param parent Null if arrayMap and parts are not null
* @param renderProperties
*/
public StaticMesh(@Nullable Map<String, Number[]> arrayMap, @Nullable Map<MeshPartDefinition, List<VertexBuilder>> partBuilders, @Nullable StaticMesh<P> parent, Mesh.RenderProperties renderProperties) {
this.positions = (parent == null) ? ParseUtil.unwrapFloatWrapperArray(arrayMap.get("positions")) : parent.positions;
this.normals = (parent == null) ? ParseUtil.unwrapFloatWrapperArray(arrayMap.get("normals")) : parent.normals;
this.uvs = (parent == null) ? ParseUtil.unwrapFloatWrapperArray(arrayMap.get("uvs")) : parent.uvs;
this.parts = (parent == null) ? this.createModelPart(partBuilders) : parent.parts;
this.renderProperties = renderProperties;
int totalV = 0;
for (MeshPart modelpart : this.parts.values()) {
totalV += modelpart.getVertices().size();
}
this.vertexCount = totalV;
if (this.canStartSoftBodySimulation()) {
ImmutableList.Builder<Vec3> normalBuilder = ImmutableList.builder();
for (int i = 0; i < this.normals.length / 3; i++) {
normalBuilder.add(new Vec3(this.normals[i * 3], this.normals[i * 3 + 1], this.normals[i * 3 + 2]));
}
this.normalList = normalBuilder.build();
} else {
this.normalList = null;
}
}
protected abstract Map<String, P> createModelPart(Map<MeshPartDefinition, List<VertexBuilder>> partBuilders);
protected abstract P getOrLogException(Map<String, P> parts, String name);
public boolean hasPart(String part) {
return this.parts.containsKey(part);
}
public MeshPart getPart(String part) {
return this.parts.get(part);
}
public Collection<P> getAllParts() {
return this.parts.values();
}
public Set<Map.Entry<String, P>> getPartEntry() {
return this.parts.entrySet();
}
public void putSoftBodySimulationInfo(Map<String, ClothSimulationInfo> sofyBodySimulationInfo) {
this.softBodySimulationInfo = sofyBodySimulationInfo;
}
public Map<String, ClothSimulationInfo> getSoftBodySimulationInfo() {
return this.softBodySimulationInfo;
}
public Mesh.RenderProperties getRenderProperties() {
return this.renderProperties;
}
public void getVertexPosition(int positionIndex, Vector4f dest) {
int index = positionIndex * 3;
dest.set(this.positions[index], this.positions[index + 1], this.positions[index + 2], 1.0F);
}
public void getVertexNormal(int normalIndex, Vector3f dest) {
int index = normalIndex * 3;
dest.set(this.normals[index], this.normals[index + 1], this.normals[index + 2]);
}
public void getVertexPosition(int positionIndex, Vector4f dest, @Nullable OpenMatrix4f[] poses) {
this.getVertexPosition(positionIndex, dest);
}
public void getVertexNormal(int positionIndex, int normalIndex, Vector3f dest, @Nullable OpenMatrix4f[] poses) {
this.getVertexNormal(normalIndex, dest);
}
public float[] positions() {
return this.positions;
}
public float[] normals() {
return this.normals;
}
public float[] uvs() {
return this.uvs;
}
@Nullable
public List<Vec3> normalList() {
return this.normalList;
}
@Override
public void initialize() {
this.parts.values().forEach((part) -> part.setHidden(false));
}
@SuppressWarnings("unchecked")
@Override
public ClothSimulator.ClothObject createSimulationData(@Nullable SoftBodyTranslatable provider, ClothSimulatable simObject, ClothSimulator.ClothObjectBuilder simBuilder) {
return new ClothObject(simBuilder, provider == null ? this : provider, (Map<String, MeshPart>)this.parts, this.positions);
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.mesh;
import java.util.List;
import com.google.common.collect.Lists;
// Vertex Indices
public class VertexBuilder {
public static List<VertexBuilder> create(int[] drawingIndices) {
List<VertexBuilder> vertexIndicators = Lists.newArrayList();
for (int i = 0; i < drawingIndices.length / 3; i++) {
int k = i * 3;
int position = drawingIndices[k];
int uv = drawingIndices[k + 1];
int normal = drawingIndices[k + 2];
VertexBuilder vi = new VertexBuilder(position, uv, normal);
vertexIndicators.add(vi);
}
return vertexIndicators;
}
public final int position;
public final int uv;
public final int normal;
public VertexBuilder(int position, int uv, int normal) {
this.position = position;
this.uv = uv;
this.normal = normal;
}
@Override
public boolean equals(Object o) {
if (o instanceof VertexBuilder vb) {
return this.position == vb.position && this.uv == vb.uv && this.normal == vb.normal;
}
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.position;
result = prime * result + this.uv;
result = prime * result + this.normal;
return result;
}
}

View File

@@ -0,0 +1,122 @@
/*
* 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.mesh.transformer;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
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.JsonObject;
import net.minecraft.SharedConstants;
import net.minecraft.client.model.HumanoidModel;
import net.minecraft.client.model.Model;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.PackType;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ArmorItem;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.registries.ForgeRegistries;
import com.tiedup.remake.rig.mesh.SkinnedMesh;
import com.tiedup.remake.rig.mesh.HumanoidMesh;
import com.tiedup.remake.rig.TiedUpRigConstants;
@OnlyIn(Dist.CLIENT)
public class HumanoidModelBaker {
static final Map<ResourceLocation, SkinnedMesh> BAKED_MODELS = Maps.newHashMap();
static final List<HumanoidModelTransformer> MODEL_TRANSFORMERS = Lists.newArrayList();
static final Set<ArmorItem> EXCEPTIONAL_MODELS = Sets.newHashSet();
static final Set<ModelPart> MODEL_PARTS = Sets.newHashSet();
public static final HumanoidModelTransformer VANILLA_TRANSFORMER = new VanillaModelTransformer();
public interface ModelProvider {
public Model get(LivingEntity entityLiving, ItemStack itemStack, EquipmentSlot slot, HumanoidModel<?> _default);
}
public static void registerNewTransformer(HumanoidModelTransformer transformer) {
MODEL_TRANSFORMERS.add(transformer);
}
public static void exportModels(File resourcePackDirectory) throws IOException {
File zipFile = new File(resourcePackDirectory, "epicfight_custom_armors.zip");
ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile));
for (Map.Entry<ResourceLocation, SkinnedMesh> entry : BAKED_MODELS.entrySet()) {
ZipEntry zipEntry = new ZipEntry(String.format("assets/%s/animmodels/armor/%s.json", entry.getKey().getNamespace(), entry.getKey().getPath()));
Gson gson = new GsonBuilder().create();
out.putNextEntry(zipEntry);
out.write(gson.toJson(entry.getValue().toJsonObject()).getBytes());
out.closeEntry();
TiedUpRigConstants.LOGGER.info("Exported custom armor model : " + entry.getKey());
}
ZipEntry zipEntry = new ZipEntry("pack.mcmeta");
Gson gson = new GsonBuilder().setPrettyPrinting().create();
JsonObject root = new JsonObject();
JsonObject pack = new JsonObject();
pack.addProperty("description", "epicfight_custom_armor_models");
pack.addProperty("pack_format", SharedConstants.getCurrentVersion().getPackVersion(PackType.CLIENT_RESOURCES));
root.add("pack", pack);
out.putNextEntry(zipEntry);
out.write(gson.toJson(root).getBytes());
out.closeEntry();
out.close();
}
public static SkinnedMesh bakeArmor(LivingEntity entityLiving, ItemStack itemstack, ArmorItem armorItem, EquipmentSlot slot, HumanoidModel<?> originalModel, Model forgeModel, HumanoidModel<?> entityModel, HumanoidMesh entityMesh) {
SkinnedMesh skinnedArmorModel = null;
if (!EXCEPTIONAL_MODELS.contains(armorItem)) {
if (forgeModel == originalModel || !(forgeModel instanceof HumanoidModel humanoidModel)) {
// RIG Phase 0 : Meshes.HELMET/CHESTPLATE/LEGGINS/BOOTS strippés →
// HumanoidMesh.getHumanoidArmorModel(slot) retourne null.
// Safe-guard ici pour éviter NPE si bakeArmor est appelé avant que
// V3-REW-04 soit implémenté (armor rendering rework).
var armorAccessor = entityMesh.getHumanoidArmorModel(slot);
return armorAccessor != null ? armorAccessor.get() : null;
}
for (HumanoidModelTransformer modelTransformer : MODEL_TRANSFORMERS) {
try {
skinnedArmorModel = modelTransformer.transformArmorModel(humanoidModel);
} catch (Exception e) {
TiedUpRigConstants.LOGGER.warn("Can't transform the model of " + ForgeRegistries.ITEMS.getKey(armorItem) + " because of :");
e.printStackTrace();
EXCEPTIONAL_MODELS.add(armorItem);
}
if (skinnedArmorModel != null) {
break;
}
}
if (skinnedArmorModel == null) {
skinnedArmorModel = VANILLA_TRANSFORMER.transformArmorModel(humanoidModel);
}
}
BAKED_MODELS.put(ForgeRegistries.ITEMS.getKey(armorItem), skinnedArmorModel);
return skinnedArmorModel;
}
}

View File

@@ -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.mesh.transformer;
import java.util.List;
import java.util.Map;
import com.mojang.blaze3d.vertex.PoseStack;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import net.minecraft.client.model.HumanoidModel;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import com.tiedup.remake.rig.mesh.MeshPartDefinition;
import com.tiedup.remake.rig.mesh.SingleGroupVertexBuilder;
import com.tiedup.remake.rig.mesh.SkinnedMesh;
@OnlyIn(Dist.CLIENT)
public abstract class HumanoidModelTransformer {
public abstract SkinnedMesh transformArmorModel(HumanoidModel<?> humanoidModel);
public static abstract class PartTransformer<T> {
public abstract void bakeCube(PoseStack poseStack, MeshPartDefinition partDefinition, T cube, List<SingleGroupVertexBuilder> vertices, Map<MeshPartDefinition, IntList> indices, IndexCounter indexCounter);
static void triangluatePolygon(Map<MeshPartDefinition, IntList> indices, MeshPartDefinition partDefinition, IndexCounter indexCounter) {
IntList list = indices.computeIfAbsent(partDefinition, (key) -> new IntArrayList());
//Optimization: do not split vertices in a cube.
for (int i = 0; i < 3; i++) {
list.add(indexCounter.first());
}
for (int i = 0; i < 3; i++) {
list.add(indexCounter.second());
}
for (int i = 0; i < 3; i++) {
list.add(indexCounter.fourth());
}
for (int i = 0; i < 3; i++) {
list.add(indexCounter.fourth());
}
for (int i = 0; i < 3; i++) {
list.add(indexCounter.second());
}
for (int i = 0; i < 3; i++) {
list.add(indexCounter.third());
}
indexCounter.count();
}
public static class IndexCounter {
private int indexCounter = 0;
private int first() {
return this.indexCounter;
}
private int second() {
return this.indexCounter + 1;
}
private int third() {
return this.indexCounter + 2;
}
private int fourth() {
return this.indexCounter + 3;
}
private void count() {
this.indexCounter += 4;
}
}
}
}

View File

@@ -0,0 +1,758 @@
/*
* 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.mesh.transformer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
import org.joml.Vector3f;
import org.joml.Vector4f;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.mojang.blaze3d.vertex.PoseStack;
import it.unimi.dsi.fastutil.ints.IntList;
import net.minecraft.client.model.HumanoidModel;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.model.geom.PartPose;
import net.minecraft.core.Direction;
import net.minecraft.core.Vec3i;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import com.tiedup.remake.rig.mesh.Mesh.RenderProperties;
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.math.OpenMatrix4f;
import com.tiedup.remake.rig.math.QuaternionUtils;
import com.tiedup.remake.rig.math.Vec2f;
import com.tiedup.remake.rig.math.Vec3f;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import com.tiedup.remake.rig.mixin.client.MixinAgeableListModel;
@OnlyIn(Dist.CLIENT)
public class VanillaModelTransformer extends HumanoidModelTransformer {
public static final SimpleTransformer HEAD = new SimpleTransformer(AABB.ofSize(new Vec3(0.0D, -4.0D, 0.0D), 8.0D, 8.0D, 8.0D), 9);
public static final SimpleTransformer LEFT_FEET = new SimpleTransformer(AABB.ofSize(new Vec3(0.0D, -4.0D, 0.0D), 8.0D, 8.0D, 8.0D), 5);
public static final SimpleTransformer RIGHT_FEET = new SimpleTransformer(AABB.ofSize(new Vec3(0.0D, -4.0D, 0.0D), 8.0D, 8.0D, 8.0D), 2);
public static final LimbPartTransformer LEFT_ARM = new LimbPartTransformer(AABB.ofSize(new Vec3(1.0D, 6.0D, 0.0D), 4.0D, 12.0D, 4.0D), 16, 17, 19, 19.0F, false, AABB.ofSize(new Vec3(-6.0D, 18.0D, 0), 8.0D, 14.0D, 8.0D));
public static final LimbPartTransformer RIGHT_ARM = new LimbPartTransformer(AABB.ofSize(new Vec3(-1.0D, 6.0D, 0.0D), 4.0D, 12.0D, 4.0D), 11, 12, 14, 19.0F, false, AABB.ofSize(new Vec3(6.0D, 18.0D, 0), 8.0D, 14.0D, 8.0D));
public static final LimbPartTransformer LEFT_LEG = new LimbPartTransformer(AABB.ofSize(new Vec3(1.9D, 18.0D, 0.0D), 4.0D, 12.0D, 4.0D), 4, 5, 6, 6.0F, true, AABB.ofSize(new Vec3(-2.0D, 6.0D, 0), 8.0D, 14.0D, 8.0D));
public static final LimbPartTransformer RIGHT_LEG = new LimbPartTransformer(AABB.ofSize(new Vec3(-1.9D, 18.0D, 0.0D), 4.0D, 12.0D, 4.0D), 1, 2, 3, 6.0F, true, AABB.ofSize(new Vec3(2.0D, 6.0D, 0), 8.0D, 14.0D, 8.0D));
public static final ChestPartTransformer CHEST = new ChestPartTransformer(AABB.ofSize(new Vec3(0.0D, 6.0D, 0.0D), 8.0D, 12.0D, 4.0D), 8, 7, 18.0F, AABB.ofSize(new Vec3(0, 18.0D, 0), 12.0D, 14.0D, 6.0D));
private static PartTransformer<ModelPart.Cube> getModelPartTransformer(ModelPart modelPart) {
if (HEAD.coverArea.contains(modelPart.x, modelPart.y, modelPart.z)) {
return HEAD;
} else if (LEFT_FEET.coverArea.contains(modelPart.x, modelPart.y, modelPart.z)) {
return LEFT_FEET;
} else if (RIGHT_FEET.coverArea.contains(modelPart.x, modelPart.y, modelPart.z)) {
return RIGHT_FEET;
} else if (LEFT_ARM.coverArea.contains(modelPart.x, modelPart.y, modelPart.z)) {
return LEFT_ARM;
} else if (RIGHT_ARM.coverArea.contains(modelPart.x, modelPart.y, modelPart.z)) {
return RIGHT_ARM;
} else if (LEFT_LEG.coverArea.contains(modelPart.x, modelPart.y, modelPart.z)) {
return LEFT_LEG;
} else if (RIGHT_LEG.coverArea.contains(modelPart.x, modelPart.y, modelPart.z)) {
return RIGHT_LEG;
} else if (CHEST.coverArea.contains(modelPart.x, modelPart.y, modelPart.z)) {
return CHEST;
}
return CHEST;
}
static record VanillaModelPartition(PartTransformer<ModelPart.Cube> partTransformer, ModelPart modelPart, String partName) {
}
@Override
public SkinnedMesh transformArmorModel(HumanoidModel<?> humanoidModel) {
List<VanillaModelPartition> partitions = Lists.newArrayList();
//Remove entity animation
humanoidModel.head.loadPose(humanoidModel.head.getInitialPose());
humanoidModel.hat.loadPose(humanoidModel.hat.getInitialPose());
humanoidModel.body.loadPose(humanoidModel.body.getInitialPose());
humanoidModel.leftArm.loadPose(humanoidModel.leftArm.getInitialPose());
humanoidModel.rightArm.loadPose(humanoidModel.rightArm.getInitialPose());
humanoidModel.leftLeg.loadPose(humanoidModel.leftLeg.getInitialPose());
humanoidModel.rightLeg.loadPose(humanoidModel.rightLeg.getInitialPose());
List<ModelPart> modelParts = Lists.newArrayList();
MixinAgeableListModel accessorAgeableListModel = ((MixinAgeableListModel)humanoidModel);
Iterable<ModelPart> headParts = accessorAgeableListModel.invoke_headParts();
Iterable<ModelPart> bodyParts = accessorAgeableListModel.invoke_bodyParts();
if (headParts != null) {
headParts.forEach(modelParts::add);
}
if (bodyParts != null) {
bodyParts.forEach(modelParts::add);
}
modelParts.forEach((modelPart) -> modelPart.loadPose(modelPart.getInitialPose()));
if (humanoidModel.head.skipDraw || humanoidModel.head.visible) {
partitions.add(new VanillaModelPartition(HEAD, humanoidModel.head, "head"));
}
if (humanoidModel.hat.skipDraw || humanoidModel.hat.visible) {
partitions.add(new VanillaModelPartition(HEAD, humanoidModel.hat, "hat"));
}
if (humanoidModel.body.skipDraw || humanoidModel.body.visible) {
partitions.add(new VanillaModelPartition(CHEST, humanoidModel.body, "body"));
}
if (humanoidModel.rightArm.skipDraw || humanoidModel.rightArm.visible) {
partitions.add(new VanillaModelPartition(RIGHT_ARM, humanoidModel.rightArm, "rightArm"));
}
if (humanoidModel.leftArm.skipDraw || humanoidModel.leftArm.visible) {
partitions.add(new VanillaModelPartition(LEFT_ARM, humanoidModel.leftArm, "leftArm"));
}
if (humanoidModel.leftLeg.skipDraw || humanoidModel.leftLeg.visible) {
partitions.add(new VanillaModelPartition(LEFT_LEG, humanoidModel.leftLeg, "leftLeg"));
}
if (humanoidModel.rightLeg.skipDraw || humanoidModel.rightLeg.visible) {
partitions.add(new VanillaModelPartition(RIGHT_LEG, humanoidModel.rightLeg, "rightLeg"));
}
modelParts.remove(humanoidModel.head);
modelParts.remove(humanoidModel.hat);
modelParts.remove(humanoidModel.body);
modelParts.remove(humanoidModel.rightArm);
modelParts.remove(humanoidModel.leftArm);
modelParts.remove(humanoidModel.rightLeg);
modelParts.remove(humanoidModel.leftLeg);
int i = 0;
for (ModelPart modelpart : modelParts) {
if (modelpart.skipDraw || modelpart.visible) {
partitions.add(new VanillaModelPartition(getModelPartTransformer(modelpart), modelpart, "part" + (i++)));
}
}
return bakeMeshFromCubes(partitions);
}
private static SkinnedMesh bakeMeshFromCubes(List<VanillaModelPartition> partitions) {
List<SingleGroupVertexBuilder> vertices = Lists.newArrayList();
Map<MeshPartDefinition, IntList> indices = Maps.newHashMap();
PoseStack poseStack = new PoseStack();
PartTransformer.IndexCounter indexCounter = new PartTransformer.IndexCounter();
poseStack.mulPose(QuaternionUtils.YP.rotationDegrees(180.0F));
poseStack.mulPose(QuaternionUtils.XP.rotationDegrees(180.0F));
poseStack.translate(0.0F, -24.0F, 0.0F);
for (VanillaModelPartition modelpartition : partitions) {
bake(poseStack, modelpartition.partName, modelpartition, modelpartition.modelPart, vertices, indices, Lists.newArrayList(), indexCounter, false);
}
return SingleGroupVertexBuilder.loadVertexInformation(vertices, indices);
}
private static void bake(PoseStack poseStack, String partName, VanillaModelPartition modelpartition, ModelPart part, List<SingleGroupVertexBuilder> vertices, Map<MeshPartDefinition, IntList> indices, List<String> path, PartTransformer.IndexCounter indexCounter, boolean bindPart) {
PartPose initialPose = part.getInitialPose();
poseStack.pushPose();
poseStack.translate(initialPose.x, initialPose.y, initialPose.z);
poseStack.mulPose(new Quaternionf().rotationZYX(initialPose.zRot, initialPose.yRot, initialPose.xRot));
if (!bindPart) {
poseStack.scale(part.xScale, part.yScale, part.zScale);
}
List<String> newList = new ArrayList<>(path);
if (bindPart) {
newList.add(partName);
}
if (part.visible && !part.skipDraw) {
MeshPartDefinition partDefinition = VanillaMeshPartDefinition.of(partName);
if (bindPart) {
OpenMatrix4f invertedParentTransform = OpenMatrix4f.importFromMojangMatrix(poseStack.last().pose());
invertedParentTransform.m30 *= 0.0625F;
invertedParentTransform.m31 *= 0.0625F;
invertedParentTransform.m32 *= 0.0625F;
invertedParentTransform.invert();
partDefinition = VanillaMeshPartDefinition.of(partName, newList, invertedParentTransform, modelpartition.modelPart);
}
for (ModelPart.Cube cube : part.cubes) {
modelpartition.partTransformer.bakeCube(poseStack, partDefinition, cube, vertices, indices, indexCounter);
}
}
for (Map.Entry<String, ModelPart> child : part.children.entrySet()) {
bake(poseStack, child.getKey(), modelpartition, child.getValue(), vertices, indices, newList, indexCounter, true);
}
poseStack.popPose();
}
static class SimpleTransformer extends PartTransformer<ModelPart.Cube> {
final int jointId;
final AABB coverArea;
public SimpleTransformer(AABB coverArea, int jointId) {
this.coverArea = coverArea;
this.jointId = jointId;
}
public void bakeCube(PoseStack poseStack, MeshPartDefinition partDefinition, ModelPart.Cube cube, List<SingleGroupVertexBuilder> vertices, Map<MeshPartDefinition, IntList> indices, PartTransformer.IndexCounter indexCounter) {
for (ModelPart.Polygon quad : cube.polygons) {
Vector3f norm = new Vector3f(quad.normal);
norm.mul(poseStack.last().normal());
for (ModelPart.Vertex vertex : quad.vertices) {
Vector4f pos = new Vector4f(vertex.pos, 1.0F);
pos.mul(poseStack.last().pose());
vertices.add(new SingleGroupVertexBuilder()
.setPosition(new Vec3f(pos.x(), pos.y(), pos.z()).scale(0.0625F))
.setNormal(new Vec3f(norm.x(), norm.y(), norm.z()))
.setTextureCoordinate(new Vec2f(vertex.u, vertex.v))
.setEffectiveJointIDs(new Vec3f(this.jointId, 0, 0))
.setEffectiveJointWeights(new Vec3f(1.0F, 0.0F, 0.0F))
.setEffectiveJointNumber(1)
);
}
triangluatePolygon(indices, partDefinition, indexCounter);
}
}
}
static class ChestPartTransformer extends PartTransformer<ModelPart.Cube> {
static final float X_PLANE = 0.0F;
static final VertexWeight[] WEIGHT_ALONG_Y = { new VertexWeight(13.6666F, 0.230F, 0.770F), new VertexWeight(15.8333F, 0.254F, 0.746F), new VertexWeight(18.0F, 0.5F, 0.5F), new VertexWeight(20.1666F, 0.744F, 0.256F), new VertexWeight(22.3333F, 0.770F, 0.230F)};
final SimpleTransformer upperAttachmentTransformer;
final SimpleTransformer lowerAttachmentTransformer;
final AABB noneAttachmentArea;
final AABB coverArea;
final float yClipCoord;
public ChestPartTransformer(AABB coverArea, int upperJoint, int lowerJoint, float yBasis, AABB noneAttachmentArea) {
this.coverArea = coverArea;
this.noneAttachmentArea = noneAttachmentArea;
this.upperAttachmentTransformer = new SimpleTransformer(null, upperJoint);
this.lowerAttachmentTransformer = new SimpleTransformer(null, lowerJoint);
this.yClipCoord = yBasis;
}
@Override
public void bakeCube(PoseStack poseStack, MeshPartDefinition partDefinition, ModelPart.Cube cube, List<SingleGroupVertexBuilder> vertices, Map<MeshPartDefinition, IntList> indices, PartTransformer.IndexCounter indexCounter) {
Vec3 centerOfCube = getCenterOfCube(poseStack, cube);
if (!this.noneAttachmentArea.contains(centerOfCube)) {
if (centerOfCube.y < this.yClipCoord) {
this.lowerAttachmentTransformer.bakeCube(poseStack, partDefinition, cube, vertices, indices, indexCounter);
} else {
this.upperAttachmentTransformer.bakeCube(poseStack, partDefinition, cube, vertices, indices, indexCounter);
}
return;
}
List<AnimatedPolygon> xClipPolygons = Lists.<AnimatedPolygon>newArrayList();
List<AnimatedPolygon> xyClipPolygons = Lists.<AnimatedPolygon>newArrayList();
for (ModelPart.Polygon polygon : cube.polygons) {
Matrix4f matrix = poseStack.last().pose();
ModelPart.Vertex pos0 = getTranslatedVertex(polygon.vertices[0], matrix);
ModelPart.Vertex pos1 = getTranslatedVertex(polygon.vertices[1], matrix);
ModelPart.Vertex pos2 = getTranslatedVertex(polygon.vertices[2], matrix);
ModelPart.Vertex pos3 = getTranslatedVertex(polygon.vertices[3], matrix);
Direction direction = getDirectionFromVector(polygon.normal);
VertexWeight pos0Weight = getYClipWeight(pos0.pos.y());
VertexWeight pos1Weight = getYClipWeight(pos1.pos.y());
VertexWeight pos2Weight = getYClipWeight(pos2.pos.y());
VertexWeight pos3Weight = getYClipWeight(pos3.pos.y());
if (pos1.pos.x() > X_PLANE != pos2.pos.x() > X_PLANE) {
float distance = pos2.pos.x() - pos1.pos.x();
float textureU = pos1.u + (pos2.u - pos1.u) * ((X_PLANE - pos1.pos.x()) / distance);
ModelPart.Vertex pos4 = new ModelPart.Vertex(X_PLANE, pos0.pos.y(), pos0.pos.z(), textureU, pos0.v);
ModelPart.Vertex pos5 = new ModelPart.Vertex(X_PLANE, pos1.pos.y(), pos1.pos.z(), textureU, pos1.v);
xClipPolygons.add(new AnimatedPolygon(new AnimatedVertex[] {
new AnimatedVertex(pos0, 8, 7, 0, pos0Weight.chestWeight, pos0Weight.torsoWeight, 0),
new AnimatedVertex(pos4, 8, 7, 0, pos0Weight.chestWeight, pos0Weight.torsoWeight, 0),
new AnimatedVertex(pos5, 8, 7, 0, pos1Weight.chestWeight, pos1Weight.torsoWeight, 0),
new AnimatedVertex(pos3, 8, 7, 0, pos3Weight.chestWeight, pos3Weight.torsoWeight, 0)
}, direction));
xClipPolygons.add(new AnimatedPolygon(new AnimatedVertex[] {
new AnimatedVertex(pos4, 8, 7, 0, pos0Weight.chestWeight, pos0Weight.torsoWeight, 0),
new AnimatedVertex(pos1, 8, 7, 0, pos1Weight.chestWeight, pos1Weight.torsoWeight, 0),
new AnimatedVertex(pos2, 8, 7, 0, pos2Weight.chestWeight, pos2Weight.torsoWeight, 0),
new AnimatedVertex(pos5, 8, 7, 0, pos1Weight.chestWeight, pos1Weight.torsoWeight, 0)
}, direction));
} else {
xClipPolygons.add(new AnimatedPolygon(new AnimatedVertex[] {
new AnimatedVertex(pos0, 8, 7, 0, pos0Weight.chestWeight, pos0Weight.torsoWeight, 0),
new AnimatedVertex(pos1, 8, 7, 0, pos1Weight.chestWeight, pos1Weight.torsoWeight, 0),
new AnimatedVertex(pos2, 8, 7, 0, pos2Weight.chestWeight, pos2Weight.torsoWeight, 0),
new AnimatedVertex(pos3, 8, 7, 0, pos3Weight.chestWeight, pos3Weight.torsoWeight, 0)
}, direction));
}
}
for (AnimatedPolygon polygon : xClipPolygons) {
boolean upsideDown = polygon.animatedVertexPositions[1].pos.y() > polygon.animatedVertexPositions[2].pos.y();
AnimatedVertex pos0 = upsideDown ? polygon.animatedVertexPositions[2] : polygon.animatedVertexPositions[0];
AnimatedVertex pos1 = upsideDown ? polygon.animatedVertexPositions[3] : polygon.animatedVertexPositions[1];
AnimatedVertex pos2 = upsideDown ? polygon.animatedVertexPositions[0] : polygon.animatedVertexPositions[2];
AnimatedVertex pos3 = upsideDown ? polygon.animatedVertexPositions[1] : polygon.animatedVertexPositions[3];
Direction direction = getDirectionFromVector(polygon.normal);
List<VertexWeight> vertexWeights = getMiddleYClipWeights(pos1.pos.y(), pos2.pos.y());
List<AnimatedVertex> animatedVertices = Lists.<AnimatedVertex>newArrayList();
animatedVertices.add(pos0);
animatedVertices.add(pos1);
if (vertexWeights.size() > 0) {
for (VertexWeight vertexWeight : vertexWeights) {
float distance = pos2.pos.y() - pos1.pos.y();
float textureV = pos1.v + (pos2.v - pos1.v) * ((vertexWeight.yClipCoord - pos1.pos.y()) / distance);
Vector3f clipPos1 = getClipPoint(pos1.pos, pos2.pos, vertexWeight.yClipCoord);
Vector3f clipPos2 = getClipPoint(pos0.pos, pos3.pos, vertexWeight.yClipCoord);
ModelPart.Vertex pos4 = new ModelPart.Vertex(clipPos2, pos0.u, textureV);
ModelPart.Vertex pos5 = new ModelPart.Vertex(clipPos1, pos1.u, textureV);
animatedVertices.add(new AnimatedVertex(pos4, 8, 7, 0, vertexWeight.chestWeight, vertexWeight.torsoWeight, 0));
animatedVertices.add(new AnimatedVertex(pos5, 8, 7, 0, vertexWeight.chestWeight, vertexWeight.torsoWeight, 0));
}
}
animatedVertices.add(pos3);
animatedVertices.add(pos2);
for (int i = 0; i < (animatedVertices.size() - 2) / 2; i++) {
int start = i*2;
AnimatedVertex p0 = animatedVertices.get(start);
AnimatedVertex p1 = animatedVertices.get(start + 1);
AnimatedVertex p2 = animatedVertices.get(start + 3);
AnimatedVertex p3 = animatedVertices.get(start + 2);
xyClipPolygons.add(new AnimatedPolygon(new AnimatedVertex[] {
new AnimatedVertex(p0, 8, 7, 0, p0.weight.x, p0.weight.y, 0),
new AnimatedVertex(p1, 8, 7, 0, p1.weight.x, p1.weight.y, 0),
new AnimatedVertex(p2, 8, 7, 0, p2.weight.x, p2.weight.y, 0),
new AnimatedVertex(p3, 8, 7, 0, p3.weight.x, p3.weight.y, 0)
}, direction));
}
}
for (AnimatedPolygon polygon : xyClipPolygons) {
Vector3f norm = new Vector3f(polygon.normal);
norm.mul(poseStack.last().normal());
for (AnimatedVertex vertex : polygon.animatedVertexPositions) {
Vector4f pos = new Vector4f(vertex.pos, 1.0F);
float weight1 = vertex.weight.x;
float weight2 = vertex.weight.y;
int joint1 = vertex.jointId.getX();
int joint2 = vertex.jointId.getY();
int count = weight1 > 0.0F && weight2 > 0.0F ? 2 : 1;
if (weight1 <= 0.0F) {
joint1 = joint2;
weight1 = weight2;
}
vertices.add(new SingleGroupVertexBuilder()
.setPosition(new Vec3f(pos.x(), pos.y(), pos.z()).scale(0.0625F))
.setNormal(new Vec3f(norm.x(), norm.y(), norm.z()))
.setTextureCoordinate(new Vec2f(vertex.u, vertex.v))
.setEffectiveJointIDs(new Vec3f(joint1, joint2, 0))
.setEffectiveJointWeights(new Vec3f(weight1, weight2, 0.0F))
.setEffectiveJointNumber(count)
);
}
triangluatePolygon(indices, partDefinition, indexCounter);
}
}
static VertexWeight getYClipWeight(float y) {
if (y < WEIGHT_ALONG_Y[0].yClipCoord) {
return new VertexWeight(y, 0.0F, 1.0F);
}
int index = -1;
for (int i = 0; i < WEIGHT_ALONG_Y.length; i++) {
}
if (index > 0) {
VertexWeight pair = WEIGHT_ALONG_Y[index];
return new VertexWeight(y, pair.chestWeight, pair.torsoWeight);
}
return new VertexWeight(y, 1.0F, 0.0F);
}
static class VertexWeight {
final float yClipCoord;
final float chestWeight;
final float torsoWeight;
public VertexWeight(float yClipCoord, float chestWeight, float torsoWeight) {
this.yClipCoord = yClipCoord;
this.chestWeight = chestWeight;
this.torsoWeight = torsoWeight;
}
}
static List<VertexWeight> getMiddleYClipWeights(float minY, float maxY) {
List<VertexWeight> cutYs = Lists.<VertexWeight>newArrayList();
for (VertexWeight vertexWeight : WEIGHT_ALONG_Y) {
if (vertexWeight.yClipCoord > minY && maxY >= vertexWeight.yClipCoord) {
cutYs.add(vertexWeight);
}
}
return cutYs;
}
}
static class LimbPartTransformer extends PartTransformer<ModelPart.Cube> {
final int upperJoint;
final int lowerJoint;
final int middleJoint;
final boolean bendInFront;
final SimpleTransformer upperAttachmentTransformer;
final SimpleTransformer lowerAttachmentTransformer;
final AABB noneAttachmentArea;
final AABB coverArea;
final float yClipCoord;
public LimbPartTransformer(AABB coverArea, int upperJoint, int lowerJoint, int middleJoint, float yClipCoord, boolean bendInFront, AABB noneAttachmentArea) {
this.upperJoint = upperJoint;
this.lowerJoint = lowerJoint;
this.middleJoint = middleJoint;
this.bendInFront = bendInFront;
this.upperAttachmentTransformer = new SimpleTransformer(null, upperJoint);
this.lowerAttachmentTransformer = new SimpleTransformer(null, lowerJoint);
this.noneAttachmentArea = noneAttachmentArea;
this.coverArea = coverArea;
this.yClipCoord = yClipCoord;
}
@Override
public void bakeCube(PoseStack poseStack, MeshPartDefinition partDefinition, ModelPart.Cube cube, List<SingleGroupVertexBuilder> vertices, Map<MeshPartDefinition, IntList> indices, PartTransformer.IndexCounter indexCounter) {
List<AnimatedPolygon> polygons = Lists.<AnimatedPolygon>newArrayList();
for (ModelPart.Polygon quad : cube.polygons) {
Matrix4f matrix = poseStack.last().pose();
ModelPart.Vertex pos0 = getTranslatedVertex(quad.vertices[0], matrix);
ModelPart.Vertex pos1 = getTranslatedVertex(quad.vertices[1], matrix);
ModelPart.Vertex pos2 = getTranslatedVertex(quad.vertices[2], matrix);
ModelPart.Vertex pos3 = getTranslatedVertex(quad.vertices[3], matrix);
Direction direction = getDirectionFromVector(quad.normal);
if (pos1.pos.y() > this.yClipCoord != pos2.pos.y() > this.yClipCoord) {
float distance = pos2.pos.y() - pos1.pos.y();
float textureV = pos1.v + (pos2.v - pos1.v) * ((this.yClipCoord - pos1.pos.y()) / distance);
Vector3f clipPos1 = getClipPoint(pos1.pos, pos2.pos, this.yClipCoord);
Vector3f clipPos2 = getClipPoint(pos0.pos, pos3.pos, this.yClipCoord);
ModelPart.Vertex pos4 = new ModelPart.Vertex(clipPos2, pos0.u, textureV);
ModelPart.Vertex pos5 = new ModelPart.Vertex(clipPos1, pos1.u, textureV);
int upperId, lowerId;
if (distance > 0) {
upperId = this.lowerJoint;
lowerId = this.upperJoint;
} else {
upperId = this.upperJoint;
lowerId = this.lowerJoint;
}
polygons.add(new AnimatedPolygon(new AnimatedVertex[] {
new AnimatedVertex(pos0, upperId), new AnimatedVertex(pos1, upperId),
new AnimatedVertex(pos5, upperId), new AnimatedVertex(pos4, upperId)
}, direction));
polygons.add(new AnimatedPolygon(new AnimatedVertex[] {
new AnimatedVertex(pos4, lowerId), new AnimatedVertex(pos5, lowerId),
new AnimatedVertex(pos2, lowerId), new AnimatedVertex(pos3, lowerId)
}, direction));
boolean hasSameZ = pos4.pos.z() < 0.0F == pos5.pos.z() < 0.0F;
boolean isFront = hasSameZ && (pos4.pos.z() < 0.0F == this.bendInFront);
if (isFront) {
polygons.add(new AnimatedPolygon(new AnimatedVertex[] {
new AnimatedVertex(pos4, this.middleJoint), new AnimatedVertex(pos5, this.middleJoint),
new AnimatedVertex(pos5, this.upperJoint), new AnimatedVertex(pos4, this.upperJoint)
}, 0.001F, direction));
polygons.add(new AnimatedPolygon(new AnimatedVertex[] {
new AnimatedVertex(pos4, this.lowerJoint), new AnimatedVertex(pos5, this.lowerJoint),
new AnimatedVertex(pos5, this.middleJoint), new AnimatedVertex(pos4, this.middleJoint)
}, 0.001F, direction));
} else if (!hasSameZ) {
boolean startFront = pos4.pos.z() > 0;
int firstJoint = this.lowerJoint;
int secondJoint = this.lowerJoint;
int thirdJoint = startFront ? this.upperJoint : this.middleJoint;
int fourthJoint = startFront ? this.middleJoint : this.upperJoint;
int fifthJoint = this.upperJoint;
int sixthJoint = this.upperJoint;
polygons.add(new AnimatedPolygon(new AnimatedVertex[] {
new AnimatedVertex(pos4, firstJoint), new AnimatedVertex(pos5, secondJoint),
new AnimatedVertex(pos5, thirdJoint), new AnimatedVertex(pos4, fourthJoint)
}, 0.001F, direction));
polygons.add(new AnimatedPolygon(new AnimatedVertex[] {
new AnimatedVertex(pos4, fourthJoint), new AnimatedVertex(pos5, thirdJoint),
new AnimatedVertex(pos5, fifthJoint), new AnimatedVertex(pos4, sixthJoint)
}, 0.001F, direction));
}
} else {
int jointId = pos0.pos.y() > this.yClipCoord ? this.upperJoint : this.lowerJoint;
polygons.add(new AnimatedPolygon(new AnimatedVertex[] {
new AnimatedVertex(pos0, jointId), new AnimatedVertex(pos1, jointId),
new AnimatedVertex(pos2, jointId), new AnimatedVertex(pos3, jointId)
}, direction));
}
}
for (AnimatedPolygon quad : polygons) {
Vector3f norm = new Vector3f(quad.normal);
norm.mul(poseStack.last().normal());
for (AnimatedVertex vertex : quad.animatedVertexPositions) {
Vector4f pos = new Vector4f(vertex.pos, 1.0F);
vertices.add(new SingleGroupVertexBuilder()
.setPosition(new Vec3f(pos.x(), pos.y(), pos.z()).scale(0.0625F))
.setNormal(new Vec3f(norm.x(), norm.y(), norm.z()))
.setTextureCoordinate(new Vec2f(vertex.u, vertex.v))
.setEffectiveJointIDs(new Vec3f(vertex.jointId.getX(), 0, 0))
.setEffectiveJointWeights(new Vec3f(1.0F, 0.0F, 0.0F))
.setEffectiveJointNumber(1)
);
}
triangluatePolygon(indices, partDefinition, indexCounter);
}
}
}
static Direction getDirectionFromVector(Vector3f directionVec) {
for (Direction direction : Direction.values()) {
Vector3f direcVec = new Vector3f(Float.compare(directionVec.x(), -0.0F) == 0 ? 0.0F : directionVec.x(), directionVec.y(), directionVec.z());
if (direcVec.equals(direction.step())) {
return direction;
}
}
return null;
}
static Vec3 getCenterOfCube(PoseStack poseStack, ModelPart.Cube cube) {
double minX = Double.MAX_VALUE;
double minY = Double.MAX_VALUE;
double minZ = Double.MAX_VALUE;
double maxX = Double.MIN_VALUE;
double maxY = Double.MIN_VALUE;
double maxZ = Double.MIN_VALUE;
Matrix4f matrix = poseStack.last().pose();
for (ModelPart.Polygon quad : cube.polygons) {
for (ModelPart.Vertex v : quad.vertices) {
Vector4f translatedPosition = new Vector4f(v.pos, 1.0F);
translatedPosition.mul(matrix);
if (minX > translatedPosition.x()) {
minX = translatedPosition.x();
}
if (minY > translatedPosition.y()) {
minY = translatedPosition.y();
}
if (minZ > translatedPosition.z()) {
minZ = translatedPosition.z();
}
if (maxX < translatedPosition.x()) {
maxX = translatedPosition.x();
}
if (maxY < translatedPosition.y()) {
maxY = translatedPosition.y();
}
if (maxZ < translatedPosition.z()) {
maxZ = translatedPosition.z();
}
}
}
return new Vec3(minX + (maxX - minX) * 0.5D, minY + (maxY - minY) * 0.5D, minZ + (maxZ - minZ) * 0.5D);
}
static Vector3f getClipPoint(Vector3f pos1, Vector3f pos2, float yClip) {
Vector3f direct = new Vector3f(pos2);
direct.sub(pos1);
direct.mul((yClip - pos1.y()) / (pos2.y() - pos1.y()));
Vector3f clipPoint = new Vector3f(pos1);
clipPoint.add(direct);
return clipPoint;
}
static ModelPart.Vertex getTranslatedVertex(ModelPart.Vertex original, Matrix4f matrix) {
Vector4f translatedPosition = new Vector4f(original.pos, 1.0F);
translatedPosition.mul(matrix);
return new ModelPart.Vertex(translatedPosition.x(), translatedPosition.y(), translatedPosition.z(), original.u, original.v);
}
static class AnimatedVertex extends ModelPart.Vertex {
final Vec3i jointId;
final Vec3f weight;
public AnimatedVertex(ModelPart.Vertex posTexVertx, int jointId) {
this(posTexVertx, jointId, 0, 0, 1.0F, 0.0F, 0.0F);
}
public AnimatedVertex(ModelPart.Vertex posTexVertx, int jointId1, int jointId2, int jointId3, float weight1, float weight2, float weight3) {
this(posTexVertx, new Vec3i(jointId1, jointId2, jointId3), new Vec3f(weight1, weight2, weight3));
}
public AnimatedVertex(ModelPart.Vertex posTexVertx, Vec3i ids, Vec3f weights) {
this(posTexVertx, posTexVertx.u, posTexVertx.v, ids, weights);
}
public AnimatedVertex(ModelPart.Vertex posTexVertx, float u, float v, Vec3i ids, Vec3f weights) {
super(posTexVertx.pos.x(), posTexVertx.pos.y(), posTexVertx.pos.z(), u, v);
this.jointId = ids;
this.weight = weights;
}
}
static class AnimatedPolygon {
public final AnimatedVertex[] animatedVertexPositions;
public final Vector3f normal;
public AnimatedPolygon(AnimatedVertex[] positionsIn, Direction directionIn) {
this.animatedVertexPositions = positionsIn;
this.normal = directionIn.step();
}
public AnimatedPolygon(AnimatedVertex[] positionsIn, float cor, Direction directionIn) {
this.animatedVertexPositions = positionsIn;
positionsIn[0] = new AnimatedVertex(positionsIn[0], positionsIn[0].u, positionsIn[0].v + cor, positionsIn[0].jointId, positionsIn[0].weight);
positionsIn[1] = new AnimatedVertex(positionsIn[1], positionsIn[1].u, positionsIn[1].v + cor, positionsIn[1].jointId, positionsIn[1].weight);
positionsIn[2] = new AnimatedVertex(positionsIn[2], positionsIn[2].u, positionsIn[2].v - cor, positionsIn[2].jointId, positionsIn[2].weight);
positionsIn[3] = new AnimatedVertex(positionsIn[3], positionsIn[3].u, positionsIn[3].v - cor, positionsIn[3].jointId, positionsIn[3].weight);
this.normal = directionIn.step();
}
}
public record VanillaMeshPartDefinition(String partName, RenderProperties renderProperties, List<String> path, OpenMatrix4f invertedParentTransform, ModelPart root) implements MeshPartDefinition {
public static MeshPartDefinition of(String partName, RenderProperties renderProperties) {
return new VanillaMeshPartDefinition(partName, renderProperties, null, null, null);
}
public static MeshPartDefinition of(String partName) {
return new VanillaMeshPartDefinition(partName, null, null, null, null);
}
/**
* For animated models
* @param partName
* @param path
* @param invertedParentTransform
* @param root
* @return
*/
public static MeshPartDefinition of(String partName, List<String> path, OpenMatrix4f invertedParentTransform, ModelPart root) {
return new VanillaMeshPartDefinition(partName, null, path, invertedParentTransform, root);
}
public Supplier<OpenMatrix4f> getModelPartAnimationProvider() {
return this.root == null ? () -> null : () -> {
PoseStack poseStack = new PoseStack();
poseStack.mulPose(QuaternionUtils.YP.rotationDegrees(180.0F));
poseStack.mulPose(QuaternionUtils.XP.rotationDegrees(180.0F));
poseStack.translate(0.0F, -24.0F, 0.0F);
this.progress(this.root, poseStack, false);
ModelPart part = this.root;
int idx = 0;
for (String childPartName : this.path) {
idx++;
part = part.getChild(childPartName);
this.progress(part, poseStack, idx == this.path.size());
}
OpenMatrix4f animParentTransform = OpenMatrix4f.importFromMojangMatrix(poseStack.last().pose());
animParentTransform.m30 *= 0.0625F;
animParentTransform.m31 *= 0.0625F;
animParentTransform.m32 *= 0.0625F;
ModelPart lastPart = part;
PartPose partPose = part.getInitialPose();
OpenMatrix4f partAnimation = OpenMatrix4f.mulMatrices(animParentTransform,
new OpenMatrix4f().mulBack(OpenMatrix4f.fromQuaternion(new Quaternionf().rotationZYX(partPose.zRot, partPose.yRot, partPose.xRot)).transpose().invert())
.translate(new Vec3f(lastPart.x - partPose.x, lastPart.y - partPose.y, lastPart.z - partPose.z).scale(0.0625F))
.mulBack(OpenMatrix4f.fromQuaternion(new Quaternionf().rotationZYX(partPose.zRot, partPose.yRot, partPose.xRot)).transpose())
.mulBack(OpenMatrix4f.fromQuaternion(new Quaternionf().rotationZYX(lastPart.zRot - partPose.zRot, lastPart.yRot - partPose.yRot, lastPart.xRot - partPose.xRot)).transpose())
.scale(new Vec3f(lastPart.xScale, lastPart.yScale, lastPart.zScale)),
this.invertedParentTransform);
return partAnimation;
};
}
private void progress(ModelPart part, PoseStack poseStack, boolean last) {
PartPose initialPose = part.getInitialPose();
if (last) {
poseStack.translate(initialPose.x, initialPose.y, initialPose.z);
poseStack.mulPose(new Quaternionf().rotationZYX(initialPose.zRot, initialPose.yRot, initialPose.xRot));
} else {
poseStack.translate(part.x, part.y, part.z);
poseStack.mulPose(new Quaternionf().rotationZYX(part.zRot, part.yRot, part.xRot));
poseStack.scale(part.xScale, part.yScale, part.zScale);
}
}
public boolean equals(Object o) {
if (this == o) {
return true;
} else if (o instanceof MeshPartDefinition comparision) {
return this.partName.equals(comparision.partName());
}
return false;
}
public int hashCode() {
return this.partName.hashCode();
}
}
}

Some files were not shown because too many files have changed in this diff Show More