package com.tiedup.remake.prison; import com.tiedup.remake.core.TiedUpMod; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.nbt.Tag; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.saveddata.SavedData; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** * Central manager for all prisoner data. * * Replaces: * - CaptivityStateManager (state tracking) * - Prisoner tracking in CellRegistry * - Prisoner tracking in CampOwnership * * This is the SINGLE SOURCE OF TRUTH for prisoner state. * All transitions must go through this class. */ public class PrisonerManager extends SavedData { private static final String DATA_NAME = "tiedup_prisoner_manager"; // ==================== PRIMARY DATA ==================== /** Player UUID -> PrisonerRecord */ private final Map prisoners = new ConcurrentHashMap<>(); /** Player UUID -> LaborRecord */ private final Map laborRecords = new ConcurrentHashMap<>(); /** Player UUID -> RansomRecord */ private final Map ransomRecords = new ConcurrentHashMap<>(); // ==================== INDEXES ==================== /** Camp UUID -> Set of prisoner UUIDs */ private final Map> prisonersByCamp = new ConcurrentHashMap<>(); /** Cell UUID -> Set of prisoner UUIDs */ private final Map> prisonersByCell = new ConcurrentHashMap<>(); // ==================== STATIC ACCESS ==================== public static PrisonerManager get(ServerLevel level) { return level .getDataStorage() .computeIfAbsent( PrisonerManager::load, PrisonerManager::new, DATA_NAME ); } public static PrisonerManager get(MinecraftServer server) { return get(server.overworld()); } @Nullable public static PrisonerManager get(ServerPlayer player) { if (player.getServer() == null) return null; return get(player.getServer()); } // ==================== RECORD ACCESS ==================== /** * Mark this SavedData as dirty so it gets persisted on next save. * Exposed for external callers that modify records directly (e.g., clearing stale guard refs). */ public void markDirty() { setDirty(); } /** * Get or create the prisoner record for a player. * Never returns null - creates a FREE record if none exists. */ public PrisonerRecord getRecord(UUID playerId) { return prisoners.computeIfAbsent(playerId, id -> new PrisonerRecord()); } /** * Get the prisoner record if the player is a prisoner (non-FREE state). * Returns null if player is FREE or has no record. */ @Nullable public PrisonerRecord getPrisoner(UUID playerId) { PrisonerRecord record = prisoners.get(playerId); if (record == null || record.getState() == PrisonerState.FREE) { return null; } return record; } /** * Get or create the labor record for a player. */ public LaborRecord getLaborRecord(UUID playerId) { return laborRecords.computeIfAbsent(playerId, id -> new LaborRecord()); } /** * Get labor record only if one exists. Does not create ghost entries. */ @Nullable public LaborRecord getLaborRecordIfExists(UUID playerId) { return laborRecords.get(playerId); } /** * Set the labor record for a player. */ public void setLaborRecord(UUID playerId, LaborRecord record) { laborRecords.put(playerId, record); setDirty(); } /** * Get the ransom record for a player (may be null). */ @Nullable public RansomRecord getRansomRecord(UUID playerId) { return ransomRecords.get(playerId); } /** * Set the ransom record for a player. */ public void setRansomRecord(UUID playerId, @Nullable RansomRecord record) { if (record == null) { ransomRecords.remove(playerId); } else { ransomRecords.put(playerId, record); } setDirty(); } /** * Create or get ransom record for a player. */ public RansomRecord getOrCreateRansomRecord(UUID playerId) { return ransomRecords.computeIfAbsent(playerId, id -> new RansomRecord() ); } /** * Check if a player has any prisoner data. */ public boolean hasRecord(UUID playerId) { PrisonerRecord record = prisoners.get(playerId); return record != null && record.getState() != PrisonerState.FREE; } // ==================== STATE QUERIES ==================== /** * Get the current state for a player. */ public PrisonerState getState(UUID playerId) { PrisonerRecord record = prisoners.get(playerId); return record != null ? record.getState() : PrisonerState.FREE; } /** * Check if player is captive (captured or imprisoned). */ public boolean isCaptive(UUID playerId) { return getState(playerId).isCaptive(); } /** * Check if player is imprisoned (in cell or working). */ public boolean isImprisoned(UUID playerId) { return getState(playerId).isImprisoned(); } /** * Check if player can be targeted by kidnappers. */ public boolean isTargetable(UUID playerId, long currentTime) { PrisonerRecord record = prisoners.get(playerId); if (record == null) return true; return record.isTargetable(currentTime); } /** * Check if player is protected from capture. */ public boolean isProtected(UUID playerId, long currentTime) { PrisonerRecord record = prisoners.get(playerId); if (record == null) return false; return record.isProtected(currentTime); } // ==================== INDEX QUERIES ==================== /** * Get all prisoners in a camp. */ public Set getPrisonersInCamp(UUID campId) { Set prisoners = prisonersByCamp.get(campId); return prisoners != null ? new HashSet<>(prisoners) : Collections.emptySet(); } /** * Get all prisoners in a cell. */ public Set getPrisonersInCell(UUID cellId) { Set prisoners = prisonersByCell.get(cellId); return prisoners != null ? new HashSet<>(prisoners) : Collections.emptySet(); } /** * Get prisoner count in a camp. */ public int getPrisonerCountInCamp(UUID campId) { Set prisoners = prisonersByCamp.get(campId); return prisoners != null ? prisoners.size() : 0; } /** * Get prisoner count in a cell. */ public int getPrisonerCountInCell(UUID cellId) { Set prisoners = prisonersByCell.get(cellId); return prisoners != null ? prisoners.size() : 0; } /** * Get all prisoner IDs (for iteration). */ public Set getAllPrisonerIds() { return prisoners .keySet() .stream() .filter(id -> prisoners.get(id).getState() != PrisonerState.FREE) .collect(Collectors.toSet()); } /** * Get all prisoners in a specific state. */ public Set getPrisonersInState(PrisonerState state) { return prisoners .entrySet() .stream() .filter(e -> e.getValue().getState() == state) .map(Map.Entry::getKey) .collect(Collectors.toSet()); } /** * Get prisoners in camp with specific state. */ public Set getPrisonersInCampWithState( UUID campId, PrisonerState state ) { Set campPrisoners = prisonersByCamp.get(campId); if (campPrisoners == null) return Collections.emptySet(); return campPrisoners .stream() .filter(id -> { PrisonerRecord record = prisoners.get(id); return record != null && record.getState() == state; }) .collect(Collectors.toSet()); } // ==================== STATE TRANSITIONS ==================== /** * Capture a free player. * * @param playerId Player to capture * @param captorId UUID of the kidnapper * @param currentTime Current game time * @return true if capture was successful */ public boolean capture(UUID playerId, UUID captorId, long currentTime) { PrisonerRecord record = getRecord(playerId); if ( !PrisonerTransition.capture(record, captorId, currentTime, playerId) ) { return false; } setDirty(); return true; } /** * Imprison a captured player in a camp cell. * * @param playerId Player to imprison * @param campId Camp UUID * @param cellId Cell UUID * @param currentTime Current game time * @return true if imprisonment was successful */ public boolean imprison( UUID playerId, UUID campId, UUID cellId, long currentTime ) { PrisonerRecord record = getRecord(playerId); if ( !PrisonerTransition.imprison( record, campId, cellId, currentTime, playerId ) ) { return false; } // Update indexes addToIndex(prisonersByCamp, campId, playerId); addToIndex(prisonersByCell, cellId, playerId); // Initialize labor record LaborRecord labor = getLaborRecord(playerId); labor.setPhase(LaborRecord.WorkPhase.IDLE, currentTime); setDirty(); return true; } /** * Extract prisoner from cell for labor. * * @param playerId Player to extract * @param currentTime Current game time * @return true if extraction was successful */ public boolean extract(UUID playerId, long currentTime) { PrisonerRecord record = getRecord(playerId); if (!PrisonerTransition.extract(record, currentTime, playerId)) { return false; } // Remove from cell index (still in camp though) UUID cellId = record.getCellId(); if (cellId != null) { removeFromIndex(prisonersByCell, cellId, playerId); } setDirty(); return true; } /** * Return prisoner to cell after labor. * * @param playerId Player to return * @param cellId Cell to return to (may be different from original) * @param currentTime Current game time * @return true if return was successful */ public boolean returnToCell(UUID playerId, UUID cellId, long currentTime) { PrisonerRecord record = getRecord(playerId); if (!PrisonerTransition.returnToCell(record, currentTime, playerId)) { return false; } // Update cell assignment and index record.setCellId(cellId); addToIndex(prisonersByCell, cellId, playerId); setDirty(); return true; } /** * Release a prisoner with grace period. * * @param playerId Player to release * @param currentTime Current game time * @param gracePeriodTicks Protection duration (default: 6000 = 5 min) * @return true if release was successful */ public boolean release( UUID playerId, long currentTime, long gracePeriodTicks ) { PrisonerRecord record = getRecord(playerId); UUID campId = record.getCampId(); UUID cellId = record.getCellId(); if ( !PrisonerTransition.release( record, currentTime, gracePeriodTicks, playerId ) ) { return false; } // Remove from indexes if (campId != null) { removeFromIndex(prisonersByCamp, campId, playerId); } if (cellId != null) { removeFromIndex(prisonersByCell, cellId, playerId); } // Clear labor and ransom records laborRecords.remove(playerId); ransomRecords.remove(playerId); setDirty(); return true; } /** * Release a prisoner with default 5-minute grace period. * * @param playerId Player to release * @param currentTime Current game time * @return true if release was successful */ public boolean release(UUID playerId, long currentTime) { return release(playerId, currentTime, 6000); // 5 minutes default } /** * Transition prisoner from IMPRISONED to WORKING state. * Used when extracting for labor. * * @param playerId Player to transition * @param currentTime Current game time * @return true if transition was successful */ public boolean transitionToWorking(UUID playerId, long currentTime) { return extract(playerId, currentTime); } /** * Mark a prisoner as escaped (goes directly to FREE). * * @param playerId Player who escaped * @param currentTime Current game time * @param reason Reason for escape (for logging) * @return true if escape was processed */ public boolean escape(UUID playerId, long currentTime, String reason) { PrisonerRecord record = getRecord(playerId); UUID campId = record.getCampId(); UUID cellId = record.getCellId(); if (!PrisonerTransition.escape(record, currentTime, playerId, reason)) { return false; } // Remove from indexes if (campId != null) { removeFromIndex(prisonersByCamp, campId, playerId); } if (cellId != null) { removeFromIndex(prisonersByCell, cellId, playerId); } // Clear labor and ransom records laborRecords.remove(playerId); ransomRecords.remove(playerId); setDirty(); return true; } /** * Expire protection (PROTECTED -> FREE). */ public boolean expireProtection(UUID playerId, long currentTime) { PrisonerRecord record = prisoners.get(playerId); if (record == null || record.getState() != PrisonerState.PROTECTED) { return false; } PrisonerTransition.forceTransition( record, PrisonerState.FREE, currentTime ); setDirty(); return true; } // ==================== RANSOM MANAGEMENT ==================== /** * Create a ransom for a prisoner. * * @param playerId Prisoner UUID * @param totalDebt Total debt amount * @param currentTime Current game time */ public void createRansom(UUID playerId, int totalDebt, long currentTime) { RansomRecord ransom = getOrCreateRansomRecord(playerId); ransom.setTotalDebt(totalDebt); setDirty(); } /** * Add payment to a prisoner's ransom. * * @param playerId Prisoner UUID * @param amount Payment amount * @param contributorId Player who paid (null for labor) * @return true if ransom is now fully paid */ public boolean addRansomPayment( UUID playerId, int amount, @Nullable UUID contributorId ) { RansomRecord ransom = ransomRecords.get(playerId); if (ransom == null) return false; boolean paid = ransom.addPayment(amount, contributorId); setDirty(); return paid; } /** * Increase prisoner's debt (punishment). */ public void increaseDebt(UUID playerId, int amount) { RansomRecord ransom = ransomRecords.get(playerId); if (ransom != null) { ransom.increaseDebt(amount); setDirty(); } } // ==================== LABOR MANAGEMENT ==================== /** * Assign a task to a prisoner. */ public boolean assignTask( UUID playerId, com.tiedup.remake.labor.LaborTask task, UUID maidId, long currentTime ) { LaborRecord labor = getLaborRecord(playerId); if (!labor.canAssignTask()) { return false; } labor.assignTask(task, maidId, currentTime); setDirty(); return true; } /** * Get the current work phase for a prisoner. */ public LaborRecord.WorkPhase getWorkPhase(UUID playerId) { LaborRecord labor = laborRecords.get(playerId); return labor != null ? labor.getPhase() : LaborRecord.WorkPhase.IDLE; } // ==================== INDEX HELPERS ==================== private void addToIndex(Map> index, UUID key, UUID value) { if (key == null) return; index .computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet()) .add(value); } private void removeFromIndex( Map> index, UUID key, UUID value ) { if (key == null) return; Set set = index.get(key); if (set != null) { set.remove(value); if (set.isEmpty()) { index.remove(key); } } } // ==================== CLEANUP ==================== /** * Clean up stale records for offline players. * Called periodically. */ public void cleanupOfflinePlayers( MinecraftServer server, long currentTime, long offlineTimeoutTicks ) { List toCleanup = new ArrayList<>(); for (Map.Entry entry : prisoners.entrySet()) { UUID playerId = entry.getKey(); PrisonerRecord record = entry.getValue(); // Skip FREE players if (record.getState() == PrisonerState.FREE) { continue; } // Check if player is online ServerPlayer player = server.getPlayerList().getPlayer(playerId); if (player != null) { continue; // Online, skip } // Check timeout long timeInState = record.getTimeInState(currentTime); if (timeInState > offlineTimeoutTicks) { toCleanup.add(playerId); } } for (UUID playerId : toCleanup) { TiedUpMod.LOGGER.info( "[PrisonerManager] Cleaning up offline prisoner: {}", playerId.toString().substring(0, 8) ); escape(playerId, currentTime, "offline timeout"); } } /** * Expire protection for players whose grace period ended. */ public void tickProtectionExpiry(long currentTime) { List toExpire = new ArrayList<>(); for (Map.Entry entry : prisoners.entrySet()) { PrisonerRecord record = entry.getValue(); if ( record.getState() == PrisonerState.PROTECTED && currentTime >= record.getProtectionExpiry() ) { toExpire.add(entry.getKey()); } } for (UUID playerId : toExpire) { expireProtection(playerId, currentTime); } } // ==================== PERSISTENCE ==================== @Override public @NotNull CompoundTag save(@NotNull CompoundTag tag) { // Save prisoners ListTag prisonerList = new ListTag(); for (Map.Entry entry : prisoners.entrySet()) { CompoundTag prisonerTag = new CompoundTag(); prisonerTag.putUUID("id", entry.getKey()); prisonerTag.put("record", entry.getValue().save()); prisonerList.add(prisonerTag); } tag.put("prisoners", prisonerList); // Save labor records ListTag laborList = new ListTag(); for (Map.Entry entry : laborRecords.entrySet()) { CompoundTag laborTag = new CompoundTag(); laborTag.putUUID("id", entry.getKey()); laborTag.put("record", entry.getValue().save()); laborList.add(laborTag); } tag.put("laborRecords", laborList); // Save ransom records ListTag ransomList = new ListTag(); for (Map.Entry entry : ransomRecords.entrySet()) { CompoundTag ransomTag = new CompoundTag(); ransomTag.putUUID("id", entry.getKey()); ransomTag.put("record", entry.getValue().save()); ransomList.add(ransomTag); } tag.put("ransomRecords", ransomList); return tag; } public static PrisonerManager load(CompoundTag tag) { PrisonerManager manager = new PrisonerManager(); // Load prisoners if (tag.contains("prisoners")) { ListTag prisonerList = tag.getList("prisoners", Tag.TAG_COMPOUND); for (int i = 0; i < prisonerList.size(); i++) { CompoundTag prisonerTag = prisonerList.getCompound(i); UUID id = prisonerTag.getUUID("id"); PrisonerRecord record = PrisonerRecord.load( prisonerTag.getCompound("record") ); manager.prisoners.put(id, record); // Rebuild indexes if (record.getCampId() != null) { manager.addToIndex( manager.prisonersByCamp, record.getCampId(), id ); } if (record.getCellId() != null) { manager.addToIndex( manager.prisonersByCell, record.getCellId(), id ); } } } // Load labor records if (tag.contains("laborRecords")) { ListTag laborList = tag.getList("laborRecords", Tag.TAG_COMPOUND); for (int i = 0; i < laborList.size(); i++) { CompoundTag laborTag = laborList.getCompound(i); UUID id = laborTag.getUUID("id"); LaborRecord record = LaborRecord.load( laborTag.getCompound("record") ); manager.laborRecords.put(id, record); } } // Load ransom records if (tag.contains("ransomRecords")) { ListTag ransomList = tag.getList("ransomRecords", Tag.TAG_COMPOUND); for (int i = 0; i < ransomList.size(); i++) { CompoundTag ransomTag = ransomList.getCompound(i); UUID id = ransomTag.getUUID("id"); RansomRecord record = RansomRecord.load( ransomTag.getCompound("record") ); manager.ransomRecords.put(id, record); } } return manager; } // ==================== DEBUG ==================== public String toDebugString() { StringBuilder sb = new StringBuilder(); sb.append("PrisonerManager:\n"); sb.append(" Total records: ").append(prisoners.size()).append("\n"); sb .append(" Active prisoners: ") .append(getAllPrisonerIds().size()) .append("\n"); for (PrisonerState state : PrisonerState.values()) { int count = getPrisonersInState(state).size(); if (count > 0) { sb .append(" - ") .append(state) .append(": ") .append(count) .append("\n"); } } return sb.toString(); } }