diff --git a/src/test/java/com/tiedup/remake/rig/anim/LivingMotionIsSameTest.java b/src/test/java/com/tiedup/remake/rig/anim/LivingMotionIsSameTest.java new file mode 100644 index 0000000..6396d03 --- /dev/null +++ b/src/test/java/com/tiedup/remake/rig/anim/LivingMotionIsSameTest.java @@ -0,0 +1,85 @@ +/* + * © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Tests de {@link LivingMotion#isSame} — en particulier la symetrie speciale + * IDLE == INACTION definie dans le code source. + * + * Aucun MC runtime requis — LivingMotions est un enum Java pur. + * Note : le chargement de LivingMotions trigger ExtendableEnumManager.assign() + * qui utilise TiedUpRigConstants.LOGGER (LogUtils, SLF4J pur) — pas de + * bootstrap Forge requis. + */ +class LivingMotionIsSameTest { + + /** + * IDLE.isSame(INACTION) doit etre true selon le contrat du code source. + * Ce comportement special est utilise par RigAnimationTickHandler pour + * ne pas re-trigger l'animation idle quand la motion courante est INACTION. + */ + @Test + void idle_isSame_inaction_returnsTrue() { + assertTrue(LivingMotions.IDLE.isSame(LivingMotions.INACTION), + "IDLE.isSame(INACTION) doit etre true (alias semantique)"); + } + + /** Symetrie : INACTION.isSame(IDLE) aussi. */ + @Test + void inaction_isSame_idle_returnsTrue() { + assertTrue(LivingMotions.INACTION.isSame(LivingMotions.IDLE), + "INACTION.isSame(IDLE) doit etre true (symetrique)"); + } + + /** Une motion quelconque est identique a elle-meme. */ + @Test + void walk_isSame_walk_returnsTrue() { + assertTrue(LivingMotions.WALK.isSame(LivingMotions.WALK)); + } + + /** WALK != IDLE — isSame doit retourner false. */ + @Test + void walk_isSame_idle_returnsFalse() { + assertFalse(LivingMotions.WALK.isSame(LivingMotions.IDLE), + "WALK.isSame(IDLE) doit etre false"); + } + + /** IDLE != RUN — pas d'alias entre autres motions. */ + @Test + void idle_isSame_run_returnsFalse() { + assertFalse(LivingMotions.IDLE.isSame(LivingMotions.RUN)); + } + + /** DEATH != DEATH_other — verification que l'alias IDLE/INACTION est exclusif. */ + @Test + void death_isSame_walk_returnsFalse() { + assertFalse(LivingMotions.DEATH.isSame(LivingMotions.WALK)); + } + + /** + * Verification que tous les enums ont un universalOrdinal unique (pas de + * collision dans ExtendableEnumManager.assign). + * BUG potentiel : si deux enums avaient le meme nom lowercase, assign() + * throwrait IllegalArgumentException. Ce test verifie qu'il n'y a pas de + * doublon en triggerant le chargement de la classe. + */ + @Test + void allLivingMotions_haveUniqueOrdinals() { + LivingMotions[] values = LivingMotions.values(); + long uniqueOrdinals = java.util.Arrays.stream(values) + .mapToInt(LivingMotions::universalOrdinal) + .distinct() + .count(); + org.junit.jupiter.api.Assertions.assertEquals( + values.length, uniqueOrdinals, + "Chaque LivingMotions doit avoir un universalOrdinal unique" + ); + } +} diff --git a/src/test/java/com/tiedup/remake/rig/anim/PoseTest.java b/src/test/java/com/tiedup/remake/rig/anim/PoseTest.java new file mode 100644 index 0000000..9283f37 --- /dev/null +++ b/src/test/java/com/tiedup/remake/rig/anim/PoseTest.java @@ -0,0 +1,189 @@ +/* + * © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +import com.tiedup.remake.rig.armature.JointTransform; +import com.tiedup.remake.rig.math.Vec3f; + +/** + * Tests de {@link Pose} : interpolatePose, orElseEmpty, load (SET / OVERWRITE / + * APPEND_ABSENT), disableAllJoints. + * + * Aucun MC runtime requis — Pose utilise uniquement des collections Java et + * JointTransform (JOML + Vec3f, pas de Minecraft bootstrap). + */ +class PoseTest { + + // --- helpers --- + + private static JointTransform translation(float x, float y, float z) { + return JointTransform.translation(new Vec3f(x, y, z)); + } + + // --- orElseEmpty --- + + /** Un joint absent retourne JointTransform.empty() (pas null). */ + @Test + void orElseEmpty_unknownJoint_returnsEmpty() { + Pose pose = new Pose(); + JointTransform result = pose.orElseEmpty("nonexistent"); + assertNotNull(result, "orElseEmpty ne doit pas retourner null"); + // Verifier que la translation est zeros (empty = identite) + assertEquals(0.0f, result.translation().x, 1e-5f); + assertEquals(0.0f, result.translation().y, 1e-5f); + assertEquals(0.0f, result.translation().z, 1e-5f); + } + + /** Un joint present retourne son transform, pas le fallback. */ + @Test + void orElseEmpty_knownJoint_returnsTransform() { + Pose pose = new Pose(); + JointTransform jt = translation(1.0f, 2.0f, 3.0f); + pose.putJointData("spine", jt); + assertEquals(jt, pose.orElseEmpty("spine")); + } + + // --- interpolatePose --- + + /** + * interpolatePose a progression 0.0 doit retourner les valeurs de pose1. + */ + @Test + void interpolatePose_progressionZero_returnsFirstPose() { + Pose pose1 = new Pose(); + pose1.putJointData("arm", translation(0.0f, 0.0f, 0.0f)); + + Pose pose2 = new Pose(); + pose2.putJointData("arm", translation(1.0f, 0.0f, 0.0f)); + + Pose result = Pose.interpolatePose(pose1, pose2, 0.0f); + assertTrue(result.hasTransform("arm")); + assertEquals(0.0f, result.get("arm").translation().x, 1e-5f); + } + + /** + * interpolatePose a progression 1.0 doit retourner les valeurs de pose2. + */ + @Test + void interpolatePose_progressionOne_returnsSecondPose() { + Pose pose1 = new Pose(); + pose1.putJointData("arm", translation(0.0f, 0.0f, 0.0f)); + + Pose pose2 = new Pose(); + pose2.putJointData("arm", translation(10.0f, 0.0f, 0.0f)); + + Pose result = Pose.interpolatePose(pose1, pose2, 1.0f); + assertEquals(10.0f, result.get("arm").translation().x, 1e-4f); + } + + /** + * interpolatePose fusionne les joints des deux poses. + * Un joint present dans pose2 seulement doit apparaitre dans le resultat + * (interpole avec JointTransform.empty() comme pose1). + */ + @Test + void interpolatePose_mergesJointsFromBothPoses() { + Pose pose1 = new Pose(); + pose1.putJointData("head", translation(0.0f, 1.0f, 0.0f)); + + Pose pose2 = new Pose(); + pose2.putJointData("hand", translation(2.0f, 0.0f, 0.0f)); + + Pose result = Pose.interpolatePose(pose1, pose2, 0.5f); + // Les deux joints doivent etre presents + assertTrue(result.hasTransform("head"), "head from pose1 doit etre dans le resultat"); + assertTrue(result.hasTransform("hand"), "hand from pose2 doit etre dans le resultat"); + } + + /** + * interpolatePose a 0.5 doit etre a mi-chemin de la translation. + */ + @Test + void interpolatePose_halfProgression_midpointTranslation() { + Pose pose1 = new Pose(); + pose1.putJointData("leg", translation(0.0f, 0.0f, 0.0f)); + + Pose pose2 = new Pose(); + pose2.putJointData("leg", translation(4.0f, 0.0f, 0.0f)); + + Pose result = Pose.interpolatePose(pose1, pose2, 0.5f); + assertEquals(2.0f, result.get("leg").translation().x, 1e-4f); + } + + // --- load(LoadOperation) --- + + /** SET efface les joints existants puis copie depuis la source. */ + @Test + void load_set_replacesAllJoints() { + Pose dest = new Pose(); + dest.putJointData("old_joint", JointTransform.empty()); + + Pose src = new Pose(); + src.putJointData("new_joint", translation(1.0f, 0.0f, 0.0f)); + + dest.load(src, Pose.LoadOperation.SET); + + assertFalse(dest.hasTransform("old_joint"), + "SET doit supprimer les joints de la destination avant fusion"); + assertTrue(dest.hasTransform("new_joint"), + "SET doit copier les joints source"); + } + + /** OVERWRITE ecrase les joints communs sans supprimer les joints uniquement dans dest. */ + @Test + void load_overwrite_keepsExistingAndOverwritesCommon() { + Pose dest = new Pose(); + dest.putJointData("kept", JointTransform.empty()); + dest.putJointData("shared", translation(0.0f, 0.0f, 0.0f)); + + Pose src = new Pose(); + src.putJointData("shared", translation(5.0f, 0.0f, 0.0f)); + + dest.load(src, Pose.LoadOperation.OVERWRITE); + + assertTrue(dest.hasTransform("kept"), + "OVERWRITE doit conserver les joints de dest non presents dans src"); + assertEquals(5.0f, dest.get("shared").translation().x, 1e-4f, + "OVERWRITE doit ecraser le joint commun avec la valeur src"); + } + + /** APPEND_ABSENT n'ajoute que les joints absents de dest. */ + @Test + void load_appendAbsent_doesNotOverwriteExisting() { + Pose dest = new Pose(); + dest.putJointData("existing", translation(9.0f, 0.0f, 0.0f)); + + Pose src = new Pose(); + src.putJointData("existing", translation(1.0f, 0.0f, 0.0f)); + src.putJointData("added", translation(2.0f, 0.0f, 0.0f)); + + dest.load(src, Pose.LoadOperation.APPEND_ABSENT); + + assertEquals(9.0f, dest.get("existing").translation().x, 1e-4f, + "APPEND_ABSENT ne doit pas ecraser un joint existant"); + assertTrue(dest.hasTransform("added"), + "APPEND_ABSENT doit ajouter les joints absents"); + } + + // --- disableAllJoints --- + + /** disableAllJoints vide la map de transforms. */ + @Test + void disableAllJoints_clearsTransforms() { + Pose pose = new Pose(); + pose.putJointData("head", JointTransform.empty()); + pose.putJointData("arm", JointTransform.empty()); + pose.disableAllJoints(); + assertTrue(pose.getJointTransformData().isEmpty(), + "disableAllJoints doit vider la map de transforms"); + } +} diff --git a/src/test/java/com/tiedup/remake/rig/anim/TransformSheetTest.java b/src/test/java/com/tiedup/remake/rig/anim/TransformSheetTest.java new file mode 100644 index 0000000..fbf835e --- /dev/null +++ b/src/test/java/com/tiedup/remake/rig/anim/TransformSheetTest.java @@ -0,0 +1,149 @@ +/* + * © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.anim; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +import org.junit.jupiter.api.Test; + +import com.tiedup.remake.rig.anim.TransformSheet.InterpolationInfo; +import com.tiedup.remake.rig.armature.JointTransform; + +/** + * Tests de {@link TransformSheet} : interpolation (binary search), + * maxFrameTime, copy/copyAll. + * + * Aucun MC runtime requis — TransformSheet n'utilise que + * net.minecraft.util.Mth (pure math, pas de bootstrap) et des types + * RIG internes (Vec3f, JointTransform). + */ +class TransformSheetTest { + + private static Keyframe kf(float time) { + return new Keyframe(time, JointTransform.empty()); + } + + private static TransformSheet sheet(float... times) { + Keyframe[] frames = new Keyframe[times.length]; + for (int i = 0; i < times.length; i++) { + frames[i] = kf(times[i]); + } + return new TransformSheet(frames); + } + + // --- getInterpolationInfo --- + + /** Sheet vide doit retourner INVALID, pas NPE. */ + @Test + void interpolationInfo_emptySheet_returnsInvalid() { + TransformSheet ts = new TransformSheet(new Keyframe[0]); + assertSame(InterpolationInfo.INVALID, ts.getInterpolationInfo(0.5f), + "Sheet vide doit retourner INVALID"); + } + + /** Sur [0.0, 1.0] au temps 0.5, delta = 0.5. */ + @Test + void interpolationInfo_twoKeyframes_midpointDelta() { + TransformSheet ts = sheet(0.0f, 1.0f); + InterpolationInfo info = ts.getInterpolationInfo(0.5f); + assertEquals(0, info.prev()); + assertEquals(1, info.next()); + assertEquals(0.5f, info.delta(), 1e-5f); + } + + /** + * Temps negatif est converti en maxTime + currentTime. + * Sur [0.0, 1.0], temps=-0.25 -> effectif=0.75 -> delta=0.75. + */ + @Test + void interpolationInfo_negativeTime_wrapsToEnd() { + TransformSheet ts = sheet(0.0f, 1.0f); + InterpolationInfo info = ts.getInterpolationInfo(-0.25f); + assertEquals(0.75f, info.delta(), 1e-5f, + "Temps negatif doit wrapper : maxTime + t"); + } + + /** Binary search sur 5 keyframes irreguliers — segment correct. */ + @Test + void interpolationInfo_multipleKeyframes_correctSegment() { + TransformSheet ts = sheet(0.0f, 0.1f, 0.5f, 0.9f, 1.0f); + InterpolationInfo info = ts.getInterpolationInfo(0.7f); + assertEquals(2, info.prev(), "0.7 entre index 2 (0.5) et 3 (0.9)"); + assertEquals(3, info.next()); + float expected = (0.7f - 0.5f) / (0.9f - 0.5f); + assertEquals(expected, info.delta(), 1e-5f); + } + + /** Temps exactement sur un keyframe interne : delta = 0. */ + @Test + void interpolationInfo_exactKeyframeTime_deltaZero() { + TransformSheet ts = sheet(0.0f, 0.5f, 1.0f); + InterpolationInfo info = ts.getInterpolationInfo(0.5f); + assertEquals(0.0f, info.delta(), 1e-5f, + "Temps exact sur keyframe => delta 0"); + } + + /** Temps au-dela du max : delta clamp a 1.0. */ + @Test + void interpolationInfo_timeExceedsMax_deltaClampedToOne() { + TransformSheet ts = sheet(0.0f, 1.0f); + InterpolationInfo info = ts.getInterpolationInfo(2.0f); + assertEquals(1.0f, info.delta(), 1e-5f, + "Temps > max => delta clamp a 1.0"); + } + + // --- maxFrameTime --- + + /** Sheet vide : maxFrameTime retourne la sentinelle -1. */ + @Test + void maxFrameTime_emptySheet_returnsMinusOne() { + TransformSheet ts = new TransformSheet(new Keyframe[0]); + assertEquals(-1.0f, ts.maxFrameTime(), 1e-5f); + } + + /** maxFrameTime retourne le max quel que soit l'ordre des keyframes. */ + @Test + void maxFrameTime_multipleKeyframes_returnsMax() { + TransformSheet ts = sheet(0.0f, 0.3f, 1.5f, 0.7f); + assertEquals(1.5f, ts.maxFrameTime(), 1e-5f); + } + + // --- copy / copyAll --- + + /** copyAll produit une instance distincte de meme taille. */ + @Test + void copyAll_producesIndependentCopy() { + TransformSheet ts = sheet(0.0f, 0.5f, 1.0f); + TransformSheet copy = ts.copyAll(); + assertNotSame(ts, copy); + assertEquals(ts.getKeyframes().length, copy.getKeyframes().length); + } + + /** copy(start, end) extrait le bon sous-intervalle. */ + @Test + void copy_subrange_correctLength() { + TransformSheet ts = sheet(0.0f, 0.25f, 0.5f, 0.75f, 1.0f); + TransformSheet sub = ts.copy(1, 4); + assertEquals(3, sub.getKeyframes().length, + "copy(1,4) extrait 3 keyframes"); + assertEquals(0.25f, sub.getKeyframes()[0].time(), 1e-5f, + "Premier keyframe de la sous-copie = index 1 de l'original"); + } + + /** + * BUG FLAG (pas de correctif demande) : getFirstFrame() appelle copy(0, 2) + * sans verifier que la sheet a au moins 2 keyframes. Sur une sheet de 1 + * keyframe, ArrayIndexOutOfBoundsException. Ce test documente le cas sain + * (2 keyframes) pour catcher toute regression. + */ + @Test + void getFirstFrame_twoKeyframes_doesNotThrow() { + TransformSheet ts = sheet(0.0f, 1.0f); + TransformSheet first = ts.getFirstFrame(); + assertEquals(2, first.getKeyframes().length); + } +} diff --git a/src/test/java/com/tiedup/remake/rig/tick/RigAnimationTickHandlerTest.java b/src/test/java/com/tiedup/remake/rig/tick/RigAnimationTickHandlerTest.java new file mode 100644 index 0000000..c95d6cc --- /dev/null +++ b/src/test/java/com/tiedup/remake/rig/tick/RigAnimationTickHandlerTest.java @@ -0,0 +1,68 @@ +/* + * © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.tick; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * Tests de {@link RigAnimationTickHandler} — uniquement la logique pure Java + * accessible sans MC runtime. + * + * Ce qui est SKIP ici (et pourquoi) : + * - onClientTick : necessite Minecraft.getInstance() / ClientLevel / Player — + * impossible sans bootstrap Forge. + * - tickPlayer : necessite TiedUpCapabilities.getEntityPatch() qui utilise + * LazyOptional (Forge runtime) + un Player reel. + * - maybePlayIdle : necessite ClientAnimator + LivingEntityPatch (Forge + * capability + MC class hierarchy). + * + * Ce qui est couvert : + * - resetLoggedErrors() : expose explicitement pour les tests (Javadoc l'indique). + * Verifie que la methode est idempotente et ne throw pas. + */ +class RigAnimationTickHandlerTest { + + @AfterEach + void cleanUp() { + RigAnimationTickHandler.resetLoggedErrors(); + } + + /** + * resetLoggedErrors() doit etre un no-op sur un set deja vide. + * Double-appel ne doit pas throw. + */ + @Test + void resetLoggedErrors_idempotent_doesNotThrow() { + assertDoesNotThrow(() -> { + RigAnimationTickHandler.resetLoggedErrors(); + RigAnimationTickHandler.resetLoggedErrors(); + }, "Double resetLoggedErrors() ne doit pas throw"); + } + + /** + * Verifie que la constante MAX_LOGGED_UUIDS guard est coherente avec le + * comportement documente : le set est reinitialisable. Ce test appelle + * reset, puis reverifie que reset fonctionne, garantissant que le set + * n'est pas final non-clearable. + * + * Pattern : si ConcurrentHashMap.newKeySet() etait remplace par un Set + * immutable par erreur de refacto, ce test detecterait le throw. + */ + @Test + void resetLoggedErrors_afterReset_secondResetStillDoesNotThrow() { + assertDoesNotThrow(() -> { + RigAnimationTickHandler.resetLoggedErrors(); + }); + + // Second reset simule la sequence F3+T reload (appelee deux fois + // si deux resource-packs swappent rapidement). + assertDoesNotThrow(() -> { + RigAnimationTickHandler.resetLoggedErrors(); + }, "Second reset doit rester idempotent (simulation F3+T double-reload)"); + } +} diff --git a/src/test/java/com/tiedup/remake/rig/util/TimePairListTest.java b/src/test/java/com/tiedup/remake/rig/util/TimePairListTest.java new file mode 100644 index 0000000..1c576e1 --- /dev/null +++ b/src/test/java/com/tiedup/remake/rig/util/TimePairListTest.java @@ -0,0 +1,85 @@ +/* + * © 2026 TiedUp! Remake Contributors, distributed under GPLv3. + */ + +package com.tiedup.remake.rig.util; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Tests de {@link TimePairList} : creation, isTimeInPairs, bornes. + * + * Classe completement pure Java — aucune dependance MC ou Forge. + */ +class TimePairListTest { + + /** Un nombre impair d'arguments doit lever IllegalArgumentException. */ + @Test + void create_oddNumberOfArgs_throwsIllegalArgument() { + assertThrows(IllegalArgumentException.class, + () -> TimePairList.create(0.0f, 1.0f, 2.0f), + "create() avec nombre impair d'args doit lever IllegalArgumentException"); + } + + /** Aucune paire (create()) : rien n'est dans les paires. */ + @Test + void create_empty_nothingIsInPairs() { + TimePairList list = TimePairList.create(); + assertFalse(list.isTimeInPairs(0.5f), + "Liste vide : aucun temps ne doit etre dans les paires"); + } + + /** Temps dans une paire [0.0, 1.0]. */ + @Test + void isTimeInPairs_timeInsidePair_returnsTrue() { + TimePairList list = TimePairList.create(0.0f, 1.0f); + assertTrue(list.isTimeInPairs(0.0f), "0.0 est la borne de debut (inclusive)"); + assertTrue(list.isTimeInPairs(0.5f), "0.5 est dans [0.0, 1.0)"); + } + + /** + * La borne de fin est EXCLUSIVE (code : time >= begin && time < end). + * Temps = 1.0 ne doit PAS etre dans la paire [0.0, 1.0). + */ + @Test + void isTimeInPairs_exactEndBound_returnsFalse() { + TimePairList list = TimePairList.create(0.0f, 1.0f); + assertFalse(list.isTimeInPairs(1.0f), + "La borne de fin est exclusive : 1.0 ne doit pas etre dans [0.0, 1.0)"); + } + + /** Temps avant toute paire. */ + @Test + void isTimeInPairs_beforeFirstPair_returnsFalse() { + TimePairList list = TimePairList.create(0.5f, 1.0f); + assertFalse(list.isTimeInPairs(0.2f), + "0.2 < 0.5 ne doit pas etre dans [0.5, 1.0)"); + } + + /** Deux paires non-contigues — le trou entre elles ne doit pas matcher. */ + @Test + void isTimeInPairs_gapBetweenPairs_returnsFalse() { + TimePairList list = TimePairList.create(0.0f, 0.3f, 0.7f, 1.0f); + assertFalse(list.isTimeInPairs(0.5f), + "0.5 est dans le trou entre [0.0,0.3) et [0.7,1.0)"); + } + + /** Temps dans la deuxieme paire d'une liste multi-paires. */ + @Test + void isTimeInPairs_secondPair_returnsTrue() { + TimePairList list = TimePairList.create(0.0f, 0.3f, 0.7f, 1.0f); + assertTrue(list.isTimeInPairs(0.8f), + "0.8 est dans la deuxieme paire [0.7, 1.0)"); + } + + /** Valeur negative — hors de [0.5, 1.0). */ + @Test + void isTimeInPairs_negativeTime_returnsFalse() { + TimePairList list = TimePairList.create(0.5f, 1.0f); + assertFalse(list.isTimeInPairs(-0.1f)); + } +}