Full data-driven : migrate CONTEXT_STAND_IDLE to datapack path
Remove the last Java-hardcoded StaticAnimation registration. context_stand_idle.json now has the EF-native 'constructor' block so AnimationManager picks it up at datapack reload, same as the 5 Phase 3 placeholders. Consumers (PlayerPatch, LivingEntityPatch, RigAnimationTickHandler, TiedUpMod, tests) refactored to lookup via resolveWithFallback(CONTEXT_STAND_IDLE_ID) instead of direct static field access. resolveWithFallback handles the pre- reload window by returning EMPTY_ANIMATION — no more null-checks in consumers. TiedUpAnimationRegistry keeps only : - CONTEXT_STAND_IDLE_ID constant (canonical ID) - resolveWithFallback() helper - WARNED_MISSING_ANIMS dedup + reset hook Design goal : zero Java code required for any modder to add bondage anims. Drop a JSON in assets/<modid>/animmodels/animations/, reference it from an item JSON binding, /reload, see it fire.
This commit is contained in:
@@ -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.
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <h2>Data-driven — zéro Java hardcoding</h2>
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>Goal : un modder peut ajouter une anim TiedUp compatible sans écrire UNE
|
||||
* ligne de Java — il drop un JSON dans
|
||||
* {@code assets/<modid>/animmodels/animations/}, le référence depuis un item
|
||||
* JSON binding, {@code /reload}, l'anim fire.</p>
|
||||
*
|
||||
* <h2>Ce que le registry expose</h2>
|
||||
* <ul>
|
||||
* <li>{@link #CONTEXT_STAND_IDLE_ID} — ID canonique de l'anim idle par
|
||||
* défaut (résolue en
|
||||
* {@code assets/tiedup/animmodels/animations/context_stand_idle.json}).
|
||||
* Consommée par {@code PlayerPatch.initAnimator},
|
||||
* {@code RigAnimationTickHandler.maybePlayIdle}, etc.</li>
|
||||
* <li>{@link #resolveWithFallback(ResourceLocation)} — lookup
|
||||
* {@link AnimationManager#byKey} avec fallback safe sur
|
||||
* {@link TiedUpRigRegistry#EMPTY_ANIMATION} si l'ID est inconnu (datapack
|
||||
* pas encore rechargé, typo modder, etc.). Jamais null.</li>
|
||||
* <li>{@link #resetWarnedMissing()} — hook tests / hot-reload pour purger le
|
||||
* set dedup des WARN de miss.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Placeholder assets</h2>
|
||||
* <p>Les JSON associés sont des <b>placeholders procéduraux</b> (2 keyframes
|
||||
* <p>Les JSON actuels sont des <b>placeholders procéduraux</b> (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).</p>
|
||||
*
|
||||
* <h2>Lifecycle</h2>
|
||||
* <ul>
|
||||
* <li>{@link #initStaticAnimations()} appelé au {@code FMLCommonSetupEvent}
|
||||
* (via {@code event.enqueueWork(...)}). Crée les instances
|
||||
* {@link DirectStaticAnimation} — pas de chargement JSON ici, juste les
|
||||
* métadonnées (registry name, armature, repeat flag).</li>
|
||||
* <li>Le chargement effectif du JSON ({@code JsonAssetLoader}) est lazy :
|
||||
* à la première lecture de {@link StaticAnimation#getAnimationClip()},
|
||||
* donc typiquement à la première frame où l'animation est jouée.</li>
|
||||
* <li>Si l'asset JSON est absent / corrompu, {@code StaticAnimation.loadAnimation}
|
||||
* relance une {@code AssetLoadingException}. Le tick handler
|
||||
* ({@link com.tiedup.remake.rig.tick.RigAnimationTickHandler}) attrape ces
|
||||
* throwables pour éviter un crash complet.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Dist</h2>
|
||||
* <p>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()}).</p>
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* <p><b>Attention init order</b> : 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.).</p>
|
||||
*
|
||||
* <p>Utilise {@link DirectStaticAnimation} (vs un hand-written
|
||||
* {@code StaticAnimation}) pour hériter du pattern accessor=self +
|
||||
* registryName() utilisés dans {@link TiedUpRigRegistry#EMPTY_ANIMATION}.</p>
|
||||
*/
|
||||
public static DirectStaticAnimation CONTEXT_STAND_IDLE;
|
||||
|
||||
/**
|
||||
* Construit les {@link StaticAnimation} TiedUp. À appeler exactement une
|
||||
* fois par game, en {@code FMLCommonSetupEvent.enqueueWork(...)}.
|
||||
*
|
||||
* <p>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}.</p>
|
||||
*
|
||||
* <p>Idempotent (re-appel sans effet visible) mais pas thread-safe. Ne
|
||||
* devrait jamais être appelé hors du mod bus.</p>
|
||||
*/
|
||||
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}.
|
||||
*
|
||||
* <p>{@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)}).</p>
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* <p>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 :</p>
|
||||
* ({@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 :</p>
|
||||
* <ul>
|
||||
* <li>Typo modder dans un JSON data-driven bondage item</li>
|
||||
* <li>Datapack pas encore rechargé ({@code /reload} pending)</li>
|
||||
* <li>Datapack/resource-pack pas encore rechargé (pré-{@code apply} au
|
||||
* bootstrap, entre deux {@code /reload})</li>
|
||||
* <li>Animation supprimée entre deux versions du mod</li>
|
||||
* <li>Race entre packet réception et
|
||||
* {@code AnimationManager.apply()} en début de session</li>
|
||||
@@ -210,7 +117,7 @@ public final class TiedUpAnimationRegistry {
|
||||
* runtime penserait qu'une anim réelle joue alors qu'en fait c'est un
|
||||
* empty différent. Le singleton canonique évite ce piège.</p>
|
||||
*
|
||||
* @param id l'ID registry à résoudre (ex. {@code tiedup:idle_context_bound})
|
||||
* @param id l'ID registry à résoudre (ex. {@code tiedup:context_stand_idle})
|
||||
* @return l'{@link AnimationAccessor} enregistré, ou
|
||||
* {@link TiedUpRigRegistry#EMPTY_ANIMATION} si l'ID est inconnu.
|
||||
* Jamais null.
|
||||
|
||||
@@ -77,7 +77,10 @@ public abstract class LivingEntityPatch<T extends LivingEntity> extends EntityPa
|
||||
* <p>Exemple subclass :</p>
|
||||
* <pre>
|
||||
* protected void initAnimator(Animator a) {
|
||||
* a.addLivingAnimation(LivingMotions.IDLE, TiedUpAnimationRegistry.CONTEXT_STAND_IDLE);
|
||||
* a.addLivingAnimation(
|
||||
* LivingMotions.IDLE,
|
||||
* TiedUpAnimationRegistry.resolveWithFallback(
|
||||
* TiedUpAnimationRegistry.CONTEXT_STAND_IDLE_ID));
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,7 @@ import net.minecraft.world.entity.player.Player;
|
||||
|
||||
import com.tiedup.remake.rig.TiedUpAnimationRegistry;
|
||||
import com.tiedup.remake.rig.TiedUpArmatures;
|
||||
import com.tiedup.remake.rig.TiedUpRigRegistry;
|
||||
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
|
||||
import com.tiedup.remake.rig.anim.Animator;
|
||||
import com.tiedup.remake.rig.anim.LivingMotion;
|
||||
import com.tiedup.remake.rig.anim.LivingMotions;
|
||||
@@ -35,9 +35,10 @@ import com.tiedup.remake.v2.furniture.EntityFurniture;
|
||||
* {@link LocalPlayerPatch} gère le cas first-person</li>
|
||||
* <li>Fournir un {@link #getModelMatrix(float)} avec scale player vanilla
|
||||
* ({@value #PLAYER_SCALE}) — cf. EF {@code PlayerPatch:82,176}</li>
|
||||
* <li>Stub {@link #initAnimator(Animator)} qui bind
|
||||
* {@code LivingMotions.IDLE → EMPTY_ANIMATION} (Phase 2.7 remplacera par
|
||||
* {@code CONTEXT_STAND_IDLE} co-authored)</li>
|
||||
* <li>{@link #initAnimator(Animator)} bind {@code LivingMotions.IDLE →
|
||||
* tiedup:context_stand_idle} (résolu via
|
||||
* {@link TiedUpAnimationRegistry#resolveWithFallback} — EMPTY si le
|
||||
* datapack n'est pas encore chargé)</li>
|
||||
* <li>Implémentation {@link #updateMotion(boolean)} (P3-08) qui route
|
||||
* {@code currentLivingMotion} selon l'état bondage/locomotion du joueur
|
||||
* — délègue à {@link BondageStateHelpers} pour la détection d'état et
|
||||
@@ -266,29 +267,35 @@ public abstract class PlayerPatch<T extends Player> extends LivingEntityPatch<T>
|
||||
|
||||
/**
|
||||
* Hook {@link LivingEntityPatch#initAnimator(Animator)} : bind la motion
|
||||
* IDLE sur l'animation "ne fait rien" par défaut. Phase 2.7 remplacera
|
||||
* par {@code TiedUpAnimationRegistry.CONTEXT_STAND_IDLE} avec quelques
|
||||
* keyframes de balancement.
|
||||
* IDLE sur {@code tiedup:context_stand_idle}, résolue via le registry
|
||||
* data-driven ({@link TiedUpAnimationRegistry#resolveWithFallback}).
|
||||
*
|
||||
* <p><b>Pre-reload window</b> : si le patch est construit avant que
|
||||
* {@code AnimationManager.apply} n'ait tourné (bootstrap client, race
|
||||
* entre join et resource-pack reload, etc.), l'ID {@code context_stand_idle}
|
||||
* n'est pas encore dans {@code animationByName} et
|
||||
* {@code resolveWithFallback} retourne {@link TiedUpRigRegistry#EMPTY_ANIMATION}.
|
||||
* C'est idempotent côté animator — une prochaine passe de
|
||||
* {@link com.tiedup.remake.rig.tick.RigAnimationTickHandler#maybePlayIdle}
|
||||
* re-bind sur la vraie anim une fois le datapack chargé (self-heal).</p>
|
||||
*
|
||||
* <p>L'ordre EF ajoute ~25 motions différentes (WALK, RUN, SNEAK, SIT,
|
||||
* SLEEP, etc.). On se limite à IDLE Phase 2.4 — ajouter les autres
|
||||
* sans anim source = pollution registre pour rien. Au fur et à mesure
|
||||
* que les JSON co-authored arrivent (Phase 2.7 / 4), on ajoute les
|
||||
* binds correspondants.</p>
|
||||
* SLEEP, etc.). On se limite à IDLE ici — ajouter les autres sans anim
|
||||
* source = pollution registre pour rien. Au fur et à mesure que les JSON
|
||||
* co-authored arrivent, on ajoute les binds correspondants (ou on délègue
|
||||
* au pipeline d'équipement data-driven pour les motions bondage-gated).</p>
|
||||
*/
|
||||
@Override
|
||||
protected void initAnimator(Animator animator) {
|
||||
super.initAnimator(animator);
|
||||
// Phase 2.7 : si le registry a init, on bind sur CONTEXT_STAND_IDLE
|
||||
// (placeholder procédural 2-keyframes identity — voir
|
||||
// TiedUpAnimationRegistry + ASSETS_NEEDED.md §2). Sinon fallback sur
|
||||
// EMPTY_ANIMATION — peut arriver si le patch est construit avant que
|
||||
// FMLCommonSetupEvent n'ait tourné (rare mais pas impossible en dev).
|
||||
StaticAnimation idle = TiedUpAnimationRegistry.CONTEXT_STAND_IDLE;
|
||||
animator.addLivingAnimation(
|
||||
LivingMotions.IDLE,
|
||||
idle != null ? idle.getAccessor() : TiedUpRigRegistry.EMPTY_ANIMATION
|
||||
);
|
||||
// Full data-driven : lookup par ID, EMPTY_ANIMATION fallback si
|
||||
// l'asset n'est pas encore chargé (rare mais possible au bootstrap).
|
||||
// Le tick handler self-heal le bind une fois le datapack loadé.
|
||||
AnimationAccessor<? extends StaticAnimation> idle =
|
||||
TiedUpAnimationRegistry.resolveWithFallback(
|
||||
TiedUpAnimationRegistry.CONTEXT_STAND_IDLE_ID
|
||||
);
|
||||
animator.addLivingAnimation(LivingMotions.IDLE, idle);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,9 +19,12 @@ import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
import com.tiedup.remake.core.TiedUpMod;
|
||||
import com.tiedup.remake.rig.TiedUpAnimationRegistry;
|
||||
import com.tiedup.remake.rig.TiedUpRigRegistry;
|
||||
import com.tiedup.remake.rig.anim.AnimationManager.AnimationAccessor;
|
||||
import com.tiedup.remake.rig.anim.Animator;
|
||||
import com.tiedup.remake.rig.anim.LivingMotions;
|
||||
import com.tiedup.remake.rig.anim.client.ClientAnimator;
|
||||
@@ -41,8 +44,9 @@ import com.tiedup.remake.rig.patch.TiedUpCapabilities;
|
||||
* etc.)</li>
|
||||
* <li>Si aucune animation "réelle" n'est active (= on joue encore
|
||||
* l'{@code EMPTY_ANIMATION} du fallback initial), déclenche une
|
||||
* transition vers {@link TiedUpAnimationRegistry#CONTEXT_STAND_IDLE}
|
||||
* (0.2s) — preuve vivante que le pipeline tourne Phase 2.7.</li>
|
||||
* transition vers {@code tiedup:context_stand_idle} (résolu via
|
||||
* {@link TiedUpAnimationRegistry#resolveWithFallback}, 0.2s) —
|
||||
* preuve vivante que le pipeline tourne Phase 2.7.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h2>Choix de design</h2>
|
||||
@@ -58,11 +62,11 @@ import com.tiedup.remake.rig.patch.TiedUpCapabilities;
|
||||
* tick. Un patch qui throw ne cascade pas sur les autres. L'erreur
|
||||
* est logguée <b>une seule fois par UUID</b> pour éviter de spammer
|
||||
* la console.</li>
|
||||
* <li><b>Registry pas ready → noop</b> : si
|
||||
* {@link TiedUpAnimationRegistry#CONTEXT_STAND_IDLE} est null (setup
|
||||
* pas encore exécuté, ou fallback d'échec), on ne crash pas — on
|
||||
* skippe juste le trigger idle. L'animator tourne quand même
|
||||
* (EMPTY_ANIMATION en boucle).</li>
|
||||
* <li><b>Datapack pas encore appliqué → noop</b> : si
|
||||
* {@link TiedUpAnimationRegistry#resolveWithFallback} retourne
|
||||
* {@link TiedUpRigRegistry#EMPTY_ANIMATION} (ID inconnu pendant la
|
||||
* fenêtre pré-reload), on ne crash pas — on skippe juste le trigger
|
||||
* idle. L'animator tourne quand même (EMPTY_ANIMATION en boucle).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Futur Phase 3+</h2>
|
||||
@@ -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 :
|
||||
* <ul>
|
||||
* <li>Le registry est ready (placeholder asset loadé)</li>
|
||||
* <li>Le datapack a été appliqué (l'ID est résolu par
|
||||
* {@link TiedUpAnimationRegistry#resolveWithFallback} — si fallback
|
||||
* EMPTY, on skip le trigger).</li>
|
||||
* <li>Le patch est dans {@code LivingMotions.IDLE} (pas en action
|
||||
* particulière — walk, sit, etc. Phase 3+)</li>
|
||||
* <li>L'animation actuellement jouée sur le base layer n'est pas déjà
|
||||
* CONTEXT_STAND_IDLE (évite le re-trigger à chaque tick)</li>
|
||||
* {@code tiedup:context_stand_idle} (compare par {@code registryName()},
|
||||
* stable à travers les reloads — évite le re-trigger à chaque tick)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
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<? extends StaticAnimation> 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<? extends DynamicAnimation> 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<? extends StaticAnimation> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
* <p>Ces tests verrouillent :</p>
|
||||
* <p>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 :</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>L'idempotence de {@link TiedUpAnimationRegistry#initStaticAnimations()}
|
||||
* — un double-appel ne doit pas créer une nouvelle instance / doubler
|
||||
* l'enregistrement. Pattern important car {@code FMLCommonSetupEvent}
|
||||
* peut firer plusieurs fois sur hot-reload dev ou test runner.</li>
|
||||
* <li>Le {@code registryName()} de {@link TiedUpAnimationRegistry#CONTEXT_STAND_IDLE}
|
||||
* — alignement avec le path du JSON asset
|
||||
* <li>{@link TiedUpAnimationRegistry#CONTEXT_STAND_IDLE_ID} — l'ID canonique
|
||||
* est une constante stable, doit matcher le path du JSON
|
||||
* ({@code assets/tiedup/animmodels/animations/context_stand_idle.json}).
|
||||
* Si quelqu'un renomme l'asset sans toucher le code (ou vice-versa), ce
|
||||
* test attrape immédiatement la désynchronisation.</li>
|
||||
* <li>{@link TiedUpAnimationRegistry#resolveWithFallback(ResourceLocation)}
|
||||
* — pré-reload (pas d'appel à {@code AnimationManager.apply}) retourne
|
||||
* {@link TiedUpRigRegistry#EMPTY_ANIMATION}. C'est le comportement fallback
|
||||
* safe attendu par les consumers ({@code PlayerPatch.initAnimator},
|
||||
* {@code RigAnimationTickHandler.maybePlayIdle}).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>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.</p>
|
||||
* <p>Les tests n'ont PAS besoin du runtime MC — on teste les invariants purs
|
||||
* du registry (constante + fallback) sans toucher au resource manager.</p>
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
@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}.
|
||||
*
|
||||
* <p>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.</p>
|
||||
* <p>{@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.</p>
|
||||
*/
|
||||
@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).
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
@Test
|
||||
void resolveContextStandIdle_preReload_fallbacksToEmpty() {
|
||||
AnimationAccessor<? extends StaticAnimation> 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).");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user