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:
NotEvil
2026-04-18 17:34:03 +02:00
parent 17815873ac
commit 355e2936c9
63 changed files with 4965 additions and 2226 deletions

View File

@@ -72,8 +72,55 @@ public final class GltfClientSetup {
}
/**
* Register resource reload listener to clear GLB caches on resource pack reload.
* This ensures re-exported GLB models are picked up without restarting the game.
* Register resource reload listeners in the order required by the
* cache/consumer dependency graph.
*
* <p><b>ORDER MATTERS — do not rearrange without checking the
* invariants below.</b> Forge does not guarantee parallel-safe
* ordering between listeners registered on the same event; we rely
* on {@code apply()} running sequentially in the order of
* {@code registerReloadListener} calls.</p>
*
* <ol>
* <li><b>GLB cache clear</b> (inline listener below) — must run
* first. Inside this single listener's {@code apply()}:
* <ol type="a">
* <li>Blow away the raw GLB byte caches
* ({@code GltfCache.clearCache},
* {@code GltfAnimationApplier.invalidateCache},
* {@code GltfMeshRenderer.clearRenderTypeCache}).
* These three caches are mutually independent — none
* of them reads from the others — so their relative
* order is not load-bearing.</li>
* <li>Reload {@code ContextGlbRegistry} from the new
* resource packs <i>before</i> clearing
* {@code ContextAnimationFactory.clearCache()} — if
* the order is swapped, the next factory lookup will
* lazily rebuild clips against the stale registry
* (which is still populated at that moment), cache
* them, and keep serving old data until the next
* reload.</li>
* <li>Clear {@code FurnitureGltfCache} last, after the GLB
* layer has repopulated its registry but before any
* downstream item listener queries furniture models.</li>
* </ol>
* </li>
* <li><b>Data-driven item reload</b>
* ({@code DataDrivenItemReloadListener}) — consumes the
* reloaded GLB registry indirectly via item JSON references.
* Must run <i>after</i> the GLB cache clear so any item that
* reaches into the GLB layer during load picks up fresh data.</li>
* <li><b>GLB validation</b>
* ({@code GlbValidationReloadListener}) — runs last. It walks
* both the item registry and the GLB cache to surface
* authoring issues via toast. If it ran earlier, missing
* items would falsely trip the "referenced but not found"
* diagnostic.</li>
* </ol>
*
* <p>When adding a new listener: decide where it sits in this
* producer/consumer chain. If you're not sure, add it at the end
* (the safest position — the rest of the graph is already built).</p>
*/
@SubscribeEvent
public static void onRegisterReloadListeners(
@@ -96,7 +143,6 @@ public final class GltfClientSetup {
ProfilerFiller profiler
) {
GltfCache.clearCache();
GltfSkinCache.clearAll();
GltfAnimationApplier.invalidateCache();
GltfMeshRenderer.clearRenderTypeCache();
// Reload context GLB animations from resource packs FIRST,
@@ -125,10 +171,7 @@ public final class GltfClientSetup {
}
}
/**
* FORGE bus event subscribers for entity lifecycle cleanup.
* Removes skin cache entries when entities leave the level, preventing memory leaks.
*/
/** FORGE bus event subscribers (client-side commands). */
@Mod.EventBusSubscriber(
modid = "tiedup",
bus = Mod.EventBusSubscriber.Bus.FORGE,
@@ -136,15 +179,6 @@ public final class GltfClientSetup {
)
public static class ForgeBusEvents {
@SubscribeEvent
public static void onEntityLeaveLevel(
net.minecraftforge.event.entity.EntityLeaveLevelEvent event
) {
if (event.getLevel().isClientSide()) {
GltfSkinCache.removeEntity(event.getEntity().getId());
}
}
@SubscribeEvent
public static void onRegisterClientCommands(
net.minecraftforge.client.event.RegisterClientCommandsEvent event