Phase 2.7 : animation registry + tick handler + placeholder assets
TiedUpAnimationRegistry.CONTEXT_STAND_IDLE instancié au FMLCommonSetupEvent via DirectStaticAnimation + armature BIPED. Placeholder JSON 2-keyframes identity (joueur statique) à assets/tiedup/animmodels/animations/ jusqu'à ce qu'un export Blender authored remplace. biped.json de même (hiérarchie identity) placé dans assets/tiedup/armatures/ — parse via JsonAssetLoader mais pas encore chargé au runtime (l'armature reste procédurale côté Java). RigAnimationTickHandler tick chaque player visible côté client (phase END) : - patch.getAnimator().tick() → avance les layers EF - trigger playAnimation(CONTEXT_STAND_IDLE, 0.2f) quand motion=IDLE et anim courante ≠ CONTEXT_STAND_IDLE (idempotent) - try/catch per-entity avec dedup des erreurs par UUID (pattern TiedUpRenderEngine.loggedRenderErrors) - skip si level null / paused PlayerPatch.initAnimator bind désormais IDLE → CONTEXT_STAND_IDLE quand le registry est prêt (fallback EMPTY_ANIMATION si patch construit avant setup). Voir docs/plans/rig/ASSETS_NEEDED.md pour la spec des assets authored définitifs (anim idle swing respiration 3 keyframes + offsets biped anatomiques).
This commit is contained in:
@@ -140,6 +140,11 @@ public class TiedUpMod {
|
|||||||
|
|
||||||
// RIG Phase 2 — dispatcher EntityType → EntityPatch (PLAYER Phase 2, NPCs Phase 5)
|
// RIG Phase 2 — dispatcher EntityType → EntityPatch (PLAYER Phase 2, NPCs Phase 5)
|
||||||
event.enqueueWork(com.tiedup.remake.rig.patch.EntityPatchProvider::registerEntityPatches);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
156
src/main/java/com/tiedup/remake/rig/TiedUpAnimationRegistry.java
Normal file
156
src/main/java/com/tiedup/remake/rig/TiedUpAnimationRegistry.java
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig;
|
||||||
|
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||||
|
|
||||||
|
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.
|
||||||
|
*
|
||||||
|
* <h2>Placeholder assets</h2>
|
||||||
|
* <p>Les JSON associés 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}). */
|
||||||
|
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 (6 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,10 +8,12 @@ package com.tiedup.remake.rig.patch;
|
|||||||
|
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
|
||||||
|
import com.tiedup.remake.rig.TiedUpAnimationRegistry;
|
||||||
import com.tiedup.remake.rig.TiedUpArmatures;
|
import com.tiedup.remake.rig.TiedUpArmatures;
|
||||||
import com.tiedup.remake.rig.TiedUpRigRegistry;
|
import com.tiedup.remake.rig.TiedUpRigRegistry;
|
||||||
import com.tiedup.remake.rig.anim.Animator;
|
import com.tiedup.remake.rig.anim.Animator;
|
||||||
import com.tiedup.remake.rig.anim.LivingMotions;
|
import com.tiedup.remake.rig.anim.LivingMotions;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
import com.tiedup.remake.rig.armature.HumanoidArmature;
|
import com.tiedup.remake.rig.armature.HumanoidArmature;
|
||||||
import com.tiedup.remake.rig.math.MathUtils;
|
import com.tiedup.remake.rig.math.MathUtils;
|
||||||
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
import com.tiedup.remake.rig.math.OpenMatrix4f;
|
||||||
@@ -152,7 +154,16 @@ public abstract class PlayerPatch<T extends Player> extends LivingEntityPatch<T>
|
|||||||
@Override
|
@Override
|
||||||
protected void initAnimator(Animator animator) {
|
protected void initAnimator(Animator animator) {
|
||||||
super.initAnimator(animator);
|
super.initAnimator(animator);
|
||||||
animator.addLivingAnimation(LivingMotions.IDLE, TiedUpRigRegistry.EMPTY_ANIMATION);
|
// 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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
/*
|
||||||
|
* © 2026 TiedUp! Remake Contributors, distributed under GPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.tiedup.remake.rig.tick;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
import com.mojang.logging.LogUtils;
|
||||||
|
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraftforge.api.distmarker.Dist;
|
||||||
|
import net.minecraftforge.event.TickEvent;
|
||||||
|
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||||
|
import net.minecraftforge.fml.common.Mod;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
|
import com.tiedup.remake.core.TiedUpMod;
|
||||||
|
import com.tiedup.remake.rig.TiedUpAnimationRegistry;
|
||||||
|
import com.tiedup.remake.rig.anim.Animator;
|
||||||
|
import com.tiedup.remake.rig.anim.LivingMotions;
|
||||||
|
import com.tiedup.remake.rig.anim.client.ClientAnimator;
|
||||||
|
import com.tiedup.remake.rig.anim.types.DynamicAnimation;
|
||||||
|
import com.tiedup.remake.rig.anim.types.StaticAnimation;
|
||||||
|
import com.tiedup.remake.rig.asset.AssetAccessor;
|
||||||
|
import com.tiedup.remake.rig.patch.LivingEntityPatch;
|
||||||
|
import com.tiedup.remake.rig.patch.TiedUpCapabilities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Phase 2.7 — tick handler RIG côté client. Pour chaque player visible :
|
||||||
|
*
|
||||||
|
* <ol>
|
||||||
|
* <li>Récupère le {@link LivingEntityPatch} via
|
||||||
|
* {@link TiedUpCapabilities#getEntityPatch(net.minecraft.world.entity.Entity, Class)}</li>
|
||||||
|
* <li>Ticke l'{@link Animator} (avance les layers EF, gère les transitions,
|
||||||
|
* 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>
|
||||||
|
* </ol>
|
||||||
|
*
|
||||||
|
* <h2>Choix de design</h2>
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Phase END</b> : on tick après la logique vanilla de la frame pour
|
||||||
|
* que l'animator voit le dernier {@code yBodyRot}, position, vélocité
|
||||||
|
* écrits par MC. Pattern EF standard.</li>
|
||||||
|
* <li><b>Itération {@code level.players()}</b> : uniquement les joueurs
|
||||||
|
* dans la dimension courante — les autres ont leurs patches suspendus
|
||||||
|
* de toutes façons. Évite d'itérer les entities tiers non-patchées
|
||||||
|
* (Phase 5+ quand NPCs auront RIG).</li>
|
||||||
|
* <li><b>Error handling per-entity</b> : try/catch autour de chaque patch
|
||||||
|
* 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>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <h2>Futur Phase 3+</h2>
|
||||||
|
* <p>Ce handler est minimal — Phase 3 ajoutera :</p>
|
||||||
|
* <ul>
|
||||||
|
* <li>Détection motion (WALK, SIT, KNEEL, etc.) via {@code updateMotion}
|
||||||
|
* et binding dynamic des anims correspondantes</li>
|
||||||
|
* <li>Resolve context bondage (arms_bound / legs_bound / etc.) via
|
||||||
|
* {@code RigContextResolver}</li>
|
||||||
|
* <li>Tick des NPCs TiedUp quand leurs patches seront enregistrés</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Cf. {@code docs/plans/rig/MIGRATION.md} Phase 3 — Animation consumers.</p>
|
||||||
|
*/
|
||||||
|
@Mod.EventBusSubscriber(
|
||||||
|
modid = TiedUpMod.MOD_ID,
|
||||||
|
bus = Mod.EventBusSubscriber.Bus.FORGE,
|
||||||
|
value = Dist.CLIENT
|
||||||
|
)
|
||||||
|
public final class RigAnimationTickHandler {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogUtils.getLogger();
|
||||||
|
|
||||||
|
/** Set des UUIDs ayant déjà loggé une erreur tick — évite spam console. */
|
||||||
|
private static final Set<UUID> LOGGED_ERRORS = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
/** Cap de taille — reset si dépassé (évite leak en session infinie). */
|
||||||
|
private static final int MAX_LOGGED_UUIDS = 1024;
|
||||||
|
|
||||||
|
private RigAnimationTickHandler() {}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onClientTick(TickEvent.ClientTickEvent event) {
|
||||||
|
if (event.phase != TickEvent.Phase.END) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
|
||||||
|
// Loading screen, splash, disconnect, F3+T reload in progress — level null.
|
||||||
|
// Early-return silencieux (évite les logs parasites).
|
||||||
|
if (mc.level == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause menu / options screen — on skip le tick anim pour matcher
|
||||||
|
// le comportement vanilla MC (tick world paused).
|
||||||
|
if (mc.isPaused()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap de taille du set loggé — reset préventif.
|
||||||
|
if (LOGGED_ERRORS.size() > MAX_LOGGED_UUIDS) {
|
||||||
|
LOGGED_ERRORS.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copie défensive : le set level.players() peut muter pendant notre
|
||||||
|
// itération si un joueur join/leave (MP). Le CME-safe serait itérer
|
||||||
|
// sur une copie. On tolère le CME via try/catch, le coût copy n'en
|
||||||
|
// vaut pas la peine pour une liste typiquement petite (~10 joueurs
|
||||||
|
// visibles max).
|
||||||
|
for (Player player : new HashSet<>(mc.level.players())) {
|
||||||
|
tickPlayer(player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ticke le patch RIG d'un joueur. Noop si :
|
||||||
|
* <ul>
|
||||||
|
* <li>Pas de capability attachée (ex : le type d'entity n'est pas dans
|
||||||
|
* {@code EntityPatchProvider.CAPABILITIES})</li>
|
||||||
|
* <li>L'{@code Animator} est null (entitypatch pas encore onConstructed)</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Toute exception thrown par le patch / animator est caught et logguée
|
||||||
|
* une seule fois par UUID — le player reste rendu par le fallback vanilla
|
||||||
|
* du pipeline render engine (cf. {@code TiedUpRenderEngine.onRenderLiving}).</p>
|
||||||
|
*/
|
||||||
|
private static void tickPlayer(Player player) {
|
||||||
|
LivingEntityPatch<?> patch =
|
||||||
|
TiedUpCapabilities.getEntityPatch(player, LivingEntityPatch.class);
|
||||||
|
|
||||||
|
if (patch == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Animator animator = patch.getAnimator();
|
||||||
|
if (animator == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID uuid = player.getUUID();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Tick de l'animator — avance les layers + gère les transitions
|
||||||
|
// EF natifs. C'est ce que le MixinLivingEntity.tick() ferait en
|
||||||
|
// Phase 3+ pattern push EF (cf. MIGRATION.md §2.2.1). Ici on
|
||||||
|
// tick côté client uniquement pour Phase 2.7.
|
||||||
|
animator.tick();
|
||||||
|
|
||||||
|
// 2. Trigger transition vers CONTEXT_STAND_IDLE si rien de mieux
|
||||||
|
// n'est actif. Idempotent : ne re-déclenche pas si l'anim
|
||||||
|
// courante est déjà CONTEXT_STAND_IDLE.
|
||||||
|
maybePlayIdle(patch, animator);
|
||||||
|
|
||||||
|
} catch (Throwable t) {
|
||||||
|
if (LOGGED_ERRORS.add(uuid)) {
|
||||||
|
LOGGER.error(
|
||||||
|
"RigAnimationTickHandler: tick animator failed for player {} ({}) — "
|
||||||
|
+ "subsequent errors suppressed for this UUID.",
|
||||||
|
player.getGameProfile().getName(), uuid, t
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Pas de rethrow — le joueur continue d'être rendu en vanilla (ou
|
||||||
|
// stale pose). Le pipeline render dispose de son propre fallback.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déclenche la transition vers {@code CONTEXT_STAND_IDLE} si :
|
||||||
|
* <ul>
|
||||||
|
* <li>Le registry est ready (placeholder asset loadé)</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>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>Transition 0.2s (4 ticks @ 20tps) pour un fondu doux avec
|
||||||
|
* l'EMPTY_ANIMATION de départ. Ça reste court — si Phase 3+ veut un fondu
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(animator instanceof ClientAnimator clientAnimator)) {
|
||||||
|
// Server-side Animator — rien à rendre. Phase 3+ si on veut sync
|
||||||
|
// motion serveur, c'est là qu'on le fera.
|
||||||
|
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).
|
||||||
|
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) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clientAnimator.playAnimation(target.getAccessor(), 0.2F);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset des erreurs loggées — utile aux tests unitaires et au F3+T
|
||||||
|
* reload (aligné avec {@code TiedUpRenderEngine.onAddLayers}).
|
||||||
|
*/
|
||||||
|
public static void resetLoggedErrors() {
|
||||||
|
LOGGED_ERRORS.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"_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.",
|
||||||
|
"format": "ATTRIBUTES",
|
||||||
|
"armature": "tiedup:biped",
|
||||||
|
"animation": [
|
||||||
|
{
|
||||||
|
"name": "Root",
|
||||||
|
"time": [0.0, 2.0],
|
||||||
|
"transform": [
|
||||||
|
{ "loc": [0.0, 0.0, 0.0], "rot": [1.0, 0.0, 0.0, 0.0], "sca": [1.0, 1.0, 1.0] },
|
||||||
|
{ "loc": [0.0, 0.0, 0.0], "rot": [1.0, 0.0, 0.0, 0.0], "sca": [1.0, 1.0, 1.0] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
225
src/main/resources/assets/tiedup/armatures/biped.json
Normal file
225
src/main/resources/assets/tiedup/armatures/biped.json
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
{
|
||||||
|
"_comment": "PLACEHOLDER Phase 2.7 — identity transforms (joints empilés à l'origine). Doit parser avec JsonAssetLoader.loadArmature() mais n'est PAS encore utilisé au runtime (TiedUpArmatures.BIPED est construit proceduralement en Java). À remplacer par un export Blender addon Antikythera-Studios quand dispo. Voir docs/plans/rig/ASSETS_NEEDED.md section 1.",
|
||||||
|
"armature": {
|
||||||
|
"armature_format": "ATTRIBUTES",
|
||||||
|
"joints": [
|
||||||
|
"Root",
|
||||||
|
"Thigh_R",
|
||||||
|
"Leg_R",
|
||||||
|
"Knee_R",
|
||||||
|
"Thigh_L",
|
||||||
|
"Leg_L",
|
||||||
|
"Knee_L",
|
||||||
|
"Torso",
|
||||||
|
"Chest",
|
||||||
|
"Head",
|
||||||
|
"Shoulder_R",
|
||||||
|
"Arm_R",
|
||||||
|
"Hand_R",
|
||||||
|
"Tool_R",
|
||||||
|
"Elbow_R",
|
||||||
|
"Shoulder_L",
|
||||||
|
"Arm_L",
|
||||||
|
"Hand_L",
|
||||||
|
"Tool_L",
|
||||||
|
"Elbow_L"
|
||||||
|
],
|
||||||
|
"hierarchy": [
|
||||||
|
{
|
||||||
|
"name": "Root",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Thigh_R",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Leg_R",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Knee_R",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Thigh_L",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Leg_L",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Knee_L",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Torso",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Chest",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Head",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Shoulder_R",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Arm_R",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Elbow_R",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Hand_R",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Tool_R",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Shoulder_L",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Arm_L",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Elbow_L",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Hand_L",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"name": "Tool_L",
|
||||||
|
"transform": {
|
||||||
|
"loc": [0.0, 0.0, 0.0],
|
||||||
|
"rot": [1.0, 0.0, 0.0, 0.0],
|
||||||
|
"sca": [1.0, 1.0, 1.0]
|
||||||
|
},
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user