Strip all Phase references, TODO/FUTURE roadmap notes, and internal planning comments from the codebase. Run Prettier for consistent formatting across all Java files.
750 lines
27 KiB
Java
750 lines
27 KiB
Java
package com.tiedup.remake.client.animation;
|
|
|
|
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.KeyframeAnimationPlayer;
|
|
import dev.kosmx.playerAnim.api.layered.ModifierLayer;
|
|
import dev.kosmx.playerAnim.core.data.KeyframeAnimation;
|
|
import dev.kosmx.playerAnim.impl.IAnimatedPlayer;
|
|
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationAccess;
|
|
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationFactory;
|
|
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
|
|
import java.util.Map;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import net.minecraft.client.player.AbstractClientPlayer;
|
|
import net.minecraft.resources.ResourceLocation;
|
|
import net.minecraft.world.entity.Entity;
|
|
import net.minecraft.world.entity.LivingEntity;
|
|
import net.minecraft.world.entity.player.Player;
|
|
import net.minecraftforge.api.distmarker.Dist;
|
|
import net.minecraftforge.api.distmarker.OnlyIn;
|
|
import org.slf4j.Logger;
|
|
|
|
/**
|
|
* Unified animation manager for bondage animations.
|
|
*
|
|
* <p>Handles both players and NPCs (any entity implementing IAnimatedPlayer).
|
|
* Uses PlayerAnimator library for smooth keyframe animations with bendy-lib support.
|
|
*
|
|
* <p>This replaces the previous split system:
|
|
* <ul>
|
|
* <li>PlayerAnimatorBridge (for players)</li>
|
|
* <li>DamselAnimationManager (for NPCs)</li>
|
|
* </ul>
|
|
*/
|
|
@OnlyIn(Dist.CLIENT)
|
|
public class BondageAnimationManager {
|
|
|
|
private static final Logger LOGGER = LogUtils.getLogger();
|
|
|
|
/** Cache of ModifierLayers for NPC entities (players use PlayerAnimationAccess) */
|
|
private static final Map<UUID, ModifierLayer<IAnimation>> npcLayers =
|
|
new ConcurrentHashMap<>();
|
|
|
|
/** Cache of context ModifierLayers for NPC entities */
|
|
private static final Map<UUID, ModifierLayer<IAnimation>> npcContextLayers =
|
|
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) */
|
|
private static final int CONTEXT_LAYER_PRIORITY = 40;
|
|
/** Priority for item animation layer (higher = overrides context layer) */
|
|
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.
|
|
* Must be called during client setup to register the player animation factory.
|
|
*/
|
|
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
|
|
);
|
|
}
|
|
|
|
// PLAY ANIMATION
|
|
|
|
/**
|
|
* Play an animation on any entity (player or NPC).
|
|
*
|
|
* @param entity The entity to animate
|
|
* @param animId Animation ID string (will be prefixed with "tiedup:" namespace)
|
|
* @return true if animation started successfully, false if layer not available
|
|
*/
|
|
public static boolean playAnimation(LivingEntity entity, String animId) {
|
|
ResourceLocation location = ResourceLocation.fromNamespaceAndPath(
|
|
"tiedup",
|
|
animId
|
|
);
|
|
return playAnimation(entity, location);
|
|
}
|
|
|
|
/**
|
|
* Play an animation on any entity (player or NPC).
|
|
*
|
|
* <p>If the animation layer is not available (e.g., remote player not fully
|
|
* initialized), the animation will be queued for retry via PendingAnimationManager.
|
|
*
|
|
* @param entity The entity to animate
|
|
* @param animId Full ResourceLocation of the animation
|
|
* @return true if animation started successfully, false if layer not available
|
|
*/
|
|
public static boolean playAnimation(
|
|
LivingEntity entity,
|
|
ResourceLocation animId
|
|
) {
|
|
if (entity == null || !entity.level().isClientSide()) {
|
|
return false;
|
|
}
|
|
|
|
KeyframeAnimation anim = PlayerAnimationRegistry.getAnimation(animId);
|
|
if (anim == null) {
|
|
// Try fallback: remove _sneak_ suffix if present
|
|
ResourceLocation fallbackId = tryFallbackAnimation(animId);
|
|
if (fallbackId != null) {
|
|
anim = PlayerAnimationRegistry.getAnimation(fallbackId);
|
|
if (anim != null) {
|
|
LOGGER.debug(
|
|
"Using fallback animation '{}' for missing '{}'",
|
|
fallbackId,
|
|
animId
|
|
);
|
|
}
|
|
}
|
|
if (anim == null) {
|
|
LOGGER.warn("Animation not found in registry: {}", animId);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
ModifierLayer<IAnimation> layer = getOrCreateLayer(entity);
|
|
if (layer != null) {
|
|
// Check if same animation is already playing
|
|
// Use reference comparison (==) instead of equals() because:
|
|
// 1. PlayerAnimationRegistry caches animations by ID
|
|
// 2. Same ID = same cached object reference
|
|
// 3. This avoids issues with KeyframeAnimation.equals() implementation
|
|
IAnimation current = layer.getAnimation();
|
|
if (current instanceof KeyframeAnimationPlayer player) {
|
|
if (player.getData() == anim) {
|
|
// Same animation already playing, don't reset
|
|
return true; // Still counts as success
|
|
}
|
|
}
|
|
layer.setAnimation(new KeyframeAnimationPlayer(anim));
|
|
|
|
// Remove from pending queue if it was waiting
|
|
PendingAnimationManager.remove(entity.getUUID());
|
|
|
|
LOGGER.debug(
|
|
"Playing animation '{}' on entity: {}",
|
|
animId,
|
|
entity.getUUID()
|
|
);
|
|
return true;
|
|
} else {
|
|
// Layer not available - queue for retry if it's a player
|
|
if (entity instanceof AbstractClientPlayer) {
|
|
PendingAnimationManager.queueForRetry(
|
|
entity.getUUID(),
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Play a pre-converted KeyframeAnimation directly on an entity, bypassing the registry.
|
|
* Used by GltfAnimationApplier for GLB-converted poses.
|
|
*
|
|
* @param entity The entity to animate
|
|
* @param anim The KeyframeAnimation to play
|
|
* @return true if animation started successfully
|
|
*/
|
|
public static boolean playDirect(
|
|
LivingEntity entity,
|
|
KeyframeAnimation anim
|
|
) {
|
|
if (entity == null || anim == null || !entity.level().isClientSide()) {
|
|
return false;
|
|
}
|
|
|
|
ModifierLayer<IAnimation> layer = getOrCreateLayer(entity);
|
|
if (layer != null) {
|
|
IAnimation current = layer.getAnimation();
|
|
if (current instanceof KeyframeAnimationPlayer player) {
|
|
if (player.getData() == anim) {
|
|
return true; // Same animation already playing
|
|
}
|
|
}
|
|
layer.setAnimation(new KeyframeAnimationPlayer(anim));
|
|
PendingAnimationManager.remove(entity.getUUID());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// STOP ANIMATION
|
|
|
|
/**
|
|
* Stop any currently playing animation on an entity.
|
|
*
|
|
* @param entity The entity
|
|
*/
|
|
public static void stopAnimation(LivingEntity entity) {
|
|
if (entity == null || !entity.level().isClientSide()) {
|
|
return;
|
|
}
|
|
|
|
ModifierLayer<IAnimation> layer = getLayer(entity);
|
|
if (layer != null) {
|
|
layer.setAnimation(null);
|
|
LOGGER.debug("Stopped animation on entity: {}", entity.getUUID());
|
|
}
|
|
}
|
|
|
|
// LAYER MANAGEMENT
|
|
|
|
/**
|
|
* Get the ModifierLayer for an entity (without creating).
|
|
*/
|
|
private static ModifierLayer<IAnimation> getLayer(LivingEntity entity) {
|
|
// Players: try PlayerAnimationAccess first, then cache
|
|
if (entity instanceof AbstractClientPlayer player) {
|
|
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());
|
|
}
|
|
|
|
/**
|
|
* Get or create the ModifierLayer for an entity.
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
private static ModifierLayer<IAnimation> getOrCreateLayer(
|
|
LivingEntity entity
|
|
) {
|
|
UUID uuid = entity.getUUID();
|
|
|
|
// Players: try factory-based access first, fallback to direct stack access
|
|
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;
|
|
});
|
|
}
|
|
}
|
|
|
|
// NPCs implementing IAnimatedPlayer: create/cache layer
|
|
if (entity instanceof IAnimatedPlayer animated) {
|
|
return npcLayers.computeIfAbsent(uuid, k -> {
|
|
ModifierLayer<IAnimation> newLayer = new ModifierLayer<>();
|
|
animated
|
|
.getAnimationStack()
|
|
.addAnimLayer(ITEM_LAYER_PRIORITY, newLayer);
|
|
LOGGER.debug("Created animation layer for NPC: {}", uuid);
|
|
return newLayer;
|
|
});
|
|
}
|
|
|
|
LOGGER.warn(
|
|
"Entity {} does not support animations (not a player or IAnimatedPlayer)",
|
|
uuid
|
|
);
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the animation layer for a player from PlayerAnimationAccess.
|
|
*/
|
|
@SuppressWarnings("unchecked")
|
|
private static ModifierLayer<IAnimation> getPlayerLayer(
|
|
AbstractClientPlayer player
|
|
) {
|
|
try {
|
|
return (ModifierLayer<
|
|
IAnimation
|
|
>) PlayerAnimationAccess.getPlayerAssociatedData(player).get(
|
|
FACTORY_ID
|
|
);
|
|
} catch (Exception e) {
|
|
LOGGER.error(
|
|
"Failed to get animation layer for player: {}",
|
|
player.getName().getString(),
|
|
e
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Safely get the animation layer for a player.
|
|
* Returns null if the layer is not yet initialized.
|
|
*
|
|
* <p>Public method for PendingAnimationManager to access.
|
|
* Checks both the factory-based layer and the NPC cache fallback.
|
|
*
|
|
* @param player The player
|
|
* @return The animation layer, or null if not available
|
|
*/
|
|
@javax.annotation.Nullable
|
|
public static ModifierLayer<IAnimation> getPlayerLayerSafe(
|
|
AbstractClientPlayer player
|
|
) {
|
|
// Try factory first
|
|
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)
|
|
|
|
/**
|
|
* 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.
|
|
* Uses CONTEXT_LAYER_PRIORITY, below the item layer at ITEM_LAYER_PRIORITY.
|
|
*/
|
|
@javax.annotation.Nullable
|
|
private static ModifierLayer<IAnimation> getOrCreateNpcContextLayer(
|
|
LivingEntity entity
|
|
) {
|
|
if (entity instanceof IAnimatedPlayer animated) {
|
|
return npcContextLayers.computeIfAbsent(entity.getUUID(), k -> {
|
|
ModifierLayer<IAnimation> layer = new ModifierLayer<>();
|
|
animated
|
|
.getAnimationStack()
|
|
.addAnimLayer(CONTEXT_LAYER_PRIORITY, layer);
|
|
return layer;
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Play a context animation on the context layer (lower priority).
|
|
* Context animations (sit, kneel, sneak) can be overridden by item animations
|
|
* on the main layer which has higher priority.
|
|
*
|
|
* @param entity The entity to animate
|
|
* @param anim The KeyframeAnimation to play on the context layer
|
|
* @return true if animation started successfully
|
|
*/
|
|
public static boolean playContext(
|
|
LivingEntity entity,
|
|
KeyframeAnimation anim
|
|
) {
|
|
if (entity == null || anim == null || !entity.level().isClientSide()) {
|
|
return false;
|
|
}
|
|
|
|
ModifierLayer<IAnimation> layer;
|
|
if (entity instanceof AbstractClientPlayer player) {
|
|
layer = getPlayerContextLayer(player);
|
|
} else {
|
|
layer = getOrCreateNpcContextLayer(entity);
|
|
}
|
|
|
|
if (layer != null) {
|
|
layer.setAnimation(new KeyframeAnimationPlayer(anim));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Stop the context layer animation.
|
|
*
|
|
* @param entity The entity whose context animation should stop
|
|
*/
|
|
public static void stopContext(LivingEntity entity) {
|
|
if (entity == null || !entity.level().isClientSide()) {
|
|
return;
|
|
}
|
|
|
|
ModifierLayer<IAnimation> layer;
|
|
if (entity instanceof AbstractClientPlayer player) {
|
|
layer = getPlayerContextLayer(player);
|
|
} else {
|
|
layer = npcContextLayers.get(entity.getUUID());
|
|
}
|
|
|
|
if (layer != null) {
|
|
layer.setAnimation(null);
|
|
}
|
|
}
|
|
|
|
// FURNITURE LAYER (highest priority, for seat poses)
|
|
|
|
/**
|
|
* Play a furniture animation on the furniture layer (highest priority).
|
|
*
|
|
* <p>The furniture layer sits above the item layer so it controls blocked-region
|
|
* bones. Non-blocked bones should already be disabled in the provided animation
|
|
* (via {@link com.tiedup.remake.v2.furniture.client.FurnitureAnimationContext#create}).
|
|
* This allows bondage items on free regions to still animate via the item layer.</p>
|
|
*
|
|
* @param player the player to animate
|
|
* @param animation the KeyframeAnimation from FurnitureAnimationContext
|
|
* @return true if animation started successfully
|
|
*/
|
|
public static boolean playFurniture(
|
|
Player player,
|
|
KeyframeAnimation animation
|
|
) {
|
|
if (
|
|
player == null ||
|
|
animation == null ||
|
|
!player.level().isClientSide()
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
ModifierLayer<IAnimation> layer = getFurnitureLayer(player);
|
|
if (layer != null) {
|
|
layer.setAnimation(new KeyframeAnimationPlayer(animation));
|
|
// 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;
|
|
}
|
|
|
|
/**
|
|
* Stop the furniture layer animation for a player.
|
|
*
|
|
* @param player the player whose furniture animation should stop
|
|
*/
|
|
public static void stopFurniture(Player player) {
|
|
if (player == null || !player.level().isClientSide()) {
|
|
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.
|
|
*
|
|
* @param player the player to check
|
|
* @return true if the furniture layer has an active animation
|
|
*/
|
|
public static boolean hasFurnitureAnimation(Player player) {
|
|
if (player == null || !player.level().isClientSide()) {
|
|
return false;
|
|
}
|
|
|
|
ModifierLayer<IAnimation> layer = getFurnitureLayer(player);
|
|
return layer != null && layer.getAnimation() != null;
|
|
}
|
|
|
|
/**
|
|
* Get the furniture ModifierLayer for a player.
|
|
* Uses PlayerAnimationAccess for local/factory-registered players,
|
|
* falls back to NPC cache for remote players.
|
|
*/
|
|
@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());
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
* {@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) {
|
|
if (player == null || !player.level().isClientSide()) {
|
|
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
|
|
|
|
/**
|
|
* Try to find a fallback animation ID when the requested one doesn't exist.
|
|
*
|
|
* <p>Fallback chain:
|
|
* <ol>
|
|
* <li>Remove _sneak_ suffix (sneak variants often missing)</li>
|
|
* <li>For sit_dog/kneel_dog variants, fall back to basic standing DOG</li>
|
|
* <li>For _arms_ variants, try FULL variant</li>
|
|
* </ol>
|
|
*
|
|
* @param originalId The original animation ID that wasn't found
|
|
* @return A fallback ResourceLocation to try, or null if no fallback
|
|
*/
|
|
@javax.annotation.Nullable
|
|
private static ResourceLocation tryFallbackAnimation(
|
|
ResourceLocation originalId
|
|
) {
|
|
String path = originalId.getPath();
|
|
String namespace = originalId.getNamespace();
|
|
|
|
// 1. Remove _sneak_ suffix
|
|
if (path.contains("_sneak_")) {
|
|
String fallback = path.replace("_sneak_", "_");
|
|
return ResourceLocation.fromNamespaceAndPath(namespace, fallback);
|
|
}
|
|
|
|
// 2. sit_dog_* / kneel_dog_* -> tied_up_dog_*
|
|
if (path.startsWith("sit_dog_") || path.startsWith("kneel_dog_")) {
|
|
String suffix = path.substring(path.lastIndexOf("_")); // _idle or _struggle
|
|
return ResourceLocation.fromNamespaceAndPath(
|
|
namespace,
|
|
"tied_up_dog" + suffix
|
|
);
|
|
}
|
|
|
|
// 3. _arms_ variants -> try FULL variant (remove _arms)
|
|
if (path.contains("_arms_")) {
|
|
String fallback = path.replace("_arms_", "_");
|
|
return ResourceLocation.fromNamespaceAndPath(namespace, fallback);
|
|
}
|
|
|
|
// 4. Struggle variants for free/legs -> idle variant
|
|
if (
|
|
(path.startsWith("sit_free_") ||
|
|
path.startsWith("kneel_free_") ||
|
|
path.startsWith("sit_legs_") ||
|
|
path.startsWith("kneel_legs_")) &&
|
|
path.endsWith("_struggle")
|
|
) {
|
|
String fallback = path.replace("_struggle", "_idle");
|
|
return ResourceLocation.fromNamespaceAndPath(namespace, fallback);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// CLEANUP
|
|
|
|
/**
|
|
* Clean up animation layer for an NPC when it's removed.
|
|
*
|
|
* @param entityId UUID of the removed entity
|
|
*/
|
|
/** All NPC layer caches, for bulk cleanup operations. */
|
|
private static final Map<UUID, ModifierLayer<IAnimation>>[] ALL_NPC_CACHES =
|
|
new Map[] { npcLayers, npcContextLayers, npcFurnitureLayers };
|
|
|
|
public static void cleanup(UUID entityId) {
|
|
for (Map<UUID, ModifierLayer<IAnimation>> cache : ALL_NPC_CACHES) {
|
|
ModifierLayer<IAnimation> layer = cache.remove(entityId);
|
|
if (layer != null) {
|
|
layer.setAnimation(null);
|
|
}
|
|
}
|
|
furnitureGraceTicks.remove(entityId);
|
|
LOGGER.debug("Cleaned up animation layers for entity: {}", entityId);
|
|
}
|
|
|
|
/**
|
|
* Clear all NPC animation layers.
|
|
* Should be called on world unload.
|
|
*/
|
|
public static void clearAll() {
|
|
for (Map<UUID, ModifierLayer<IAnimation>> cache : ALL_NPC_CACHES) {
|
|
cache.values().forEach(layer -> layer.setAnimation(null));
|
|
cache.clear();
|
|
}
|
|
furnitureGraceTicks.clear();
|
|
LOGGER.info("Cleared all NPC animation layers");
|
|
}
|
|
}
|