split PrisonerService + decompose EntityKidnapper

PrisonerService 1057L -> 474L lifecycle + 616L EscapeMonitorService
EntityKidnapper 2035L -> 1727L via LootManager, Dialogue, CaptivePriority extraction
This commit is contained in:
NotEvil
2026-04-16 14:08:52 +02:00
parent ea14fc2cec
commit f4aa5ffdc5
25 changed files with 1421 additions and 968 deletions

View File

@@ -6,40 +6,28 @@ import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.entities.AbstractTiedUpNpc;
import com.tiedup.remake.entities.EntityDamsel;
import com.tiedup.remake.personality.PersonalityState;
import com.tiedup.remake.prison.LaborRecord;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
import com.tiedup.remake.prison.PrisonerState;
import com.tiedup.remake.prison.service.BondageService;
import com.tiedup.remake.state.CollarRegistry;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.state.ICaptor;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.v2.BodyRegionV2;
import java.util.*;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
/**
* Centralized prisoner lifecycle service.
* Prisoner lifecycle service.
*
* Manages ALL prisoner state transitions atomically:
* Manages prisoner state transitions atomically:
* - PrisonerManager (SavedData state)
* - CellRegistryV2 (cell assignment)
* - Leash/captor refs (IBondageState / ICaptor)
* - Escape detection (tick)
*
* Replaces EscapeService and unifies scattered capture/imprison/extract/return/transfer logic.
* Handles: capture, imprison, extractFromCell, returnToCell, transferCaptive.
* Escape detection and release are handled by {@link EscapeMonitorService}.
*
* AI goals manage: Navigation, Teleport, Equipment, Dialogue.
* PrisonerService manages: State transitions + leash coordination.
@@ -54,29 +42,6 @@ public class PrisonerService {
private PrisonerService() {}
// ==================== CONSTANTS (from EscapeService) ====================
/** Maximum distance from cell for IMPRISONED prisoners (blocks) */
public static final double CELL_ESCAPE_DISTANCE = 20.0;
/** Maximum distance from camp for WORKING prisoners (blocks) */
public static final double WORK_ESCAPE_DISTANCE = 100.0;
/** Maximum time offline before escape (30 minutes in ticks) */
public static final long OFFLINE_TIMEOUT_TICKS = 30 * 60 * 20L;
/** Maximum time in WORKING state before forced return (10 minutes in ticks) */
public static final long WORK_TIMEOUT_TICKS = 10 * 60 * 20L;
/** Maximum time in RETURNING phase before escape (5 minutes in ticks) */
public static final long RETURN_TIMEOUT_TICKS = 5 * 60 * 20L;
/** Maximum time in PENDING_RETURN phase before forced return (2.5 minutes in ticks) */
public static final long PENDING_RETURN_TIMEOUT_TICKS = 3000;
/** Check interval in ticks (every 5 seconds) */
public static final int CHECK_INTERVAL_TICKS = 100;
// ==================== CAPTURE ====================
/**
@@ -506,552 +471,4 @@ public class PrisonerService {
return true;
}
// ==================== ESCAPE (from EscapeService) ====================
/**
* CENTRAL ESCAPE METHOD - All escapes must go through here.
*
* Handles complete cleanup:
* - State transition (PrisonerManager)
* - Cell registry cleanup
* - Guard despawn
* - Restraints removal (if online)
* - Inventory NOT restored (stays in camp chest as punishment)
*
* @param level The server level
* @param playerId Player UUID
* @param reason Reason for escape (for logging)
* @return true if escape was processed successfully
*/
public boolean escape(ServerLevel level, UUID playerId, String reason) {
long currentTime = level.getGameTime();
PrisonerManager manager = PrisonerManager.get(level);
CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
TiedUpMod.LOGGER.info(
"[PrisonerService] Processing escape for {} - reason: {}",
playerId.toString().substring(0, 8),
reason
);
// Step 1: Save guard ID BEFORE state transition (manager.escape() removes the LaborRecord)
LaborRecord laborBeforeEscape = manager.getLaborRecord(playerId);
UUID guardId = laborBeforeEscape.getGuardId();
// Step 2: Transition prisoner state to FREE
boolean stateChanged = manager.escape(playerId, currentTime, reason);
if (!stateChanged) {
TiedUpMod.LOGGER.warn(
"[PrisonerService] Failed to change state for {} - invalid transition",
playerId.toString().substring(0, 8)
);
return false;
}
// Step 3: Cleanup CellRegistryV2 - remove from all cells
int cellsCleared = cellRegistry.releasePrisonerFromAllCells(playerId);
if (cellsCleared > 0) {
TiedUpMod.LOGGER.debug(
"[PrisonerService] Cleared {} from {} cells",
playerId.toString().substring(0, 8),
cellsCleared
);
}
// Step 4: Cleanup guard using saved reference (LaborRecord was removed by manager.escape())
if (guardId != null) {
net.minecraft.world.entity.Entity guardEntity = level.getEntity(
guardId
);
if (guardEntity != null) {
guardEntity.discard();
TiedUpMod.LOGGER.debug(
"[PrisonerService] Despawned guard {} during escape",
guardId.toString().substring(0, 8)
);
}
}
// Step 5: Free from restraints (if player is online)
// NOTE: Inventory is NOT restored on escape - items remain in camp chest as punishment
ServerPlayer player = level
.getServer()
.getPlayerList()
.getPlayer(playerId);
if (player != null) {
IBondageState cap = KidnappedHelper.getKidnappedState(player);
if (cap != null) {
cap.free(false);
TiedUpMod.LOGGER.info(
"[PrisonerService] Freed {} from restraints (inventory remains in camp chest)",
player.getName().getString()
);
}
} else {
TiedUpMod.LOGGER.debug(
"[PrisonerService] Player {} offline - restraints cleanup deferred",
playerId.toString().substring(0, 8)
);
}
TiedUpMod.LOGGER.info(
"[PrisonerService] Escape complete for {} - items stay in camp chest",
playerId.toString().substring(0, 8)
);
return true;
}
// ==================== RELEASE (from EscapeService) ====================
/**
* CENTRAL RELEASE METHOD - All legitimate releases must go through here.
*
* Similar to escape() but transitions to PROTECTED state with grace period.
* Handles complete cleanup:
* - State transition to PROTECTED (PrisonerManager)
* - Cell registry cleanup
* - Inventory restoration
* - Restraints removal (if online)
*
* @param level The server level
* @param playerId Player UUID
* @param gracePeriodTicks Grace period before returning to FREE (0 = instant FREE)
* @return true if release was processed successfully
*/
public boolean release(
ServerLevel level,
UUID playerId,
long gracePeriodTicks
) {
long currentTime = level.getGameTime();
PrisonerManager manager = PrisonerManager.get(level);
CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
com.tiedup.remake.cells.ConfiscatedInventoryRegistry inventoryRegistry =
com.tiedup.remake.cells.ConfiscatedInventoryRegistry.get(level);
TiedUpMod.LOGGER.info(
"[PrisonerService] Processing release for {} - grace period: {} ticks",
playerId.toString().substring(0, 8),
gracePeriodTicks
);
// Step 1: Transition prisoner state to PROTECTED (or FREE if gracePeriod = 0)
boolean stateChanged = manager.release(
playerId,
currentTime,
gracePeriodTicks
);
if (!stateChanged) {
TiedUpMod.LOGGER.warn(
"[PrisonerService] Failed to change state for {} - invalid transition",
playerId.toString().substring(0, 8)
);
return false;
}
// Step 2: Cleanup CellRegistryV2 - remove from all cells
int cellsCleared = cellRegistry.releasePrisonerFromAllCells(playerId);
if (cellsCleared > 0) {
TiedUpMod.LOGGER.debug(
"[PrisonerService] Cleared {} from {} cells",
playerId.toString().substring(0, 8),
cellsCleared
);
}
// Step 3: Restore confiscated inventory (if player is online)
ServerPlayer player = level
.getServer()
.getPlayerList()
.getPlayer(playerId);
if (player != null) {
// Restore inventory
if (inventoryRegistry.hasConfiscatedInventory(playerId)) {
boolean restored = inventoryRegistry.restoreInventory(player);
if (restored) {
TiedUpMod.LOGGER.info(
"[PrisonerService] Restored confiscated inventory for {}",
player.getName().getString()
);
}
}
// Free from restraints
IBondageState cap = KidnappedHelper.getKidnappedState(player);
if (cap != null) {
cap.free(false);
TiedUpMod.LOGGER.debug(
"[PrisonerService] Freed {} from restraints",
player.getName().getString()
);
}
} else {
TiedUpMod.LOGGER.debug(
"[PrisonerService] Player {} offline - inventory cleanup deferred",
playerId.toString().substring(0, 8)
);
}
TiedUpMod.LOGGER.info(
"[PrisonerService] Release complete for {} - full cleanup done",
playerId.toString().substring(0, 8)
);
return true;
}
// ==================== TICK (escape detection, from EscapeService) ====================
/**
* Tick escape detection.
* Called from CampManagementHandler.
*
* @param server The server
* @param currentTime Current game time
*/
public void tick(MinecraftServer server, long currentTime) {
if (currentTime % CHECK_INTERVAL_TICKS != 0) {
return;
}
ServerLevel level = server.overworld();
PrisonerManager manager = PrisonerManager.get(level);
CellRegistryV2 cells = CellRegistryV2.get(level);
CollarRegistry collars = CollarRegistry.get(level);
List<EscapeCandidate> escapees = new ArrayList<>();
for (UUID playerId : manager.getAllPrisonerIds()) {
PrisonerRecord record = manager.getRecord(playerId);
PrisonerState state = record.getState();
if (!state.isCaptive()) {
continue;
}
ServerPlayer player = server.getPlayerList().getPlayer(playerId);
// === OFFLINE CHECK ===
if (player == null) {
long timeInState = record.getTimeInState(currentTime);
if (timeInState > OFFLINE_TIMEOUT_TICKS) {
escapees.add(
new EscapeCandidate(playerId, "offline timeout")
);
}
continue;
}
// === COLLAR CHECK ===
if (state != PrisonerState.CAPTURED) {
boolean hasCollarOwners = collars.hasOwners(playerId);
if (!hasCollarOwners) {
IBondageState cap = KidnappedHelper.getKidnappedState(
player
);
ItemStack collar =
cap != null
? cap.getEquipment(BodyRegionV2.NECK)
: ItemStack.EMPTY;
if (
!collar.isEmpty() &&
com.tiedup.remake.v2.bondage.CollarHelper.isCollar(collar)
) {
List<UUID> nbtOwners = com.tiedup.remake.v2.bondage.CollarHelper.getOwners(collar);
if (!nbtOwners.isEmpty()) {
for (UUID ownerUUID : nbtOwners) {
collars.registerCollar(playerId, ownerUUID);
}
TiedUpMod.LOGGER.info(
"[PrisonerService] Re-synced collar registry for {} - found {} owners in NBT",
playerId.toString().substring(0, 8),
nbtOwners.size()
);
continue;
}
}
TiedUpMod.LOGGER.warn(
"[PrisonerService] Collar check failed for {} (state={}): collarEquipped={}, collarItem={}",
playerId.toString().substring(0, 8),
state,
!collar.isEmpty(),
collar.isEmpty() ? "none" : collar.getItem().toString()
);
escapees.add(
new EscapeCandidate(playerId, "collar removed")
);
continue;
}
}
// === STATE-SPECIFIC CHECKS ===
switch (state) {
case IMPRISONED -> {
UUID cellId = record.getCellId();
if (cellId != null) {
CellDataV2 cell = cells.getCell(cellId);
if (cell == null) {
escapees.add(
new EscapeCandidate(
playerId,
"cell no longer exists"
)
);
TiedUpMod.LOGGER.warn(
"[PrisonerService] Prisoner {} has orphaned cellId {} - triggering escape",
playerId.toString().substring(0, 8),
cellId.toString().substring(0, 8)
);
} else {
double distance = Math.sqrt(
player
.blockPosition()
.distSqr(cell.getCorePos())
);
if (distance > CELL_ESCAPE_DISTANCE) {
escapees.add(
new EscapeCandidate(
playerId,
String.format(
"too far from cell (%.1f blocks)",
distance
)
)
);
}
}
}
}
case WORKING -> {
LaborRecord labor = manager.getLaborRecord(playerId);
long timeInPhase = labor.getTimeInPhase(currentTime);
if (
labor.getPhase() == LaborRecord.WorkPhase.WORKING &&
timeInPhase > WORK_TIMEOUT_TICKS
) {
labor.failTask(currentTime);
TiedUpMod.LOGGER.info(
"[PrisonerService] {} work timeout - forcing return",
playerId.toString().substring(0, 8)
);
continue;
}
if (
labor.getPhase() == LaborRecord.WorkPhase.RETURNING &&
timeInPhase > RETURN_TIMEOUT_TICKS
) {
escapees.add(
new EscapeCandidate(playerId, "return timeout")
);
continue;
}
// PENDING_RETURN timeout: maid failed to pick up prisoner — force return to cell
if (
labor.getPhase() ==
LaborRecord.WorkPhase.PENDING_RETURN &&
timeInPhase > PENDING_RETURN_TIMEOUT_TICKS
) {
PrisonerRecord rec = manager.getRecord(playerId);
UUID cellId = rec.getCellId();
if (cellId != null) {
CellDataV2 cell = cells.getCell(cellId);
if (cell != null) {
BlockPos teleportPos =
cell.getSpawnPoint() != null
? cell.getSpawnPoint()
: cell.getCorePos().above();
player.teleportTo(
teleportPos.getX() + 0.5,
teleportPos.getY(),
teleportPos.getZ() + 0.5
);
IBondageState cap =
KidnappedHelper.getKidnappedState(player);
if (cap != null) {
CompoundTag snapshot =
labor.getBondageSnapshot();
if (snapshot != null) {
BondageService.get().restoreSnapshot(
cap,
snapshot
);
}
}
returnToCell(level, player, null, cell);
labor.startRest(currentTime);
if (labor.getGuardId() != null) {
net.minecraft.world.entity.Entity guardEntity =
level.getEntity(labor.getGuardId());
if (
guardEntity != null
) guardEntity.discard();
}
player.sendSystemMessage(
Component.translatable(
"msg.tiedup.prison.returned_to_cell"
).withStyle(ChatFormatting.GRAY)
);
TiedUpMod.LOGGER.info(
"[PrisonerService] Force-returned {} to cell (PENDING_RETURN timeout)",
playerId.toString().substring(0, 8)
);
continue;
}
}
// Cell gone → trigger escape
escapees.add(
new EscapeCandidate(
playerId,
"pending_return timeout, cell gone"
)
);
continue;
}
if (labor.getGuardId() != null) {
net.minecraft.world.entity.Entity guardEntity =
level.getEntity(labor.getGuardId());
if (guardEntity != null && guardEntity.isAlive()) {
continue;
}
}
UUID campId = record.getCampId();
if (campId != null) {
List<CellDataV2> campCells = cells.getCellsByCamp(
campId
);
if (campCells.isEmpty()) {
escapees.add(
new EscapeCandidate(
playerId,
"camp no longer exists"
)
);
TiedUpMod.LOGGER.warn(
"[PrisonerService] Worker {} has orphaned campId {} with no cells - triggering escape",
playerId.toString().substring(0, 8),
campId.toString().substring(0, 8)
);
} else {
BlockPos campCenter = campCells.get(0).getCorePos();
double distance = Math.sqrt(
player.blockPosition().distSqr(campCenter)
);
if (distance > WORK_ESCAPE_DISTANCE) {
escapees.add(
new EscapeCandidate(
playerId,
String.format(
"too far from camp (%.1f blocks)",
distance
)
)
);
}
}
}
}
case CAPTURED -> {
// During transport - handled by kidnapper goals
}
default -> {
// Other states don't need distance checks
}
}
}
for (EscapeCandidate candidate : escapees) {
escape(level, candidate.playerId, candidate.reason);
}
}
// ==================== VALIDATION ====================
/**
* Check if a player should be considered escaped.
* Called on-demand (e.g., when player moves).
*/
@Nullable
public String checkEscape(ServerLevel level, ServerPlayer player) {
PrisonerManager manager = PrisonerManager.get(level);
CellRegistryV2 cells = CellRegistryV2.get(level);
CollarRegistry collars = CollarRegistry.get(level);
UUID playerId = player.getUUID();
PrisonerRecord record = manager.getRecord(playerId);
PrisonerState state = record.getState();
if (!state.isCaptive()) {
return null;
}
if (!collars.hasOwners(playerId)) {
return "collar removed";
}
switch (state) {
case IMPRISONED -> {
UUID cellId = record.getCellId();
if (cellId != null) {
CellDataV2 cell = cells.getCell(cellId);
if (cell != null) {
double distance = Math.sqrt(
player.blockPosition().distSqr(cell.getCorePos())
);
if (distance > CELL_ESCAPE_DISTANCE) {
return String.format(
"too far from cell (%.1f blocks)",
distance
);
}
}
}
}
case WORKING -> {
UUID campId = record.getCampId();
if (campId != null) {
List<CellDataV2> campCells = cells.getCellsByCamp(campId);
if (!campCells.isEmpty()) {
BlockPos campCenter = campCells.get(0).getCorePos();
double distance = Math.sqrt(
player.blockPosition().distSqr(campCenter)
);
if (distance > WORK_ESCAPE_DISTANCE) {
return String.format(
"too far from camp (%.1f blocks)",
distance
);
}
}
}
}
default -> {
// Other states don't have distance limits
}
}
return null;
}
/**
* Handle player movement event.
* Called from event handler to check distance-based escapes.
*/
public void onPlayerMove(ServerPlayer player, ServerLevel level) {
String escapeReason = checkEscape(level, player);
if (escapeReason != null) {
escape(level, player.getUUID(), escapeReason);
}
}
// ==================== HELPER ====================
private record EscapeCandidate(UUID playerId, String reason) {}
}