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) {}
}