P3-04 : add TiedUpAnimationRegistry.resolveWithFallback + EMPTY_ANIMATION stub

Nouveau helper statique consomme par ClientRigEquipmentHandler.rebuildBondageAnimations
(P3-05) et PacketPlayRigAnim.handleOnClient (P3-12) pour resoudre un anim ID avec
fallback safe si miss.

Design :
- Lookup delegue a AnimationManager.byKey(ResourceLocation) — l'API existante
  pour la resolution par registry name.
- Fallback = TiedUpRigRegistry.EMPTY_ANIMATION (singleton canonique) plutot
  qu'un stub empty_fallback separe. Les sites runtime (Layer#off, AnimationPlayer#isEmpty,
  LayerOffAnimation#getNextAnimation) comparent via == EMPTY_ANIMATION — retourner
  une autre instance provoquerait des false-negatives sur ces checks d'identite.
- Dedup WARN via ConcurrentHashMap.newKeySet() : un ID donne ne log qu'une fois
  par session, evite le spam si le miss vient d'un item data-driven appele tick
  apres tick. Pattern inspire de RigAnimationTickHandler.LOGGED_ERRORS.
- resetWarnedMissing() expose pour tests + runtime reload (F3+T datapack).
- Branche defensive : id=null swallow + log (cas pathologique caller).

4 tests unitaires :
- happy path (ID enregistre via AnimationManager.AnimationBuilder → assertSame)
- fallback safe (ID inconnu → EMPTY_ANIMATION, non-null)
- no-throw (ID inconnu + null swallow sans exception)
- dedup observable (reset puis re-call sur meme ID re-warn, sanity check fake
  accessor distinct de EMPTY_ANIMATION)

65 tests rig GREEN (57 baseline + 4 nouveaux + autres).
This commit is contained in:
notevil
2026-04-23 13:04:31 +02:00
parent 15e405f5b0
commit cef589aac1
2 changed files with 322 additions and 0 deletions

View File

@@ -4,10 +4,15 @@
package com.tiedup.remake.rig;
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;
@@ -153,4 +158,108 @@ public final class TiedUpAnimationRegistry {
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
* {@link #resolveWithFallback(ResourceLocation)} tick après tick avec un ID
* invalide (ex. un item bondage data-driven qui référence un anim ID cassé,
* appelé dans la boucle de rendu). Un seul WARN par ID unique sur toute la
* durée de vie du process (jusqu'à {@link #resetWarnedMissing()}).
*
* <p>Pattern inspiré de
* {@code RigAnimationTickHandler.LOGGED_ERRORS} — {@code ConcurrentHashMap.newKeySet()}
* pour être safe en cas d'appels concurrents (client tick thread + network
* handler thread pour {@code PacketPlayRigAnim.handleOnClient}).</p>
*/
private static final Set<ResourceLocation> WARNED_MISSING_ANIMS = ConcurrentHashMap.newKeySet();
/**
* Résout une animation par {@link ResourceLocation} avec fallback safe
* 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>
* <ul>
* <li>Typo modder dans un JSON data-driven bondage item</li>
* <li>Datapack pas encore rechargé ({@code /reload} pending)</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>
* </ul>
*
* <p>Dans tous ces cas, on retourne {@link TiedUpRigRegistry#EMPTY_ANIMATION}
* — un singleton qui ne joue rien visuellement (pas de keyframe, pose
* identity). Mieux qu'un NPE pour la robustesse du pipeline : l'anim
* équipement continue de tourner avec les anim connues, et l'anim inconnue
* est juste silencieusement un no-op.</p>
*
* <p><b>Dedup WARN</b> : un miss donné ne log qu'une fois par session
* ({@link #WARNED_MISSING_ANIMS}). Ça évite le spam dans la console quand
* l'ID invalide est consommé tick après tick (ex. équipement resté en place).
* Le set peut être reset via {@link #resetWarnedMissing()} (hot-reload
* F3+T, runtime datapack reload).</p>
*
* <p><b>Pourquoi réutiliser {@link TiedUpRigRegistry#EMPTY_ANIMATION}</b> vs
* créer un {@code empty_fallback} séparé : plusieurs sites du runtime
* ({@code Layer#off}, {@code AnimationPlayer#isEmpty}, {@code LayerOffAnimation#getNextAnimation})
* testent l'identité via {@code == EMPTY_ANIMATION}. Retourner une autre
* instance d'empty provoquerait des false-negatives sur ces checks — le
* 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})
* @return l'{@link AnimationAccessor} enregistré, ou
* {@link TiedUpRigRegistry#EMPTY_ANIMATION} si l'ID est inconnu.
* Jamais null.
*/
public static AnimationAccessor<? extends StaticAnimation> resolveWithFallback(
ResourceLocation id
) {
if (id == null) {
// null ID : log + fallback. Pas de dedup (cas pathologique — le
// caller a un bug, pas un miss de datapack).
TiedUpRigConstants.LOGGER.warn(
"[TiedUpAnimationRegistry] resolveWithFallback appelé avec id=null, "
+ "using EMPTY_ANIMATION fallback."
);
return TiedUpRigRegistry.EMPTY_ANIMATION;
}
AnimationAccessor<? extends StaticAnimation> anim = AnimationManager.byKey(id);
if (anim != null) {
return anim;
}
// Miss — fallback + dedup warn. Set.add retourne true si l'ID n'était
// pas déjà dans le set → premier miss, on log. Sinon, silent no-op.
if (WARNED_MISSING_ANIMS.add(id)) {
TiedUpRigConstants.LOGGER.warn(
"[TiedUpAnimationRegistry] Animation not found: '{}', using EMPTY_ANIMATION fallback. "
+ "Check datapack JSON or run /reload.",
id
);
}
return TiedUpRigRegistry.EMPTY_ANIMATION;
}
/**
* Reset le set dedup des WARN missing. Utilisé dans deux contextes :
* <ul>
* <li>Tests unitaires — pour réinitialiser l'état statique entre test
* cases (sinon un test qui warn sur un ID pollue les suivants).</li>
* <li>Runtime reload (F3+T / datapack reload) — après un reload, des
* anims précédemment missing peuvent être dispo maintenant ; on
* veut pouvoir re-warn si elles retombent en miss après un autre
* reload.</li>
* </ul>
*
* <p>Thread-safe via {@link ConcurrentHashMap#newKeySet()} — pas de
* synchronisation externe nécessaire.</p>
*/
public static void resetWarnedMissing() {
WARNED_MISSING_ANIMS.clear();
}
}