package com.tiedup.remake.cells; import com.tiedup.remake.core.TiedUpMod; import com.tiedup.remake.items.base.ILockable; import com.tiedup.remake.items.base.ItemCollar; import com.tiedup.remake.prison.PrisonerManager; import com.tiedup.remake.state.IRestrainable; import com.tiedup.remake.util.KidnappedHelper; import com.tiedup.remake.v2.BodyRegionV2; import java.util.List; import java.util.Set; import java.util.UUID; import net.minecraft.ChatFormatting; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; /** * Handles camp lifecycle events: camp death, prisoner freeing, and camp defense alerts. * *

This is a stateless utility class. All state lives in {@link CampOwnership} * (the SavedData singleton). Methods here orchestrate multi-system side effects * that don't belong in the data layer. */ public final class CampLifecycleManager { private CampLifecycleManager() {} // utility class /** * Mark a camp as dead (trader killed) and perform full cleanup. * This: * - Cancels all ransoms for the camp's prisoners * - Frees all prisoners (untie, unlock collars) * - Clears all labor states * - Removes all cells belonging to the camp * - Makes the camp inactive * * @param campId The camp UUID * @param level The server level for entity lookups */ public static void markCampDead(UUID campId, ServerLevel level) { CampOwnership ownership = CampOwnership.get(level); CampOwnership.CampData data = ownership.getCamp(campId); if (data == null) { return; } UUID traderUUID = data.getTraderUUID(); TiedUpMod.LOGGER.info( "[CampLifecycleManager] Camp {} dying - freeing all prisoners", campId.toString().substring(0, 8) ); // PERFORMANCE FIX: Use PrisonerManager's index instead of CellRegistry // This is O(1) lookup instead of iterating all cells PrisonerManager manager = PrisonerManager.get(level); Set prisonerIds = manager.getPrisonersInCamp(campId); TiedUpMod.LOGGER.debug( "[CampLifecycleManager] Found {} prisoners in camp {} via index", prisonerIds.size(), campId.toString().substring(0, 8) ); // Cancel ransoms and free each prisoner for (UUID prisonerId : prisonerIds) { // Cancel ransom by clearing it if (manager.getRansomRecord(prisonerId) != null) { manager.setRansomRecord(prisonerId, null); TiedUpMod.LOGGER.debug( "[CampLifecycleManager] Cancelled ransom for prisoner {}", prisonerId.toString().substring(0, 8) ); } // Free the prisoner ServerPlayer prisoner = level .getServer() .getPlayerList() .getPlayer(prisonerId); if (prisoner != null) { // Online: untie, unlock collar, release with 5-min grace period removeLaborTools(prisoner); freePrisonerOnCampDeath(prisoner, traderUUID, level); // Cell cleanup only -- freePrisonerOnCampDeath already called release() // which transitions to PROTECTED with grace period. // Calling escape() here would override PROTECTED->FREE, losing the grace. CellRegistryV2.get(level).releasePrisonerFromAllCells( prisonerId ); } else { // Offline: full escape via PrisonerService (no grace period needed) com.tiedup.remake.prison.service.PrisonerService.get().escape( level, prisonerId, "camp death" ); } ownership.unmarkPrisonerProcessed(prisonerId); } // HIGH FIX: Remove all cells belonging to this camp from CellRegistryV2 // Prevents memory leak and stale data in indices CellRegistryV2 cellRegistry = CellRegistryV2.get(level); List campCells = cellRegistry.getCellsByCamp(campId); for (CellDataV2 cell : campCells) { cellRegistry.removeCell(cell.getId()); TiedUpMod.LOGGER.debug( "[CampLifecycleManager] Removed cell {} from registry", cell.getId().toString().substring(0, 8) ); } TiedUpMod.LOGGER.info( "[CampLifecycleManager] Removed {} cells for dead camp {}", campCells.size(), campId.toString().substring(0, 8) ); // Mark camp as dead data.setAlive(false); ownership.setDirty(); TiedUpMod.LOGGER.info( "[CampLifecycleManager] Camp {} is now dead, {} prisoners freed", campId.toString().substring(0, 8), prisonerIds.size() ); } /** * Alert all NPCs in a camp to defend against an attacker. * Called when someone tries to restrain the trader or maid. * * @param campId The camp UUID * @param attacker The player attacking * @param level The server level */ public static void alertCampToDefend( UUID campId, Player attacker, ServerLevel level ) { CampOwnership ownership = CampOwnership.get(level); CampOwnership.CampData camp = ownership.getCamp(campId); if (camp == null) return; int alertedCount = 0; // 1. Alert the trader UUID traderUUID = camp.getTraderUUID(); if (traderUUID != null) { net.minecraft.world.entity.Entity traderEntity = level.getEntity( traderUUID ); if ( traderEntity instanceof com.tiedup.remake.entities.EntitySlaveTrader trader ) { if (trader.isAlive() && !trader.isTiedUp()) { trader.setTarget(attacker); trader.setLastAttacker(attacker); alertedCount++; } } } // 2. Alert the maid UUID maidUUID = camp.getMaidUUID(); if (maidUUID != null) { net.minecraft.world.entity.Entity maidEntity = level.getEntity( maidUUID ); if ( maidEntity instanceof com.tiedup.remake.entities.EntityMaid maid ) { if (maid.isAlive() && !maid.isTiedUp()) { maid.setTarget(attacker); maid.setMaidState( com.tiedup.remake.entities.ai.maid.MaidState.DEFENDING ); alertedCount++; } } } // 3. Alert all kidnappers Set kidnapperUUIDs = camp.getLinkedKidnappers(); for (UUID kidnapperUUID : kidnapperUUIDs) { net.minecraft.world.entity.Entity entity = level.getEntity( kidnapperUUID ); if ( entity instanceof com.tiedup.remake.entities.EntityKidnapper kidnapper ) { if (kidnapper.isAlive() && !kidnapper.isTiedUp()) { kidnapper.setTarget(attacker); kidnapper.setLastAttacker(attacker); alertedCount++; } } } TiedUpMod.LOGGER.info( "[CampLifecycleManager] Camp {} alerted {} NPCs to defend against {}", campId.toString().substring(0, 8), alertedCount, attacker.getName().getString() ); } // ==================== PRIVATE HELPERS ==================== /** * Free a prisoner when their camp dies. * Untie, unlock collar, cancel sale, notify. * Uses legitimate removal flag to prevent alerting kidnappers. */ private static void freePrisonerOnCampDeath( ServerPlayer prisoner, UUID traderUUID, ServerLevel level ) { IRestrainable state = KidnappedHelper.getKidnappedState(prisoner); if (state == null) { return; } // Suppress collar removal alerts - this is a legitimate release (camp death) ItemCollar.runWithSuppressedAlert(() -> { // Unlock collar if owned by the dead camp/trader unlockCollarIfOwnedBy(prisoner, state, traderUUID); // Remove all restraints (including collar if any) state.untie(true); // Cancel sale state.cancelSale(); }); // Clear client HUD com.tiedup.remake.network.ModNetwork.sendToPlayer( new com.tiedup.remake.network.labor.PacketSyncLaborProgress(), prisoner ); // Notify prisoner prisoner.sendSystemMessage( Component.literal("Your captor has died. You are FREE!").withStyle( ChatFormatting.GREEN, ChatFormatting.BOLD ) ); // Grant grace period (5 minutes = 6000 ticks) PrisonerManager manager = PrisonerManager.get(level); manager.release(prisoner.getUUID(), level.getGameTime(), 6000); prisoner.sendSystemMessage( Component.literal( "You have 5 minutes of protection from kidnappers." ).withStyle(ChatFormatting.AQUA) ); TiedUpMod.LOGGER.info( "[CampLifecycleManager] Freed prisoner {} on camp death (no alert)", prisoner.getName().getString() ); } /** * Unlock a prisoner's collar if it's owned by the specified owner (trader/kidnapper). */ private static void unlockCollarIfOwnedBy( ServerPlayer prisoner, IRestrainable state, UUID ownerUUID ) { ItemStack collar = state.getEquipment(BodyRegionV2.NECK); if (collar.isEmpty()) { return; } if (collar.getItem() instanceof ItemCollar collarItem) { List owners = collarItem.getOwners(collar); // If the dead trader/camp is an owner, unlock the collar if (owners.contains(ownerUUID)) { if (collar.getItem() instanceof ILockable lockable) { lockable.setLockedByKeyUUID(collar, null); // Unlock and clear } TiedUpMod.LOGGER.debug( "[CampLifecycleManager] Unlocked collar for {} (owner {} died)", prisoner.getName().getString(), ownerUUID.toString().substring(0, 8) ); } } } /** * SECURITY: Remove all labor tools from player inventory. * Prevents prisoners from keeping unbreakable tools when freed/released. */ private static void removeLaborTools(ServerPlayer player) { var inventory = player.getInventory(); int removedCount = 0; for (int i = 0; i < inventory.getContainerSize(); i++) { ItemStack stack = inventory.getItem(i); if (!stack.isEmpty() && stack.hasTag()) { CompoundTag tag = stack.getTag(); if (tag != null && tag.getBoolean("LaborTool")) { inventory.setItem(i, ItemStack.EMPTY); removedCount++; } } } if (removedCount > 0) { TiedUpMod.LOGGER.debug( "[CampLifecycleManager] Removed {} labor tools from {} on camp death", removedCount, player.getName().getString() ); } } }