Files
TiedUp-/src/main/java/com/tiedup/remake/events/lifecycle/PlayerDisconnectHandler.java
NotEvil 11188bc621 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.
2026-04-18 17:34:03 +02:00

179 lines
6.8 KiB
Java

package com.tiedup.remake.events.lifecycle;
import com.mojang.logging.LogUtils;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.events.captivity.ForcedSeatingHandler;
import com.tiedup.remake.events.restriction.LaborToolProtectionHandler;
import com.tiedup.remake.events.restriction.PetPlayRestrictionHandler;
import com.tiedup.remake.network.PacketRateLimiter;
import net.minecraftforge.event.entity.player.PlayerEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import org.slf4j.Logger;
/**
* Handler for player disconnect events to clean up server-side resources.
*
* <p>This handler ensures that resources associated with disconnected players
* are properly cleaned up to prevent memory leaks and unbounded map growth.
*
* <p>Phase: Server Resource Management
*
* <p>Cleanup includes:
* <ul>
* <li>Rate limiter token buckets ({@link PacketRateLimiter})</li>
* <li>Future: Session data, pending tasks, etc.</li>
* </ul>
*/
@Mod.EventBusSubscriber(
modid = TiedUpMod.MOD_ID,
bus = Mod.EventBusSubscriber.Bus.FORGE
)
public class PlayerDisconnectHandler {
private static final Logger LOGGER = LogUtils.getLogger();
/**
* Clean up resources when a player logs out.
*
* <p>This event fires when a player disconnects from the server,
* either by logging out normally or being kicked/timing out.
*
* @param event The player logout event
*/
@SubscribeEvent
public static void onPlayerLogout(PlayerEvent.PlayerLoggedOutEvent event) {
java.util.UUID playerId = event.getEntity().getUUID();
// NOTE: PlayerBindState.removeInstance() is called by PlayerLifecycleHandler (EventPriority.HIGH)
// We don't duplicate it here to avoid redundant cleanup
// Clean up rate limiter state
PacketRateLimiter.cleanup(playerId);
// Clean up static cooldown maps to prevent memory leaks
com.tiedup.remake.commands.SocialCommand.cleanupPlayer(playerId);
LaborToolProtectionHandler.cleanupPlayer(playerId);
// BUG FIX: Memory leak cleanup for event handlers
// Clean up ForcedSeatingHandler maps
ForcedSeatingHandler.clearPlayer(playerId);
// Clean up PetPlayRestrictionHandler timestamp map
PetPlayRestrictionHandler.clearPlayer(playerId);
// Clean up minigame sessions
com.tiedup.remake.minigame.MiniGameSessionManager.getInstance().cleanupPlayer(
playerId
);
// Clean up pet cage state
com.tiedup.remake.v2.blocks.PetCageManager.onPlayerDisconnect(playerId);
// Clean up pet bed state (has onPlayerDisconnect but was never wired)
com.tiedup.remake.v2.blocks.PetBedManager.onPlayerDisconnect(playerId);
// Clean up active conversations
com.tiedup.remake.dialogue.conversation.ConversationManager.cleanupPlayer(
playerId
);
// Clean up cell selection mode
com.tiedup.remake.cells.CellSelectionManager.cleanup(playerId);
// NOTE: tiedup_locked_furniture is intentionally NOT cleaned on logout —
// it's load-bearing for NetworkEventHandler.handleFurnitureReconnection
// (the "disconnect to escape" prevention).
//
// tiedup_furniture_lockpick_ctx IS cleaned: it's session-ephemeral,
// valid only during an active lockpick mini-game. If left stale it
// causes PacketLockpickAttempt to mis-route a later body-item
// lockpick as a furniture pick and silently return without ending
// the session.
if (event.getEntity() instanceof net.minecraft.server.level.ServerPlayer serverPlayer) {
serverPlayer.getPersistentData().remove("tiedup_furniture_lockpick_ctx");
}
// BUG FIX: Security - Remove labor tools from disconnecting player
// This prevents players from keeping unbreakable tools by disconnecting
if (
event.getEntity() instanceof
net.minecraft.server.level.ServerPlayer player
) {
removeLaborTools(player);
}
// BUG FIX: Memory leak cleanup for entities
// Clean up EntityKidnapperMerchant tradingPlayers set (O(1) reverse-lookup)
if (
event.getEntity().level() instanceof
net.minecraft.server.level.ServerLevel serverLevel
) {
java.util.UUID merchantUUID =
com.tiedup.remake.entities.EntityKidnapperMerchant.getMerchantForPlayer(
playerId
);
if (merchantUUID != null) {
net.minecraft.world.entity.Entity merchantEntity =
serverLevel.getEntity(merchantUUID);
if (
merchantEntity instanceof
com.tiedup.remake.entities.EntityKidnapperMerchant merchant
) {
merchant.cleanupTradingPlayer(playerId);
}
}
// Kidnapper robbery immunity: cheap per-entity Map.remove(), disconnect-only — acceptable scan
// Uses getAllEntities since there's no UUID index for this reverse lookup
for (net.minecraft.world.entity.Entity entity : serverLevel.getAllEntities()) {
if (
entity instanceof
com.tiedup.remake.entities.EntityKidnapper kidnapper
) {
kidnapper.clearRobbedImmunity(playerId);
}
}
}
LOGGER.debug(
"Cleaned up server resources for player: {} ({})",
event.getEntity().getName().getString(),
playerId
);
}
/**
* SECURITY: Remove all labor tools from player inventory on disconnect.
* Prevents exploit where players disconnect to keep unbreakable tools.
*/
private static void removeLaborTools(
net.minecraft.server.level.ServerPlayer player
) {
var inventory = player.getInventory();
int removedCount = 0;
for (int i = 0; i < inventory.getContainerSize(); i++) {
net.minecraft.world.item.ItemStack stack = inventory.getItem(i);
if (!stack.isEmpty() && stack.hasTag()) {
net.minecraft.nbt.CompoundTag tag = stack.getTag();
if (tag != null && tag.getBoolean("LaborTool")) {
inventory.setItem(
i,
net.minecraft.world.item.ItemStack.EMPTY
);
removedCount++;
}
}
}
if (removedCount > 0) {
LOGGER.info(
"[PlayerDisconnectHandler] Removed {} labor tools from {} on disconnect",
removedCount,
player.getName().getString()
);
}
}
}