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,93 @@
|
||||
package com.tiedup.remake.client.animation.context;
|
||||
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
|
||||
/**
|
||||
* Represents the current player/NPC posture and action state for animation selection.
|
||||
* Determines which base body posture animation to play.
|
||||
*
|
||||
* <p>Each context maps to a GLB animation name via a prefix + variant scheme:
|
||||
* <ul>
|
||||
* <li>Prefix: "Sit", "Kneel", "Sneak", "Walk", or "" (standing)</li>
|
||||
* <li>Variant: "Idle" or "Struggle"</li>
|
||||
* </ul>
|
||||
* The {@link GlbAnimationResolver} uses these to build a fallback chain
|
||||
* (e.g., SitStruggle -> Struggle -> SitIdle -> Idle).</p>
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public enum AnimationContext {
|
||||
|
||||
STAND_IDLE("stand_idle", false),
|
||||
STAND_WALK("stand_walk", false),
|
||||
STAND_SNEAK("stand_sneak", false),
|
||||
STAND_STRUGGLE("stand_struggle", true),
|
||||
SIT_IDLE("sit_idle", false),
|
||||
SIT_STRUGGLE("sit_struggle", true),
|
||||
KNEEL_IDLE("kneel_idle", false),
|
||||
KNEEL_STRUGGLE("kneel_struggle", true),
|
||||
|
||||
// Movement style contexts
|
||||
SHUFFLE_IDLE("shuffle_idle", false),
|
||||
SHUFFLE_WALK("shuffle_walk", false),
|
||||
HOP_IDLE("hop_idle", false),
|
||||
HOP_WALK("hop_walk", false),
|
||||
WADDLE_IDLE("waddle_idle", false),
|
||||
WADDLE_WALK("waddle_walk", false),
|
||||
CRAWL_IDLE("crawl_idle", false),
|
||||
CRAWL_MOVE("crawl_move", false);
|
||||
|
||||
private final String animationSuffix;
|
||||
private final boolean struggling;
|
||||
|
||||
AnimationContext(String animationSuffix, boolean struggling) {
|
||||
this.animationSuffix = animationSuffix;
|
||||
this.struggling = struggling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suffix used as key for context animation JSON files (e.g., "stand_idle").
|
||||
*/
|
||||
public String getAnimationSuffix() {
|
||||
return animationSuffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this context represents an active struggle state.
|
||||
*/
|
||||
public boolean isStruggling() {
|
||||
return struggling;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GLB animation name prefix for this context's posture.
|
||||
* Used by the fallback chain in {@link GlbAnimationResolver}.
|
||||
*
|
||||
* @return "Sit", "Kneel", "Sneak", "Walk", or "" for standing
|
||||
*/
|
||||
public String getGlbContextPrefix() {
|
||||
return switch (this) {
|
||||
case SIT_IDLE, SIT_STRUGGLE -> "Sit";
|
||||
case KNEEL_IDLE, KNEEL_STRUGGLE -> "Kneel";
|
||||
case STAND_SNEAK -> "Sneak";
|
||||
case STAND_WALK -> "Walk";
|
||||
case STAND_IDLE, STAND_STRUGGLE -> "";
|
||||
case SHUFFLE_IDLE, SHUFFLE_WALK -> "Shuffle";
|
||||
case HOP_IDLE, HOP_WALK -> "Hop";
|
||||
case WADDLE_IDLE, WADDLE_WALK -> "Waddle";
|
||||
case CRAWL_IDLE, CRAWL_MOVE -> "Crawl";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the GLB animation variant name: "Struggle" or "Idle".
|
||||
*/
|
||||
public String getGlbVariant() {
|
||||
return switch (this) {
|
||||
case STAND_STRUGGLE, SIT_STRUGGLE, KNEEL_STRUGGLE -> "Struggle";
|
||||
case STAND_WALK, SHUFFLE_WALK, HOP_WALK, WADDLE_WALK -> "Walk";
|
||||
case CRAWL_MOVE -> "Move";
|
||||
default -> "Idle";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.tiedup.remake.client.animation.context;
|
||||
|
||||
import com.tiedup.remake.client.state.PetBedClientState;
|
||||
import com.tiedup.remake.entities.AbstractTiedUpNpc;
|
||||
import com.tiedup.remake.state.PlayerBindState;
|
||||
import com.tiedup.remake.v2.bondage.movement.MovementStyle;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Resolves the current {@link AnimationContext} for players and NPCs based on their state.
|
||||
*
|
||||
* <p>This is a pure function with no side effects -- it reads entity state and returns
|
||||
* the appropriate animation context. The resolution priority is:
|
||||
* <ol>
|
||||
* <li><b>Sitting</b> (pet bed for players, pose for NPCs) -- highest priority posture</li>
|
||||
* <li><b>Kneeling</b> (NPCs only)</li>
|
||||
* <li><b>Struggling</b> (standing struggle if not sitting/kneeling)</li>
|
||||
* <li><b>Sneaking</b> (players only)</li>
|
||||
* <li><b>Walking</b> (horizontal movement detected)</li>
|
||||
* <li><b>Standing idle</b> (fallback)</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>For players, the "sitting" state is determined by the client-side pet bed cache
|
||||
* ({@link PetBedClientState}) rather than entity data, since pet bed state is not
|
||||
* synced via entity data accessors.</p>
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public final class AnimationContextResolver {
|
||||
|
||||
private AnimationContextResolver() {}
|
||||
|
||||
/**
|
||||
* Resolve the animation context for a player based on their bind state and movement.
|
||||
*
|
||||
* <p>Priority chain:
|
||||
* <ol>
|
||||
* <li>Sitting (pet bed/furniture) -- highest priority posture</li>
|
||||
* <li>Struggling -- standing struggle if not sitting</li>
|
||||
* <li>Movement style -- style-specific idle/walk based on movement</li>
|
||||
* <li>Sneaking</li>
|
||||
* <li>Walking</li>
|
||||
* <li>Standing idle -- fallback</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param player the player entity (must not be null)
|
||||
* @param state the player's bind state, or null if not bound
|
||||
* @param activeStyle the active movement style from client state, or null
|
||||
* @return the resolved animation context, never null
|
||||
*/
|
||||
public static AnimationContext resolve(Player player, @Nullable PlayerBindState state,
|
||||
@Nullable MovementStyle activeStyle) {
|
||||
boolean sitting = PetBedClientState.get(player.getUUID()) != 0;
|
||||
boolean struggling = state != null && state.isStruggling();
|
||||
boolean sneaking = player.isCrouching();
|
||||
boolean moving = player.getDeltaMovement().horizontalDistanceSqr() > 1.0E-6;
|
||||
|
||||
if (sitting) {
|
||||
return struggling ? AnimationContext.SIT_STRUGGLE : AnimationContext.SIT_IDLE;
|
||||
}
|
||||
if (struggling) {
|
||||
return AnimationContext.STAND_STRUGGLE;
|
||||
}
|
||||
if (activeStyle != null) {
|
||||
return resolveStyleContext(activeStyle, moving);
|
||||
}
|
||||
if (sneaking) {
|
||||
return AnimationContext.STAND_SNEAK;
|
||||
}
|
||||
if (moving) {
|
||||
return AnimationContext.STAND_WALK;
|
||||
}
|
||||
return AnimationContext.STAND_IDLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a movement style + moving flag to the appropriate AnimationContext.
|
||||
*/
|
||||
private static AnimationContext resolveStyleContext(MovementStyle style, boolean moving) {
|
||||
return switch (style) {
|
||||
case SHUFFLE -> moving ? AnimationContext.SHUFFLE_WALK : AnimationContext.SHUFFLE_IDLE;
|
||||
case HOP -> moving ? AnimationContext.HOP_WALK : AnimationContext.HOP_IDLE;
|
||||
case WADDLE -> moving ? AnimationContext.WADDLE_WALK : AnimationContext.WADDLE_IDLE;
|
||||
case CRAWL -> moving ? AnimationContext.CRAWL_MOVE : AnimationContext.CRAWL_IDLE;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the animation context for a Damsel NPC based on pose and movement.
|
||||
*
|
||||
* <p>Unlike players, NPCs support kneeling as a distinct posture and do not sneak.</p>
|
||||
*
|
||||
* @param entity the damsel entity (must not be null)
|
||||
* @return the resolved animation context, never null
|
||||
*/
|
||||
public static AnimationContext resolveNpc(AbstractTiedUpNpc entity) {
|
||||
boolean sitting = entity.isSitting();
|
||||
boolean kneeling = entity.isKneeling();
|
||||
boolean struggling = entity.isStruggling();
|
||||
boolean moving = entity.getDeltaMovement().horizontalDistanceSqr() > 1.0E-6;
|
||||
|
||||
if (sitting) {
|
||||
return struggling ? AnimationContext.SIT_STRUGGLE : AnimationContext.SIT_IDLE;
|
||||
}
|
||||
if (kneeling) {
|
||||
return struggling ? AnimationContext.KNEEL_STRUGGLE : AnimationContext.KNEEL_IDLE;
|
||||
}
|
||||
if (struggling) {
|
||||
return AnimationContext.STAND_STRUGGLE;
|
||||
}
|
||||
if (moving) {
|
||||
return AnimationContext.STAND_WALK;
|
||||
}
|
||||
return AnimationContext.STAND_IDLE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.tiedup.remake.client.animation.context;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import dev.kosmx.playerAnim.core.data.KeyframeAnimation;
|
||||
import dev.kosmx.playerAnim.minecraftApi.PlayerAnimationRegistry;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
/**
|
||||
* Builds context {@link KeyframeAnimation}s with item-owned body parts disabled.
|
||||
*
|
||||
* <p>Context animations (loaded from {@code context_*.json} files in the PlayerAnimator
|
||||
* registry) control the base body posture -- standing, sitting, walking, etc.
|
||||
* When a V2 bondage item "owns" certain body parts (e.g., handcuffs own rightArm + leftArm),
|
||||
* those parts must NOT be driven by the context animation because the item's own
|
||||
* GLB animation controls them instead.</p>
|
||||
*
|
||||
* <p>This factory loads the base context animation, creates a mutable copy, disables
|
||||
* the owned parts, and builds an immutable result. Results are cached by
|
||||
* {@code contextSuffix|ownedPartsHash} to avoid repeated copies.</p>
|
||||
*
|
||||
* <p>Thread safety: the cache uses {@link ConcurrentHashMap}. All methods are
|
||||
* called from the render thread, but the concurrent map avoids issues if
|
||||
* resource reload triggers on a different thread.</p>
|
||||
*
|
||||
* @see AnimationContext
|
||||
* @see RegionBoneMapper#computeOwnedParts
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public final class ContextAnimationFactory {
|
||||
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final String NAMESPACE = "tiedup";
|
||||
|
||||
/**
|
||||
* Cache keyed by "contextSuffix|ownedPartsHashCode".
|
||||
* Null values are stored as sentinels for missing animations to avoid repeated lookups.
|
||||
*/
|
||||
private static final Map<String, KeyframeAnimation> CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Sentinel set used to track cache keys where the base animation was not found,
|
||||
* so we don't log the same warning repeatedly.
|
||||
*/
|
||||
private static final Set<String> MISSING_WARNED = ConcurrentHashMap.newKeySet();
|
||||
|
||||
private ContextAnimationFactory() {}
|
||||
|
||||
/**
|
||||
* Create (or retrieve from cache) a context animation with the given parts disabled.
|
||||
*
|
||||
* <p>If no parts need disabling, the base animation is returned as-is (no copy needed).
|
||||
* If the base animation is not found in the PlayerAnimator registry, returns null.</p>
|
||||
*
|
||||
* @param context the current animation context (determines which context_*.json to load)
|
||||
* @param disabledParts set of PlayerAnimator part names to disable on the context layer
|
||||
* (e.g., {"rightArm", "leftArm"}), typically from
|
||||
* {@link RegionBoneMapper.BoneOwnership#disabledOnContext()}
|
||||
* @return the context animation with disabled parts suppressed, or null if not found
|
||||
*/
|
||||
@Nullable
|
||||
public static KeyframeAnimation create(AnimationContext context, Set<String> disabledParts) {
|
||||
String cacheKey = context.getAnimationSuffix() + "|" + String.join(",", new java.util.TreeSet<>(disabledParts));
|
||||
// computeIfAbsent cannot store null values, so we handle the missing case
|
||||
// by checking the MISSING_WARNED set to avoid redundant work.
|
||||
KeyframeAnimation cached = CACHE.get(cacheKey);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
if (MISSING_WARNED.contains(cacheKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
KeyframeAnimation result = buildContextAnimation(context, disabledParts);
|
||||
if (result != null) {
|
||||
CACHE.put(cacheKey, result);
|
||||
} else {
|
||||
MISSING_WARNED.add(cacheKey);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a context animation with the specified parts disabled.
|
||||
*
|
||||
* <p>Flow:
|
||||
* <ol>
|
||||
* <li>Check {@link ContextGlbRegistry} for a GLB-based context animation (takes priority)</li>
|
||||
* <li>Fall back to {@code tiedup:context_<suffix>} in PlayerAnimationRegistry (JSON-based)</li>
|
||||
* <li>If no parts need disabling, return the base animation directly (immutable, shared)</li>
|
||||
* <li>Otherwise, create a mutable copy via {@link KeyframeAnimation#mutableCopy()}</li>
|
||||
* <li>Disable each part via {@link KeyframeAnimation.StateCollection#setEnabled(boolean)}</li>
|
||||
* <li>Build and return the new immutable animation</li>
|
||||
* </ol>
|
||||
*/
|
||||
@Nullable
|
||||
private static KeyframeAnimation buildContextAnimation(AnimationContext context,
|
||||
Set<String> disabledParts) {
|
||||
String suffix = context.getAnimationSuffix();
|
||||
|
||||
// Priority 1: GLB-based context animation from ContextGlbRegistry
|
||||
KeyframeAnimation baseAnim = ContextGlbRegistry.get(suffix);
|
||||
|
||||
// Priority 2: JSON-based context animation from PlayerAnimationRegistry
|
||||
if (baseAnim == null) {
|
||||
ResourceLocation animId = ResourceLocation.fromNamespaceAndPath(
|
||||
NAMESPACE, "context_" + suffix
|
||||
);
|
||||
baseAnim = PlayerAnimationRegistry.getAnimation(animId);
|
||||
}
|
||||
|
||||
if (baseAnim == null) {
|
||||
LOGGER.warn("[V2Animation] Context animation not found for suffix: {}", suffix);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (disabledParts.isEmpty()) {
|
||||
return baseAnim;
|
||||
}
|
||||
|
||||
// Create mutable copy so we can disable parts without affecting the registry/cache original
|
||||
KeyframeAnimation.AnimationBuilder builder = baseAnim.mutableCopy();
|
||||
disableParts(builder, disabledParts);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable all animation axes on the specified parts.
|
||||
*
|
||||
* <p>Uses {@link KeyframeAnimation.AnimationBuilder#getPart(String)} to look up parts
|
||||
* by name, then {@link KeyframeAnimation.StateCollection#setEnabled(boolean)} to disable
|
||||
* all axes (x, y, z, pitch, yaw, roll, and bend/bendDirection if applicable).</p>
|
||||
*
|
||||
* <p>Unknown part names are silently ignored -- this can happen if the disabled parts set
|
||||
* includes future bone names not present in the current context animation.</p>
|
||||
*/
|
||||
private static void disableParts(KeyframeAnimation.AnimationBuilder builder,
|
||||
Set<String> disabledParts) {
|
||||
for (String partName : disabledParts) {
|
||||
KeyframeAnimation.StateCollection part = builder.getPart(partName);
|
||||
if (part != null) {
|
||||
part.setEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached animations. Call this on resource reload or when equipped items change
|
||||
* in a way that might invalidate cached part ownership.
|
||||
*/
|
||||
public static void clearCache() {
|
||||
CACHE.clear();
|
||||
MISSING_WARNED.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.tiedup.remake.client.animation.context;
|
||||
|
||||
import com.tiedup.remake.client.gltf.GlbParser;
|
||||
import com.tiedup.remake.client.gltf.GltfData;
|
||||
import com.tiedup.remake.client.gltf.GltfPoseConverter;
|
||||
import dev.kosmx.playerAnim.core.data.KeyframeAnimation;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.packs.resources.Resource;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Registry for context animations loaded from GLB files.
|
||||
*
|
||||
* <p>Scans the {@code tiedup_contexts/} resource directory for {@code .glb} files,
|
||||
* parses each one via {@link GlbParser}, converts to a {@link KeyframeAnimation}
|
||||
* via {@link GltfPoseConverter#convert(GltfData)}, and stores the result keyed by
|
||||
* the file name suffix (e.g., {@code "stand_walk"} from {@code tiedup_contexts/stand_walk.glb}).</p>
|
||||
*
|
||||
* <p>GLB context animations take priority over JSON-based PlayerAnimator context
|
||||
* animations. This allows artists to author posture animations directly in Blender
|
||||
* instead of hand-editing JSON keyframes.</p>
|
||||
*
|
||||
* <p>Reloaded on resource pack reload (F3+T) via the listener registered in
|
||||
* {@link com.tiedup.remake.client.gltf.GltfClientSetup}.</p>
|
||||
*
|
||||
* <p>Thread safety: the registry field is a volatile reference to an unmodifiable map.
|
||||
* {@link #reload} builds a new map on the reload thread then atomically swaps the
|
||||
* reference, so the render thread never sees a partially populated registry.</p>
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public final class ContextGlbRegistry {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger("GltfPipeline");
|
||||
|
||||
/** Resource directory containing context GLB files. */
|
||||
private static final String DIRECTORY = "tiedup_contexts";
|
||||
|
||||
/**
|
||||
* Registry keyed by context suffix (e.g., "stand_walk", "sit_idle").
|
||||
* Values are fully converted KeyframeAnimations with all parts enabled.
|
||||
*
|
||||
* <p>Volatile reference to an unmodifiable map. Reload builds a new map
|
||||
* and swaps atomically; the render thread always sees a consistent snapshot.</p>
|
||||
*/
|
||||
private static volatile Map<String, KeyframeAnimation> REGISTRY = Map.of();
|
||||
|
||||
private ContextGlbRegistry() {}
|
||||
|
||||
/**
|
||||
* Reload all context GLB files from the resource manager.
|
||||
*
|
||||
* <p>Scans {@code assets/<namespace>/tiedup_contexts/} for {@code .glb} files.
|
||||
* Each file is parsed and converted to a full-body KeyframeAnimation.
|
||||
* The context suffix is extracted from the file path:
|
||||
* {@code tiedup_contexts/stand_walk.glb} becomes key {@code "stand_walk"}.</p>
|
||||
*
|
||||
* <p>GLB files without animation data or with parse errors are logged and skipped.</p>
|
||||
*
|
||||
* @param resourceManager the current resource manager (from reload listener)
|
||||
*/
|
||||
public static void reload(ResourceManager resourceManager) {
|
||||
Map<String, KeyframeAnimation> newRegistry = new HashMap<>();
|
||||
|
||||
Map<ResourceLocation, Resource> resources = resourceManager.listResources(
|
||||
DIRECTORY, loc -> loc.getPath().endsWith(".glb"));
|
||||
|
||||
for (Map.Entry<ResourceLocation, Resource> entry : resources.entrySet()) {
|
||||
ResourceLocation loc = entry.getKey();
|
||||
Resource resource = entry.getValue();
|
||||
|
||||
// Extract suffix from path: "tiedup_contexts/stand_walk.glb" -> "stand_walk"
|
||||
String path = loc.getPath();
|
||||
String fileName = path.substring(path.lastIndexOf('/') + 1);
|
||||
String suffix = fileName.substring(0, fileName.length() - 4); // strip ".glb"
|
||||
|
||||
try (InputStream is = resource.open()) {
|
||||
GltfData data = GlbParser.parse(is, loc.toString());
|
||||
|
||||
// Convert to a full-body KeyframeAnimation (all parts enabled)
|
||||
KeyframeAnimation anim = GltfPoseConverter.convert(data);
|
||||
newRegistry.put(suffix, anim);
|
||||
|
||||
LOGGER.info("[GltfPipeline] Loaded context GLB: '{}' -> suffix '{}'", loc, suffix);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error("[GltfPipeline] Failed to load context GLB: {}", loc, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Atomic swap: render thread never sees a partially populated registry
|
||||
REGISTRY = Collections.unmodifiableMap(newRegistry);
|
||||
LOGGER.info("[ContextGlb] Loaded {} context GLB animations", newRegistry.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a context animation by suffix.
|
||||
*
|
||||
* @param contextSuffix the context suffix (e.g., "stand_walk", "sit_idle")
|
||||
* @return the KeyframeAnimation, or null if no GLB was found for this suffix
|
||||
*/
|
||||
@Nullable
|
||||
public static KeyframeAnimation get(String contextSuffix) {
|
||||
return REGISTRY.get(contextSuffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached context animations.
|
||||
* Called on resource reload and world unload.
|
||||
*/
|
||||
public static void clear() {
|
||||
REGISTRY = Map.of();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.tiedup.remake.client.animation.context;
|
||||
|
||||
import com.tiedup.remake.client.gltf.GltfCache;
|
||||
import com.tiedup.remake.client.gltf.GltfData;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
|
||||
/**
|
||||
* Resolves which named animation to play from a GLB file based on the current
|
||||
* {@link AnimationContext}. Implements three features:
|
||||
*
|
||||
* <ol>
|
||||
* <li><b>Context-based resolution with fallback chain</b> — tries progressively
|
||||
* less specific animation names until one is found:
|
||||
* <pre>SitStruggle -> Struggle -> SitIdle -> Sit -> Idle -> null</pre></li>
|
||||
* <li><b>Animation variants</b> — if {@code Struggle.1}, {@code Struggle.2},
|
||||
* {@code Struggle.3} exist in the GLB, one is picked at random each time</li>
|
||||
* <li><b>Shared animation templates</b> — animations can come from a separate GLB
|
||||
* file (passed as {@code animationSource} to {@link #resolveAnimationData})</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>This class is stateless and thread-safe. All methods are static.</p>
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public final class GlbAnimationResolver {
|
||||
|
||||
private GlbAnimationResolver() {}
|
||||
|
||||
/**
|
||||
* Resolve the animation data source.
|
||||
* If {@code animationSource} is non-null, load that GLB for animations
|
||||
* (shared template). Otherwise use the item's own model GLB.
|
||||
*
|
||||
* @param itemModelLoc the item's GLB model resource location
|
||||
* @param animationSource optional separate GLB containing shared animations
|
||||
* @return parsed GLB data, or null if loading failed
|
||||
*/
|
||||
@Nullable
|
||||
public static GltfData resolveAnimationData(ResourceLocation itemModelLoc,
|
||||
@Nullable ResourceLocation animationSource) {
|
||||
ResourceLocation source = animationSource != null ? animationSource : itemModelLoc;
|
||||
return GltfCache.get(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the best animation name from a GLB for the given context.
|
||||
* Supports variant selection ({@code Struggle.1}, {@code Struggle.2} -> random pick)
|
||||
* and full-body animations ({@code FullWalk}, {@code FullStruggle}).
|
||||
*
|
||||
* <p>Fallback chain (Full variants checked first at each step):</p>
|
||||
* <pre>
|
||||
* FullSitStruggle -> SitStruggle -> FullStruggle -> Struggle
|
||||
* -> FullSitIdle -> SitIdle -> FullSit -> Sit
|
||||
* -> FullIdle -> Idle -> null
|
||||
* </pre>
|
||||
*
|
||||
* @param data the parsed GLB data containing named animations
|
||||
* @param context the current animation context (posture + action)
|
||||
* @return the animation name to use, or null to use the default (first) clip
|
||||
*/
|
||||
@Nullable
|
||||
public static String resolve(GltfData data, AnimationContext context) {
|
||||
String prefix = context.getGlbContextPrefix(); // "Sit", "Kneel", "Sneak", "Walk", ""
|
||||
String variant = context.getGlbVariant(); // "Idle" or "Struggle"
|
||||
|
||||
// 1. Exact match: "FullSitIdle" then "SitIdle" (with variants)
|
||||
String exact = prefix + variant;
|
||||
if (!exact.isEmpty()) {
|
||||
String picked = pickWithVariants(data, "Full" + exact);
|
||||
if (picked != null) return picked;
|
||||
picked = pickWithVariants(data, exact);
|
||||
if (picked != null) return picked;
|
||||
}
|
||||
|
||||
// 2. For struggles: try "FullStruggle" then "Struggle" (with variants)
|
||||
if (context.isStruggling()) {
|
||||
String picked = pickWithVariants(data, "FullStruggle");
|
||||
if (picked != null) return picked;
|
||||
picked = pickWithVariants(data, "Struggle");
|
||||
if (picked != null) return picked;
|
||||
}
|
||||
|
||||
// 3. Context-only: "FullSit" then "Sit" (with variants)
|
||||
if (!prefix.isEmpty()) {
|
||||
String picked = pickWithVariants(data, "Full" + prefix);
|
||||
if (picked != null) return picked;
|
||||
picked = pickWithVariants(data, prefix);
|
||||
if (picked != null) return picked;
|
||||
}
|
||||
|
||||
// 4. Variant-only: "FullIdle" then "Idle" (with variants)
|
||||
{
|
||||
String picked = pickWithVariants(data, "Full" + variant);
|
||||
if (picked != null) return picked;
|
||||
picked = pickWithVariants(data, variant);
|
||||
if (picked != null) return picked;
|
||||
}
|
||||
|
||||
// 5. Default: return null = use first animation clip in GLB
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for an animation by base name, including numbered variants.
|
||||
* <ul>
|
||||
* <li>If "Struggle" exists alone, return "Struggle"</li>
|
||||
* <li>If "Struggle.1" and "Struggle.2" exist, pick one randomly</li>
|
||||
* <li>If both "Struggle" and "Struggle.1" exist, include all in the random pool</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Variant numbering starts at 1 and tolerates a missing {@code .1}
|
||||
* (continues to check {@code .2}). Gaps after index 1 stop the scan.
|
||||
* For example, {@code Struggle.1, Struggle.3} would only find
|
||||
* {@code Struggle.1} because the gap at index 2 stops iteration.
|
||||
* However, if only {@code Struggle.2} exists (no {@code .1}), it will
|
||||
* still be found because the scan skips the first gap.</p>
|
||||
*
|
||||
* @param data the parsed GLB data
|
||||
* @param baseName the base animation name (e.g., "Struggle", "SitIdle")
|
||||
* @return the selected animation name, or null if no match found
|
||||
*/
|
||||
@Nullable
|
||||
private static String pickWithVariants(GltfData data, String baseName) {
|
||||
Map<String, GltfData.AnimationClip> anims = data.namedAnimations();
|
||||
List<String> candidates = new ArrayList<>();
|
||||
|
||||
if (anims.containsKey(baseName)) {
|
||||
candidates.add(baseName);
|
||||
}
|
||||
|
||||
// Check numbered variants: baseName.1, baseName.2, ...
|
||||
for (int i = 1; i <= 99; i++) {
|
||||
String variantName = baseName + "." + i;
|
||||
if (anims.containsKey(variantName)) {
|
||||
candidates.add(variantName);
|
||||
} else if (i > 1) {
|
||||
break; // Stop at first gap after .1
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.isEmpty()) return null;
|
||||
if (candidates.size() == 1) return candidates.get(0);
|
||||
return candidates.get(ThreadLocalRandom.current().nextInt(candidates.size()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
package com.tiedup.remake.client.animation.context;
|
||||
|
||||
import com.tiedup.remake.v2.BodyRegionV2;
|
||||
import com.tiedup.remake.v2.bondage.IV2BondageItem;
|
||||
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenBondageItem;
|
||||
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemDefinition;
|
||||
import com.tiedup.remake.v2.bondage.datadriven.DataDrivenItemRegistry;
|
||||
import java.util.*;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.api.distmarker.OnlyIn;
|
||||
|
||||
/**
|
||||
* Maps V2 body regions to PlayerAnimator part names.
|
||||
* Bridge between gameplay regions and animation bones.
|
||||
*
|
||||
* <p>PlayerAnimator uses 6 named parts: head, body, rightArm, leftArm, rightLeg, leftLeg.
|
||||
* This mapper translates the 14 {@link BodyRegionV2} gameplay regions into those bone names,
|
||||
* enabling the animation system to know which bones are "owned" by equipped bondage items.</p>
|
||||
*
|
||||
* <p>Regions without a direct bone mapping (NECK, FINGERS, TAIL, WINGS) return empty sets.
|
||||
* These regions still affect gameplay (blocking, escape difficulty) but don't directly
|
||||
* constrain animation bones.</p>
|
||||
*/
|
||||
@OnlyIn(Dist.CLIENT)
|
||||
public final class RegionBoneMapper {
|
||||
|
||||
/** All PlayerAnimator part names for the player model. */
|
||||
public static final Set<String> ALL_PARTS = Set.of(
|
||||
"head", "body", "rightArm", "leftArm", "rightLeg", "leftLeg"
|
||||
);
|
||||
|
||||
/**
|
||||
* Describes bone ownership for a specific item in the context of all equipped items.
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code thisParts} — parts owned exclusively by the winning item</li>
|
||||
* <li>{@code otherParts} — parts owned by other equipped items</li>
|
||||
* <li>{@link #freeParts()} — parts not owned by any item (available for animation)</li>
|
||||
* <li>{@link #enabledParts()} — parts the winning item may animate (owned + free)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>When both the winning item and another item claim the same bone,
|
||||
* the other item takes precedence (the bone goes to {@code otherParts}).</p>
|
||||
*/
|
||||
public record BoneOwnership(Set<String> thisParts, Set<String> otherParts) {
|
||||
|
||||
/**
|
||||
* Parts not owned by any item. These are "free" and can be animated
|
||||
* by the winning item IF the GLB contains keyframes for them.
|
||||
*/
|
||||
public Set<String> freeParts() {
|
||||
Set<String> free = new HashSet<>(ALL_PARTS);
|
||||
free.removeAll(thisParts);
|
||||
free.removeAll(otherParts);
|
||||
return Collections.unmodifiableSet(free);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parts the winning item is allowed to animate: its own parts + free parts.
|
||||
* Free parts are only actually enabled if the GLB has keyframes for them.
|
||||
*/
|
||||
public Set<String> enabledParts() {
|
||||
Set<String> enabled = new HashSet<>(thisParts);
|
||||
enabled.addAll(freeParts());
|
||||
return Collections.unmodifiableSet(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parts that must be disabled on the context layer: parts owned by this item
|
||||
* (handled by item layer) + parts owned by other items (handled by their layer).
|
||||
* This equals ALL_PARTS minus freeParts.
|
||||
*/
|
||||
public Set<String> disabledOnContext() {
|
||||
Set<String> disabled = new HashSet<>(thisParts);
|
||||
disabled.addAll(otherParts);
|
||||
return Collections.unmodifiableSet(disabled);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Map<BodyRegionV2, Set<String>> REGION_TO_PARTS;
|
||||
|
||||
static {
|
||||
Map<BodyRegionV2, Set<String>> map = new EnumMap<>(BodyRegionV2.class);
|
||||
map.put(BodyRegionV2.HEAD, Set.of("head"));
|
||||
map.put(BodyRegionV2.EYES, Set.of("head"));
|
||||
map.put(BodyRegionV2.EARS, Set.of("head"));
|
||||
map.put(BodyRegionV2.MOUTH, Set.of("head"));
|
||||
map.put(BodyRegionV2.NECK, Set.of());
|
||||
map.put(BodyRegionV2.TORSO, Set.of("body"));
|
||||
map.put(BodyRegionV2.ARMS, Set.of("rightArm", "leftArm"));
|
||||
map.put(BodyRegionV2.HANDS, Set.of("rightArm", "leftArm"));
|
||||
map.put(BodyRegionV2.FINGERS, Set.of());
|
||||
map.put(BodyRegionV2.WAIST, Set.of("body"));
|
||||
map.put(BodyRegionV2.LEGS, Set.of("rightLeg", "leftLeg"));
|
||||
map.put(BodyRegionV2.FEET, Set.of("rightLeg", "leftLeg"));
|
||||
map.put(BodyRegionV2.TAIL, Set.of());
|
||||
map.put(BodyRegionV2.WINGS, Set.of());
|
||||
REGION_TO_PARTS = Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
private RegionBoneMapper() {}
|
||||
|
||||
/**
|
||||
* Get the PlayerAnimator part names affected by a single body region.
|
||||
*
|
||||
* @param region the V2 body region
|
||||
* @return unmodifiable set of part name strings, never null (may be empty)
|
||||
*/
|
||||
public static Set<String> getPartsForRegion(BodyRegionV2 region) {
|
||||
return REGION_TO_PARTS.getOrDefault(region, Set.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the union of all PlayerAnimator parts "owned" by equipped bondage items.
|
||||
*
|
||||
* <p>Iterates over the equipped map (as returned by
|
||||
* {@link com.tiedup.remake.v2.bondage.IV2BondageEquipment#getAllEquipped()})
|
||||
* and collects every bone affected by each item's occupied regions.</p>
|
||||
*
|
||||
* @param equipped map from representative region to equipped ItemStack
|
||||
* @return unmodifiable set of owned part name strings
|
||||
*/
|
||||
public static Set<String> computeOwnedParts(Map<BodyRegionV2, ItemStack> equipped) {
|
||||
Set<String> owned = new HashSet<>();
|
||||
for (Map.Entry<BodyRegionV2, ItemStack> entry : equipped.entrySet()) {
|
||||
ItemStack stack = entry.getValue();
|
||||
if (stack.getItem() instanceof IV2BondageItem v2Item) {
|
||||
for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) {
|
||||
owned.addAll(getPartsForRegion(region));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableSet(owned);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute per-item bone ownership for a specific "winning" item.
|
||||
*
|
||||
* <p>Iterates over all equipped items. Parts owned by the winning item
|
||||
* go to {@code thisParts}; parts owned by other items go to {@code otherParts}.
|
||||
* If both the winning item and another item claim the same bone, the other
|
||||
* item takes precedence (conflict resolution: other wins).</p>
|
||||
*
|
||||
* <p>Uses ItemStack reference equality ({@code ==}) to identify the winning item
|
||||
* because the same ItemStack instance is used in the equipped map.</p>
|
||||
*
|
||||
* @param equipped map from representative region to equipped ItemStack
|
||||
* @param winningItemStack the ItemStack of the highest-priority V2 item with a GLB model
|
||||
* @return BoneOwnership describing this item's parts vs other items' parts
|
||||
*/
|
||||
public static BoneOwnership computePerItemParts(Map<BodyRegionV2, ItemStack> equipped,
|
||||
ItemStack winningItemStack) {
|
||||
Set<String> thisParts = new HashSet<>();
|
||||
Set<String> otherParts = new HashSet<>();
|
||||
|
||||
// Track which ItemStacks we've already processed to avoid duplicate work
|
||||
// (multiple regions can map to the same ItemStack)
|
||||
Set<ItemStack> processed = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
|
||||
for (Map.Entry<BodyRegionV2, ItemStack> entry : equipped.entrySet()) {
|
||||
ItemStack stack = entry.getValue();
|
||||
if (processed.contains(stack)) continue;
|
||||
processed.add(stack);
|
||||
|
||||
if (stack.getItem() instanceof IV2BondageItem v2Item) {
|
||||
Set<String> itemParts = new HashSet<>();
|
||||
for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) {
|
||||
itemParts.addAll(getPartsForRegion(region));
|
||||
}
|
||||
|
||||
if (stack == winningItemStack) {
|
||||
thisParts.addAll(itemParts);
|
||||
} else {
|
||||
otherParts.addAll(itemParts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Conflict resolution: if both this item and another claim the same bone,
|
||||
// the other item takes precedence
|
||||
thisParts.removeAll(otherParts);
|
||||
|
||||
return new BoneOwnership(
|
||||
Collections.unmodifiableSet(thisParts),
|
||||
Collections.unmodifiableSet(otherParts)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of resolving the highest-priority V2 item with a GLB model.
|
||||
* Combines the model location, optional animation source, and the winning ItemStack
|
||||
* into a single object so callers don't need two separate iteration passes.
|
||||
*
|
||||
* @param modelLoc the GLB model ResourceLocation of the winning item
|
||||
* @param animSource separate GLB for animations (shared template), or null to use modelLoc
|
||||
* @param winningItem the actual ItemStack reference (for identity comparison in
|
||||
* {@link #computePerItemParts})
|
||||
*/
|
||||
public record GlbModelResult(ResourceLocation modelLoc, @Nullable ResourceLocation animSource,
|
||||
ItemStack winningItem) {}
|
||||
|
||||
/**
|
||||
* Animation info for a single equipped V2 item.
|
||||
* Used by the multi-item animation pipeline to process each item independently.
|
||||
*
|
||||
* @param modelLoc GLB model location (for rendering + default animation source)
|
||||
* @param animSource separate animation GLB, or null to use modelLoc
|
||||
* @param ownedParts parts this item exclusively owns (after conflict resolution)
|
||||
* @param posePriority the item's pose priority (for free-bone assignment)
|
||||
* @param animationBones per-animation bone whitelist from the data-driven definition.
|
||||
* Empty map for hardcoded items (no filtering applied).
|
||||
*/
|
||||
public record V2ItemAnimInfo(ResourceLocation modelLoc, @Nullable ResourceLocation animSource,
|
||||
Set<String> ownedParts, int posePriority,
|
||||
Map<String, Set<String>> animationBones) {}
|
||||
|
||||
/**
|
||||
* Find the highest-priority V2 item with a GLB model in the equipped map.
|
||||
*
|
||||
* <p>Single pass over all equipped items, comparing their
|
||||
* {@link IV2BondageItem#getPosePriority()} to select the dominant model.
|
||||
* Returns both the model location and the winning ItemStack reference so
|
||||
* callers can pass the ItemStack to {@link #computePerItemParts} without
|
||||
* a second iteration.</p>
|
||||
*
|
||||
* @param equipped map of equipped V2 items by body region (may be empty, never null)
|
||||
* @return the winning item's model info, or null if no V2 item has a GLB model (V1 fallback)
|
||||
*/
|
||||
@Nullable
|
||||
public static GlbModelResult resolveWinningItem(Map<BodyRegionV2, ItemStack> equipped) {
|
||||
ItemStack bestStack = null;
|
||||
ResourceLocation bestModel = null;
|
||||
int bestPriority = Integer.MIN_VALUE;
|
||||
for (Map.Entry<BodyRegionV2, ItemStack> entry : equipped.entrySet()) {
|
||||
ItemStack stack = entry.getValue();
|
||||
if (stack.getItem() instanceof IV2BondageItem v2Item) {
|
||||
ResourceLocation model = v2Item.getModelLocation(stack);
|
||||
if (model != null && v2Item.getPosePriority(stack) > bestPriority) {
|
||||
bestPriority = v2Item.getPosePriority(stack);
|
||||
bestModel = model;
|
||||
bestStack = stack;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bestStack == null || bestModel == null) return null;
|
||||
|
||||
// Extract animation source from data-driven item definitions.
|
||||
// For hardcoded IV2BondageItem implementations, animSource stays null
|
||||
// (the model's own animations are used).
|
||||
ResourceLocation animSource = null;
|
||||
if (bestStack.getItem() instanceof DataDrivenBondageItem) {
|
||||
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(bestStack);
|
||||
if (def != null) {
|
||||
animSource = def.animationSource();
|
||||
}
|
||||
}
|
||||
|
||||
return new GlbModelResult(bestModel, animSource, bestStack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve ALL equipped V2 items with GLB models, with per-item bone ownership.
|
||||
*
|
||||
* <p>Each item gets ownership of its declared regions' bones. When two items claim
|
||||
* the same bone, the higher-priority item wins. The highest-priority item is also
|
||||
* designated as the "free bone donor" — it can animate free bones if its GLB has
|
||||
* keyframes for them.</p>
|
||||
*
|
||||
* @param equipped map from representative region to equipped ItemStack
|
||||
* @return list of V2ItemAnimInfo, sorted by priority descending. Empty if no V2 items.
|
||||
* The first element (if any) is the free-bone donor.
|
||||
*/
|
||||
public static List<V2ItemAnimInfo> resolveAllV2Items(Map<BodyRegionV2, ItemStack> equipped) {
|
||||
record ItemEntry(ItemStack stack, IV2BondageItem v2Item, ResourceLocation model,
|
||||
@Nullable ResourceLocation animSource, Set<String> rawParts, int priority,
|
||||
Map<String, Set<String>> animationBones) {}
|
||||
|
||||
List<ItemEntry> entries = new ArrayList<>();
|
||||
Set<ItemStack> seen = Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
|
||||
for (Map.Entry<BodyRegionV2, ItemStack> entry : equipped.entrySet()) {
|
||||
ItemStack stack = entry.getValue();
|
||||
if (seen.contains(stack)) continue;
|
||||
seen.add(stack);
|
||||
|
||||
if (stack.getItem() instanceof IV2BondageItem v2Item) {
|
||||
ResourceLocation model = v2Item.getModelLocation(stack);
|
||||
if (model == null) continue;
|
||||
|
||||
Set<String> rawParts = new HashSet<>();
|
||||
for (BodyRegionV2 region : v2Item.getOccupiedRegions(stack)) {
|
||||
rawParts.addAll(getPartsForRegion(region));
|
||||
}
|
||||
if (rawParts.isEmpty()) continue;
|
||||
|
||||
ResourceLocation animSource = null;
|
||||
Map<String, Set<String>> animBones = Map.of();
|
||||
if (stack.getItem() instanceof DataDrivenBondageItem) {
|
||||
DataDrivenItemDefinition def = DataDrivenItemRegistry.get(stack);
|
||||
if (def != null) {
|
||||
animSource = def.animationSource();
|
||||
animBones = def.animationBones();
|
||||
}
|
||||
}
|
||||
|
||||
entries.add(new ItemEntry(stack, v2Item, model, animSource, rawParts,
|
||||
v2Item.getPosePriority(stack), animBones));
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.isEmpty()) return List.of();
|
||||
|
||||
entries.sort((a, b) -> Integer.compare(b.priority(), a.priority()));
|
||||
|
||||
Set<String> claimed = new HashSet<>();
|
||||
List<V2ItemAnimInfo> result = new ArrayList<>();
|
||||
|
||||
for (ItemEntry e : entries) {
|
||||
Set<String> ownedParts = new HashSet<>(e.rawParts());
|
||||
ownedParts.removeAll(claimed);
|
||||
if (ownedParts.isEmpty()) continue;
|
||||
claimed.addAll(ownedParts);
|
||||
result.add(new V2ItemAnimInfo(e.model(), e.animSource(),
|
||||
Collections.unmodifiableSet(ownedParts), e.priority(), e.animationBones()));
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the set of all bone parts owned by any item in the resolved list.
|
||||
* Used to disable owned parts on the context layer.
|
||||
*/
|
||||
public static Set<String> computeAllOwnedParts(List<V2ItemAnimInfo> items) {
|
||||
Set<String> allOwned = new HashSet<>();
|
||||
for (V2ItemAnimInfo item : items) {
|
||||
allOwned.addAll(item.ownedParts());
|
||||
}
|
||||
return Collections.unmodifiableSet(allOwned);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user