package com.tiedup.remake.client.animation; import com.mojang.logging.LogUtils; 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.minecraftApi.PlayerAnimationRegistry; import java.util.Iterator; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.player.AbstractClientPlayer; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Player; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import org.slf4j.Logger; /** * Manages pending animations for remote players whose animation layers * may not be immediately available due to timing issues. * *

When a player is tied, the sync packet may arrive before the remote player's * animation layer is initialized by PlayerAnimator. This class queues failed * animation attempts and retries them each tick until success or timeout. * *

This follows the same pattern as SyncManager's pending queue for inventory sync. */ @OnlyIn(Dist.CLIENT) public class PendingAnimationManager { private static final Logger LOGGER = LogUtils.getLogger(); /** Pending animations waiting for layer initialization */ private static final Map pending = new ConcurrentHashMap<>(); /** Maximum retry attempts before giving up (~2 seconds at 20 ticks/sec) */ private static final int MAX_RETRIES = 40; /** * Queue a player's animation for retry. * Called when playAnimation fails due to null layer. * * @param uuid The player's UUID * @param animId The animation ID (without namespace) */ public static void queueForRetry(UUID uuid, String animId) { pending.compute(uuid, (k, existing) -> { if (existing == null) { LOGGER.debug( "Queued animation '{}' for retry on player {}", animId, uuid ); return new PendingEntry(animId, 0); } // Update animation ID but preserve retry count return new PendingEntry(animId, existing.retries); }); } /** * Remove a player from the pending queue. * Called when animation succeeds or player disconnects. * * @param uuid The player's UUID */ public static void remove(UUID uuid) { pending.remove(uuid); } /** * Check if a player has a pending animation. * * @param uuid The player's UUID * @return true if pending */ public static boolean hasPending(UUID uuid) { return pending.containsKey(uuid); } /** * Process pending animations. Called every tick from AnimationTickHandler. * Attempts to play queued animations and removes successful or expired entries. * * @param level The client level */ public static void processPending(ClientLevel level) { if (pending.isEmpty()) return; Iterator> it = pending .entrySet() .iterator(); while (it.hasNext()) { Map.Entry entry = it.next(); UUID uuid = entry.getKey(); PendingEntry pe = entry.getValue(); // Check expiration if (pe.retries >= MAX_RETRIES) { LOGGER.warn("Animation retry exhausted for player {}", uuid); it.remove(); continue; } // Try to find player and play animation Player player = level.getPlayerByUUID(uuid); if (player instanceof AbstractClientPlayer clientPlayer) { ModifierLayer layer = BondageAnimationManager.getPlayerLayerSafe(clientPlayer); if (layer != null) { ResourceLocation loc = ResourceLocation.fromNamespaceAndPath( "tiedup", pe.animId ); KeyframeAnimation anim = PlayerAnimationRegistry.getAnimation(loc); if (anim != null) { layer.setAnimation(new KeyframeAnimationPlayer(anim)); LOGGER.info( "Animation retry succeeded for {} after {} attempts", clientPlayer.getName().getString(), pe.retries ); it.remove(); continue; } } } // Increment retry count pending.put(uuid, new PendingEntry(pe.animId, pe.retries + 1)); } } /** * Clear all pending animations. * Called on world unload. */ public static void clearAll() { pending.clear(); LOGGER.debug("Cleared all pending animations"); } /** * Record to store pending animation data. */ private record PendingEntry(String animId, int retries) {} }