Phase 2.8 review fixes : strip player fallback + backlog V3-REW-12-14 + QA edge cases

- BondageAnimationManager : strip total du path joueur, tous les call sites
  'player' retournent null/false avec WARN once-per-UUID. getOrCreateLayer
  court-circuite sur Player direct. Retire dead code factory/furniture
  (FACTORY_ID, FURNITURE_*, npcFurnitureLayers, getPlayerLayer, etc.).
  Javadoc init() reflete la semantique NPC-only.
- DogPoseHelper.applyHeadCompensationClamped : @Deprecated(since=2.8)
  pointant vers V3-REW-07 (dead apres retrait MixinPlayerModel).
- DogPoseRenderHandler.getAppliedRotationDelta + isDogPoseMoving :
  @Deprecated(since=2.8) meme raison.
- Docs (gitignored) : V3_REWORK_BACKLOG.md ajoute V3-REW-12/13/14 (pet bed
  body-lock, human chair yaw clamp, context layer sit/kneel/sneak), tableau
  recap 14 -> 17 items. PHASE2_QA.md ajoute sec 2.5 edge cases + corrige
  le grep pattern 4 -> >=4 lignes. PHASE0_DEGRADATIONS.md ajoute la section
  Phase 2.8 findings.

Compile GREEN. 20/20 tests rig GREEN. Net LOC src : -239 (strip dead code
+ guards player).

Call sites player no-op (attendu par design) :
- PacketSyncPetBedState.playAnimation -> V3-REW-12
- PacketPlayTestAnimation.playAnimation (debug) -> V3-REW-14
- FurnitureClientAnimator.playFurniture -> furniture seat rework V3
This commit is contained in:
notevil
2026-04-23 01:10:02 +02:00
parent 5a39fb0c1c
commit f80dc68c0b
3 changed files with 134 additions and 352 deletions

View File

