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:
@@ -126,11 +126,13 @@ public final class GltfAnimationApplier {
|
|||||||
String enabledKey = canonicalPartsKey(ownership.enabledParts());
|
String enabledKey = canonicalPartsKey(ownership.enabledParts());
|
||||||
String partsKey = ownedKey + ";" + enabledKey;
|
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 stateKey = animSource + "|" + context.name() + "|" + partsKey;
|
||||||
String currentKey = activeStateKeys.get(entity.getUUID());
|
String currentKey = activeStateKeys.get(entity.getUUID());
|
||||||
if (stateKey.equals(currentKey)) {
|
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) ===
|
// === Layer 1: Context animation (base body posture) ===
|
||||||
@@ -153,26 +155,36 @@ public final class GltfAnimationApplier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
KeyframeAnimation itemAnim = itemAnimCache.get(itemCacheKey);
|
// Resolve animation data first (needed for variant resolution)
|
||||||
|
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).
|
||||||
|
// 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) {
|
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
|
// Pass both owned parts and enabled parts (owned + free) for selective enabling
|
||||||
itemAnim = GltfPoseConverter.convertSelective(
|
itemAnim = GltfPoseConverter.convertSelective(
|
||||||
animData,
|
animData,
|
||||||
@@ -180,7 +192,7 @@ public final class GltfAnimationApplier {
|
|||||||
ownership.thisParts(),
|
ownership.thisParts(),
|
||||||
ownership.enabledParts()
|
ownership.enabledParts()
|
||||||
);
|
);
|
||||||
itemAnimCache.put(itemCacheKey, itemAnim);
|
itemAnimCache.put(variantCacheKey, itemAnim);
|
||||||
}
|
}
|
||||||
|
|
||||||
BondageAnimationManager.playDirect(entity, itemAnim);
|
BondageAnimationManager.playDirect(entity, itemAnim);
|
||||||
|
|||||||
@@ -587,7 +587,10 @@ public final class GltfPoseConverter {
|
|||||||
// Head is protected by default — only enabled as a free bone when the animation
|
// Head is protected by default — only enabled as a free bone when the animation
|
||||||
// name contains "Head" (e.g., FullHeadStruggle, FullHeadIdle).
|
// name contains "Head" (e.g., FullHeadStruggle, FullHeadIdle).
|
||||||
// This lets artists opt-in per animation without affecting the item's regions.
|
// 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 = {
|
String[] allParts = {
|
||||||
"head",
|
"head",
|
||||||
|
|||||||
Reference in New Issue
Block a user