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.
This commit is contained in:
@@ -41,16 +41,20 @@ import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||
* <h2>Hiérarchie biped EF (20 joints)</h2>
|
||||
*
|
||||
* <pre>
|
||||
* Root
|
||||
* ├─ Thigh_R ── Leg_R ── Knee_R
|
||||
* ├─ Thigh_L ── Leg_L ── Knee_L
|
||||
* └─ Torso
|
||||
* └─ Chest
|
||||
* ├─ Head
|
||||
* ├─ Shoulder_R ── Arm_R ── Elbow_R ── Hand_R ── Tool_R
|
||||
* └─ Shoulder_L ── Arm_L ── Elbow_L ── Hand_L ── Tool_L
|
||||
* 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
|
||||
@@ -69,19 +73,29 @@ public final class TiedUpArmatures {
|
||||
ResourceLocation.fromNamespaceAndPath(TiedUpRigConstants.MODID, "armature/biped");
|
||||
|
||||
/**
|
||||
* Singleton biped — chargé à la première ref (init order :
|
||||
* {@link TiedUpRigConstants} → chaînes d'ids → ici via
|
||||
* {@link com.tiedup.remake.rig.patch.PlayerPatch#getArmature()}).
|
||||
* Holder idiome pour init lazy + thread-safe sans synchronized.
|
||||
*
|
||||
* <p>Si construit plus tôt (ex. static init combat-lourd avant FMLCommonSetup)
|
||||
* risquerait de toucher des classes client — garder cet accès lazy si les
|
||||
* call sites évoluent.</p>
|
||||
* <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 HumanoidArmature BIPED_INSTANCE;
|
||||
private static final class Holder {
|
||||
static final HumanoidArmature INSTANCE = buildBiped();
|
||||
private Holder() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* AssetAccessor biped biped TiedUp. Construit à la première lecture de
|
||||
* {@link AssetAccessor#get()}.
|
||||
* 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>
|
||||
@@ -89,10 +103,7 @@ public final class TiedUpArmatures {
|
||||
public static final AssetAccessor<HumanoidArmature> BIPED = new AssetAccessor<>() {
|
||||
@Override
|
||||
public HumanoidArmature get() {
|
||||
if (BIPED_INSTANCE == null) {
|
||||
BIPED_INSTANCE = buildBiped();
|
||||
}
|
||||
return BIPED_INSTANCE;
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -110,17 +121,24 @@ public final class TiedUpArmatures {
|
||||
};
|
||||
|
||||
/**
|
||||
* Build procédural de la hiérarchie biped EF. 20 joints, IDs 0..19 dans
|
||||
* l'ordre d'insertion (important pour {@link Joint#getId()} stabilité
|
||||
* inter-runs).
|
||||
* 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() {
|
||||
// Tous les joints (ID == ordre d'insertion dans la map).
|
||||
// On utilise LinkedHashMap pour garantir l'ordre d'itération stable
|
||||
// (OpenMatrix4f.allocateMatrixArray dimensionne sur jointCount).
|
||||
// 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.
|
||||
@@ -141,19 +159,25 @@ public final class TiedUpArmatures {
|
||||
Joint chest = joint(joints, "Chest", 8);
|
||||
Joint head = joint(joints, "Head", 9);
|
||||
|
||||
// Bras droit
|
||||
// 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 elbowR = joint(joints, "Elbow_R", 12);
|
||||
Joint handR = joint(joints, "Hand_R", 13);
|
||||
Joint toolR = joint(joints, "Tool_R", 14);
|
||||
Joint handR = joint(joints, "Hand_R", 12);
|
||||
Joint toolR = joint(joints, "Tool_R", 13);
|
||||
Joint elbowR = joint(joints, "Elbow_R", 14);
|
||||
|
||||
// Bras gauche
|
||||
// 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 elbowL = joint(joints, "Elbow_L", 17);
|
||||
Joint handL = joint(joints, "Hand_L", 18);
|
||||
Joint toolL = joint(joints, "Tool_L", 19);
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user