@@ -1,13 +1,11 @@
package com.tiedup.remake.client.animation; package com.tiedup.remake.client.animation;
import com.mojang.logging.LogUtils; import com.mojang.logging.LogUtils;
import com.tiedup.remake.v2.furniture.ISeatProvider;
import dev.kosmx.playerAnim.api.layered.IAnimation; import dev.kosmx.playerAnim.api.layered.IAnimation;
import dev.kosmx.playerAnim.api.layered.KeyframeAnimationPlayer; import dev.kosmx.playerAnim.api.layered.KeyframeAnimationPlayer;
import dev.kosmx.playerAnim.api.layered.ModifierLayer; import dev.kosmx.playerAnim.api.layered.ModifierLayer;
import dev.kosmx.playerAnim.core.data.KeyframeAnimation; import dev.kosmx.playerAnim.core.data.KeyframeAnimation;
import dev.kosmx.playerAnim.impl.IAnimatedPlayer; import dev.kosmx.playerAnim.impl.IAnimatedPlayer;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationFactory; import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationFactory;
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry; import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
import java.util.Map; import java.util.Map;
@@ -15,7 +13,6 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.client.player.AbstractClientPlayer; import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.Dist;
@@ -39,81 +36,42 @@ public class BondageAnimationManager {
private static final Logger LOGGER = LogUtils.getLogger(); private static final Logger LOGGER = LogUtils.getLogger();
/** Cache of ModifierLayers for NPC entities (players use PlayerAnimationAccess) */ /** Cache of item-layer ModifierLayers for NPC entities. */
private static final Map<UUID, ModifierLayer<IAnimation>> npcLayers = private static final Map<UUID, ModifierLayer<IAnimation>> npcLayers =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
/** Cache of context ModifierLayers for NPC entities */ /** Cache of context-layer ModifierLayers for NPC entities. */
private static final Map<UUID, ModifierLayer<IAnimation>> npcContextLayers = private static final Map<UUID, ModifierLayer<IAnimation>> npcContextLayers =
new ConcurrentHashMap<>(); new ConcurrentHashMap<>();
/** Cache of furniture ModifierLayers for NPC entities */
private static final Map<
UUID,
ModifierLayer<IAnimation>
> npcFurnitureLayers = new ConcurrentHashMap<>();
/** Factory ID for PlayerAnimator item layer (players only) */
private static final ResourceLocation FACTORY_ID =
ResourceLocation.fromNamespaceAndPath("tiedup", "bondage");
/** Factory ID for PlayerAnimator context layer (players only) */
private static final ResourceLocation CONTEXT_FACTORY_ID =
ResourceLocation.fromNamespaceAndPath("tiedup", "bondage_context");
/** Factory ID for PlayerAnimator furniture layer (players only) */
private static final ResourceLocation FURNITURE_FACTORY_ID =
ResourceLocation.fromNamespaceAndPath("tiedup", "bondage_furniture");
/** Priority for context animation layer (lower = overridable by item layer) */ /** Priority for context animation layer (lower = overridable by item layer) */
private static final int CONTEXT_LAYER_PRIORITY = 40; private static final int CONTEXT_LAYER_PRIORITY = 40;
/** Priority for item animation layer (higher = overrides context layer) */ /** Priority for item animation layer (higher = overrides context layer) */
private static final int ITEM_LAYER_PRIORITY = 42; private static final int ITEM_LAYER_PRIORITY = 42;
/**
* Priority for furniture animation layer (highest = overrides item layer on blocked bones).
* Non-blocked bones are disabled so items can still animate them via the item layer.
*/
private static final int FURNITURE_LAYER_PRIORITY = 43;
/** Number of ticks to wait before removing a stale furniture animation. */
private static final int FURNITURE_GRACE_TICKS = 3;
/**
* Tracks ticks since a player with an active furniture animation stopped riding
* an ISeatProvider. After {@link #FURNITURE_GRACE_TICKS}, the animation is removed
* to prevent stuck poses from entity death or network issues.
*
* <p>Uses ConcurrentHashMap for safe access from both client tick and render thread.</p>
*/
private static final Map<UUID, Integer> furnitureGraceTicks =
new ConcurrentHashMap<>();
/** /**
* Initialize the animation system. * Initialize the animation system.
* *
* <p>Phase 2.8 RIG cleanup — les 3 {@link PlayerAnimationFactory} * <p><b>Pipeline NPC-only</b> — depuis Phase 2.7, les joueurs sont tickés par
* (context / item / furniture) ont été supprimées : le renderer RIG * {@code RigAnimationTickHandler} via le renderer RIG patched. Aucune
* patched ne passe plus par le pipeline PlayerAnimator pour le joueur, * {@link PlayerAnimationFactory} n'est enregistrée pour le joueur et tous
* donc les factories devenaient dead code (aucun call site n'atteint * les chemins joueur dans cette classe sont de short-circuits logués.</p>
* jamais la map associée côté joueur).</p>
* *
* <p>Les NPCs continuent d'être animés via cette classe : le chemin * <p>Cette classe reste active <b>uniquement pour les NPCs</b>
* {@link #getOrCreateLayer} pour les entités {@code IAnimatedPlayer} * (entités implémentant {@link IAnimatedPlayer} qui ne sont pas un
* non-joueur utilise {@code animated.getAnimationStack().addAnimLayer(...)} * {@link Player}) : {@link #getOrCreateLayer} leur crée un {@link ModifierLayer}
* en direct — ça ne dépend d'aucune factory. Voir {@code NpcAnimationTickHandler} * via accès direct au stack d'animation
* pour le consumer. Le path player dans {@link #getOrCreateLayer} est * ({@code animated.getAnimationStack().addAnimLayer(...)}) — ce path ne dépend
* laissé en place volontairement : il retombe proprement sur null * d'aucune factory. Consumer principal : {@code NpcAnimationTickHandler}.</p>
* (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 * <p>Conservé comme méthode publique pour ne pas casser les call sites
* externes et pour documenter la bascule. Rework V3 (player anim * externes. Rework V3 (player anim natives RIG) : voir V3-REW-01 dans
* natives RIG) : voir V3-REW-01 dans {@code docs/plans/rig/V3_REWORK_BACKLOG.md}.</p> * {@code docs/plans/rig/V3_REWORK_BACKLOG.md}.</p>
*/ */
public static void init() { public static void init() {
LOGGER.info( LOGGER.info(
"BondageAnimationManager: player-side factories no-op (Phase 2.8 RIG cleanup). " + "BondageAnimationManager: NPC-only pipeline (Phase 2.8 RIG cleanup). " +
"NPC-side animation stack access untouched." "Players handled by RigAnimationTickHandler; all player call sites no-op."
); );
} }
@@ -140,6 +98,11 @@ public class BondageAnimationManager {
* <p>If the animation layer is not available (e.g., remote player not fully * <p>If the animation layer is not available (e.g., remote player not fully
* initialized), the animation will be queued for retry via PendingAnimationManager. * initialized), the animation will be queued for retry via PendingAnimationManager.
* *
* <p><b>Phase 2.8</b> — les appels sur un {@link Player} sont no-op : le pipeline
* joueur est désormais RIG-native (voir {@link #init} Javadoc). Un WARN est logué
* une fois par UUID pour signaler les call sites stale qui devraient être purgés
* lors du rework V3.</p>
*
* @param entity The entity to animate * @param entity The entity to animate
* @param animId Full ResourceLocation of the animation * @param animId Full ResourceLocation of the animation
* @return true if animation started successfully, false if layer not available * @return true if animation started successfully, false if layer not available
@@ -152,6 +115,12 @@ public class BondageAnimationManager {
return false; return false;
} }
// Phase 2.8 : player path is dead. Log once per UUID and no-op.
if (entity instanceof Player player) {
logPlayerCallOnce(player, "playAnimation(" + animId + ")");
return false;
}
KeyframeAnimation anim = PlayerAnimationRegistry.getAnimation(animId); KeyframeAnimation anim = PlayerAnimationRegistry.getAnimation(animId);
if (anim == null) { if (anim == null) {
// Try fallback: remove _sneak_ suffix if present // Try fallback: remove _sneak_ suffix if present
@@ -188,7 +157,7 @@ public class BondageAnimationManager {
} }
layer.setAnimation(new KeyframeAnimationPlayer(anim)); layer.setAnimation(new KeyframeAnimationPlayer(anim));
// Remove from pending queue if it was waiting // Remove from pending queue if it was waiting (legacy, may still hold NPC entries)
PendingAnimationManager.remove(entity.getUUID()); PendingAnimationManager.remove(entity.getUUID());
LOGGER.debug( LOGGER.debug(
@@ -198,23 +167,11 @@ public class BondageAnimationManager {
); );
return true; return true;
} else { } else {
// Layer not available - queue for retry if it's a player LOGGER.warn(
if (entity instanceof AbstractClientPlayer) { "Animation layer is NULL for NPC: {} (type: {})",
PendingAnimationManager.queueForRetry( entity.getName().getString(),
entity.getUUID(), entity.getClass().getSimpleName()
animId.getPath() );
);
LOGGER.debug(
"Animation layer not ready for {}, queued for retry",
entity.getName().getString()
);
} else {
LOGGER.warn(
"Animation layer is NULL for NPC: {} (type: {})",
entity.getName().getString(),
entity.getClass().getSimpleName()
);
}
return false; return false;
} }
} }
@@ -235,6 +192,12 @@ public class BondageAnimationManager {
return false; return false;
} }
// Phase 2.8 : player path is dead.
if (entity instanceof Player player) {
logPlayerCallOnce(player, "playDirect");
return false;
}
ModifierLayer<IAnimation> layer = getOrCreateLayer(entity); ModifierLayer<IAnimation> layer = getOrCreateLayer(entity);
if (layer != null) { if (layer != null) {
IAnimation current = layer.getAnimation(); IAnimation current = layer.getAnimation();
@@ -262,6 +225,11 @@ public class BondageAnimationManager {
return; return;
} }
// Phase 2.8 : player path is dead — no layer to clear.
if (entity instanceof Player) {
return;
}
ModifierLayer<IAnimation> layer = getLayer(entity); ModifierLayer<IAnimation> layer = getLayer(entity);
if (layer != null) { if (layer != null) {
layer.setAnimation(null); layer.setAnimation(null);
@@ -273,56 +241,36 @@ public class BondageAnimationManager {
/** /**
* Get the ModifierLayer for an entity (without creating). * Get the ModifierLayer for an entity (without creating).
*
* <p>Phase 2.8 : returns {@code null} directly for any {@link Player} — the
* player animation pipeline is RIG-native, this manager only tracks NPCs.</p>
*/ */
private static ModifierLayer<IAnimation> getLayer(LivingEntity entity) { private static ModifierLayer<IAnimation> getLayer(LivingEntity entity) {
// Players: try PlayerAnimationAccess first, then cache if (entity instanceof Player) {
if (entity instanceof AbstractClientPlayer player) { return null;
ModifierLayer<IAnimation> factoryLayer = getPlayerLayer(player);
if (factoryLayer != null) {
return factoryLayer;
}
// Check cache (for remote players using fallback)
return npcLayers.get(entity.getUUID());
} }
// NPCs: use cache
return npcLayers.get(entity.getUUID()); return npcLayers.get(entity.getUUID());
} }
/** /**
* Get or create the ModifierLayer for an entity. * Get or create the ModifierLayer for an entity.
*
* <p>Phase 2.8 : returns {@code null} directly for any {@link Player} — the
* player fallback via {@code IAnimatedPlayer.getAnimationStack()} has been
* retired because it was partially alive (FP vanilla render consumed it,
* TP RIG override bypassed it), producing a confusing behavior split. All
* player anim needs are now handled by {@code RigAnimationTickHandler}.</p>
*/ */
@SuppressWarnings("unchecked")
private static ModifierLayer<IAnimation> getOrCreateLayer( private static ModifierLayer<IAnimation> getOrCreateLayer(
LivingEntity entity LivingEntity entity
) { ) {
UUID uuid = entity.getUUID(); // Phase 2.8 : strip player path entirely (no partially-alive fallback).
if (entity instanceof Player) {
// Players: try factory-based access first, fallback to direct stack access return null;
if (entity instanceof AbstractClientPlayer player) {
// Try the registered factory first (works for local player)
ModifierLayer<IAnimation> factoryLayer = getPlayerLayer(player);
if (factoryLayer != null) {
return factoryLayer;
}
// Fallback for remote players: use direct stack access like NPCs
// This handles cases where the factory data isn't available
if (player instanceof IAnimatedPlayer animated) {
return npcLayers.computeIfAbsent(uuid, k -> {
ModifierLayer<IAnimation> newLayer = new ModifierLayer<>();
animated
.getAnimationStack()
.addAnimLayer(ITEM_LAYER_PRIORITY, newLayer);
LOGGER.info(
"Created animation layer for remote player via stack: {}",
player.getName().getString()
);
return newLayer;
});
}
} }
UUID uuid = entity.getUUID();
// NPCs implementing IAnimatedPlayer: create/cache layer // NPCs implementing IAnimatedPlayer: create/cache layer
if (entity instanceof IAnimatedPlayer animated) { if (entity instanceof IAnimatedPlayer animated) {
return npcLayers.computeIfAbsent(uuid, k -> { return npcLayers.computeIfAbsent(uuid, k -> {
@@ -342,87 +290,49 @@ public class BondageAnimationManager {
return null; return null;
} }
/** Per-player dedup set so we log the factory-access failure at most once per UUID. */ /** Per-player-UUID dedup so stale call sites log at most once per session. */
private static final java.util.Set<UUID> layerFailureLogged = private static final java.util.Set<UUID> playerCallLogged =
java.util.concurrent.ConcurrentHashMap.newKeySet(); java.util.concurrent.ConcurrentHashMap.newKeySet();
/** /**
* Get the animation layer for a player from PlayerAnimationAccess. * Log once per player UUID that a stale call site is invoking this manager.
* * Used by the player no-op short-circuits ({@link #playAnimation},
* <p>Throws during the factory-race window for remote players (the factory * {@link #playDirect}) to surface call sites that should be migrated to the
* hasn't yet initialized their associated data). This is the expected path * RIG pipeline (tracked in V3_REWORK_BACKLOG).
* for the {@link PendingAnimationManager} retry loop, so we log at DEBUG
* and at most once per UUID — a per-tick log would flood during busy
* multiplayer.</p>
*/ */
@SuppressWarnings("unchecked") private static void logPlayerCallOnce(Player player, String op) {
private static ModifierLayer<IAnimation> getPlayerLayer( if (playerCallLogged.add(player.getUUID())) {
AbstractClientPlayer player LOGGER.warn(
) { "BondageAnimationManager.{} called on player {} — no-op " +
try { "(RIG owns player anims since Phase 2.7). " +
return (ModifierLayer< "Migrate call site to RigAnimationTickHandler (V3 rework).",
IAnimation op,
>) PlayerAnimationAccess.getPlayerAssociatedData(player).get( player.getName().getString()
FACTORY_ID
); );
} catch (Exception e) {
if (layerFailureLogged.add(player.getUUID())) {
LOGGER.debug(
"Animation layer not yet available for player {} (will retry): {}",
player.getName().getString(),
e.toString()
);
}
return null;
} }
} }
/** /**
* Safely get the animation layer for a player. * Safely get the animation layer for a player.
* Returns null if the layer is not yet initialized.
* *
* <p>Public method for PendingAnimationManager to access. * <p>Phase 2.8 : always returns {@code null}. The player pipeline is
* Checks both the factory-based layer and the NPC cache fallback. * RIG-native; the {@link PendingAnimationManager} retry loop is no
* longer fed (player calls to {@link #playAnimation} short-circuit
* before queueing), so this getter is maintained only to preserve the
* public signature for external call sites.</p>
* *
* @param player The player * @param player The player (unused)
* @return The animation layer, or null if not available * @return always null in Phase 2.8+
*/ */
@javax.annotation.Nullable @javax.annotation.Nullable
public static ModifierLayer<IAnimation> getPlayerLayerSafe( public static ModifierLayer<IAnimation> getPlayerLayerSafe(
AbstractClientPlayer player AbstractClientPlayer player
) { ) {
// Try factory first return null;
ModifierLayer<IAnimation> factoryLayer = getPlayerLayer(player);
if (factoryLayer != null) {
return factoryLayer;
}
// Check NPC cache (for remote players using fallback path)
return npcLayers.get(player.getUUID());
} }
// CONTEXT LAYER (lower priority, for sit/kneel/sneak) // CONTEXT LAYER (lower priority, for sit/kneel/sneak)
/**
* Get the context animation layer for a player from PlayerAnimationAccess.
* Returns null if the layer is not yet initialized.
*/
@SuppressWarnings("unchecked")
@javax.annotation.Nullable
private static ModifierLayer<IAnimation> getPlayerContextLayer(
AbstractClientPlayer player
) {
try {
return (ModifierLayer<
IAnimation
>) PlayerAnimationAccess.getPlayerAssociatedData(player).get(
CONTEXT_FACTORY_ID
);
} catch (Exception e) {
return null;
}
}
/** /**
* Get or create the context animation layer for an NPC entity. * Get or create the context animation layer for an NPC entity.
* Uses CONTEXT_LAYER_PRIORITY, below the item layer at ITEM_LAYER_PRIORITY. * Uses CONTEXT_LAYER_PRIORITY, below the item layer at ITEM_LAYER_PRIORITY.
@@ -460,13 +370,14 @@ public class BondageAnimationManager {
return false; return false;
} }
ModifierLayer<IAnimation> layer; // Phase 2.8 : player context layer is dead (sit/kneel/sneak visuals
if (entity instanceof AbstractClientPlayer player) { // will be re-expressed as RIG StaticAnimations — cf. V3-REW-14).
layer = getPlayerContextLayer(player); if (entity instanceof Player player) {
} else { logPlayerCallOnce(player, "playContext");
layer = getOrCreateNpcContextLayer(entity); return false;
} }
ModifierLayer<IAnimation> layer = getOrCreateNpcContextLayer(entity);
if (layer != null) { if (layer != null) {
layer.setAnimation(new KeyframeAnimationPlayer(anim)); layer.setAnimation(new KeyframeAnimationPlayer(anim));
return true; return true;
@@ -484,13 +395,12 @@ public class BondageAnimationManager {
return; return;
} }
ModifierLayer<IAnimation> layer; // Phase 2.8 : player path is dead — no layer to clear.
if (entity instanceof AbstractClientPlayer player) { if (entity instanceof Player) {
layer = getPlayerContextLayer(player); return;
} else {
layer = npcContextLayers.get(entity.getUUID());
} }
ModifierLayer<IAnimation> layer = npcContextLayers.get(entity.getUUID());
if (layer != null) { if (layer != null) {
layer.setAnimation(null); layer.setAnimation(null);
} }
@@ -522,194 +432,46 @@ public class BondageAnimationManager {
return false; return false;
} }
ModifierLayer<IAnimation> layer = getOrCreateFurnitureLayer(player); // Phase 2.8 : player furniture seat pose is dead (will be ported to
if (layer != null) { // RIG StaticAnimations — cf. V3_REWORK_BACKLOG furniture seat entry).
layer.setAnimation(new KeyframeAnimationPlayer(animation)); logPlayerCallOnce(player, "playFurniture");
// Reset grace ticks since we just started/refreshed the animation
furnitureGraceTicks.remove(player.getUUID());
LOGGER.debug(
"Playing furniture animation on player: {}",
player.getName().getString()
);
return true;
}
LOGGER.warn(
"Furniture layer not available for player: {}",
player.getName().getString()
);
return false; return false;
} }
/** /**
* Stop the furniture layer animation for a player. * Stop the furniture layer animation for a player.
* *
* <p>Phase 2.8 : no-op — the player furniture layer is dead. Kept for
* signature compatibility with {@code EntityFurniture} cleanup call site.</p>
*
* @param player the player whose furniture animation should stop * @param player the player whose furniture animation should stop
*/ */
public static void stopFurniture(Player player) { public static void stopFurniture(Player player) {
if (player == null || !player.level().isClientSide()) { // Phase 2.8 : dead path. Retained signature for backward-compat.
return;
}
ModifierLayer<IAnimation> layer = getFurnitureLayer(player);
if (layer != null) {
layer.setAnimation(null);
}
furnitureGraceTicks.remove(player.getUUID());
LOGGER.debug(
"Stopped furniture animation on player: {}",
player.getName().getString()
);
} }
/** /**
* Check whether a player currently has an active furniture animation. * Check whether a player currently has an active furniture animation.
* *
* <p>Phase 2.8 : always returns {@code false} — player furniture layer is dead.</p>
*
* @param player the player to check * @param player the player to check
* @return true if the furniture layer has an active animation * @return always false in Phase 2.8+
*/ */
public static boolean hasFurnitureAnimation(Player player) { public static boolean hasFurnitureAnimation(Player player) {
if (player == null || !player.level().isClientSide()) { return false;
return false;
}
ModifierLayer<IAnimation> layer = getFurnitureLayer(player);
return layer != null && layer.getAnimation() != null;
} }
/** /**
* Get the furniture ModifierLayer for a player (READ-ONLY). * Safety tick for furniture animations.
* Uses PlayerAnimationAccess for local/factory-registered players,
* falls back to NPC cache for remote players. Returns null if no layer
* has been created yet — callers that need to guarantee a layer should use
* {@link #getOrCreateFurnitureLayer}.
*/
@SuppressWarnings("unchecked")
@javax.annotation.Nullable
private static ModifierLayer<IAnimation> getFurnitureLayer(Player player) {
if (player instanceof AbstractClientPlayer clientPlayer) {
try {
ModifierLayer<IAnimation> layer = (ModifierLayer<
IAnimation
>) PlayerAnimationAccess.getPlayerAssociatedData(
clientPlayer
).get(FURNITURE_FACTORY_ID);
if (layer != null) {
return layer;
}
} catch (Exception e) {
// Fall through to NPC cache
}
// Fallback for remote players: check NPC furniture cache
return npcFurnitureLayers.get(player.getUUID());
}
// Non-player entities: use NPC cache
return npcFurnitureLayers.get(player.getUUID());
}
/**
* Get or create the furniture ModifierLayer for a player. Mirrors
* {@link #getOrCreateLayer} but for the FURNITURE layer priority.
* *
* <p>For the local player (factory-registered), returns the factory layer. * <p>Phase 2.8 : no-op — the player furniture layer is dead, nothing to
* For remote players, creates a new layer on first call and caches it in * guard. Kept as an empty stub in case older call sites remain.</p>
* {@link #npcFurnitureLayers} — remote players don't own a factory layer,
* so without a fallback they can't receive any furniture seat pose.</p>
*/
@SuppressWarnings("unchecked")
@javax.annotation.Nullable
private static ModifierLayer<IAnimation> getOrCreateFurnitureLayer(
Player player
) {
if (player instanceof AbstractClientPlayer clientPlayer) {
try {
ModifierLayer<IAnimation> layer = (ModifierLayer<
IAnimation
>) PlayerAnimationAccess.getPlayerAssociatedData(
clientPlayer
).get(FURNITURE_FACTORY_ID);
if (layer != null) {
return layer;
}
} catch (Exception e) {
// Fall through to fallback-create below.
}
// Remote players: fallback-create via the animation stack.
if (clientPlayer instanceof IAnimatedPlayer animated) {
return npcFurnitureLayers.computeIfAbsent(
clientPlayer.getUUID(),
k -> {
ModifierLayer<IAnimation> newLayer =
new ModifierLayer<>();
animated
.getAnimationStack()
.addAnimLayer(FURNITURE_LAYER_PRIORITY, newLayer);
LOGGER.debug(
"Created furniture animation layer for remote player via stack: {}",
clientPlayer.getName().getString()
);
return newLayer;
}
);
}
return npcFurnitureLayers.get(clientPlayer.getUUID());
}
// Non-player entities: use NPC cache (read-only; NPC furniture animation
// is not currently produced by this codebase).
return npcFurnitureLayers.get(player.getUUID());
}
/**
* Safety tick for furniture animations. Call once per client tick per player.
* *
* <p>If a player has an active furniture animation but is NOT riding an * @param player the player to check (unused)
* {@link ISeatProvider}, increment a grace counter. After
* {@link #FURNITURE_GRACE_TICKS} consecutive ticks without a seat, the
* animation is removed to prevent stuck poses from entity death, network
* desync, or teleportation.</p>
*
* <p>If the player IS riding an ISeatProvider, the counter is reset.</p>
*
* @param player the player to check
*/ */
public static void tickFurnitureSafety(Player player) { public static void tickFurnitureSafety(Player player) {
if (player == null || !player.level().isClientSide()) { // Phase 2.8 : dead path. Retained signature for backward-compat.
return;
}
if (!hasFurnitureAnimation(player)) {
// No furniture animation active, nothing to guard
furnitureGraceTicks.remove(player.getUUID());
return;
}
UUID uuid = player.getUUID();
// Check if the player is riding an ISeatProvider
Entity vehicle = player.getVehicle();
boolean ridingSeat = vehicle instanceof ISeatProvider;
if (ridingSeat) {
// Player is properly seated, reset grace counter
furnitureGraceTicks.remove(uuid);
} else {
// Player has furniture anim but no seat -- increment grace
int ticks = furnitureGraceTicks.merge(uuid, 1, Integer::sum);
if (ticks >= FURNITURE_GRACE_TICKS) {
LOGGER.info(
"Removing stale furniture animation for player {} " +
"(not riding ISeatProvider for {} ticks)",
player.getName().getString(),
ticks
);
stopFurniture(player);
}
}
} }
// FALLBACK ANIMATION HANDLING // FALLBACK ANIMATION HANDLING
@@ -778,8 +540,9 @@ public class BondageAnimationManager {
* @param entityId UUID of the removed entity * @param entityId UUID of the removed entity
*/ */
/** All NPC layer caches, for bulk cleanup operations. */ /** All NPC layer caches, for bulk cleanup operations. */
@SuppressWarnings({ "unchecked", "rawtypes" })
private static final Map<UUID, ModifierLayer<IAnimation>>[] ALL_NPC_CACHES = private static final Map<UUID, ModifierLayer<IAnimation>>[] ALL_NPC_CACHES =
new Map[] { npcLayers, npcContextLayers, npcFurnitureLayers }; new Map[] { npcLayers, npcContextLayers };
public static void cleanup(UUID entityId) { public static void cleanup(UUID entityId) {
for (Map<UUID, ModifierLayer<IAnimation>> cache : ALL_NPC_CACHES) { for (Map<UUID, ModifierLayer<IAnimation>> cache : ALL_NPC_CACHES) {
@@ -788,8 +551,7 @@ public class BondageAnimationManager {
layer.setAnimation(null); layer.setAnimation(null);
} }
} }
furnitureGraceTicks.remove(entityId); playerCallLogged.remove(entityId);
layerFailureLogged.remove(entityId);
LOGGER.debug("Cleaned up animation layers for entity: {}", entityId); LOGGER.debug("Cleaned up animation layers for entity: {}", entityId);
} }
@@ -802,8 +564,7 @@ public class BondageAnimationManager {
cache.values().forEach(layer -> layer.setAnimation(null)); cache.values().forEach(layer -> layer.setAnimation(null));
cache.clear(); cache.clear();
} }
furnitureGraceTicks.clear(); playerCallLogged.clear();
layerFailureLogged.clear();
LOGGER.info("Cleared all NPC animation layers"); LOGGER.info("Cleared all NPC animation layers");
} }
} }

View File

@@ -52,8 +52,15 @@ public class DogPoseRenderHandler {
/** /**
* Get the rotation delta applied to a player's render for DOG pose. * Get the rotation delta applied to a player's render for DOG pose.
* Used by MixinPlayerModel to compensate head rotation. *
* @deprecated since Phase 2.8 — this getter fed {@code MixinPlayerModel}
* (removed Phase 2.8 RIG cleanup) so head rotation could be compensated
* against the body's -90° pitch. No remaining reader. To be deleted
* when V3-REW-07 re-expresses dog pose head compensation as a RIG
* {@code StaticAnimation pose_dog.json}. See
* {@code docs/plans/rig/V3_REWORK_BACKLOG.md#V3-REW-07}.
*/ */
@Deprecated(since = "2.8")
public static float getAppliedRotationDelta(UUID playerUuid) { public static float getAppliedRotationDelta(UUID playerUuid) {
float[] state = dogPoseState.get(playerUuid); float[] state = dogPoseState.get(playerUuid);
return state != null ? state[IDX_DELTA] : 0f; return state != null ? state[IDX_DELTA] : 0f;
@@ -61,7 +68,14 @@ public class DogPoseRenderHandler {
/** /**
* Check if a player is currently moving in DOG pose. * Check if a player is currently moving in DOG pose.
*
* @deprecated since Phase 2.8 — same cause as {@link #getAppliedRotationDelta}
* (fed {@code MixinPlayerModel}, now removed). To be deleted alongside
* V3-REW-07 when dog pose head compensation is re-expressed as a RIG
* {@code StaticAnimation pose_dog.json}. See
* {@code docs/plans/rig/V3_REWORK_BACKLOG.md#V3-REW-07}.
*/ */
@Deprecated(since = "2.8")
public static boolean isDogPoseMoving(UUID playerUuid) { public static boolean isDogPoseMoving(UUID playerUuid) {
float[] state = dogPoseState.get(playerUuid); float[] state = dogPoseState.get(playerUuid);
return state != null && state[IDX_MOVING] > 0.5f; return state != null && state[IDX_MOVING] > 0.5f;

View File

@@ -110,7 +110,14 @@ public final class DogPoseHelper {
* @param headPitch Player's up/down look angle in degrees * @param headPitch Player's up/down look angle in degrees
* @param headYaw Head yaw relative to body in degrees * @param headYaw Head yaw relative to body in degrees
* @param maxYaw Maximum allowed yaw angle in degrees * @param maxYaw Maximum allowed yaw angle in degrees
* @deprecated since Phase 2.8 — player dog pose head compensation was
* previously applied via {@code MixinPlayerModel.setupAnim @TAIL}
* (removed Phase 2.8 RIG cleanup). No remaining call site; retained
* only to preserve the API until V3-REW-07 re-expresses the behavior
* as a RIG {@code StaticAnimation pose_dog.json}. See
* {@code docs/plans/rig/V3_REWORK_BACKLOG.md#V3-REW-07}.
*/ */
@Deprecated(since = "2.8")
public static void applyHeadCompensationClamped( public static void applyHeadCompensationClamped(
ModelPart head, ModelPart head,
ModelPart hat, ModelPart hat,