fix(animation): variant randomness no longer permanently cached + fix FullHead false-positive

Two fixes from the animation audit:

1. Variant cache key now includes the resolved animation name (e.g., Struggle.2).
   Previously, the cache key only used context+parts, so the first random variant
   pick was reused forever. Now each variant gets its own cache entry, and a fresh
   random pick happens each time the context changes.

2. FullHead check changed from contains("Head") to startsWith("gltf_FullHead")
   to prevent false positives on names like FullOverhead or FullAhead.
This commit is contained in:
NotEvil
2026-04-17 03:11:29 +02:00
parent e56e6dd551
commit a3287b7db8
2 changed files with 38 additions and 23 deletions

View File

@@ -126,11 +126,13 @@ public final class GltfAnimationApplier {
String enabledKey = canonicalPartsKey(ownership.enabledParts());
String partsKey = ownedKey + ";" + enabledKey;
// Build composite state key to avoid redundant updates
// Build composite state key to detect context changes.
// NOTE: This key does NOT include the variant name — that is resolved fresh
// each time the context changes, enabling random variant selection.
String stateKey = animSource + "|" + context.name() + "|" + partsKey;
String currentKey = activeStateKeys.get(entity.getUUID());
if (stateKey.equals(currentKey)) {
return true; // Already active, no-op
return true; // Same context, same parts — no need to re-resolve
}
// === Layer 1: Context animation (base body posture) ===
@@ -153,8 +155,7 @@ public final class GltfAnimationApplier {
return false;
}
KeyframeAnimation itemAnim = itemAnimCache.get(itemCacheKey);
if (itemAnim == null) {
// Resolve animation data first (needed for variant resolution)
GltfData animData = GlbAnimationResolver.resolveAnimationData(
modelLoc,
animationSource
@@ -168,11 +169,22 @@ public final class GltfAnimationApplier {
activeStateKeys.put(entity.getUUID(), stateKey);
return false;
}
// Resolve which named animation to use (with fallback chain + variant selection)
// Resolve which named animation to use (with fallback chain + variant selection).
// This must happen BEFORE the cache lookup because variant selection is random —
// we want a fresh random pick each time the context changes, not a permanently
// cached first pick.
String glbAnimName = GlbAnimationResolver.resolve(
animData,
context
);
// Include the resolved animation name in the cache key so different variants
// (Struggle.1 vs Struggle.2) get separate cache entries.
String variantCacheKey = itemCacheKey + "#" + (glbAnimName != null ? glbAnimName : "default");
KeyframeAnimation itemAnim = itemAnimCache.get(variantCacheKey);
if (itemAnim == null) {
// Pass both owned parts and enabled parts (owned + free) for selective enabling
itemAnim = GltfPoseConverter.convertSelective(
animData,
@@ -180,7 +192,7 @@ public final class GltfAnimationApplier {
ownership.thisParts(),
ownership.enabledParts()
);
itemAnimCache.put(itemCacheKey, itemAnim);
itemAnimCache.put(variantCacheKey, itemAnim);
}
BondageAnimationManager.playDirect(entity, itemAnim);

View File

@@ -587,7 +587,10 @@ public final class GltfPoseConverter {
// Head is protected by default — only enabled as a free bone when the animation
// name contains "Head" (e.g., FullHeadStruggle, FullHeadIdle).
// This lets artists opt-in per animation without affecting the item's regions.
boolean allowFreeHead = isFullBodyAnimation && animName.contains("Head");
// FullHead prefix (e.g., FullHeadStruggle) opts into head as a free bone.
// Use startsWith to avoid false positives (e.g., FullOverhead, FullAhead).
boolean allowFreeHead = isFullBodyAnimation &&
animName.startsWith("gltf_FullHead");
String[] allParts = {
"head",