L'entrée publique {@link ClientRigEquipmentHandler#rebuildBondageAnimations}
* nécessite un {@code Player} réel + capability MC — non testable sans
* bootstrap. On vérifie uniquement son null-safety. La logique métier est
* extraite dans deux méthodes package-private pures :
{@link ClientRigEquipmentHandler#applyDefinitions} — itère +
* adder pour chaque binding non-null/non-vide, reset d'abord.
*
*
*
Les ItemStack sont remplacés par des {@link Object} dummy : la logique
* ne lit aucune méthode d'instance, seule l'identité objet compte pour la
* dédup. Les {@link DataDrivenItemDefinition} sont construites via le
* {@link #makeDef} helper avec des defaults neutres (seuls posePriority +
* animations comptent pour ces tests).
*/
class ClientRigEquipmentHandlerTest {
private static final ResourceLocation ITEM_A =
ResourceLocation.fromNamespaceAndPath("tiedup", "item_a");
private static final ResourceLocation ITEM_B =
ResourceLocation.fromNamespaceAndPath("tiedup", "item_b");
private static final ResourceLocation ANIM_IDLE_A =
ResourceLocation.fromNamespaceAndPath("tiedup", "anim_idle_a");
private static final ResourceLocation ANIM_WALK_A =
ResourceLocation.fromNamespaceAndPath("tiedup", "anim_walk_a");
private static final ResourceLocation ANIM_WALK_B =
ResourceLocation.fromNamespaceAndPath("tiedup", "anim_walk_b");
/**
* Helper : construit une {@link DataDrivenItemDefinition} minimale avec
* les seuls champs testés (posePriority + animations). Les autres champs
* reçoivent des defaults neutres pour satisfaire le compact ctor.
*/
private static DataDrivenItemDefinition makeDef(
ResourceLocation id,
int posePriority,
AnimationBindings animations
) {
return new DataDrivenItemDefinition(
id,
/* displayName */ id.getPath(),
/* translationKey */ null,
/* modelLocation */ ResourceLocation.fromNamespaceAndPath("tiedup", "model"),
/* slimModelLocation */ null,
/* animationSource */ null,
/* occupiedRegions */ Set.of(BodyRegionV2.ARMS),
/* blockedRegions */ Set.of(BodyRegionV2.ARMS),
/* poseType */ null,
posePriority,
/* escapeDifficulty */ 100,
/* lockable */ false,
/* canAttachPadlock */ true,
/* supportsColor */ false,
/* tintChannels */ Map.of(),
/* icon */ null,
/* movementStyle */ (MovementStyle) null,
/* movementModifier */ (MovementModifier) null,
/* creator */ null,
/* animationBones */ Map.of(),
animations,
/* componentConfigs */ Map.of()
);
}
/**
* Fake adder qui capture chaque {@code (motion, accessor)} dans une
* liste en ordre chronologique. L'accessor réel n'est pas inspecté —
* on matérialise sa {@link AssetAccessor#registryName() registryName}
* pour faciliter les assertions.
*/
private static final class CapturingAdder
implements ClientRigEquipmentHandler.LivingAnimationAdder {
final List> calls = new ArrayList<>();
@Override
public void add(LivingMotion motion, AssetAccessor extends StaticAnimation> accessor) {
calls.add(Map.entry(motion, accessor.registryName()));
}
}
/** Stub {@link AssetAccessor} qui retourne juste un {@link ResourceLocation}. */
private static AssetAccessor extends StaticAnimation> stubAccessor(ResourceLocation id) {
return new AssetAccessor() {
@Override
public StaticAnimation get() {
return null; // jamais dereferenced dans le handler, juste adder capture
}
@Override
public ResourceLocation registryName() {
return id;
}
@Override
public boolean inRegistry() {
return false;
}
};
}
/** Résolver passthrough : id → stubAccessor(id). */
private static final Function> PASSTHROUGH_RESOLVER =
ClientRigEquipmentHandlerTest::stubAccessor;
// ========== null-safety public API ==========
/**
* {@link ClientRigEquipmentHandler#rebuildBondageAnimations(net.minecraft.world.entity.player.Player)}
* avec null ne doit rien throw — early return propre.
*/
@Test
void rebuildBondageAnimations_nullPlayer_noThrow() {
assertDoesNotThrow(() ->
ClientRigEquipmentHandler.rebuildBondageAnimations(null));
}
// ========== extractSortedDefinitions ==========
/** Iterable vide → liste vide (pas de crash, pas de resolver appelé). */
@Test
void extractSortedDefinitions_emptyInput_returnsEmpty() {
List result =
ClientRigEquipmentHandler.extractSortedDefinitions(
Collections.emptyList(),
stack -> { throw new AssertionError("resolver ne doit pas etre appele"); }
);
assertNotNull(result);
assertTrue(result.isEmpty());
}
/** Items non data-driven (resolver → null) sont skip. */
@Test
void extractSortedDefinitions_nullResolver_skipsItem() {
Object stackA = new Object();
Object stackB = new Object();
List result =
ClientRigEquipmentHandler.extractSortedDefinitions(
List.of(stackA, stackB),
any -> null // aucun item n'est data-driven
);
assertTrue(result.isEmpty());
}
/**
* Un même {@link Object} répété N fois dans l'input ne doit produire
* qu'une seule def dans la sortie — cas armbinder (3 régions, 1 stack).
*/
@Test
void extractSortedDefinitions_duplicateIdentity_dedups() {
Object stack = new Object();
DataDrivenItemDefinition def = makeDef(ITEM_A, 10, null);
// Identity resolver — retourne def pour n'importe quel input
Function