Unlocks 3 artist-freedom quick wins :
1. ClientAnimationProperties.LAYER_TYPE / PRIORITY / JOINT_MASK now
serializable via name + Codec. Modders can override these in their
anim JSON 'properties' block (previously threw IllegalStateException).
2. AnimationBindings.equipSound / unequipSound — per-item JSON override
for on_equip/on_unequip audio feedback. ClientRigEquipmentHandler
resolves via ForgeRegistries.SOUND_EVENTS, falls back to vanilla
ARMOR_EQUIP_LEATHER if unknown.
3. AnimationBindings.restraintLevel / tooltipOverride — metadata fields
for UX consumers (HUD severity indicator, tooltip customization).
Parsed but not yet rendered — available for Phase 4 features without
schema change.
All additive. Existing items without these fields work unchanged.
Remove the last Java-hardcoded StaticAnimation registration. context_stand_idle.json
now has the EF-native 'constructor' block so AnimationManager picks it up at
datapack reload, same as the 5 Phase 3 placeholders.
Consumers (PlayerPatch, LivingEntityPatch, RigAnimationTickHandler, TiedUpMod,
tests) refactored to lookup via resolveWithFallback(CONTEXT_STAND_IDLE_ID)
instead of direct static field access. resolveWithFallback handles the pre-
reload window by returning EMPTY_ANIMATION — no more null-checks in consumers.
TiedUpAnimationRegistry keeps only :
- CONTEXT_STAND_IDLE_ID constant (canonical ID)
- resolveWithFallback() helper
- WARNED_MISSING_ANIMS dedup + reset hook
Design goal : zero Java code required for any modder to add bondage anims.
Drop a JSON in assets/<modid>/animmodels/animations/, reference it from an
item JSON binding, /reload, see it fire.
Replace InstantiateInvoker.getArmature() throw with real lookup in
TiedUpArmatures registry. This unblocks the datapack auto-register
path for anim JSONs placed in assets/tiedup/animmodels/animations/.
5 Phase 3 placeholder JSONs updated with EF-native 'constructor' block
— they now auto-register via AnimationManager.readResourcepackAnimation
without any Java-side hardcoding.
Data-driven pipeline now fully working end-to-end : modder drops a
JSON in their datapack's animmodels folder, it's automatically picked
up at reload, and referenced via resolveWithFallback() from item
bindings. No recompile needed for new items/anims.
Adds minimum viable assets to exercise the Phase 3 anim pipeline
in-game :
- 5 placeholder anim JSON files in assets/tiedup/animmodels/animations/
(identity keyframes matching context_stand_idle template) :
armbinder_idle, armbinder_walk, armbinder_struggle,
armbinder_equip_oneshot, classic_collar_idle
- armbinder.json enriched with 'animations' block binding IDLE + WALK
+ WALK_BOUND + STRUGGLE_BOUND living_motions + on_equip oneshot
- classic_collar.json enriched with IDLE binding for multi-item
composition test
Purpose : validate end-to-end that the data-driven pipeline fires
correctly (rebuild, motion switch, oneshot sound, rehydrate) via F6
debug overlay. Visual pose does not change (identity anims) — body
keyframe authoring remains blocked on Blender artist assets.
Runtime note : the placeholder JSONs lack the EF 'constructor' block,
so readResourcepackAnimation does not register them in
AnimationManager.animationByName. resolveWithFallback will return
EMPTY_ANIMATION with a dedup WARN per missing ID — this exercises
the binding resolution path without needing a real StaticAnimation
(the map livingAnimations still receives the accessor, F6 overlay
still reports bindings:N). Real registration will land when either
InstantiateInvoker.getArmature() is implemented (Phase 2 armature
registry) or a dedicated AnimationRegistryEvent handler is added.
Gameday verification steps documented in docs/plans/rig/GAMEDAY_CHECKLIST.md
(gitignored, local only).
HIGH fix : currentCompositeMotion was initialized to IDLE and never
written — any COMPOSITE_LAYER overlay bound to a non-IDLE motion
would never fire (e.g. cuffs_arms_idle overlay on WALK_BOUND). Set
composite = currentLivingMotion in updateMotion, matching EF pattern
AbstractClientPlayerPatch:129-141.
MEDIUM cleanup : triggerOneshot<T> was 85% duplicate of triggerOneshotById
and had 0 prod callers (tests only). Removed + migrated 6 tests to
triggerOneshotById.
LOW doc fix : ExtendableEnumManager.synchronized javadoc over-sold the
risk as 'BUG-001 review fix' — it's actually conservative defense
against a theoretical cross-classloader race. EF upstream runs without
sync since 2020. Kept synchronized (no cost, no harm) but documented
honestly.
User decision 2026-04-24 : IDLE bindings must be supported because
modders need to define idle poses for their items (armbinder can't
work without arms-behind-back idle pose). TiedUp will also ship
default IDLE bindings for its core items once assets are authored.
Technical : EF ClientAnimator has 2 separate maps (livingAnimations
for BASE layer + compositeLivingAnimations for COMPOSITE layers).
Both support IDLE bindings without race — last-put-wins for base
override, or automatic composition via JointMask for partial overlay.
The previous WARN was a reviewer over-correction based on misread
of the 2-map structure.
- Removed WARN parse-time in DataDrivenItemParser.parseAnimationBindings
- Updated test : IDLE binding is accepted silently (no warn)
- Cleaned 'Option B' mentions in javadoc comments
- Design doc (gitignored) updated with 2-path explanation.
HIGH BUG-001 + MEDIUM RISK-001 : spam timestamp was burned BEFORE
preconditions were validated. Null registryName meant server played
but clients got no broadcast (state desync). Null animator meant
400ms lockout for an entity that never played. Reorder : validate
registryName + animator FIRST, record spam only after preconditions
pass, then server play + broadcast.
MEDIUM RISK-002 : document 400ms global guard as INTENTIONAL design
for cinematic one-shots. Edge case hit+fall-damage same-tick drops
the 2nd — acceptable trade-off. Per-category rate limiting is Phase 4
if gameplay needs it.
LOW SMELL-001 : merge duplicate javadoc on LAST_SYNC_PLAY_BY_ENTITY.
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).
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.