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.
179 lines
6.8 KiB
Java
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()
|
|
);
|
|
}
|
|
}
|
|
}
|