Refactor V2 animation, furniture, and GLTF rendering
Broad consolidation of the V2 bondage-item, furniture-entity, and
client-side GLTF pipeline.
Parsing and rendering
- Shared GLB parsing helpers consolidated into GlbParserUtils
(accessor reads, weight normalization, joint-index clamping,
coordinate-space conversion, animation parse, primitive loop).
- Grow-on-demand Matrix4f[] scratch pool in GltfSkinningEngine and
GltfLiveBoneReader — removes per-frame joint-matrix allocation
from the render hot path.
- emitVertex helper dedups three parallel loops in GltfMeshRenderer.
- TintColorResolver.resolve has a zero-alloc path when the item
declares no tint channels.
- itemAnimCache bounded to 256 entries (access-order LRU) with
atomic get-or-compute under the map's monitor.
Animation correctness
- First-in-joint-order wins when body and torso both map to the
same PlayerAnimator slot; duplicate writes log a single WARN.
- Multi-item composites honor the FullX / FullHeadX opt-in that
the single-item path already recognized.
- Seat transforms converted to Minecraft model-def space so
asymmetric furniture renders passengers at the correct offset.
- GlbValidator: IBM count / type / presence, JOINTS_0 presence,
animation channel target validation, multi-skin support.
Furniture correctness and anti-exploit
- Seat assignment synced via SynchedEntityData (server is
authoritative; eliminates client-server divergence on multi-seat).
- Force-mount authorization requires same dimension and a free
seat; cross-dimension distance checks rejected.
- Reconnection on login checks for seat takeover before re-mount
and force-loads the target chunk for cross-dimension cases.
- tiedup_furniture_lockpick_ctx carries a session UUID nonce so
stale context can't misroute a body-item lockpick.
- tiedup_locked_furniture survives death without keepInventory
(Forge 1.20.1 does not auto-copy persistent data on respawn).
Lifecycle and memory
- EntityCleanupHandler fans EntityLeaveLevelEvent out to every
per-entity state map on the client.
- DogPoseRenderHandler re-keyed by UUID (stable across dimension
change; entity int ids are recycled).
- PetBedRenderHandler, PlayerArmHideEventHandler, and
HeldItemHideHandler use receiveCanceled + sentinel sets so
Pre-time mutations are restored even when a downstream handler
cancels the render.
Tests
- JUnit harness with 76+ tests across GlbParserUtils, GltfPoseConverter,
FurnitureSeatGeometry, and FurnitureAuthPredicate.
This commit is contained in:
@@ -353,8 +353,18 @@ public class BondageAnimationManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Per-player dedup set so we log the factory-access failure at most once per UUID. */
|
||||
private static final java.util.Set<UUID> layerFailureLogged =
|
||||
java.util.concurrent.ConcurrentHashMap.newKeySet();
|
||||
|
||||
/**
|
||||
* Get the animation layer for a player from PlayerAnimationAccess.
|
||||
*
|
||||
* <p>Throws during the factory-race window for remote players (the factory
|
||||
* hasn't yet initialized their associated data). This is the expected path
|
||||
* for the {@link PendingAnimationManager} retry loop, so we log at DEBUG
|
||||
* and at most once per UUID — a per-tick log would flood during busy
|
||||
* multiplayer.</p>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static ModifierLayer<IAnimation> getPlayerLayer(
|
||||
@@ -367,11 +377,13 @@ public class BondageAnimationManager {
|
||||
FACTORY_ID
|
||||
);
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(
|
||||
"Failed to get animation layer for player: {}",
|
||||
player.getName().getString(),
|
||||
e
|
||||
);
|
||||
if (layerFailureLogged.add(player.getUUID())) {
|
||||
LOGGER.debug(
|
||||
"Animation layer not yet available for player {} (will retry): {}",
|
||||
player.getName().getString(),
|
||||
e.toString()
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -521,7 +533,7 @@ public class BondageAnimationManager {
|
||||
return false;
|
||||
}
|
||||
|
||||
ModifierLayer<IAnimation> layer = getFurnitureLayer(player);
|
||||
ModifierLayer<IAnimation> layer = getOrCreateFurnitureLayer(player);
|
||||
if (layer != null) {
|
||||
layer.setAnimation(new KeyframeAnimationPlayer(animation));
|
||||
// Reset grace ticks since we just started/refreshed the animation
|
||||
@@ -577,9 +589,11 @@ public class BondageAnimationManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the furniture ModifierLayer for a player.
|
||||
* Get the furniture ModifierLayer for a player (READ-ONLY).
|
||||
* Uses PlayerAnimationAccess for local/factory-registered players,
|
||||
* falls back to NPC cache for remote players.
|
||||
* falls back to NPC cache for remote players. Returns null if no layer
|
||||
* has been created yet — callers that need to guarantee a layer should use
|
||||
* {@link #getOrCreateFurnitureLayer}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@javax.annotation.Nullable
|
||||
@@ -606,6 +620,61 @@ public class BondageAnimationManager {
|
||||
return npcFurnitureLayers.get(player.getUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create the furniture ModifierLayer for a player. Mirrors
|
||||
* {@link #getOrCreateLayer} but for the FURNITURE layer priority.
|
||||
*
|
||||
* <p>For the local player (factory-registered), returns the factory layer.
|
||||
* For remote players, creates a new layer on first call and caches it in
|
||||
* {@link #npcFurnitureLayers} — remote players don't own a factory layer,
|
||||
* so without a fallback they can't receive any furniture seat pose.</p>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@javax.annotation.Nullable
|
||||
private static ModifierLayer<IAnimation> getOrCreateFurnitureLayer(
|
||||
Player player
|
||||
) {
|
||||
if (player instanceof AbstractClientPlayer clientPlayer) {
|
||||
try {
|
||||
ModifierLayer<IAnimation> layer = (ModifierLayer<
|
||||
IAnimation
|
||||
>) PlayerAnimationAccess.getPlayerAssociatedData(
|
||||
clientPlayer
|
||||
).get(FURNITURE_FACTORY_ID);
|
||||
if (layer != null) {
|
||||
return layer;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Fall through to fallback-create below.
|
||||
}
|
||||
|
||||
// Remote players: fallback-create via the animation stack.
|
||||
if (clientPlayer instanceof IAnimatedPlayer animated) {
|
||||
return npcFurnitureLayers.computeIfAbsent(
|
||||
clientPlayer.getUUID(),
|
||||
k -> {
|
||||
ModifierLayer<IAnimation> newLayer =
|
||||
new ModifierLayer<>();
|
||||
animated
|
||||
.getAnimationStack()
|
||||
.addAnimLayer(FURNITURE_LAYER_PRIORITY, newLayer);
|
||||
LOGGER.debug(
|
||||
"Created furniture animation layer for remote player via stack: {}",
|
||||
clientPlayer.getName().getString()
|
||||
);
|
||||
return newLayer;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return npcFurnitureLayers.get(clientPlayer.getUUID());
|
||||
}
|
||||
|
||||
// Non-player entities: use NPC cache (read-only; NPC furniture animation
|
||||
// is not currently produced by this codebase).
|
||||
return npcFurnitureLayers.get(player.getUUID());
|
||||
}
|
||||
|
||||
/**
|
||||
* Safety tick for furniture animations. Call once per client tick per player.
|
||||
*
|
||||
@@ -731,6 +800,7 @@ public class BondageAnimationManager {
|
||||
}
|
||||
}
|
||||
furnitureGraceTicks.remove(entityId);
|
||||
layerFailureLogged.remove(entityId);
|
||||
LOGGER.debug("Cleaned up animation layers for entity: {}", entityId);
|
||||
}
|
||||
|
||||
@@ -744,6 +814,7 @@ public class BondageAnimationManager {
|
||||
cache.clear();
|
||||
}
|
||||
furnitureGraceTicks.clear();
|
||||
layerFailureLogged.clear();
|
||||
LOGGER.info("Cleared all NPC animation layers");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user