diff --git a/src/main/java/com/tiedup/remake/core/TiedUpMod.java b/src/main/java/com/tiedup/remake/core/TiedUpMod.java index 6af229d..eb340eb 100644 --- a/src/main/java/com/tiedup/remake/core/TiedUpMod.java +++ b/src/main/java/com/tiedup/remake/core/TiedUpMod.java @@ -153,10 +153,11 @@ public class TiedUpMod { // RIG Phase 2 — dispatcher EntityType → EntityPatch (PLAYER Phase 2, NPCs Phase 5) event.enqueueWork(com.tiedup.remake.rig.patch.EntityPatchProvider::registerEntityPatches); - // RIG Phase 2.7 — registre des StaticAnimation (CONTEXT_STAND_IDLE). - // Placeholder JSON procédural jusqu'à ce que les assets Blender arrivent - // (cf. docs/plans/rig/ASSETS_NEEDED.md). - event.enqueueWork(com.tiedup.remake.rig.TiedUpAnimationRegistry::initStaticAnimations); + // RIG — zero Java-side init pour les StaticAnimation. Toutes les anims + // (y compris CONTEXT_STAND_IDLE) sont auto-registered via le bloc + // "constructor" de leur JSON respectif, parsé par + // AnimationManager.readResourcepackAnimation au datapack/resource-pack + // reload. Voir TiedUpAnimationRegistry Javadoc. } /** diff --git a/src/main/java/com/tiedup/remake/rig/TiedUpAnimationRegistry.java b/src/main/java/com/tiedup/remake/rig/TiedUpAnimationRegistry.java index 266103a..2c474d8 100644 --- a/src/main/java/com/tiedup/remake/rig/TiedUpAnimationRegistry.java +++ b/src/main/java/com/tiedup/remake/rig/TiedUpAnimationRegistry.java @@ -8,157 +8,61 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import net.minecraft.resources.ResourceLocation; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.api.distmarker.OnlyIn; import com.tiedup.remake.rig.anim.AnimationManager; import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor; -import com.tiedup.remake.rig.anim.client.Layer; -import com.tiedup.remake.rig.anim.client.property.ClientAnimationProperties; -import com.tiedup.remake.rig.anim.types.DirectStaticAnimation; import com.tiedup.remake.rig.anim.types.StaticAnimation; /** - * Phase 2.7 — registry central des {@link StaticAnimation} TiedUp. Expose les - * accessors statiques (ex. {@link #CONTEXT_STAND_IDLE}) utilisés par les - * patches + tick handler pour jouer les animations idle / walk / etc. + * Registry helper (lookup + fallback) pour les {@link StaticAnimation} TiedUp. + * + *

Data-driven — zéro Java hardcoding

+ *

Depuis la migration {@code CONTEXT_STAND_IDLE} full data-driven, ce + * registry n'instancie plus aucune {@code StaticAnimation} côté Java. Toutes + * les anims TiedUp (y compris l'idle par défaut) sont enregistrées via le bloc + * {@code "constructor"} de leur JSON respectif dans + * {@code assets/tiedup/animmodels/animations/*.json}, parsé par + * {@link AnimationManager#readResourcepackAnimation} à chaque reload de resource + * pack / datapack. Voir {@code armbinder_idle.json} ou + * {@code context_stand_idle.json} pour la forme attendue.

+ * + *

Goal : un modder peut ajouter une anim TiedUp compatible sans écrire UNE + * ligne de Java — il drop un JSON dans + * {@code assets//animmodels/animations/}, le référence depuis un item + * JSON binding, {@code /reload}, l'anim fire.

+ * + *

Ce que le registry expose

+ * * *

Placeholder assets

- *

Les JSON associés sont des placeholders procéduraux (2 keyframes + *

Les JSON actuels sont des placeholders procéduraux (2 keyframes * identity) à remplacer par des assets Blender-authored. Voir * {@code docs/plans/rig/ASSETS_NEEDED.md} section 2 pour la spec de l'anim * idle définitive (swing respiration subtle 3 keyframes, 2s boucle).

- * - *

Lifecycle

- * - * - *

Dist

- *

Les animations tournent côté client (l'{@code Animator} est créé via - * {@code ClientAnimator::getAnimator} côté physical client), donc le registry - * est indépendant du side pour le bootstrap mais toute la lecture JSON passe - * par {@code Minecraft.getInstance().getResourceManager()} côté client. Les - * champs sont accessibles côté serveur (validation / logs), seul - * {@link StaticAnimation#loadAnimation()} est client-heavy (et protégé par - * la dispatch server/client du {@code AnimationManager.getAnimationResourceManager()}).

*/ public final class TiedUpAnimationRegistry { private TiedUpAnimationRegistry() {} /** Registry name de l'anim idle par défaut (résolue en - * {@code assets/tiedup/animmodels/animations/context_stand_idle.json}). */ + * {@code assets/tiedup/animmodels/animations/context_stand_idle.json}). + * L'anim elle-même est auto-registered au datapack reload via le bloc + * {@code "constructor"} du JSON — pas d'init Java. */ public static final ResourceLocation CONTEXT_STAND_IDLE_ID = ResourceLocation.fromNamespaceAndPath(TiedUpRigConstants.MODID, "context_stand_idle"); - /** - * Anim idle par défaut — joue quand aucune motion active. Placeholder 2 - * keyframes identity (joueur sans mouvement visible) jusqu'à ce qu'un - * asset authored Blender arrive. - * - *

Attention init order : ce field est {@code null} tant que - * {@link #initStaticAnimations()} n'a pas tourné. Les sites qui - * référencent ce field doivent être gardés par un null-check, ou être - * appelés post-setup (tick handler, patch init, etc.).

- * - *

Utilise {@link DirectStaticAnimation} (vs un hand-written - * {@code StaticAnimation}) pour hériter du pattern accessor=self + - * registryName() utilisés dans {@link TiedUpRigRegistry#EMPTY_ANIMATION}.

- */ - public static DirectStaticAnimation CONTEXT_STAND_IDLE; - - /** - * Construit les {@link StaticAnimation} TiedUp. À appeler exactement une - * fois par game, en {@code FMLCommonSetupEvent.enqueueWork(...)}. - * - *

Pas de chargement JSON ici — juste l'instanciation des accessors. - * La première lecture de {@code getAnimationClip()} déclenchera le load - * via {@link com.tiedup.remake.rig.asset.JsonAssetLoader}.

- * - *

Idempotent (re-appel sans effet visible) mais pas thread-safe. Ne - * devrait jamais être appelé hors du mod bus.

- */ - public static void initStaticAnimations() { - if (CONTEXT_STAND_IDLE != null) { - // Déjà init (hot-reload setup, test double-init). Log debug seulement. - TiedUpRigConstants.LOGGER.debug( - "TiedUpAnimationRegistry.initStaticAnimations: déjà initialisé, skip." - ); - return; - } - - try { - // transitionTime=GENERAL (0.15s = 3 ticks) + isRepeat=true + registryName + - // armature=BIPED. L'ordre match le ctor DirectStaticAnimation(float, boolean, ResourceLocation, AssetAccessor). - CONTEXT_STAND_IDLE = new DirectStaticAnimation( - TiedUpRigConstants.GENERAL_ANIMATION_TRANSITION_TIME, - /* isRepeat */ true, - CONTEXT_STAND_IDLE_ID, - TiedUpArmatures.BIPED - ); - - // Layer BASE + priority LOWEST — idle default, écrasable par toute - // autre anim. Sans ces props la default du StaticAnimation (LOWEST / - // BASE_LAYER) s'applique déjà — on les set explicitement pour la doc. - setLowestBaseLayer(CONTEXT_STAND_IDLE); - - TiedUpRigConstants.LOGGER.info( - "TiedUpAnimationRegistry: CONTEXT_STAND_IDLE registered ({})", - CONTEXT_STAND_IDLE_ID - ); - } catch (Throwable t) { - // Fallback : log + laisse CONTEXT_STAND_IDLE null. Le tick handler - // verra null et skippera silencieusement. Évite de tout casser si - // un asset placeholder est malformé en dev. - TiedUpRigConstants.LOGGER.error( - "TiedUpAnimationRegistry: init échoué pour CONTEXT_STAND_IDLE — " - + "animation idle désactivée. Voir docs/plans/rig/ASSETS_NEEDED.md §2.", - t - ); - } - } - - /** - * Helper — set LayerType=BASE_LAYER + Priority=LOWEST sur une anim. - * Évite de forcer les callers à importer {@link ClientAnimationProperties} - * + {@link Layer}. - * - *

{@code @OnlyIn(CLIENT)} indirect : les properties - * {@code LAYER_TYPE}/{@code PRIORITY} sont client-only mais leur écriture - * via {@code addProperty} ne déclenche pas de class-load de - * {@code net.minecraft.client.*}. Le tag d'{@code @OnlyIn} serait - * incorrect ici (le method serait appelé depuis commonSetup). La safety - * réelle est assurée par le fait que les properties sont juste stockées - * dans la map et lues plus tard sur client uniquement (via - * {@code getLayerType()}/{@code getPriority()} tagués - * {@code @OnlyIn(CLIENT)}).

- */ - private static void setLowestBaseLayer(StaticAnimation anim) { - anim.addProperty(ClientAnimationProperties.LAYER_TYPE, Layer.LayerType.BASE_LAYER); - anim.addProperty(ClientAnimationProperties.PRIORITY, Layer.Priority.LOWEST); - } - - /** - * Helper — vrai ssi la static anim de référence a fini l'init (tick - * handler l'utilise en early-return quand Phase 2.7 assets sont absents - * en dev test). - */ - @OnlyIn(Dist.CLIENT) - public static boolean isReady() { - return CONTEXT_STAND_IDLE != null; - } - /** * Set (thread-safe) des IDs pour lesquels un WARN de fallback a déjà été * émis. Évite le spam log si un consumer appelle @@ -179,12 +83,15 @@ public final class TiedUpAnimationRegistry { * si le registry ne la connaît pas. * *

Utilisé par le pipeline d'équipement - * ({@code ClientRigEquipmentHandler.rebuildBondageAnimations}, P3-05) et - * le packet cinematic ({@code PacketPlayRigAnim.handleOnClient}, P3-12). - * Un miss dans le registry peut survenir dans plusieurs scénarios :

+ * ({@code ClientRigEquipmentHandler.rebuildBondageAnimations}, P3-05), le + * packet cinematic ({@code PacketPlayRigAnim.handleOnClient}, P3-12), et + * désormais le path idle ({@code PlayerPatch.initAnimator} + + * {@code RigAnimationTickHandler.maybePlayIdle}). Un miss dans le registry + * peut survenir dans plusieurs scénarios :

* * *

Futur Phase 3+

@@ -190,13 +194,16 @@ public final class RigAnimationTickHandler { } /** - * Déclenche la transition vers {@code CONTEXT_STAND_IDLE} si : + * Déclenche la transition vers {@code tiedup:context_stand_idle} si : * * *

Transition 0.2s (4 ticks @ 20tps) pour un fondu doux avec @@ -204,10 +211,6 @@ public final class RigAnimationTickHandler { * plus long on passera la valeur en param.

*/ private static void maybePlayIdle(LivingEntityPatch patch, Animator animator) { - if (!TiedUpAnimationRegistry.isReady()) { - return; - } - if (patch.getCurrentLivingMotion() != LivingMotions.IDLE) { return; } @@ -218,46 +221,51 @@ public final class RigAnimationTickHandler { return; } - // Check si la CONTEXT_STAND_IDLE est déjà l'anim courante du base layer. - // getPlayerFor(null) retourne base layer player (non-null en EF). + // Resolve target via le registry data-driven. Si le datapack n'a pas + // encore été appliqué (pré-reload window), on récupère EMPTY_ANIMATION + // — on skip le trigger (rien à jouer de plus que le bind EMPTY initial). + AnimationAccessor target = + TiedUpAnimationRegistry.resolveWithFallback( + TiedUpAnimationRegistry.CONTEXT_STAND_IDLE_ID + ); + if (target == TiedUpRigRegistry.EMPTY_ANIMATION) { + return; + } + + ResourceLocation targetId = target.registryName(); + + // Check si context_stand_idle est déjà l'anim courante du base layer. + // Comparaison par registryName() (stable à travers les reloads) vs. + // l'ancien check d'identité qui breakait après un datapack reload — + // chaque reload ré-instantie la StaticAnimation, donc l'ancien pointeur + // capturé à l'init ne matchait plus l'instance post-reload. AssetAccessor currentAnim = clientAnimator.baseLayer.animationPlayer.getAnimation(); - StaticAnimation target = TiedUpAnimationRegistry.CONTEXT_STAND_IDLE; - if (currentAnim != null && currentAnim.get() != null) { - // Compare directement les instances — les StaticAnimation sont - // singletons (registry constructor pattern). equals() fallback - // sur id et les accessors sont null pour EMPTY, donc ID-based - // comparison pas fiable ici. Instance check suffit. - if (currentAnim.get() == target) { + ResourceLocation currentId = currentAnim.registryName(); + if (currentId != null && currentId.equals(targetId)) { return; } } - // Self-heal : si le bind IDLE d'un patch (PlayerPatch.addAnimations) - // a été construit AVANT que TiedUpAnimationRegistry.initStaticAnimations() - // ait tourné, le bind pointe encore sur EMPTY_ANIMATION au lieu de - // CONTEXT_STAND_IDLE. On rebind ici — idempotent : les addLivingAnimation - // suivantes écrasent simplement la précédente entry dans la map. - // - // Ce cas se produit si : - // - Le patch construit la première fois sur le login (pré-setup) - // - Un test runClient démarre avant FMLCommonSetupEvent async enqueueWork - // - L'asset CONTEXT_STAND_IDLE est en fallback EMPTY après échec JSON - // (registry throw swallowed → CONTEXT_STAND_IDLE reste null cf. le - // isReady() guard au-dessus → on n'entre même pas dans ce bloc). + // Self-heal : si le bind IDLE d'un patch (PlayerPatch.initAnimator) + // a été construit AVANT que AnimationManager.apply() n'ait chargé le + // JSON context_stand_idle (bootstrap race, resource-pack reload + // pending au join), le bind pointe encore sur EMPTY_ANIMATION. On + // rebind ici — idempotent : les addLivingAnimation suivantes écrasent + // simplement la précédente entry dans la map. // // On utilise getLivingAnimation(motion, defaultGetter) avec null comme // default pour distinguer "pas de bind" d'un bind explicite vers - // EMPTY_ANIMATION. Dans les deux cas on rebind. + // EMPTY_ANIMATION. Dans les deux cas on rebind vers le target résolu. AssetAccessor currentIdleBind = clientAnimator.getLivingAnimation(LivingMotions.IDLE, null); if (currentIdleBind == null || currentIdleBind == TiedUpRigRegistry.EMPTY_ANIMATION) { - clientAnimator.addLivingAnimation(LivingMotions.IDLE, target.getAccessor()); + clientAnimator.addLivingAnimation(LivingMotions.IDLE, target); } - clientAnimator.playAnimation(target.getAccessor(), 0.2F); + clientAnimator.playAnimation(target, 0.2F); } /** diff --git a/src/main/resources/assets/tiedup/animmodels/animations/context_stand_idle.json b/src/main/resources/assets/tiedup/animmodels/animations/context_stand_idle.json index 5d2efb5..eb2eeb8 100644 --- a/src/main/resources/assets/tiedup/animmodels/animations/context_stand_idle.json +++ b/src/main/resources/assets/tiedup/animmodels/animations/context_stand_idle.json @@ -1,6 +1,9 @@ { "_comment": "PLACEHOLDER Phase 2.7 — animation idle 2-keyframes identity (joueur statique, pas de balancement). Boucle 2 secondes. DOIT parser avec JsonAssetLoader.loadClipForAnimation(). À remplacer par un export Blender authored (respiration/balancement subtle) quand dispo. Voir docs/plans/rig/ASSETS_NEEDED.md section 2.", - "_comment_armature": "Pas de champ 'armature' ici — le loader ne lit pas ce champ au runtime. L'armature est résolue par le call site (voir DirectStaticAnimation ctor dans TiedUpAnimationRegistry.initStaticAnimations, qui passe TiedUpArmatures.BIPED au super).", + "_comment_registration": "Auto-registered via AnimationManager.readResourcepackAnimation (bloc 'constructor' ci-dessous). isRepeat=true (idle loop), transition=0.15s (GENERAL). Armature résolue via InstantiateInvoker → TiedUpArmatures.get → BIPED. Plus aucun init Java-side : full data-driven, même pattern que les 5 placeholders Phase 3 (armbinder_*, classic_collar_*).", + "constructor": { + "invocation_command": "(0.15#F,true#Z,tiedup:context_stand_idle#java.lang.String,tiedup:biped#com.tiedup.remake.rig.armature.Armature)#com.tiedup.remake.rig.anim.types.StaticAnimation" + }, "format": "ATTRIBUTES", "animation": [ { diff --git a/src/test/java/com/tiedup/remake/rig/TiedUpAnimationRegistryTest.java b/src/test/java/com/tiedup/remake/rig/TiedUpAnimationRegistryTest.java index 8278534..9230efb 100644 --- a/src/test/java/com/tiedup/remake/rig/TiedUpAnimationRegistryTest.java +++ b/src/test/java/com/tiedup/remake/rig/TiedUpAnimationRegistryTest.java @@ -10,85 +10,100 @@ import static org.junit.jupiter.api.Assertions.assertSame; import net.minecraft.resources.ResourceLocation; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import com.tiedup.remake.rig.anim.types.DirectStaticAnimation; +import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor; +import com.tiedup.remake.rig.anim.types.StaticAnimation; /** * Tests du registry {@link TiedUpAnimationRegistry}. * - *

Ces tests verrouillent :

+ *

Depuis la migration full data-driven de {@code CONTEXT_STAND_IDLE}, il n'y + * a plus AUCUN field statique {@code StaticAnimation} hardcoded Java — toutes + * les anims sont enregistrées via le bloc {@code "constructor"} de leur JSON + * respectif, parsé par {@link com.tiedup.remake.rig.anim.AnimationManager#readResourcepackAnimation} + * au datapack/resource-pack reload. Les tests s'alignent sur ce contrat :

+ * * * - *

Les tests n'ont PAS besoin du runtime MC — {@code initStaticAnimations()} - * construit juste les instances {@link DirectStaticAnimation} (pas de - * {@code loadAnimation()} déclenché tant que {@code getAnimationClip()} n'est - * pas lu). La {@code ResourceLocation} créée via le constructor n'a pas besoin - * de Minecraft runtime non plus.

+ *

Les tests n'ont PAS besoin du runtime MC — on teste les invariants purs + * du registry (constante + fallback) sans toucher au resource manager.

*/ class TiedUpAnimationRegistryTest { - /** - * Appeler {@link TiedUpAnimationRegistry#initStaticAnimations()} deux fois - * de suite doit être un no-op sur le second appel — le champ - * {@code CONTEXT_STAND_IDLE} garde la même référence. - * - *

Si l'init n'était pas idempotent, un double-call (scénario hot-reload - * dev, test runner partageant la JVM, etc.) recréerait l'instance et - * invaliderait tous les sites qui l'ont capturée en field final ou local. - * Le guard {@code if (CONTEXT_STAND_IDLE != null) return;} protège de ça — - * ce test s'assure qu'il n'est pas retiré en régression.

- */ - @Test - void initStaticAnimationsIsIdempotent() { - TiedUpAnimationRegistry.initStaticAnimations(); - - assertNotNull(TiedUpAnimationRegistry.CONTEXT_STAND_IDLE, - "Après initStaticAnimations, CONTEXT_STAND_IDLE doit être non-null"); - - DirectStaticAnimation firstInstance = TiedUpAnimationRegistry.CONTEXT_STAND_IDLE; - - // Deuxième call — doit être no-op, pas recréer l'instance. - TiedUpAnimationRegistry.initStaticAnimations(); - - assertSame(firstInstance, TiedUpAnimationRegistry.CONTEXT_STAND_IDLE, - "Un second call à initStaticAnimations ne doit pas remplacer l'instance — " - + "le guard `if (CONTEXT_STAND_IDLE != null) return;` serait cassé sinon"); + @AfterEach + void resetDedupSet() { + // Isolation : un test qui warn sur un ID pollue le set dedup statique + // partagé — on purge pour ne pas fausser les tests suivants (ordre + // d'exécution JUnit non garanti). + TiedUpAnimationRegistry.resetWarnedMissing(); } /** - * Le {@code registryName()} de {@code CONTEXT_STAND_IDLE} doit matcher + * {@link TiedUpAnimationRegistry#CONTEXT_STAND_IDLE_ID} doit matcher * {@code tiedup:context_stand_idle} — alignement avec le path du JSON * asset {@code assets/tiedup/animmodels/animations/context_stand_idle.json}. * - *

Si le registry name dérive du path (via la résolution - * {@code resourceLocation = animmodels/animations/${registry.path}.json}), - * une désynchronisation code vs asset se traduit par un - * {@code AssetLoadingException} runtime. Ce test est un smoke check rapide - * au niveau de l'identité du registry name.

+ *

{@link com.tiedup.remake.rig.anim.AnimationManager#readResourcepackAnimation} + * dérive l'ID depuis le path du fichier via {@code pathToId}. Une + * désynchronisation code vs asset se traduit par un + * {@code resolveWithFallback} qui retourne EMPTY runtime — silent bug. + * Ce test est un smoke check rapide au niveau de l'identité du registry + * name.

*/ @Test - void contextStandIdleHasRegistryName() { - TiedUpAnimationRegistry.initStaticAnimations(); + void contextStandIdleIdMatchesAssetPath() { + ResourceLocation expected = ResourceLocation.fromNamespaceAndPath( + TiedUpRigConstants.MODID, "context_stand_idle" + ); - DirectStaticAnimation anim = TiedUpAnimationRegistry.CONTEXT_STAND_IDLE; - assertNotNull(anim, "CONTEXT_STAND_IDLE doit être initialisé"); + assertEquals(expected, TiedUpAnimationRegistry.CONTEXT_STAND_IDLE_ID, + "CONTEXT_STAND_IDLE_ID doit être tiedup:context_stand_idle — " + + "désynchronisation avec assets/tiedup/animmodels/animations/context_stand_idle.json sinon."); + } - ResourceLocation expected = - ResourceLocation.fromNamespaceAndPath(TiedUpRigConstants.MODID, "context_stand_idle"); + /** + * Pré-reload (pas d'appel à {@code AnimationManager.apply}), résoudre + * {@link TiedUpAnimationRegistry#CONTEXT_STAND_IDLE_ID} doit retourner + * {@link TiedUpRigRegistry#EMPTY_ANIMATION} (jamais null). + * + *

C'est le contrat fallback safe consommé par {@code PlayerPatch.initAnimator} + * (bind IDLE au construct-time de la capability, potentiellement avant le + * premier datapack apply). Le tick handler + * {@link com.tiedup.remake.rig.tick.RigAnimationTickHandler#maybePlayIdle} + * self-heal ensuite quand le datapack arrive.

+ * + *

Si ce test échoue avec une instance différente d'EMPTY, c'est qu'un + * refactor a cassé le contrat singleton — plusieurs sites runtime + * ({@code Layer#off}, {@code AnimationPlayer#isEmpty}) testent l'identité + * via {@code == EMPTY_ANIMATION}, donc retourner un autre empty casserait + * ces checks silencieusement.

+ */ + @Test + void resolveContextStandIdle_preReload_fallbacksToEmpty() { + AnimationAccessor resolved = + TiedUpAnimationRegistry.resolveWithFallback( + TiedUpAnimationRegistry.CONTEXT_STAND_IDLE_ID + ); - assertEquals(expected, anim.registryName(), - "CONTEXT_STAND_IDLE.registryName() doit être tiedup:context_stand_idle — " - + "désynchronisation avec assets/tiedup/animmodels/animations/context_stand_idle.json sinon"); + assertNotNull(resolved, + "resolveWithFallback ne doit jamais retourner null — " + + "les consumers (PlayerPatch, tick handler) supposent non-null."); + assertSame(TiedUpRigRegistry.EMPTY_ANIMATION, resolved, + "Pré-reload (AnimationManager.apply pas encore appelé), " + + "resolveWithFallback doit retourner le singleton EMPTY_ANIMATION. " + + "Les consumers détectent cette instance pour skip le play (noop)."); } }