Client handler : resolve entity via Minecraft.getInstance().level,
cast to LivingEntity, lookup LivingEntityPatch via TiedUpCapabilities,
call animator.playAnimation avec l'accessor résolu. Null-cascade guards
(level / entity / patch / animator) + debug logs. Logic pure extraite en
resolveAndPlay(level, id, animId, transition, patchLookup) — testable
sans bootstrap MC grâce au patchLookup injectable (évite
TiedUpCapabilities.<clinit> qui cascade sur Registries).
Server-side playAnimationSync : local play sur l'animator (ServerAnimator
track l'état pour StateSpectrum/EntityState même sans rendu visuel) +
broadcast via PacketDistributor TRACKING_ENTITY. Anti-spam guard 400ms
par entity pour prévenir le master-click-spam flood (hit/slap). Cleanup
automatique via WeakHashMap (weak ref sur l'entity → auto-removed au GC
post-despawn, pas de leak sur long-running server).
priorityOrdinal reste informationnel côté client (ClientAnimator.playAnimation
n'accepte pas de priority — intrinsèque au StaticAnimation JSON via
ClientAnimationProperties.PRIORITY). Encodé dans le packet pour future
use / logging.
Débloque les 3 triggers non-fonctionnels : NPC capture cinematic,
hit/slap feedback, unconscious animation.
Tests +7 (277 → 284 GREEN) :
- PacketPlayRigAnimHandlerTest : nullLevel + signature reachability
(les autres branches requièrent mock ClientLevel/LivingEntity qui
triggers Bootstrap — gameday-only, documenté)
- LivingEntityPatchSpamGuardTest : first pass, within-window drop,
boundary 400ms exact pass, cross-entity isolation, constant sanity
HIGH RISK-001 : snapshot update moved between reset+applyDefinitions
and equip oneshots. If an equip oneshot throws, animator state matches
snapshot (currentKeys already applied). Prevents next-tick desync.
MEDIUM Option B warn : parser now logs WARN if modder binds IDLE.
Points to BONDAGE_ANIMATION_DESIGN §5.1 convention. Binding still
accepted (functional), just non-idiomatic.
MEDIUM WARNED_MISSING_ANIMS reload reset : new
TiedUpRigRegistryReloadListener wires resetWarnedMissing() on
AddReloadListenerEvent + RegisterClientReloadListenersEvent. Prevents
unbounded set growth in long-running sessions with datapack swaps.
LOW naming : unified MOD_ID in Wave α files (BondageRehydrateListener
switched from TiedUpRigConstants.MODID to TiedUpMod.MOD_ID for
consistency with sibling BondageEquipmentChangeListener ; both
constants hold the literal "tiedup" so no behavioral change).
CRITICAL BUG-001 reviewer : identity-based diff triggered sound 2x per
sync because V2BondageEquipment.deserializeNBT regenerates ItemStack
instances. 19 call-sites (respawn, dialogue, lock toggle, whip) do full
resync → every interaction = sound cacophony. Switch LAST_EQUIPPED from
Set<ItemStack> (identity) to Map<BodyRegionV2, ResourceLocation> keyed
by (region, itemId). Same region+item = no change, ignored.
HIGH RISK-001 reviewer : first rebuild per player had LAST_EQUIPPED
empty → all items detected as newly equipped → sound spam at login
(5 items = 5 sounds). Add FIRST_REBUILD_DONE WeakHashMap flag to
skip oneshots+sounds on very first rebuild per player.
MEDIUM SMELL-001/002 : javadoc of diffBy removed (assumed identity
sharing which never happens). Padlock NBT patch detection promise
removed — would require NBT hash in snapshot key (Phase 4).
Tests refactored : key-based (region, itemId) semantics.
MEDIUM SMELL-01 reviewer : V2EquipmentHelper.getAllEquipped already
identity-dedupes (IdentityHashMap upstream in V2BondageEquipment:84).
The extra .mapToInt(identityHashCode).distinct() was dead defensive
code. Simplified to direct .count().
MEDIUM SMELL-02 reviewer : keybind guard 'mc.player == null || mc.level
== null' fired BEFORE RIG_DEBUG_KEY consumeClick loop. Main menu clicks
were queued and flushed on world-join (phantom toggle). Move toggle
consumption BEFORE the guard — it's a client-side boolean flip, needs
no world context.
LOW RISK-01 skipped : pin test on Layer.toString() format would require
either registry init (TiedUpRigRegistry.EMPTY_ANIMATION in AnimationPlayer
ctor) or Mockito ; test env doesn't currently init the RIG registry and
layerPriorityName() already has a defensive "?" fallback. Gameday check
is a tighter feedback loop than a flaky unit test here.
Wire ClientRigEquipmentHandler.rebuildBondageAnimations on :
- ClientPlayerNetworkEvent.LoggingIn (client connects)
- EntityJoinLevelEvent (spawn + dim change, filtered to LocalPlayer)
PlayerRespawnEvent path already covered server-side via
PlayerStateEventHandler.onPlayerRespawn -> V2EquipmentHelper.sync ->
PacketSyncV2Equipment -> rebuild (hook P3-06). This listener fills the
client-only gaps.
Without this, a player who logs in with bondage items equipped (or
teleports cross-dimension) sees their livingAnimations map empty until
the next V2 capability sync or armor slot change arrives. Visible
incoherence : item still equipped, bondage anim dropped, player plays
vanilla WALK. Demo-killing regression.
Handlers are idempotent and fire 2-3x at startup (login + spawn + first
chunk load) — rebuildBondageAnimations is designed for that :
resetLivingAnimations + re-apply current bindings, no state drift.
Pure filter extracted to shouldRehydrate(entity, level, ePred, lPred)
generic overload — mirrors the isBondageItem pattern in the sibling
BondageEquipmentChangeListener. Lets tests bypass the MC bootstrap
crash on Mockito.mock(LocalPlayer.class).
Removed the TODO(P3-20) block in BondageEquipmentChangeListener that
pointed to this task.
5 tests, all paths green (shouldRehydrate short-circuits, production
predicate wiring + null-safety).
Scope reduced to overlay only — E2E integration test deferred.
Overlay displays current living motion + composite motion, equipped bondage
count, livingAnimations bindings count, active layers w/ priority + anim.
Critical for dev-visible feedback when placeholder identity anims don't
change the visual rig but the pipeline is running correctly.
Keybind : F6 (configurable in Controls menu, category TiedUp!). F6 chosen
because unused by vanilla and preserves F3 for debug screen.
No new accessor needed on Animator — getLivingAnimations() already exists
(returns ImmutableMap.copyOf, safe for debug read). Layer priority reading
falls back on Layer.toString() parsing for composite layers where the
priority field is protected ; base layer uses the public getBaseLayerPriority().
Tests : 6 pure unit tests (toggle flag, null-player branch, motionName on
vanilla+tiedup enums+null). Real render path is MC-runtime-bound and
validated gameday only.
Diff detection tick-to-tick via WeakHashMap<Player, Set<ItemStack>>
snapshot captured at start of rebuildBondageAnimations. Identity-based
set (IdentityHashMap) respects convention V2 "un stack = une
occurrence" : deux stacks content-equals mais refs distinctes restent
distincts.
Unequip oneshots fire BEFORE rebuild (play sur l'old state,
livingAnimations encore config precedente), equip oneshots AFTER (play
sur new state, bindings de l'item pushes dans la map). Vanilla
SoundEvents.ARMOR_EQUIP_LEATHER fallback pour MVP — future per-item
custom via field equip_sound dans DataDrivenItemDefinition.
Helpers pure + generiques <T> :
- uniqueByIdentity(Iterable<T>) : dedup identity + skip nulls
- diffBy(Set<T>, Set<T>) : a \ b par ==, ordre stable
- triggerOneshot(...) : no-op si def/bindings/oneshotId null ; sinon
resolve via animResolver + play avec transition 0.15s constant
Decision actee : priority param non forwarde a playAnimation. La
priority vient de StaticAnimation.getPriority() intrinseque (property
JSON du fichier anim). PacketPlayRigAnim.priorityOrdinal (P3-11)
reste informational / loggable, priority override = TODO Phase 4.
Sound defaultEquipSound() resolu lazy via method (pas static final
field) — SoundEvents.<clinit> exige MC Bootstrap, un field eager
bloquait tous les unit tests de la classe (ExceptionInInitializerError).
Tests : +11 unit tests (28 total dans le fichier handler) :
- diffBy empty / identity / a==b
- uniqueByIdentity dedup + nulls skipped
- triggerOneshot no-def / null-bindings / null-onEquip / happy
onEquip / happy onUnequip / null-stack
Suite complete 257 GREEN, 0 failed.
Trigger rebuildBondageAnimations from :
1. PacketSyncV2Equipment.handleOnClient -- after deserializeNBT capability
2. LivingEquipmentChangeEvent -- filtered to bondage items, client only
Third source (LoggingIn / dimension change) scoped to P3-20 with TODO.
body : idempotent si double-fire, dedup via isBondageRelevant filter.
Caveat Forge doc LivingEquipmentChangeEvent = server-side only : le
handler est neanmoins enregistre @Dist.CLIENT comme filet defensif
pour les edge cases creative/admin. Le path canonique reste
PacketSyncV2Equipment qui couvre 100%% des changements V2 via la
capability dediee (V2 n'utilise pas les slots armor vanilla).
isBondageItem pattern : DataDrivenItemRegistry.get(stack) != null
(coherent avec ClientRigEquipmentHandler.extractSortedDefinitions).
Refactor generic <T> pour testabilite sans MC bootstrap (Mockito
ItemStack crash la static init registries).
HIGH RISK-001 : secondary comparator on def.id() prevents UB on
equal-priority items — no longer depends on BodyRegionV2 enum order.
New test asserts lexicographic ordering.
HIGH RISK-002 : resetLivingAnimations restores defaultLivingAnimations
snapshot. Javadoc now warns against calling setCurrentMotionsAsDefault
anywhere in the TiedUp pipeline (would leak custom bindings into
defaults, breaking unbind). Grep confirms no call site exists today.
Polish : rename tautological null test, precise null-safety doc.
First visible bondage animation pipeline orchestrator. Consumes the
primitives added in P3-01..P3-04 + P3-09 to rebuild the livingAnimations
map of a player based on currently equipped bondage items.
Design decisions :
- IdentityHashMap dedup (armbinder covers N regions -> 1 unique stack).
Defensive even though the capability already dedupes (so we don't
depend on an upstream invariant that might regress).
- Sort by posePriority ASC so highest-priority iterates last -> wins
conflicts (Map.put last-write-wins semantics in livingAnimations).
- Option B : JSON items don't bind IDLE, let EF defaults flow through
after resetLivingAnimations() which re-pushes default motions.
- null-check on animations() : 99% of V2 legacy items lack the JSON
block and return null from the parser, must be skipped silently.
- @OnlyIn(Dist.CLIENT) at class level : ClientAnimator is client-only,
server class-loader must never touch this handler.
- Extracted testable methods (extractSortedDefinitions + applyDefinitions)
with functional callbacks (Runnable + LivingAnimationAdder + Function
resolvers). Generic <T> on extractSortedDefinitions lets tests pass
Object dummies without MC ItemStack bootstrap.
Tests (15) covering :
- rebuildBondageAnimations null-safety
- extractSortedDefinitions : empty input, null resolver result, identity
dedup (multi-region), ASC priority sort, null entries skipped
- applyDefinitions : reset-only on empty, null/empty animations skipped,
multi-binding single item, two-item conflict last-write-wins, reset
ordering before any adder, end-to-end multi-region dedup, animResolver
contract (no internal dereference)
Note : this commit provides the HANDLER. The first visible bondage
animation still requires P3-06 (hook it to PacketSyncV2Equipment +
LivingEquipmentChangeEvent) and P3-08 (updateMotion state machine) to
fully light up. Jalon = pipeline viable, not yet wired.
212 tests GREEN (197 baseline + 15 new).
HIGH P3-03 : strict RL check laissait passer ':path', 'mod:', ':'
silencieusement (tryParse les accepte en 1.20.1). Fix combo check
(isEmpty || colonIdx<=0 || colonIdx>=length-1) rejette tous les cas
edge. 3 nouveaux tests couvrent les malformed variants.
MEDIUM P3-09 : javadoc de BondageStateHelpers affirmait ConcurrentHashMap
alors que le backing V2EquipmentHelper utilise EnumMap via pattern MC
main-thread + packet sync. Correction doc pour refléter la réalité.
Wire le parser JSON pour le bloc "animations" (living_motions + on_equip
+ on_unequip). Résolution motion cross-enum (LivingMotions vanilla EF +
TiedUpLivingMotions custom). Fuzzy-match Levenshtein pour suggestion
en cas de typo modder (Did you mean 'IDLE'?).
Remplace le null hardcodé à DataDrivenItemParser:352 (P3-02 TODO).
body : tolerance malformed RL, non-string values, unknown motions.
Prepare P3-12 consumption of Minecraft.getInstance() — refactor
handleOnClient() to delegate into a private ClientHandler inner class
annotated @OnlyIn(Dist.CLIENT). Server-side class-loader will not
resolve Minecraft.* references since the inner class isn't touched.
Direction guard inchangé. Import NetworkDirection passé top-level.
Issue : review transversale Phase 3 (architect agent), recommendation #3.
Record packet S->C pour animations cinematic one-shot (capture NPC, hit
stun, death). Carries entityId, animId (ResourceLocation), transitionTime
(float), et priorityOrdinal (byte = Layer.Priority.ordinal()).
Encode via writeVarInt/writeResourceLocation/writeFloat/writeByte ; decode
symétrique. Factory PacketPlayRigAnim.of() pour build lisible avec
Layer.Priority, et getter priority() avec fallback LOWEST si ordinal
hors-range (masking & 0xFF pour gérer byte négatif).
Handler client = stub qui LOGGER.debug + setPacketHandled. Le corps
complet (resolve entity, lookup LivingEntityPatch, animator.playAnimation)
viendra en P3-12 avec la méthode serveur playAnimationSync().
Registration ajoutée en fin de ModNetwork.register() — ID sequential 75
(76 packets total).
7 tests unitaires PacketPlayRigAnimTest : roundtrip nominal, entityId
boundary (0/MAX/MIN), transitionTime zero+negative (pas de validation
silencieuse), loop sur les 5 Layer.Priority, fallback LOWEST sur ordinal
malformé (99, -1, PRIORITY_COUNT), ResourceLocation avec underscores et
slashes, et sanity check equals.
Rig tests : 65 -> 72 GREEN. Full suite : 162 GREEN, 0 failure.
New record AnimationBindings (v2/bondage/datadriven/) carries the JSON
item -> rig animation bindings that P3-03 (parser) will populate and
P3-05 (ClientRigEquipmentHandler.rebuildBondageAnimations) will consume
to rebuild the player's livingAnimations map on equip/unequip.
Fields :
- livingMotions : Map<LivingMotion, ResourceLocation> (immutable, never null,
Map.copyOf defensive copy in compact ctor, null input collapsed to emptyMap)
- onEquip / onUnequip : optional nullable one-shot triggers
- EMPTY constant + isEmpty() for the "no binding" fast path
DataDrivenItemDefinition gains a nullable "animations" field (null = vanilla
behavior, preserves backward compat for items without rig bindings).
DataDrivenItemParser call-site updated to pass null for now — wiring happens
in P3-03. Single call-site updated (parser only), no factories or tests
constructed the record directly.
7 unit tests cover construction paths, null tolerance, defensive map copy,
isEmpty semantics and the EMPTY constant. Full rig suite still 65 GREEN.
BUG-001 reviewer: ExtendableEnumManager.assign() non thread-safe,
synchronize la méthode. Pas d impact perf (N×11 calls au bootstrap).
RISK-001 reviewer: load-order LivingMotions vanilla vs TiedUpLivingMotions
custom pouvait faire coller les ordinals si vanilla pas pre-loadé.
Force explicit LivingMotions.values() avant TiedUpLivingMotions.values()
dans commonSetup.
Test renommé pour refléter honnêtement ce qui est testé (vs load order
implicitement correct dans le test).
Nouveau helper statique consomme par ClientRigEquipmentHandler.rebuildBondageAnimations
(P3-05) et PacketPlayRigAnim.handleOnClient (P3-12) pour resoudre un anim ID avec
fallback safe si miss.
Design :
- Lookup delegue a AnimationManager.byKey(ResourceLocation) — l'API existante
pour la resolution par registry name.
- Fallback = TiedUpRigRegistry.EMPTY_ANIMATION (singleton canonique) plutot
qu'un stub empty_fallback separe. Les sites runtime (Layer#off, AnimationPlayer#isEmpty,
LayerOffAnimation#getNextAnimation) comparent via == EMPTY_ANIMATION — retourner
une autre instance provoquerait des false-negatives sur ces checks d'identite.
- Dedup WARN via ConcurrentHashMap.newKeySet() : un ID donne ne log qu'une fois
par session, evite le spam si le miss vient d'un item data-driven appele tick
apres tick. Pattern inspire de RigAnimationTickHandler.LOGGED_ERRORS.
- resetWarnedMissing() expose pour tests + runtime reload (F3+T datapack).
- Branche defensive : id=null swallow + log (cas pathologique caller).
4 tests unitaires :
- happy path (ID enregistre via AnimationManager.AnimationBuilder → assertSame)
- fallback safe (ID inconnu → EMPTY_ANIMATION, non-null)
- no-throw (ID inconnu + null swallow sans exception)
- dedup observable (reset puis re-call sur meme ID re-warn, sanity check fake
accessor distinct de EMPTY_ANIMATION)
65 tests rig GREEN (57 baseline + 4 nouveaux + autres).
Nouvelle enum custom etendant LivingMotion — partage le meme ENUM_MANAGER
que LivingMotions (vanilla EF), ordinals assignes a la suite sans collision.
8 motions design RIG :
- POSE_DOG
- POSE_PET_BED_SIT
- POSE_PET_BED_SLEEP
- POSE_FURNITURE_SEAT
- POSE_KNEEL_BOUND
- STRUGGLE_BOUND
- WALK_BOUND
- SNEAK_BOUND
3 ajouts UX (P0/P1) :
- POSE_SLEEP_BOUND — sleep avec restraints (P0)
- POSE_UNCONSCIOUS — steady-state post-capture (P0)
- FALL_BOUND — no flailing en chute (P1)
Class-load force dans TiedUpMod.commonSetup via values() — sans ca, les
ordinals ne sont pas assignes tant que l'enum n'est pas touche (init lazy
JLS). LivingMotions (vanilla) est class-loaded naturellement par les
patches rig, pas besoin de force.
Tests : 3 cas (11 entries, ordinals uniques intra-enum, pas de collision
avec LivingMotions apres class-load croise).
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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).
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.
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.
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.
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).