Phase 2.8 : V2 player rendering cleanup + QA checklist doc

Cut wholesale du pipeline V2 player-anim suite au bascule RIG (pas d'opt-in,
pas de rollback — accepté par le mainteneur).

- rm MixinPlayerModel : le renderer RIG patched ne passe plus par
  PlayerModel.setupAnim, donc l'injection @TAIL devenait dead code.
  Les features dog pose head compensation seront ré-exprimées en
  StaticAnimation pose_dog.json (V3-REW-07).
- Strip tiedup.mixins.json : retiré client/MixinPlayerModel, restent
  MixinCamera + MixinLivingEntitySleeping.
- BondageAnimationManager.init() : retiré les 3 PlayerAnimationFactory
  registrations (context / item / furniture), le path joueur n'en
  dépend plus. Factory IDs conservés car les getPlayerLayer*() sont
  tolérantes au null retour via try/catch existants — et restent
  utilisées par le cache fallback remote. Les NPCs continuent
  d'utiliser cette classe via l'accès direct animation stack
  (IAnimatedPlayer.getAnimationStack().addAnimLayer), inchangé.
- TiedUpMod.onClientSetup : suppression de l'appel BondageAnimationManager.init()
  (la méthode est maintenant un log no-op, conservée pour la signature
  publique + doc du changement).
- AnimationTickHandler.onClientTick : retrait de la boucle
  mc.level.players() + updatePlayerAnimation + tickFurnitureSafety +
  cold-cache furniture retry. Les joueurs sont ticked par
  RigAnimationTickHandler (Phase 2.7). Conservé : le cleanup
  périodique ClothesClientCache (hygiène mémoire indépendante), le
  hook onPlayerLogout (cleanup per-UUID des caches NPC restants), et
  le hook onWorldUnload (caches V2 encore utilisés par NpcAnimationTickHandler).
  Imports unused strippés.
- DogPoseHelper : mis à jour la javadoc pour refléter le retrait
  du path player (NPCs only désormais).

Compile GREEN. 20/20 tests rig GREEN.

QA runtime : cf. docs/plans/rig/PHASE2_QA.md (non commit — fichier
working doc sous docs/plans/ gitignored par convention repo).

Net LOC : -276.
This commit is contained in:
notevil
2026-04-23 00:42:10 +02:00
parent b494b60d60
commit 5a39fb0c1c
6 changed files with 72 additions and 348 deletions

View File

