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:
notevil
2026-04-22 22:27:01 +02:00
parent 79fc470aa0
commit 39f6177595
2 changed files with 173 additions and 35 deletions

View File

@@ -0,0 +1,114 @@
/*
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
*/
package com.tiedup.remake.rig;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import org.junit.jupiter.api.Test;
import com.tiedup.remake.rig.armature.HumanoidArmature;
/**
* Tests du biped procédural TiedUp.
*
* <p>Ces tests verrouillent en particulier le mapping {@code id → joint name}
* qui doit rester aligné sur les constantes {@code RIGHT_ARM} / {@code LEFT_ARM}
* de {@code VanillaModelTransformer} — sinon les bras rendent tordus (ex :
* {@code lowerJoint=17} pointerait vers Elbow_L au lieu de Hand_L).</p>
*
* <p>La review Phase 2.4 (P0-BUG-001) a montré que l'ordre précédent
* Arm_R=11, Elbow_R=12, Hand_R=13, Tool_R=14 cassait silencieusement le
* transformer. {@link #jointIdsMatchEfLayout()} aurait attrapé ce bug.</p>
*/
class TiedUpArmaturesTest {
@Test
void bipedHas20Joints() {
assertEquals(20, TiedUpArmatures.BIPED.get().getJointNumber(),
"Le biped EF standard doit avoir exactement 20 joints");
}
@Test
void searchBipedJointByNameReturnsNonNull() {
HumanoidArmature biped = TiedUpArmatures.BIPED.get();
String[] expectedNames = {
"Root", "Head",
"Torso", "Chest",
"Shoulder_L", "Shoulder_R",
"Arm_L", "Arm_R",
"Hand_L", "Hand_R",
"Elbow_L", "Elbow_R",
"Tool_L", "Tool_R",
"Thigh_L", "Thigh_R",
"Leg_L", "Leg_R",
"Knee_L", "Knee_R"
};
for (String name : expectedNames) {
assertNotNull(biped.searchJointByName(name),
"Joint '" + name + "' introuvable dans le biped — alignement EF cassé");
}
}
/**
* Verrou explicite contre P0-BUG-001. Les constantes EF
* {@code VanillaModelTransformer.RIGHT_ARM} encodent
* {@code upperJoint=11, lowerJoint=12, middleJoint=14}, i.e.
* Arm_R=11, Hand_R=12, Elbow_R=14 (symétrique gauche : 16/17/19).
*
* <p>Ce test DOIT échouer si quelqu'un réordonne les IDs dans
* {@link TiedUpArmatures#buildBiped()} sans mettre à jour
* {@code VanillaModelTransformer} en miroir.</p>
*/
@Test
void jointIdsMatchEfLayout() {
HumanoidArmature biped = TiedUpArmatures.BIPED.get();
// Right arm layout (VanillaModelTransformer.RIGHT_ARM : 11, 12, _, 14)
assertEquals("Arm_R", biped.searchJointById(11).getName(),
"id=11 doit être Arm_R (VanillaModelTransformer.RIGHT_ARM.upperJoint)");
assertEquals("Hand_R", biped.searchJointById(12).getName(),
"id=12 doit être Hand_R (VanillaModelTransformer.RIGHT_ARM.lowerJoint)");
assertEquals("Elbow_R", biped.searchJointById(14).getName(),
"id=14 doit être Elbow_R (VanillaModelTransformer.RIGHT_ARM.middleJoint)");
// Left arm layout (VanillaModelTransformer.LEFT_ARM : 16, 17, _, 19)
assertEquals("Arm_L", biped.searchJointById(16).getName(),
"id=16 doit être Arm_L (VanillaModelTransformer.LEFT_ARM.upperJoint)");
assertEquals("Hand_L", biped.searchJointById(17).getName(),
"id=17 doit être Hand_L (VanillaModelTransformer.LEFT_ARM.lowerJoint)");
assertEquals("Elbow_L", biped.searchJointById(19).getName(),
"id=19 doit être Elbow_L (VanillaModelTransformer.LEFT_ARM.middleJoint)");
// Tool_R=13, Tool_L=18 (pas dans les LimbPartTransformer mais utilisé
// par StaticAnimation itemInHand — on verrouille l'emplacement aussi).
assertEquals("Tool_R", biped.searchJointById(13).getName(),
"id=13 doit être Tool_R (attaché après Hand_R dans la chaîne)");
assertEquals("Tool_L", biped.searchJointById(18).getName(),
"id=18 doit être Tool_L (attaché après Hand_L dans la chaîne)");
}
/**
* Verrou contre P0-BUG-002. La première implémentation utilisait un
* double-null-check non thread-safe — SP intégré (client + server threads
* concurrent) pouvait créer deux instances distinctes.
*
* <p>Le Holder idiome (static inner class) garantit via le class-init lock
* JVM qu'il n'y a qu'une seule init. Ce test vérifie que deux appels
* successifs de {@code BIPED.get()} renvoient bien la même référence —
* ne prouve pas l'atomicité mais couvre le cas régression "retour à un
* nouveau build à chaque get".</p>
*/
@Test
void bipedSingleton() {
HumanoidArmature first = TiedUpArmatures.BIPED.get();
HumanoidArmature second = TiedUpArmatures.BIPED.get();
assertSame(first, second,
"BIPED.get() doit retourner la même instance (singleton Holder)");
}
}