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:
@@ -0,0 +1,214 @@
|
||||
package com.tiedup.remake.v2.furniture;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for the pure-logic authorization core in {@link FurnitureAuthPredicate}.
|
||||
*
|
||||
* <p>These tests pin down the truth tables for the lock and force-mount gates
|
||||
* without touching any Minecraft API. The MC-aware wrappers ({@code canLockUnlock},
|
||||
* {@code canForceMount}) extract booleans from live entities and delegate here,
|
||||
* so full-stack validation is manual (in-game).</p>
|
||||
*
|
||||
* <p>The 2026-04-17 audit documented three divergent auth paths (BUG-002, BUG-003)
|
||||
* that this predicate now unifies. If anyone re-introduces the drift, the
|
||||
* isAuthorizedForLock / isAuthorizedForForceMount methods will catch it here.</p>
|
||||
*/
|
||||
class FurnitureAuthPredicateTest {
|
||||
|
||||
@Nested
|
||||
@DisplayName("isAuthorizedForLock — AND of 4 gates")
|
||||
class LockAuthorization {
|
||||
|
||||
@Test
|
||||
@DisplayName("all four gates true → authorized")
|
||||
void allTrue() {
|
||||
assertTrue(
|
||||
FurnitureAuthPredicate.isAuthorizedForLock(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("missing master key → denied")
|
||||
void missingKey() {
|
||||
assertFalse(
|
||||
FurnitureAuthPredicate.isAuthorizedForLock(
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("seat not lockable → denied")
|
||||
void seatNotLockable() {
|
||||
assertFalse(
|
||||
FurnitureAuthPredicate.isAuthorizedForLock(
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("seat not occupied → denied")
|
||||
void seatNotOccupied() {
|
||||
assertFalse(
|
||||
FurnitureAuthPredicate.isAuthorizedForLock(
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("occupant has no collar (or sender not owner) → denied")
|
||||
void noOwnedCollar() {
|
||||
assertFalse(
|
||||
FurnitureAuthPredicate.isAuthorizedForLock(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("all gates false → denied")
|
||||
void allFalse() {
|
||||
assertFalse(
|
||||
FurnitureAuthPredicate.isAuthorizedForLock(
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("isAuthorizedForForceMount — AND of 5 gates")
|
||||
class ForceMountAuthorization {
|
||||
|
||||
@Test
|
||||
@DisplayName("all five gates true → authorized")
|
||||
void allTrue() {
|
||||
assertTrue(
|
||||
FurnitureAuthPredicate.isAuthorizedForForceMount(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("captive not alive → denied")
|
||||
void captiveDead() {
|
||||
assertFalse(
|
||||
FurnitureAuthPredicate.isAuthorizedForForceMount(
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("captive out of range → denied")
|
||||
void captiveOutOfRange() {
|
||||
assertFalse(
|
||||
FurnitureAuthPredicate.isAuthorizedForForceMount(
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("no owned collar → denied")
|
||||
void noOwnedCollar() {
|
||||
assertFalse(
|
||||
FurnitureAuthPredicate.isAuthorizedForForceMount(
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("captive not tied up (leashed) → denied (audit BUG-003)")
|
||||
void notTiedUp() {
|
||||
// This is the exact regression guard for audit BUG-003: before the
|
||||
// fix, the packet path did NOT check isTiedUp, so any collar-owning
|
||||
// captor could force-mount a captive that had never been leashed.
|
||||
assertFalse(
|
||||
FurnitureAuthPredicate.isAuthorizedForForceMount(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("captive already passenger of another entity → denied (audit BUG-003)")
|
||||
void alreadyPassenger() {
|
||||
// BUG-003 regression guard: before the fix, the packet path used
|
||||
// startRiding(force=true) which would break prior mounts silently.
|
||||
assertFalse(
|
||||
FurnitureAuthPredicate.isAuthorizedForForceMount(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("all gates false → denied")
|
||||
void allFalse() {
|
||||
assertFalse(
|
||||
FurnitureAuthPredicate.isAuthorizedForForceMount(
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.tiedup.remake.v2.furniture;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for the server-side fallback seat-geometry math extracted from the
|
||||
* three inlined {@code EntityFurniture} call sites.
|
||||
*
|
||||
* <p>The formulas themselves are trivial; these tests are a regression guard
|
||||
* against someone "cleaning up" the right-axis expression and inverting a
|
||||
* sign, or changing the seat-spacing convention (which is observable in
|
||||
* gameplay — passengers shift left/right on asymmetric furniture).</p>
|
||||
*/
|
||||
class FurnitureSeatGeometryTest {
|
||||
|
||||
private static final double EPS = 1e-9;
|
||||
|
||||
@Nested
|
||||
@DisplayName("rightAxis — entity-local right in world XZ")
|
||||
class RightAxis {
|
||||
|
||||
@Test
|
||||
@DisplayName("yaw=0 → right axis points to world -X (MC south-facing convention)")
|
||||
void yawZero() {
|
||||
Vec3 r = FurnitureSeatGeometry.rightAxis(0f);
|
||||
assertEquals(-1.0, r.x, EPS);
|
||||
assertEquals(0.0, r.y, EPS);
|
||||
assertEquals(0.0, r.z, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("yaw=180 → right axis flips to +X")
|
||||
void yaw180() {
|
||||
Vec3 r = FurnitureSeatGeometry.rightAxis(180f);
|
||||
assertEquals(1.0, r.x, EPS);
|
||||
assertEquals(0.0, r.z, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("yaw=90 → right axis points to world -Z")
|
||||
void yaw90() {
|
||||
Vec3 r = FurnitureSeatGeometry.rightAxis(90f);
|
||||
assertEquals(0.0, r.x, EPS);
|
||||
assertEquals(-1.0, r.z, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Y is always 0 (right axis is horizontal)")
|
||||
void yIsAlwaysZero() {
|
||||
for (float yaw = -360f; yaw <= 360f; yaw += 37f) {
|
||||
assertEquals(0.0, FurnitureSeatGeometry.rightAxis(yaw).y, EPS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("seatOffset — even spacing around centre")
|
||||
class SeatOffset {
|
||||
|
||||
@Test
|
||||
@DisplayName("1 seat → always 0 (centered)")
|
||||
void singleSeat() {
|
||||
assertEquals(0.0, FurnitureSeatGeometry.seatOffset(0, 1), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("2 seats → -0.5, 0.5")
|
||||
void twoSeats() {
|
||||
assertEquals(-0.5, FurnitureSeatGeometry.seatOffset(0, 2), EPS);
|
||||
assertEquals(0.5, FurnitureSeatGeometry.seatOffset(1, 2), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("3 seats → -1, 0, 1")
|
||||
void threeSeats() {
|
||||
assertEquals(-1.0, FurnitureSeatGeometry.seatOffset(0, 3), EPS);
|
||||
assertEquals(0.0, FurnitureSeatGeometry.seatOffset(1, 3), EPS);
|
||||
assertEquals(1.0, FurnitureSeatGeometry.seatOffset(2, 3), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("4 seats → -1.5, -0.5, 0.5, 1.5")
|
||||
void fourSeats() {
|
||||
assertEquals(-1.5, FurnitureSeatGeometry.seatOffset(0, 4), EPS);
|
||||
assertEquals(-0.5, FurnitureSeatGeometry.seatOffset(1, 4), EPS);
|
||||
assertEquals(0.5, FurnitureSeatGeometry.seatOffset(2, 4), EPS);
|
||||
assertEquals(1.5, FurnitureSeatGeometry.seatOffset(3, 4), EPS);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user