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. * *

This handler ensures that resources associated with disconnected players * are properly cleaned up to prevent memory leaks and unbounded map growth. * *

Phase: Server Resource Management * *

Cleanup includes: *

*/ @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. * *

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() ); } } }