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,450 @@
|
||||
package com.tiedup.remake.client.gltf;
|
||||
|
||||
import com.tiedup.remake.client.animation.BondageAnimationManager;
|
||||
import com.tiedup.remake.client.animation.context.AnimationContext;
|
||||
import com.tiedup.remake.client.animation.context.ContextAnimationFactory;
|
||||
import com.tiedup.remake.client.animation.context.GlbAnimationResolver;
|
||||
import com.tiedup.remake.client.animation.context.RegionBoneMapper;
|
||||
import dev.kosmx.playerAnim.core.data.KeyframeAnimation;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.player.AbstractClientPlayer;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* V2 Animation Applier -- manages dual-layer animation for V2 bondage items.
|
||||
*
|
||||
* <p>Orchestrates two PlayerAnimator layers simultaneously:
|
||||
* <ul>
|
||||
* <li><b>Context layer</b> (priority 40): base body posture (stand/sit/kneel/sneak/walk)
|
||||
* with item-owned parts disabled, via {@link ContextAnimationFactory}</li>
|
||||
* <li><b>Item layer</b> (priority 42): per-item GLB animation with only owned bones enabled,
|
||||
* via {@link GltfPoseConverter#convertSelective}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Each equipped V2 item controls ONLY the bones matching its occupied body regions.
|
||||
* Bones not owned by any item pass through from the context layer, which provides the
|
||||
* appropriate base posture animation.
|
||||
*
|
||||
* <p>State tracking avoids redundant animation replays: a composite key of
|
||||
* {@code animSource|context|ownedParts} is compared per-entity to skip no-op updates.
|
||||
*
|
||||
* <p>Item animations are cached by {@code animSource#context#ownedParts} since the same
|
||||
* GLB + context + owned parts always produces the same KeyframeAnimation.
|
||||
*
|
||||
* @see ContextAnimationFactory
|
||||
* @see GlbAnimationResolver
|
||||
* @see GltfPoseConverter#convertSelective
|
||||
* @see BondageAnimationManager
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public final class GltfAnimationApplier {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("GltfPipeline");
|
||||
|
||||
/**
|
||||
* Cache of converted item-layer KeyframeAnimations.
|
||||
* Keyed by "animSource#context#ownedPartsHash".
|
||||
* Same GLB + same context + same owned parts = same KeyframeAnimation.
|
||||
*/
|
||||
private static final Map<String, KeyframeAnimation> itemAnimCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Track which composite state is currently active per entity, to avoid redundant replays.
|
||||
* Keyed by entity UUID, value is "animSource|context|sortedParts".
|
||||
*/
|
||||
private static final Map<UUID, String> activeStateKeys = new ConcurrentHashMap<>();
|
||||
|
||||
/** Track cache keys where GLB loading failed, to avoid per-tick retries. */
|
||||
private static final Set<String> failedLoadKeys = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private GltfAnimationApplier() {}
|
||||
|
||||
// ========================================
|
||||
// INIT (legacy)
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Legacy init method -- called by GltfClientSetup.
|
||||
* No-op: layer registration is handled by {@link BondageAnimationManager#init()}.
|
||||
*/
|
||||
public static void init() {
|
||||
// No-op: animation layers are managed by BondageAnimationManager
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// V2 DUAL-LAYER API
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Apply the full V2 animation state: context layer + item layer.
|
||||
*
|
||||
* <p>Flow:
|
||||
* <ol>
|
||||
* <li>Build a composite state key and skip if unchanged</li>
|
||||
* <li>Create/retrieve a context animation with disabledOnContext parts disabled,
|
||||
* play on context layer via {@link BondageAnimationManager#playContext}</li>
|
||||
* <li>Load the GLB (from {@code animationSource} or {@code modelLoc}),
|
||||
* resolve the named animation via {@link GlbAnimationResolver#resolve},
|
||||
* convert with selective parts via {@link GltfPoseConverter#convertSelective},
|
||||
* play on item layer via {@link BondageAnimationManager#playDirect}</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>The ownership model enables "free bone" animation: if a bone is not claimed
|
||||
* by any item, the winning item can animate it IF its GLB has keyframes for that bone.
|
||||
* This allows a straitjacket (ARMS+TORSO) to also animate free legs.</p>
|
||||
*
|
||||
* @param entity the entity to animate
|
||||
* @param modelLoc the item's GLB model (for mesh rendering, and default animation source)
|
||||
* @param animationSource separate GLB for animations (shared template), or null to use modelLoc
|
||||
* @param context current animation context (STAND_IDLE, SIT_IDLE, etc.)
|
||||
* @param ownership bone ownership: which parts this item owns vs other items
|
||||
* @return true if the item layer animation was applied successfully
|
||||
*/
|
||||
public static boolean applyV2Animation(LivingEntity entity, ResourceLocation modelLoc,
|
||||
@Nullable ResourceLocation animationSource,
|
||||
AnimationContext context, RegionBoneMapper.BoneOwnership ownership) {
|
||||
if (entity == null || modelLoc == null) return false;
|
||||
|
||||
ResourceLocation animSource = animationSource != null ? animationSource : modelLoc;
|
||||
// Cache key includes both owned and enabled parts for full disambiguation
|
||||
String ownedKey = canonicalPartsKey(ownership.thisParts());
|
||||
String enabledKey = canonicalPartsKey(ownership.enabledParts());
|
||||
String partsKey = ownedKey + ";" + enabledKey;
|
||||
|
||||
// Build composite state key to avoid redundant updates
|
||||
String stateKey = animSource + "|" + context.name() + "|" + partsKey;
|
||||
String currentKey = activeStateKeys.get(entity.getUUID());
|
||||
if (stateKey.equals(currentKey)) {
|
||||
return true; // Already active, no-op
|
||||
}
|
||||
|
||||
// === Layer 1: Context animation (base body posture) ===
|
||||
// Parts owned by ANY item (this or others) are disabled on the context layer.
|
||||
// Only free parts remain enabled on context.
|
||||
KeyframeAnimation contextAnim = ContextAnimationFactory.create(
|
||||
context, ownership.disabledOnContext());
|
||||
if (contextAnim != null) {
|
||||
BondageAnimationManager.playContext(entity, contextAnim);
|
||||
}
|
||||
|
||||
// === Layer 2: Item animation (GLB pose with selective bones) ===
|
||||
String itemCacheKey = buildItemCacheKey(animSource, context, partsKey);
|
||||
|
||||
// Skip if this GLB already failed to load
|
||||
if (failedLoadKeys.contains(itemCacheKey)) {
|
||||
activeStateKeys.put(entity.getUUID(), stateKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
KeyframeAnimation itemAnim = itemAnimCache.get(itemCacheKey);
|
||||
if (itemAnim == null) {
|
||||
GltfData animData = GlbAnimationResolver.resolveAnimationData(modelLoc, animationSource);
|
||||
if (animData == null) {
|
||||
LOGGER.warn("[GltfPipeline] Failed to load animation GLB: {}", animSource);
|
||||
failedLoadKeys.add(itemCacheKey);
|
||||
activeStateKeys.put(entity.getUUID(), stateKey);
|
||||
return false;
|
||||
}
|
||||
// Resolve which named animation to use (with fallback chain + variant selection)
|
||||
String glbAnimName = GlbAnimationResolver.resolve(animData, context);
|
||||
// Pass both owned parts and enabled parts (owned + free) for selective enabling
|
||||
itemAnim = GltfPoseConverter.convertSelective(
|
||||
animData, glbAnimName, ownership.thisParts(), ownership.enabledParts());
|
||||
itemAnimCache.put(itemCacheKey, itemAnim);
|
||||
}
|
||||
|
||||
BondageAnimationManager.playDirect(entity, itemAnim);
|
||||
|
||||
activeStateKeys.put(entity.getUUID(), stateKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply V2 animation from ALL equipped items simultaneously.
|
||||
*
|
||||
* <p>Each item contributes keyframes for only its owned bones into a shared
|
||||
* {@link KeyframeAnimation.AnimationBuilder}. The first item in the list (highest priority)
|
||||
* can additionally animate free bones if its GLB has keyframes for them.</p>
|
||||
*
|
||||
* @param entity the entity to animate
|
||||
* @param items resolved V2 items with per-item ownership, sorted by priority desc
|
||||
* @param context current animation context
|
||||
* @param allOwnedParts union of all owned parts across all items
|
||||
* @return true if the composite animation was applied
|
||||
*/
|
||||
public static boolean applyMultiItemV2Animation(LivingEntity entity,
|
||||
List<RegionBoneMapper.V2ItemAnimInfo> items,
|
||||
AnimationContext context, Set<String> allOwnedParts) {
|
||||
if (entity == null || items.isEmpty()) return false;
|
||||
|
||||
// Build composite state key
|
||||
StringBuilder keyBuilder = new StringBuilder();
|
||||
for (RegionBoneMapper.V2ItemAnimInfo item : items) {
|
||||
ResourceLocation src = item.animSource() != null ? item.animSource() : item.modelLoc();
|
||||
keyBuilder.append(src).append(':').append(canonicalPartsKey(item.ownedParts())).append(';');
|
||||
}
|
||||
keyBuilder.append(context.name());
|
||||
String stateKey = keyBuilder.toString();
|
||||
|
||||
String currentKey = activeStateKeys.get(entity.getUUID());
|
||||
if (stateKey.equals(currentKey)) {
|
||||
return true; // Already active
|
||||
}
|
||||
|
||||
// === Layer 1: Context animation ===
|
||||
KeyframeAnimation contextAnim = ContextAnimationFactory.create(context, allOwnedParts);
|
||||
if (contextAnim != null) {
|
||||
BondageAnimationManager.playContext(entity, contextAnim);
|
||||
}
|
||||
|
||||
// === Layer 2: Composite item animation ===
|
||||
String compositeCacheKey = "multi#" + stateKey;
|
||||
|
||||
if (failedLoadKeys.contains(compositeCacheKey)) {
|
||||
activeStateKeys.put(entity.getUUID(), stateKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
KeyframeAnimation compositeAnim = itemAnimCache.get(compositeCacheKey);
|
||||
if (compositeAnim == null) {
|
||||
KeyframeAnimation.AnimationBuilder builder =
|
||||
new KeyframeAnimation.AnimationBuilder(
|
||||
dev.kosmx.playerAnim.core.data.AnimationFormat.JSON_EMOTECRAFT);
|
||||
builder.beginTick = 0;
|
||||
builder.endTick = 1;
|
||||
builder.stopTick = 1;
|
||||
builder.isLooped = true;
|
||||
builder.returnTick = 0;
|
||||
builder.name = "gltf_composite";
|
||||
|
||||
boolean anyLoaded = false;
|
||||
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
RegionBoneMapper.V2ItemAnimInfo item = items.get(i);
|
||||
ResourceLocation animSource = item.animSource() != null ? item.animSource() : item.modelLoc();
|
||||
|
||||
GltfData animData = GlbAnimationResolver.resolveAnimationData(item.modelLoc(), item.animSource());
|
||||
if (animData == null) {
|
||||
LOGGER.warn("[GltfPipeline] Failed to load GLB for multi-item: {}", animSource);
|
||||
continue;
|
||||
}
|
||||
|
||||
String glbAnimName = GlbAnimationResolver.resolve(animData, context);
|
||||
GltfData.AnimationClip rawClip;
|
||||
if (glbAnimName != null) {
|
||||
rawClip = animData.getRawAnimation(glbAnimName);
|
||||
} else {
|
||||
rawClip = null;
|
||||
}
|
||||
if (rawClip == null) {
|
||||
rawClip = animData.rawGltfAnimation();
|
||||
}
|
||||
|
||||
// Compute effective parts: intersect animation_bones whitelist with ownedParts
|
||||
// if the item declares per-animation bone filtering.
|
||||
Set<String> effectiveParts = item.ownedParts();
|
||||
if (glbAnimName != null && !item.animationBones().isEmpty()) {
|
||||
Set<String> override = item.animationBones().get(glbAnimName);
|
||||
if (override != null) {
|
||||
Set<String> filtered = new HashSet<>(override);
|
||||
filtered.retainAll(item.ownedParts());
|
||||
if (!filtered.isEmpty()) {
|
||||
effectiveParts = filtered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GltfPoseConverter.addBonesToBuilder(
|
||||
builder, animData, rawClip, effectiveParts);
|
||||
anyLoaded = true;
|
||||
|
||||
LOGGER.debug("[GltfPipeline] Multi-item: {} -> owned={}, effective={}, anim={}",
|
||||
animSource, item.ownedParts(), effectiveParts, glbAnimName);
|
||||
}
|
||||
|
||||
if (!anyLoaded) {
|
||||
failedLoadKeys.add(compositeCacheKey);
|
||||
activeStateKeys.put(entity.getUUID(), stateKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enable only owned parts on the item layer.
|
||||
// Free parts (head, body, etc. not owned by any item) are disabled here
|
||||
// so they pass through to the context layer / vanilla animation.
|
||||
String[] allPartNames = {"head", "body", "rightArm", "leftArm", "rightLeg", "leftLeg"};
|
||||
for (String partName : allPartNames) {
|
||||
KeyframeAnimation.StateCollection part = getPartByName(builder, partName);
|
||||
if (part != null) {
|
||||
if (allOwnedParts.contains(partName)) {
|
||||
part.fullyEnablePart(false);
|
||||
} else {
|
||||
part.setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compositeAnim = builder.build();
|
||||
itemAnimCache.put(compositeCacheKey, compositeAnim);
|
||||
}
|
||||
|
||||
BondageAnimationManager.playDirect(entity, compositeAnim);
|
||||
activeStateKeys.put(entity.getUUID(), stateKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CLEAR / QUERY
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Clear all V2 animation layers from an entity and remove tracking.
|
||||
* Stops both the context layer and the item layer.
|
||||
*
|
||||
* @param entity the entity to clear animations from
|
||||
*/
|
||||
public static void clearV2Animation(LivingEntity entity) {
|
||||
if (entity == null) return;
|
||||
activeStateKeys.remove(entity.getUUID());
|
||||
BondageAnimationManager.stopContext(entity);
|
||||
BondageAnimationManager.stopAnimation(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has active V2 animation state.
|
||||
*
|
||||
* @param entity the entity to check
|
||||
* @return true if the entity has an active V2 animation state key
|
||||
*/
|
||||
public static boolean hasActiveState(LivingEntity entity) {
|
||||
return entity != null && activeStateKeys.containsKey(entity.getUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove tracking for an entity (e.g., on logout/unload).
|
||||
* Does NOT stop any currently playing animation -- use {@link #clearV2Animation} for that.
|
||||
*
|
||||
* @param entityId UUID of the entity to stop tracking
|
||||
*/
|
||||
public static void removeTracking(UUID entityId) {
|
||||
activeStateKeys.remove(entityId);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// CACHE MANAGEMENT
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Invalidate all cached item animations and tracking state.
|
||||
* Call this on resource reload (F3+T) to pick up changed GLB/JSON files.
|
||||
*
|
||||
* <p>Does NOT clear ContextAnimationFactory or ContextGlbRegistry here.
|
||||
* Those are cleared in the reload listener AFTER ContextGlbRegistry.reload()
|
||||
* to prevent the render thread from caching stale JSON fallbacks during
|
||||
* the window between clear and repopulate.</p>
|
||||
*/
|
||||
public static void invalidateCache() {
|
||||
itemAnimCache.clear();
|
||||
activeStateKeys.clear();
|
||||
failedLoadKeys.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all state (cache + tracking). Called on world unload.
|
||||
* Clears everything including context caches (no concurrent reload during unload).
|
||||
*/
|
||||
public static void clearAll() {
|
||||
itemAnimCache.clear();
|
||||
activeStateKeys.clear();
|
||||
failedLoadKeys.clear();
|
||||
com.tiedup.remake.client.animation.context.ContextGlbRegistry.clear();
|
||||
ContextAnimationFactory.clearCache();
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// LEGACY F9 DEBUG TOGGLE
|
||||
// ========================================
|
||||
|
||||
private static boolean debugEnabled = false;
|
||||
|
||||
/**
|
||||
* Toggle debug mode via F9 key.
|
||||
* When enabled, applies handcuffs V2 animation (rightArm + leftArm) to the local player
|
||||
* using STAND_IDLE context. When disabled, clears all V2 animation.
|
||||
*/
|
||||
public static void toggle() {
|
||||
debugEnabled = !debugEnabled;
|
||||
LOGGER.info("[GltfPipeline] Debug toggle: {}", debugEnabled ? "ON" : "OFF");
|
||||
|
||||
AbstractClientPlayer player = Minecraft.getInstance().player;
|
||||
if (player == null) return;
|
||||
|
||||
if (debugEnabled) {
|
||||
ResourceLocation modelLoc = ResourceLocation.fromNamespaceAndPath(
|
||||
"tiedup", "models/gltf/v2/handcuffs/cuffs_prototype.glb"
|
||||
);
|
||||
Set<String> armParts = Set.of("rightArm", "leftArm");
|
||||
RegionBoneMapper.BoneOwnership debugOwnership =
|
||||
new RegionBoneMapper.BoneOwnership(armParts, Set.of());
|
||||
applyV2Animation(player, modelLoc, null, AnimationContext.STAND_IDLE, debugOwnership);
|
||||
} else {
|
||||
clearV2Animation(player);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether F9 debug mode is currently enabled.
|
||||
*/
|
||||
public static boolean isEnabled() {
|
||||
return debugEnabled;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// INTERNAL
|
||||
// ========================================
|
||||
|
||||
/**
|
||||
* Build cache key for item-layer animations.
|
||||
* Format: "animSource#contextName#sortedParts"
|
||||
*/
|
||||
private static String buildItemCacheKey(ResourceLocation animSource,
|
||||
AnimationContext context, String partsKey) {
|
||||
return animSource + "#" + context.name() + "#" + partsKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a canonical, deterministic string from the owned parts set.
|
||||
* Sorted alphabetically and joined by comma — guarantees no hash collisions.
|
||||
*/
|
||||
private static String canonicalPartsKey(Set<String> ownedParts) {
|
||||
return String.join(",", new TreeSet<>(ownedParts));
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up an {@link KeyframeAnimation.StateCollection} by part name on a builder.
|
||||
*/
|
||||
private static KeyframeAnimation.StateCollection getPartByName(
|
||||
KeyframeAnimation.AnimationBuilder builder, String name) {
|
||||
return switch (name) {
|
||||
case "head" -> builder.head;
|
||||
case "body" -> builder.body;
|
||||
case "rightArm" -> builder.rightArm;
|
||||
case "leftArm" -> builder.leftArm;
|
||||
case "rightLeg" -> builder.rightLeg;
|
||||
case "leftLeg" -> builder.leftLeg;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user