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 37da2c1716
commit 11188bc621
63 changed files with 4965 additions and 2226 deletions

View File

@@ -1,5 +1,6 @@
package com.tiedup.remake.client.gltf;
import com.mojang.blaze3d.systems.RenderSystem;
import dev.kosmx.playerAnim.core.util.Pair;
import dev.kosmx.playerAnim.impl.IAnimatedPlayer;
import dev.kosmx.playerAnim.impl.animation.AnimationApplier;
@@ -42,6 +43,22 @@ public final class GltfLiveBoneReader {
private GltfLiveBoneReader() {}
// Scratch pools for joint-matrix computation. Render-thread-only
// (asserted below). Pre-populated Matrix4f slots are reused via
// set() / identity() / mul(). See GltfSkinningEngine for the twin pool.
private static Matrix4f[] scratchJointMatrices = new Matrix4f[0];
private static Matrix4f[] scratchWorldTransforms = new Matrix4f[0];
private static final Matrix4f scratchLocal = new Matrix4f();
private static Matrix4f[] ensureScratch(Matrix4f[] current, int needed) {
if (current.length >= needed) return current;
Matrix4f[] next = new Matrix4f[needed];
int i = 0;
for (; i < current.length; i++) next[i] = current[i];
for (; i < needed; i++) next[i] = new Matrix4f();
return next;
}
/**
* Compute joint matrices by reading live skeleton state from the HumanoidModel.
* <p>
@@ -57,7 +74,9 @@ public final class GltfLiveBoneReader {
* @param model the HumanoidModel after PlayerAnimator has applied rotations
* @param data parsed glTF data (MC-converted)
* @param entity the living entity being rendered
* @return array of joint matrices ready for skinning, or null on failure
* @return live reference to an internal scratch buffer (or null on failure).
* Caller MUST consume before the next call to any {@code compute*}
* method on this class; do not store.
*/
public static Matrix4f[] computeJointMatricesFromModel(
HumanoidModel<?> model,
@@ -65,10 +84,17 @@ public final class GltfLiveBoneReader {
LivingEntity entity
) {
if (model == null || data == null || entity == null) return null;
assert RenderSystem.isOnRenderThread()
: "GltfLiveBoneReader.computeJointMatricesFromModel must run on the render thread (scratch buffers are not thread-safe)";
int jointCount = data.jointCount();
Matrix4f[] jointMatrices = new Matrix4f[jointCount];
Matrix4f[] worldTransforms = new Matrix4f[jointCount];
scratchJointMatrices = ensureScratch(scratchJointMatrices, jointCount);
scratchWorldTransforms = ensureScratch(
scratchWorldTransforms,
jointCount
);
Matrix4f[] jointMatrices = scratchJointMatrices;
Matrix4f[] worldTransforms = scratchWorldTransforms;
int[] parents = data.parentJointIndices();
String[] jointNames = data.jointNames();
@@ -109,23 +135,22 @@ public final class GltfLiveBoneReader {
}
// Build local transform: translate(restTranslation) * rotate(localRot)
Matrix4f local = new Matrix4f();
local.translate(restTranslations[j]);
local.rotate(localRot);
scratchLocal.identity();
scratchLocal.translate(restTranslations[j]);
scratchLocal.rotate(localRot);
// Compose with parent to get world transform
if (parents[j] >= 0 && worldTransforms[parents[j]] != null) {
worldTransforms[j] = new Matrix4f(
worldTransforms[parents[j]]
).mul(local);
// Compose with parent to get world transform.
// Same semantics as pre-refactor: treat as root when parent hasn't
// been processed yet (parents[j] >= j was a null in the old array).
Matrix4f world = worldTransforms[j];
if (parents[j] >= 0 && parents[j] < j) {
world.set(worldTransforms[parents[j]]).mul(scratchLocal);
} else {
worldTransforms[j] = new Matrix4f(local);
world.set(scratchLocal);
}
// Final joint matrix = worldTransform * inverseBindMatrix
jointMatrices[j] = new Matrix4f(worldTransforms[j]).mul(
data.inverseBindMatrices()[j]
);
jointMatrices[j].set(world).mul(data.inverseBindMatrices()[j]);
}
return jointMatrices;