Clean repo for open source release
Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
This commit is contained in:
@@ -0,0 +1,737 @@
|
||||
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");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user