@@ -90,41 +90,30 @@ public class BondageAnimationManager {
/**
* Initialize the animation system.
* Must be called during client setup to register the player animation factory.
*
* <p>Phase 2.8 RIG cleanup — les 3 {@link PlayerAnimationFactory}
* (context / item / furniture) ont été supprimées : le renderer RIG
* patched ne passe plus par le pipeline PlayerAnimator pour le joueur,
* donc les factories devenaient dead code (aucun call site n'atteint
* jamais la map associée côté joueur).</p>
*
* <p>Les NPCs continuent d'être animés via cette classe : le chemin
* {@link #getOrCreateLayer} pour les entités {@code IAnimatedPlayer}
* non-joueur utilise {@code animated.getAnimationStack().addAnimLayer(...)}
* en direct — ça ne dépend d'aucune factory. Voir {@code NpcAnimationTickHandler}
* pour le consumer. Le path player dans {@link #getOrCreateLayer} est
* laissé en place volontairement : il retombe proprement sur null
* (PlayerAnimationAccess throw → catch → null) et laisse le tick RIG
* s'occuper du joueur.</p>
*
* <p>Conservé comme méthode publique pour ne pas casser les call sites
* externes et pour documenter la bascule. Rework V3 (player anim
* natives RIG) : voir V3-REW-01 dans {@code docs/plans/rig/V3_REWORK_BACKLOG.md}.</p>
*/
public static void init() {
LOGGER.info("BondageAnimationManager initializing...");
// Context layer: lower priority = evaluated first, overridable by item layer.
// In AnimationStack, layers are sorted ascending by priority and evaluated in order.
// Higher priority layers override lower ones.
PlayerAnimationFactory.ANIMATION_DATA_FACTORY.registerFactory(
CONTEXT_FACTORY_ID,
CONTEXT_LAYER_PRIORITY,
player -> new ModifierLayer<>()
);
// Item layer: higher priority = evaluated last, overrides context layer
PlayerAnimationFactory.ANIMATION_DATA_FACTORY.registerFactory(
FACTORY_ID,
ITEM_LAYER_PRIORITY,
player -> new ModifierLayer<>()
);
// Furniture layer: highest priority = overrides item layer on blocked bones.
// Non-blocked bones are disabled via FurnitureAnimationContext so items
// can still animate free regions (gag, blindfold, etc.).
PlayerAnimationFactory.ANIMATION_DATA_FACTORY.registerFactory(
FURNITURE_FACTORY_ID,
FURNITURE_LAYER_PRIORITY,
player -> new ModifierLayer<>()
);
LOGGER.info(
"BondageAnimationManager: Factories registered — context (pri {}), item (pri {}), furniture (pri {})",
CONTEXT_LAYER_PRIORITY,
ITEM_LAYER_PRIORITY,
FURNITURE_LAYER_PRIORITY
"BondageAnimationManager: player-side factories no-op (Phase 2.8 RIG cleanup). " +
"NPC-side animation stack access untouched."
);
}

View File

@@ -3,29 +3,17 @@ package com.tiedup.remake.client.animation.tick;
import com.mojang.logging.LogUtils;
import com.tiedup.remake.client.animation.AnimationStateRegistry;
import com.tiedup.remake.client.animation.BondageAnimationManager;
import com.tiedup.remake.client.animation.PendingAnimationManager;
import com.tiedup.remake.client.animation.context.AnimationContext;
import com.tiedup.remake.client.animation.context.AnimationContextResolver;
import com.tiedup.remake.client.animation.context.RegionBoneMapper;
import com.tiedup.remake.client.events.CellHighlightHandler;
import com.tiedup.remake.client.events.LeashProxyClientHandler;
import com.tiedup.remake.client.gltf.GltfAnimationApplier;
import com.tiedup.remake.client.state.ClothesClientCache;
import com.tiedup.remake.client.state.MovementStyleClientState;
import com.tiedup.remake.client.state.PetBedClientState;
import com.tiedup.remake.util.HumanChairHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.v2.bondage.IV2BondageEquipment;
import com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper;
import com.tiedup.remake.v2.bondage.movement.MovementStyle;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.event.TickEvent;
@@ -35,16 +23,29 @@ import net.minecraftforge.fml.common.Mod;
import org.slf4j.Logger;
/**
* Event handler for player animation tick updates.
* Event handler for animation tick updates.
*
* <p>Simplified handler that:
* <p><b>Phase 2.8 RIG cleanup</b> : le ticking <i>player</i> V2 (boucle
* {@code mc.level.players()} + {@code updatePlayerAnimation}) est entièrement
* désactivé. Les joueurs sont désormais pilotés par
* {@link com.tiedup.remake.rig.tick.RigAnimationTickHandler} via le pipeline
* RIG (capability {@code LivingEntityPatch} + {@code Animator} natif EF).
* Les features V2 qui dépendaient du tick player sont trackées dans
* {@code docs/plans/rig/V3_REWORK_BACKLOG.md} (V3-REW-01/02/03/07).</p>
*
* <p>Restent actifs ici :
* <ul>
* <li>Tracks tied/struggling/sneaking state for players</li>
* <li>Plays animations via BondageAnimationManager when state changes</li>
* <li>Handles cleanup on logout/world unload</li>
* <li>Nettoyage périodique de {@code ClothesClientCache} (cache remote
* players, hygiène mémoire indépendante du pipeline de rendu)</li>
* <li>Cleanup logout / world unload (caches V2 encore utilisés par les
* NPCs ticked par {@link NpcAnimationTickHandler})</li>
* </ul>
*
* <p>Registered on the FORGE event bus (not MOD bus).
* <p>Le ticking NPC est assuré par {@link NpcAnimationTickHandler}. Ce
* handler ne tick plus les NPCs directement — il ne gère que les hooks
* lifecycle globaux (logout + world unload).</p>
*
* <p>Registered on the FORGE event bus (not MOD bus).</p>
*/
@Mod.EventBusSubscriber(
modid = "tiedup",
@@ -83,8 +84,20 @@ public class AnimationTickHandler {
}
/**
* Client tick event - called every tick on the client.
* Updates animations for all players when their bondage state changes.
* Client tick event called every tick on the client.
*
* <p>Phase 2.8 : la boucle {@code mc.level.players()} qui appelait
* {@code updatePlayerAnimation}, {@code tickFurnitureSafety} et le
* cold-cache retry furniture a été entièrement supprimée. Les joueurs
* sont désormais ticked par {@link com.tiedup.remake.rig.tick.RigAnimationTickHandler}
* via le pipeline RIG (capability {@code LivingEntityPatch} +
* {@code Animator}). Les régressions visuelles (V2 bondage layer cassé,
* furniture seat pose sur joueur cassée, pet bed pose cassée) sont
* listées dans {@code docs/plans/rig/V3_REWORK_BACKLOG.md}.</p>
*
* <p>Seul le nettoyage périodique de {@link ClothesClientCache} reste
* — c'est de l'hygiène mémoire sur un cache indexé UUID joueur,
* indépendant du pipeline de rendu.</p>
*/
@SubscribeEvent
public static void onClientTick(TickEvent.ClientTickEvent event) {
@@ -97,193 +110,17 @@ public class AnimationTickHandler {
return;
}
// Process pending animations first (retry failed animations for remote players)
PendingAnimationManager.processPending(mc.level);
// Periodic cleanup of stale cache entries (every 60 seconds = 1200 ticks)
// Periodic cleanup of stale clothes cache entries (every 60 seconds = 1200 ticks).
// Indépendant du rendu V2/RIG — c'est juste un cache UUID→ClothesData qui
// doit libérer la mémoire des joueurs déconnectés depuis >5min.
if (++cleanupTickCounter >= 1200) {
cleanupTickCounter = 0;
ClothesClientCache.cleanupStale();
}
// Then update all player animations
for (Player player : mc.level.players()) {
if (player instanceof AbstractClientPlayer clientPlayer) {
updatePlayerAnimation(clientPlayer);
}
// Safety: remove stale furniture animations for players no longer on seats
BondageAnimationManager.tickFurnitureSafety(player);
// Cold-cache retry: if the player is seated on furniture but has no
// active pose (GLB was not yet loaded at mount time, or the GLB cache
// entry was a transient failure), retry until the cache warms.
// FurnitureGltfCache memoizes failures via Optional.empty(), so
// retries after a genuine parse failure return instantly with no
// reparse. Bounded at MAX_FURNITURE_RETRIES so a legacy V1-only
// GLB (no Player_* armature → seatSkeleton==null → no animation
// ever possible) doesn't spam retries at 20 Hz forever.
// Single read of getVehicle() — avoids a re-read where the
// vehicle could change between instanceof and cast.
com.tiedup.remake.v2.furniture.EntityFurniture furniture =
player.getVehicle() instanceof
com.tiedup.remake.v2.furniture.EntityFurniture f ? f : null;
boolean hasAnim = BondageAnimationManager.hasFurnitureAnimation(
player
);
UUID playerUuid = player.getUUID();
if (furniture != null && !hasAnim) {
int retries = furnitureRetryCounters.getOrDefault(
playerUuid,
0
);
if (retries < MAX_FURNITURE_RETRIES) {
furnitureRetryCounters.put(playerUuid, retries + 1);
com.tiedup.remake.v2.furniture.client.FurnitureClientAnimator
.start(furniture, player);
if (retries + 1 == MAX_FURNITURE_RETRIES) {
LOGGER.debug(
"[FurnitureAnim] Giving up on furniture animation retry for {} after {} attempts — GLB likely has no Player_* armature.",
player.getName().getString(),
MAX_FURNITURE_RETRIES
);
}
}
} else {
// Dismounted or successfully applied — drop the counter so a
// later re-mount starts fresh.
furnitureRetryCounters.remove(playerUuid);
}
}
}
/**
* Update animation for a single player.
*/
private static void updatePlayerAnimation(AbstractClientPlayer player) {
// Safety check: skip for removed/dead players
if (player.isRemoved() || !player.isAlive()) {
return;
}
PlayerBindState state = PlayerBindState.getInstance(player);
UUID uuid = player.getUUID();
// Check if player has ANY V2 bondage item equipped (not just ARMS).
// isTiedUp() only checks ARMS, but items on LEGS, HEAD, etc. also need animation.
boolean isTied =
state != null &&
(state.isTiedUp() || V2EquipmentHelper.hasAnyEquipment(player));
boolean wasTied =
AnimationStateRegistry.getLastTiedState().getOrDefault(uuid, false);
// Pet bed animations take priority over bondage animations
if (PetBedClientState.get(uuid) != 0) {
// Lock body rotation to bed facing (prevents camera from rotating the model)
float lockedRot = PetBedClientState.getFacing(uuid);
player.yBodyRot = lockedRot;
player.yBodyRotO = lockedRot;
// Clamp head rotation to ±50° from body (like vehicle)
float headRot = player.getYHeadRot();
float clamped =
lockedRot +
net.minecraft.util.Mth.clamp(
net.minecraft.util.Mth.wrapDegrees(headRot - lockedRot),
-50f,
50f
);
player.setYHeadRot(clamped);
player.yHeadRotO = clamped;
AnimationStateRegistry.getLastTiedState().put(uuid, isTied);
return;
}
// Human chair: clamp 1st-person camera only (body lock handled by MixinLivingEntityBodyRot)
// NO return — animation HUMAN_CHAIR must continue playing below
if (isTied && state != null) {
ItemStack chairBind = state.getEquipment(BodyRegionV2.ARMS);
if (HumanChairHelper.isActive(chairBind)) {
// 1st person only: clamp yRot so player can't look behind
// 3rd person: yRot untouched → camera orbits freely 360°
if (
player == Minecraft.getInstance().player &&
Minecraft.getInstance().options.getCameraType() ==
net.minecraft.client.CameraType.FIRST_PERSON
) {
float lockedRot = HumanChairHelper.getFacing(chairBind);
float camClamped =
lockedRot +
net.minecraft.util.Mth.clamp(
net.minecraft.util.Mth.wrapDegrees(
player.getYRot() - lockedRot
),
-90f,
90f
);
player.setYRot(camClamped);
player.yRotO =
lockedRot +
net.minecraft.util.Mth.clamp(
net.minecraft.util.Mth.wrapDegrees(
player.yRotO - lockedRot
),
-90f,
90f
);
}
}
}
if (isTied) {
// Resolve V2 equipped items
IV2BondageEquipment equipment = V2EquipmentHelper.getEquipment(
player
);
Map<BodyRegionV2, ItemStack> equipped =
equipment != null ? equipment.getAllEquipped() : Map.of();
// Resolve ALL V2 items with GLB models and per-item bone ownership
java.util.List<RegionBoneMapper.V2ItemAnimInfo> v2Items =
RegionBoneMapper.resolveAllV2Items(equipped);
if (!v2Items.isEmpty()) {
// V2 path: multi-item composite animation
java.util.Set<String> allOwnedParts =
RegionBoneMapper.computeAllOwnedParts(v2Items);
MovementStyle activeStyle = MovementStyleClientState.get(
player.getUUID()
);
AnimationContext context = AnimationContextResolver.resolve(
player,
state,
activeStyle
);
GltfAnimationApplier.applyMultiItemV2Animation(
player,
v2Items,
context,
allOwnedParts
);
} else if (GltfAnimationApplier.hasActiveState(player)) {
// Clear any residual V2 composite animation when the player
// is still isTiedUp() but has no GLB-bearing items — e.g.
// a non-GLB item keeps the tied state, or a GLB item was
// removed while another V2 item remains on a non-animated
// region. Leaving the composite in place locks the arms in
// the pose of an item the player no longer wears.
GltfAnimationApplier.clearV2Animation(player);
}
} else if (wasTied) {
// Was tied, now free - stop all animations
if (GltfAnimationApplier.hasActiveState(player)) {
GltfAnimationApplier.clearV2Animation(player);
} else {
BondageAnimationManager.stopAnimation(player);
}
}
AnimationStateRegistry.getLastTiedState().put(uuid, isTied);
// Le tick per-player V2 (updatePlayerAnimation, tickFurnitureSafety,
// cold-cache furniture retry) est délégué à RigAnimationTickHandler
// Phase 2.7+. Rien à faire ici.
}
/**

View File

@@ -15,22 +15,14 @@ import net.minecraftforge.api.distmarker.OnlyIn;
* <li>Head yaw: convert to zRot (roll) since yRot axis is sideways</li>
* </ul>
*
* <h2>Architecture: Players vs NPCs</h2>
* <pre>
* ┌─────────────────────────────────────────────────────────────────┐
* │ PLAYERS │
* ├─────────────────────────────────────────────────────────────────┤
* │ 1. PlayerArmHideEventHandler.onRenderPlayerPre() │
* │ - Offset vertical (-6 model units) │
* │ - Rotation Y lissée (dogPoseState tracking) │
* │ │
* │ 2. Animation (PlayerAnimator) │
* │ - body.pitch = -90° → appliqué au PoseStack automatiquement │
* │ │
* │ 3. MixinPlayerModel.setupAnim() @TAIL │
* │ - Uses DogPoseHelper.applyHeadCompensationClamped() │
* └─────────────────────────────────────────────────────────────────┘
* <h2>Architecture — NPCs only (Phase 2.8 RIG cleanup)</h2>
* <p>Le path PLAYER (ex-{@code MixinPlayerModel.setupAnim @TAIL}) a été retiré
* Phase 2.8 : le renderer RIG patched ne passe plus par {@code PlayerModel.setupAnim},
* donc le mixin devenait dead code. La compensation head dog pose sera ré-exprimée
* nativement en StaticAnimation {@code pose_dog.json} (cf. V3-REW-07 dans
* {@code docs/plans/rig/V3_REWORK_BACKLOG.md}).</p>
*
* <pre>
* ┌─────────────────────────────────────────────────────────────────┐
* │ NPCs │
* ├─────────────────────────────────────────────────────────────────┤
@@ -48,25 +40,13 @@ import net.minecraftforge.api.distmarker.OnlyIn;
* └─────────────────────────────────────────────────────────────────┘
* </pre>
*
* <h2>Key Differences</h2>
* <table>
* <tr><th>Aspect</th><th>Players</th><th>NPCs</th></tr>
* <tr><td>Rotation X application</td><td>Auto by PlayerAnimator</td><td>Manual in setupRotations()</td></tr>
* <tr><td>Rotation Y smoothing</td><td>PlayerArmHideEventHandler</td><td>EntityDamsel.tick() via RotationSmoother</td></tr>
* <tr><td>Head compensation</td><td>MixinPlayerModel</td><td>DamselModel.setupAnim()</td></tr>
* <tr><td>Reset body.xRot</td><td>Not needed</td><td>Yes (prevents double rotation)</td></tr>
* <tr><td>Vertical offset</td><td>-6 model units</td><td>-7 model units</td></tr>
* </table>
*
* <h2>Usage</h2>
* <p>Used by:
* <ul>
* <li>MixinPlayerModel - for player head compensation</li>
* <li>DamselModel - for NPC head compensation</li>
* </ul>
*
* @see RotationSmoother for Y rotation smoothing
* @see com.tiedup.remake.mixin.client.MixinPlayerModel
* @see com.tiedup.remake.client.model.DamselModel
*/
@OnlyIn(Dist.CLIENT)

View File

@@ -196,9 +196,11 @@ public class TiedUpMod {
// RIG Phase 2 — override client dispatch PLAYER → Local/Client/ServerPlayerPatch
com.tiedup.remake.rig.patch.EntityPatchProvider.registerEntityPatchesClient();
// Initialize unified BondageAnimationManager
com.tiedup.remake.client.animation.BondageAnimationManager.init();
LOGGER.info("BondageAnimationManager initialized");
// Phase 2.8 RIG cleanup : BondageAnimationManager.init() (factory
// registrations PlayerAnimator côté joueur) a été supprimé — le RIG
// prend le relai pour les joueurs via RigAnimationTickHandler.
// Les NPCs continuent d'être animés via BondageAnimationManager en
// accès direct animation stack (cf. NpcAnimationTickHandler).
// Initialize OBJ model registry for 3D bondage items
com.tiedup.remake.client.renderer.obj.ObjModelRegistry.init();

View File

@@ -1,83 +0,0 @@
package com.tiedup.remake.mixin.client;
import com.tiedup.remake.client.animation.render.DogPoseRenderHandler;
import com.tiedup.remake.client.animation.util.DogPoseHelper;
import com.tiedup.remake.items.base.PoseType;
import com.tiedup.remake.v2.bondage.PoseTypeHelper;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.v2.BodyRegionV2;
import net.minecraft.client.model.PlayerModel;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/**
* Mixin for PlayerModel to handle DOG pose head adjustments.
*
* When in DOG pose (body horizontal):
* - Head pitch offset so player looks forward
* - Head yaw converted to zRot (roll) since yRot axis is sideways when body is horizontal
*/
@Mixin(PlayerModel.class)
public class MixinPlayerModel {
@Inject(method = "setupAnim", at = @At("TAIL"))
private void tiedup$adjustDogPose(
LivingEntity entity,
float limbSwing,
float limbSwingAmount,
float ageInTicks,
float netHeadYaw,
float headPitch,
CallbackInfo ci
) {
if (!(entity instanceof AbstractClientPlayer player)) {
return;
}
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null) {
return;
}
ItemStack bind = state.getEquipment(BodyRegionV2.ARMS);
if (bind.isEmpty()) {
return;
}
if (PoseTypeHelper.getPoseType(bind) != PoseType.DOG) {
return;
}
PlayerModel<?> model = (PlayerModel<?>) (Object) this;
// === HEAD ROTATION FOR HORIZONTAL BODY ===
// Body is at -90° pitch (horizontal, face down)
// We apply a rotation delta to the poseStack in PlayerArmHideEventHandler
// The head needs to compensate for this transformation
float rotationDelta = DogPoseRenderHandler.getAppliedRotationDelta(
player.getUUID()
);
boolean moving = DogPoseRenderHandler.isDogPoseMoving(player.getUUID());
// netHeadYaw is head relative to vanilla body (yHeadRot - yBodyRot)
// We rotated the model by rotationDelta, so compensate:
// effectiveHeadYaw = netHeadYaw + rotationDelta
float headYaw = netHeadYaw + rotationDelta;
// Clamp based on movement state and apply head compensation
float maxYaw = moving ? 60f : 90f;
DogPoseHelper.applyHeadCompensationClamped(
model.head,
model.hat,
headPitch,
headYaw,
maxYaw
);
}
}

View File

@@ -9,7 +9,6 @@
"MixinLivingEntityBodyRot"
],
"client": [
"client/MixinPlayerModel",
"client/MixinCamera",
"client/MixinLivingEntitySleeping"
],