Files
TiedUp-/src/main/java/com/tiedup/remake/prison/PrisonerManager.java

809 lines
24 KiB
Java

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<UUID, PrisonerRecord> prisoners =
new ConcurrentHashMap<>();
/** Player UUID -> LaborRecord */
private final Map<UUID, LaborRecord> laborRecords =
new ConcurrentHashMap<>();
/** Player UUID -> RansomRecord */
private final Map<UUID, RansomRecord> ransomRecords =
new ConcurrentHashMap<>();
// ==================== INDEXES ====================
/** Camp UUID -> Set of prisoner UUIDs */
private final Map<UUID, Set<UUID>> prisonersByCamp =
new ConcurrentHashMap<>();
/** Cell UUID -> Set of prisoner UUIDs */
private final Map<UUID, Set<UUID>> 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<UUID> getPrisonersInCamp(UUID campId) {
Set<UUID> prisoners = prisonersByCamp.get(campId);
return prisoners != null
? new HashSet<>(prisoners)
: Collections.emptySet();
}
/**
* Get all prisoners in a cell.
*/
public Set<UUID> getPrisonersInCell(UUID cellId) {
Set<UUID> prisoners = prisonersByCell.get(cellId);
return prisoners != null
? new HashSet<>(prisoners)
: Collections.emptySet();
}
/**
* Get prisoner count in a camp.
*/
public int getPrisonerCountInCamp(UUID campId) {
Set<UUID> prisoners = prisonersByCamp.get(campId);
return prisoners != null ? prisoners.size() : 0;
}
/**
* Get prisoner count in a cell.
*/
public int getPrisonerCountInCell(UUID cellId) {
Set<UUID> prisoners = prisonersByCell.get(cellId);
return prisoners != null ? prisoners.size() : 0;
}
/**
* Get all prisoner IDs (for iteration).
*/
public Set<UUID> 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<UUID> 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<UUID> getPrisonersInCampWithState(
UUID campId,
PrisonerState state
) {
Set<UUID> 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<UUID, Set<UUID>> index, UUID key, UUID value) {
if (key == null) return;
index
.computeIfAbsent(key, k -> ConcurrentHashMap.newKeySet())
.add(value);
}
private void removeFromIndex(
Map<UUID, Set<UUID>> index,
UUID key,
UUID value
) {
if (key == null) return;
Set<UUID> 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<UUID> toCleanup = new ArrayList<>();
for (Map.Entry<UUID, PrisonerRecord> 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<UUID> toExpire = new ArrayList<>();
for (Map.Entry<UUID, PrisonerRecord> 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<UUID, PrisonerRecord> 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<UUID, LaborRecord> 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<UUID, RansomRecord> 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();
}
}