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:
@@ -103,9 +103,10 @@ public class NetworkEventHandler {
|
||||
ModNetwork.sendToPlayer(enslavementPacket, tracker);
|
||||
}
|
||||
|
||||
// FIX MC-262715: Explicitly sync riding state and position
|
||||
// This fixes the "frozen player" bug when tracker reconnects after
|
||||
// the tracked player was freed from a vehicle
|
||||
// Workaround for MC-262715: vanilla doesn't resync riding state or
|
||||
// position when a tracker reconnects, so a tracked player who was
|
||||
// freed from a vehicle between the two connect events renders
|
||||
// frozen until they next move.
|
||||
if (trackedPlayer instanceof ServerPlayer trackedServerPlayer) {
|
||||
syncRidingStateAndPosition(trackedServerPlayer, tracker);
|
||||
}
|
||||
@@ -328,6 +329,16 @@ public class NetworkEventHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Force-load the target chunk before the search so
|
||||
// cross-dimension reconnects don't silently fail on an
|
||||
// unloaded chunk — that was a "disconnect then cross-dim
|
||||
// travel to escape" hole where the null-furniture path
|
||||
// below cleared the tag.
|
||||
targetLevel.getChunk(
|
||||
furniturePos.getX() >> 4,
|
||||
furniturePos.getZ() >> 4
|
||||
);
|
||||
|
||||
// Search for the furniture entity near the stored position
|
||||
Entity furniture = findFurnitureEntity(
|
||||
targetLevel,
|
||||
@@ -384,8 +395,27 @@ public class NetworkEventHandler {
|
||||
);
|
||||
}
|
||||
|
||||
// Re-mount the player
|
||||
boolean mounted = player.startRiding(furniture, true);
|
||||
// Re-mount the player.
|
||||
// If the original seat was taken (e.g. force-mounted by
|
||||
// another player while this one was offline), assignSeat
|
||||
// would overwrite the mapping and orphan the current
|
||||
// occupant. Teleport to the furniture position instead
|
||||
// and drop the tag — keeping either player in a
|
||||
// deterministic seat is better than a double-mount.
|
||||
// Uses ISeatProvider.findPassengerInSeat so future
|
||||
// non-EntityFurniture seat providers also benefit.
|
||||
boolean mounted = false;
|
||||
if (provider.findPassengerInSeat(seatId) != null) {
|
||||
TiedUpMod.LOGGER.warn(
|
||||
"[Network] Seat '{}' on furniture {} is now occupied by another entity; skipping reconnection for {}.",
|
||||
seatId,
|
||||
furnitureUuidStr,
|
||||
player.getName().getString()
|
||||
);
|
||||
} else {
|
||||
mounted = player.startRiding(furniture, true);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
provider.assignSeat(player, seatId);
|
||||
TiedUpMod.LOGGER.info(
|
||||
|
||||
@@ -53,9 +53,9 @@ public class PacketEndConversationC2S {
|
||||
damsel = d;
|
||||
}
|
||||
|
||||
// Always clean up conversation state — this is a teardown packet.
|
||||
// Distance check removed: blocking cleanup causes permanent state leak
|
||||
// in ConversationManager.activeConversations (reviewer H18 BUG-001).
|
||||
// Teardown must not gate on distance — the damsel may have
|
||||
// moved out of range while the screen was open, and blocking
|
||||
// cleanup would leak the conversation entry permanently.
|
||||
ConversationManager.endConversation(sender, damsel);
|
||||
});
|
||||
ctx.get().setPacketHandled(true);
|
||||
|
||||
@@ -80,9 +80,9 @@ public class PacketCloseMerchantScreen {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always clean up trading state — this is a teardown packet.
|
||||
// Distance check removed: blocking cleanup causes permanent state leak
|
||||
// in tradingPlayers map (reviewer H18 BUG-002).
|
||||
// Teardown must not gate on distance — the merchant may have moved
|
||||
// out of range while the screen was open, and blocking cleanup
|
||||
// would permanently leak the player's entry in tradingPlayers.
|
||||
merchant.stopTrading(player.getUUID());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,15 +115,22 @@ public class PacketLockpickAttempt {
|
||||
ServerPlayer player,
|
||||
LockpickMiniGameState session
|
||||
) {
|
||||
// Check for furniture lockpick context FIRST — if present, this is a
|
||||
// furniture seat lockpick, not a body item lockpick. The context tag is
|
||||
// written by PacketFurnitureEscape.handleLockpick() when starting the session.
|
||||
// Furniture seat lockpick path: presence of furniture_id AND a
|
||||
// session_id matching the current session. A ctx without the nonce
|
||||
// (or with a foreign nonce) is rejected — this is the branch a
|
||||
// stale-ctx bug could otherwise mis-route into.
|
||||
CompoundTag furnitureCtx = player
|
||||
.getPersistentData()
|
||||
.getCompound("tiedup_furniture_lockpick_ctx");
|
||||
if (furnitureCtx != null && furnitureCtx.contains("furniture_id")) {
|
||||
// H18: Distance check BEFORE ending session — prevents consuming session
|
||||
// without reward if player moved away (reviewer H18 RISK-001)
|
||||
boolean ctxValid =
|
||||
furnitureCtx != null
|
||||
&& furnitureCtx.contains("furniture_id")
|
||||
&& furnitureCtx.hasUUID("session_id")
|
||||
&& furnitureCtx.getUUID("session_id").equals(session.getSessionId());
|
||||
if (ctxValid) {
|
||||
// Distance check BEFORE endLockpickSession — consuming a
|
||||
// session without applying the reward (player walked away)
|
||||
// would burn the session with no visible effect.
|
||||
int furnitureId = furnitureCtx.getInt("furniture_id");
|
||||
Entity furnitureEntity = player.level().getEntity(furnitureId);
|
||||
if (
|
||||
@@ -320,13 +327,16 @@ public class PacketLockpickAttempt {
|
||||
}
|
||||
session.setRemainingUses(remainingUses);
|
||||
|
||||
// Check for JAM (5% chance on miss) — only applies to body item lockpick sessions.
|
||||
// Furniture seat locks do not have a jam mechanic (there is no ILockable item to jam).
|
||||
// Jam mechanic (5%) only applies to body-item sessions — seat locks
|
||||
// have no ILockable stack to jam.
|
||||
boolean jammed = false;
|
||||
boolean isFurnitureSession = player
|
||||
CompoundTag sessionCtx = player
|
||||
.getPersistentData()
|
||||
.getCompound("tiedup_furniture_lockpick_ctx")
|
||||
.contains("furniture_id");
|
||||
.getCompound("tiedup_furniture_lockpick_ctx");
|
||||
boolean isFurnitureSession =
|
||||
sessionCtx.contains("furniture_id")
|
||||
&& sessionCtx.hasUUID("session_id")
|
||||
&& sessionCtx.getUUID("session_id").equals(session.getSessionId());
|
||||
|
||||
if (!isFurnitureSession && player.getRandom().nextFloat() < 0.05f) {
|
||||
int targetSlot = session.getTargetSlot();
|
||||
|
||||
Reference in New Issue
Block a user