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).
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.
Architectural debt cleanup on top of the earlier V2 hardening pass.
Minigame:
- LockpickMiniGameState splits the overloaded targetSlot int into a
LockpickTargetKind enum + targetData int. Body-vs-furniture
dispatch is now a simple enum check; the NBT-tag nonce it
previously depended on is gone, along with the AIOOBE risk at
BodyRegionV2.values()[targetSlot].
- PacketLockpickAttempt.handleFurnitureLockpickSuccess takes the
entity and seat id as explicit parameters. Caller pre-validates
both before any side effect, so a corrupted ctx tag can no longer
produce a "Lock picked!" UI with a used lockpick and nothing
unlocked.
Package boundaries:
- client.gltf no longer imports v2.bondage. Render-layer attachment,
DataDrivenItemReloadListener, and GlbValidationReloadListener all
live in v2.client.V2ClientSetup.
- GlbValidationReloadListener moved to v2.bondage.client.diagnostic.
- Reload-listener ordering is preserved via EventPriority (HIGH for
the generic GLB cache clear in GltfClientSetup, LOW for bondage
consumers in V2ClientSetup).
- Removed the unused validateAgainstDefinition stub on GlbValidator.
Extractions from EntityFurniture:
- FurnitureSeatSyncCodec (pipe/semicolon serialization for the
SEAT_ASSIGNMENTS_SYNC entity data field), with 8 unit tests.
- FurnitureClientAnimator (client-only seat-pose kickoff, moved out
of the dual-side entity class).
- EntityFurniture drops ~100 lines with no behavior change.
Interface docs:
- ISeatProvider Javadoc narrowed to reflect that EntityFurniture is
the only implementation; callers that need animation state or
definition reference still downcast.
- FurnitureAuthPredicate.findOccupant uses the interface only.
- AnimationIdBuilder flagged as legacy JSON-era utility (NPC
fallback + MCA mixin).
Artist guide: corrected the "Monster Seat System (Planned)" section
to match the ISeatProvider single-impl reality.
Broad consolidation of the V2 bondage-item, furniture-entity, and
client-side GLTF pipeline.
Parsing and rendering
- Shared GLB parsing helpers consolidated into GlbParserUtils
(accessor reads, weight normalization, joint-index clamping,
coordinate-space conversion, animation parse, primitive loop).
- Grow-on-demand Matrix4f[] scratch pool in GltfSkinningEngine and
GltfLiveBoneReader — removes per-frame joint-matrix allocation
from the render hot path.
- emitVertex helper dedups three parallel loops in GltfMeshRenderer.
- TintColorResolver.resolve has a zero-alloc path when the item
declares no tint channels.
- itemAnimCache bounded to 256 entries (access-order LRU) with
atomic get-or-compute under the map's monitor.
Animation correctness
- First-in-joint-order wins when body and torso both map to the
same PlayerAnimator slot; duplicate writes log a single WARN.
- Multi-item composites honor the FullX / FullHeadX opt-in that
the single-item path already recognized.
- Seat transforms converted to Minecraft model-def space so
asymmetric furniture renders passengers at the correct offset.
- GlbValidator: IBM count / type / presence, JOINTS_0 presence,
animation channel target validation, multi-skin support.
Furniture correctness and anti-exploit
- Seat assignment synced via SynchedEntityData (server is
authoritative; eliminates client-server divergence on multi-seat).
- Force-mount authorization requires same dimension and a free
seat; cross-dimension distance checks rejected.
- Reconnection on login checks for seat takeover before re-mount
and force-loads the target chunk for cross-dimension cases.
- tiedup_furniture_lockpick_ctx carries a session UUID nonce so
stale context can't misroute a body-item lockpick.
- tiedup_locked_furniture survives death without keepInventory
(Forge 1.20.1 does not auto-copy persistent data on respawn).
Lifecycle and memory
- EntityCleanupHandler fans EntityLeaveLevelEvent out to every
per-entity state map on the client.
- DogPoseRenderHandler re-keyed by UUID (stable across dimension
change; entity int ids are recycled).
- PetBedRenderHandler, PlayerArmHideEventHandler, and
HeldItemHideHandler use receiveCanceled + sentinel sets so
Pre-time mutations are restored even when a downstream handler
cancels the render.
Tests
- JUnit harness with 76+ tests across GlbParserUtils, GltfPoseConverter,
FurnitureSeatGeometry, and FurnitureAuthPredicate.
Two fixes from the animation audit:
1. Variant cache key now includes the resolved animation name (e.g., Struggle.2).
Previously, the cache key only used context+parts, so the first random variant
pick was reused forever. Now each variant gets its own cache entry, and a fresh
random pick happens each time the context changes.
2. FullHead check changed from contains("Head") to startsWith("gltf_FullHead")
to prevent false positives on names like FullOverhead or FullAhead.
The resolver now tries FullHead* before Full* at each fallback step.
Example: FullHeadStruggle → FullStruggle → Struggle → FullHeadIdle → FullIdle → Idle
Previously FullHead* names were dead — the resolver never constructed them,
so animations named FullHeadStruggle were unreachable.
FullStruggle, FullWalk etc. animate body+legs but preserve head tracking.
FullHeadStruggle, FullHeadWalk etc. also animate the head.
The 'Head' keyword in the animation name is the opt-in signal.
Previously, any GLB with keyframes on free bones would animate them,
even for standard animations like Idle. This caused accidental bone
hijacking — e.g., handcuffs freezing the player's head because the
artist keyframed all bones in Blender.
Now the Full prefix (FullIdle, FullStruggle, FullWalk) is enforced:
only Full-prefixed animations can animate free bones. Standard
animations (Idle, Struggle, Walk) only animate owned bones.
This aligns the code with the documented convention in ARTIST_GUIDE.md.
- DataDrivenItemParser: downgrade animation_bones absence log from INFO to
DEBUG (was spamming for every item without the optional field)
- GlbValidator: read 12-byte GLB header first and reject files declaring
totalLength > 50 MB before allocating (prevents OOM on malformed GLBs)
- GlbValidator: WEIGHTS_0 check now uses the same mesh selection logic as
GlbParser (prefer "Item", fallback to last non-Player) instead of
blindly checking the first mesh
Strip pipe-delimited armature prefixes (e.g., "MyRig|body" -> "body")
from bone names in parseSeatSkeleton and remapSeatAnimations, matching
the existing stripping already present in GlbParser. Without this,
isKnownBone checks fail when Blender exports prefixed bone names.