Clean repo for open source release

Remove build artifacts, dev tool configs, unused dependencies,
and third-party source dumps. Add proper README, update .gitignore,
clean up Makefile.
This commit is contained in:
NotEvil
2026-04-12 00:51:22 +02:00
parent 2e7a1d403b
commit f6466360b6
1947 changed files with 238025 additions and 1 deletions

View File

@@ -0,0 +1,455 @@
package com.tiedup.remake.prison;
import com.tiedup.remake.labor.LaborTask;
import java.util.UUID;
import javax.annotation.Nullable;
import net.minecraft.nbt.CompoundTag;
/**
* Labor-specific data for a prisoner.
*
* Separated from PrisonerRecord to keep concerns clean.
* Contains task assignment, work phase, maid tracking, and activity monitoring.
*/
public class LaborRecord {
/**
* Sub-phases within the WORKING/IMPRISONED state.
* More granular than PrisonerState, used by maid goals.
*/
public enum WorkPhase {
/** In cell, no task assigned */
IDLE,
/** Task assigned, waiting for extraction */
PENDING_EXTRACTION,
/** Being escorted to work area */
EXTRACTING,
/** Actively working on task */
WORKING,
/** Task complete, waiting for return */
PENDING_RETURN,
/** Being escorted back to cell */
RETURNING,
/** Back in cell, resting before next task */
RESTING,
}
// ==================== TASK DATA ====================
/** Current labor task (null if no task assigned) */
@Nullable
private LaborTask task;
/** Current work phase */
private WorkPhase phase;
/** UUID of the maid managing this prisoner */
@Nullable
private UUID maidId;
/** UUID of the maid currently escorting (during extract/return) */
@Nullable
private UUID escortMaidId;
/** UUID of the guard entity assigned to this prisoner during labor */
@Nullable
private UUID guardId;
// ==================== PROGRESS ====================
/** Total emeralds earned through labor */
private int totalEarned;
/** Number of completed tasks this session */
private int completedTasks;
/** Shock level (0-3, escalating punishment for inactivity) */
private int shockLevel;
/** Whether the current/last task failed */
private boolean taskFailed;
// ==================== ACTIVITY TRACKING ====================
/** Last time activity was detected */
private long lastActivityTime;
/** Last known position (for movement detection) */
private int lastPosX, lastPosY, lastPosZ;
/** When current phase started */
private long phaseStartTime;
// ==================== BONDAGE SNAPSHOT ====================
/** Saved bondage state for restoration after labor */
@Nullable
private CompoundTag bondageSnapshot;
// ==================== CONSTRUCTOR ====================
public LaborRecord() {
this.task = null;
this.phase = WorkPhase.IDLE;
this.maidId = null;
this.escortMaidId = null;
this.guardId = null;
this.totalEarned = 0;
this.completedTasks = 0;
this.shockLevel = 0;
this.taskFailed = false;
this.lastActivityTime = 0;
this.phaseStartTime = 0;
this.bondageSnapshot = null;
}
// ==================== GETTERS ====================
@Nullable
public LaborTask getTask() {
return task;
}
public WorkPhase getPhase() {
return phase;
}
@Nullable
public UUID getMaidId() {
return maidId;
}
@Nullable
public UUID getEscortMaidId() {
return escortMaidId;
}
@Nullable
public UUID getGuardId() {
return guardId;
}
public int getTotalEarned() {
return totalEarned;
}
public int getCompletedTasks() {
return completedTasks;
}
public int getShockLevel() {
return shockLevel;
}
public boolean isTaskFailed() {
return taskFailed;
}
public long getLastActivityTime() {
return lastActivityTime;
}
public long getPhaseStartTime() {
return phaseStartTime;
}
@Nullable
public CompoundTag getBondageSnapshot() {
return bondageSnapshot;
}
// ==================== SETTERS ====================
public void setTask(@Nullable LaborTask task) {
this.task = task;
}
public void setPhase(WorkPhase phase, long currentTime) {
this.phase = phase;
this.phaseStartTime = currentTime;
}
public void setMaidId(@Nullable UUID maidId) {
this.maidId = maidId;
}
public void setEscortMaidId(@Nullable UUID escortMaidId) {
this.escortMaidId = escortMaidId;
}
public void setGuardId(@Nullable UUID guardId) {
this.guardId = guardId;
}
public void setTotalEarned(int totalEarned) {
this.totalEarned = totalEarned;
}
public void addEarnings(int amount) {
this.totalEarned += amount;
}
public void incrementCompletedTasks() {
this.completedTasks++;
}
public void setShockLevel(int shockLevel) {
this.shockLevel = Math.max(0, Math.min(3, shockLevel));
}
public void incrementShockLevel() {
setShockLevel(shockLevel + 1);
}
public void resetShockLevel() {
this.shockLevel = 0;
}
public void setTaskFailed(boolean taskFailed) {
this.taskFailed = taskFailed;
}
public void updateActivity(long currentTime, int x, int y, int z) {
this.lastActivityTime = currentTime;
this.lastPosX = x;
this.lastPosY = y;
this.lastPosZ = z;
}
public void setBondageSnapshot(@Nullable CompoundTag snapshot) {
this.bondageSnapshot = snapshot != null ? snapshot.copy() : null;
}
// ==================== QUERY METHODS ====================
/**
* Check if a task is currently assigned.
*/
public boolean hasTask() {
return task != null;
}
/**
* Check if currently being escorted.
*/
public boolean isBeingEscorted() {
return phase == WorkPhase.EXTRACTING || phase == WorkPhase.RETURNING;
}
/**
* Check if can be assigned a new task.
*/
public boolean canAssignTask() {
return phase == WorkPhase.IDLE || phase == WorkPhase.RESTING;
}
/**
* Check if prisoner has moved from last known position.
*/
public boolean hasMovedFrom(int x, int y, int z, int threshold) {
int dx = Math.abs(x - lastPosX);
int dy = Math.abs(y - lastPosY);
int dz = Math.abs(z - lastPosZ);
return dx > threshold || dy > threshold || dz > threshold;
}
/**
* Get time in current phase.
*/
public long getTimeInPhase(long currentTime) {
return currentTime - phaseStartTime;
}
// ==================== LIFECYCLE ====================
/**
* Assign a new task.
*/
public void assignTask(LaborTask task, UUID maidId, long currentTime) {
if (!canAssignTask()) {
return; // Guard: only assign during IDLE or RESTING
}
this.task = task;
this.maidId = maidId;
this.phase = WorkPhase.PENDING_EXTRACTION;
this.phaseStartTime = currentTime;
this.taskFailed = false;
this.shockLevel = 0;
}
/**
* Start extraction (maid is coming to get prisoner).
*/
public void startExtraction(UUID escortMaidId, long currentTime) {
this.escortMaidId = escortMaidId;
this.phase = WorkPhase.EXTRACTING;
this.phaseStartTime = currentTime;
}
/**
* Extraction complete, prisoner is now working.
*/
public void startWorking(long currentTime) {
this.phase = WorkPhase.WORKING;
this.phaseStartTime = currentTime;
this.lastActivityTime = currentTime;
this.escortMaidId = null;
}
/**
* Task complete, waiting for maid to return prisoner.
*/
public void completeTask(long currentTime) {
this.phase = WorkPhase.PENDING_RETURN;
this.phaseStartTime = currentTime;
}
/**
* Task failed (timeout or abandoned).
*/
public void failTask(long currentTime) {
this.taskFailed = true;
this.phase = WorkPhase.PENDING_RETURN;
this.phaseStartTime = currentTime;
}
/**
* Start return to cell.
*/
public void startReturn(UUID escortMaidId, long currentTime) {
this.escortMaidId = escortMaidId;
this.phase = WorkPhase.RETURNING;
this.phaseStartTime = currentTime;
}
/**
* Returned to cell, start rest period.
*/
public void startRest(long currentTime) {
if (!taskFailed) {
incrementCompletedTasks();
}
this.task = null;
this.phase = WorkPhase.RESTING;
this.phaseStartTime = currentTime;
this.escortMaidId = null;
this.guardId = null;
this.bondageSnapshot = null; // Clear after restoration
}
/**
* Rest complete, back to idle.
*/
public void finishRest(long currentTime) {
this.phase = WorkPhase.IDLE;
this.phaseStartTime = currentTime;
this.taskFailed = false;
}
/**
* Reset all labor data (on release).
*/
public void reset() {
this.task = null;
this.phase = WorkPhase.IDLE;
this.maidId = null;
this.escortMaidId = null;
this.guardId = null;
this.totalEarned = 0;
this.completedTasks = 0;
this.shockLevel = 0;
this.taskFailed = false;
this.lastActivityTime = 0;
this.phaseStartTime = 0;
this.bondageSnapshot = null;
}
// ==================== SERIALIZATION ====================
public CompoundTag save() {
CompoundTag tag = new CompoundTag();
if (task != null) {
tag.put("task", task.save());
}
tag.putString("phase", phase.name());
if (maidId != null) {
tag.putUUID("maidId", maidId);
}
if (escortMaidId != null) {
tag.putUUID("escortMaidId", escortMaidId);
}
if (guardId != null) {
tag.putUUID("guardId", guardId);
}
tag.putInt("totalEarned", totalEarned);
tag.putInt("completedTasks", completedTasks);
tag.putInt("shockLevel", shockLevel);
tag.putBoolean("taskFailed", taskFailed);
tag.putLong("lastActivityTime", lastActivityTime);
tag.putInt("lastPosX", lastPosX);
tag.putInt("lastPosY", lastPosY);
tag.putInt("lastPosZ", lastPosZ);
tag.putLong("phaseStartTime", phaseStartTime);
if (bondageSnapshot != null) {
tag.put("bondageSnapshot", bondageSnapshot.copy());
}
return tag;
}
public static LaborRecord load(CompoundTag tag) {
LaborRecord record = new LaborRecord();
if (tag.contains("task")) {
record.task = LaborTask.load(tag.getCompound("task"));
}
try {
record.phase = WorkPhase.valueOf(tag.getString("phase"));
} catch (IllegalArgumentException e) {
record.phase = WorkPhase.IDLE;
}
if (tag.contains("maidId")) {
record.maidId = tag.getUUID("maidId");
}
if (tag.contains("escortMaidId")) {
record.escortMaidId = tag.getUUID("escortMaidId");
}
if (tag.contains("guardId")) {
record.guardId = tag.getUUID("guardId");
}
record.totalEarned = tag.getInt("totalEarned");
record.completedTasks = tag.getInt("completedTasks");
record.shockLevel = tag.getInt("shockLevel");
record.taskFailed = tag.getBoolean("taskFailed");
record.lastActivityTime = tag.getLong("lastActivityTime");
record.lastPosX = tag.getInt("lastPosX");
record.lastPosY = tag.getInt("lastPosY");
record.lastPosZ = tag.getInt("lastPosZ");
record.phaseStartTime = tag.getLong("phaseStartTime");
if (tag.contains("bondageSnapshot")) {
record.bondageSnapshot = tag.getCompound("bondageSnapshot").copy();
}
return record;
}
@Override
public String toString() {
return String.format(
"LaborRecord{phase=%s, task=%s, earned=%d}",
phase,
task != null ? task.getDescription() : "none",
totalEarned
);
}
}

View File

@@ -0,0 +1,800 @@
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());
}
/**
* 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();
}
}

View File

@@ -0,0 +1,239 @@
package com.tiedup.remake.prison;
import java.util.UUID;
import javax.annotation.Nullable;
import net.minecraft.nbt.CompoundTag;
/**
* Minimal prisoner data record.
*
* Replaces the complex 17-field CaptivityState with essential data only.
* Labor-specific data is in LaborRecord, ransom data in RansomRecord.
*
* This is a simple mutable POJO for performance (no withXxx() overhead).
*/
public class PrisonerRecord {
// ==================== CORE STATE ====================
/** Current prisoner state */
private PrisonerState state;
/** When this state was last changed (game time) */
private long stateTimestamp;
// ==================== OWNERSHIP ====================
/** UUID of the captor (kidnapper who captured) */
@Nullable
private UUID captorId;
/** UUID of the camp this prisoner belongs to */
@Nullable
private UUID campId;
/** UUID of the cell this prisoner is assigned to */
@Nullable
private UUID cellId;
// ==================== PROTECTION ====================
/** Protection expiry time (game time) - for PROTECTED state */
private long protectionExpiry;
// ==================== CONSTRUCTOR ====================
public PrisonerRecord() {
this.state = PrisonerState.FREE;
this.stateTimestamp = 0;
this.captorId = null;
this.campId = null;
this.cellId = null;
this.protectionExpiry = 0;
}
public PrisonerRecord(PrisonerState state, long timestamp) {
this.state = state;
this.stateTimestamp = timestamp;
this.captorId = null;
this.campId = null;
this.cellId = null;
this.protectionExpiry = 0;
}
// ==================== GETTERS ====================
public PrisonerState getState() {
return state;
}
public long getStateTimestamp() {
return stateTimestamp;
}
@Nullable
public UUID getCaptorId() {
return captorId;
}
@Nullable
public UUID getCampId() {
return campId;
}
@Nullable
public UUID getCellId() {
return cellId;
}
public long getProtectionExpiry() {
return protectionExpiry;
}
// ==================== SETTERS ====================
public void setState(PrisonerState state, long timestamp) {
this.state = state;
this.stateTimestamp = timestamp;
}
public void setCaptorId(@Nullable UUID captorId) {
this.captorId = captorId;
}
public void setCampId(@Nullable UUID campId) {
this.campId = campId;
}
public void setCellId(@Nullable UUID cellId) {
this.cellId = cellId;
}
public void setProtectionExpiry(long protectionExpiry) {
this.protectionExpiry = protectionExpiry;
}
// ==================== QUERY METHODS ====================
/**
* Check if player is protected from capture at the given time.
*/
public boolean isProtected(long currentTime) {
return (
state == PrisonerState.PROTECTED || currentTime < protectionExpiry
);
}
/**
* Check if player can be targeted by kidnappers.
*/
public boolean isTargetable(long currentTime) {
return state.isTargetable() && !isProtected(currentTime);
}
/**
* Check if player is under any form of captivity.
*/
public boolean isCaptive() {
return state.isCaptive();
}
/**
* Check if player is imprisoned (in cell or working).
*/
public boolean isImprisoned() {
return state.isImprisoned();
}
/**
* Get time since last state change in ticks.
*/
public long getTimeInState(long currentTime) {
return currentTime - stateTimestamp;
}
// ==================== SERIALIZATION ====================
public CompoundTag save() {
CompoundTag tag = new CompoundTag();
tag.putString("state", state.name());
tag.putLong("stateTimestamp", stateTimestamp);
if (captorId != null) {
tag.putUUID("captorId", captorId);
}
if (campId != null) {
tag.putUUID("campId", campId);
}
if (cellId != null) {
tag.putUUID("cellId", cellId);
}
tag.putLong("protectionExpiry", protectionExpiry);
return tag;
}
public static PrisonerRecord load(CompoundTag tag) {
PrisonerRecord record = new PrisonerRecord();
try {
record.state = PrisonerState.valueOf(tag.getString("state"));
} catch (IllegalArgumentException e) {
record.state = PrisonerState.FREE;
}
record.stateTimestamp = tag.getLong("stateTimestamp");
if (tag.contains("captorId")) {
record.captorId = tag.getUUID("captorId");
}
if (tag.contains("campId")) {
record.campId = tag.getUUID("campId");
}
if (tag.contains("cellId")) {
record.cellId = tag.getUUID("cellId");
}
record.protectionExpiry = tag.getLong("protectionExpiry");
return record;
}
// ==================== RESET ====================
/**
* Reset to FREE state, clearing all ownership data.
*/
public void reset(long timestamp) {
this.state = PrisonerState.FREE;
this.stateTimestamp = timestamp;
this.captorId = null;
this.campId = null;
this.cellId = null;
this.protectionExpiry = 0;
}
/**
* Create a deep copy.
*/
public PrisonerRecord copy() {
PrisonerRecord copy = new PrisonerRecord();
copy.state = this.state;
copy.stateTimestamp = this.stateTimestamp;
copy.captorId = this.captorId;
copy.campId = this.campId;
copy.cellId = this.cellId;
copy.protectionExpiry = this.protectionExpiry;
return copy;
}
@Override
public String toString() {
return String.format(
"PrisonerRecord{state=%s, campId=%s, cellId=%s}",
state,
campId != null ? campId.toString().substring(0, 8) : "null",
cellId != null ? cellId.toString().substring(0, 8) : "null"
);
}
}

View File

@@ -0,0 +1,94 @@
package com.tiedup.remake.prison;
/**
* Simplified prisoner state enum.
*
* Replaces the complex 9-state CaptivityStateEnum with 5 clear states.
* All "in-cell" sub-states (IDLE, PENDING, RESTING) are consolidated into IMPRISONED.
* ESCAPED state is removed - escaped prisoners go straight to FREE.
*/
public enum PrisonerState {
/**
* Player is not captured and not under protection.
* Can be targeted by kidnappers normally.
*/
FREE(false, false, false),
/**
* Player has been captured and is being transported by a kidnapper.
* Cannot be retargeted by other kidnappers.
*/
CAPTURED(true, false, true),
/**
* Player is in a cell.
* This consolidates the old IDLE, PENDING, RESTING states.
* The specific sub-state is tracked via MaidWorkPhase in LaborRecord.
*/
IMPRISONED(true, false, true),
/**
* Player has been extracted from cell and is actively working on a task.
* Includes both the working phase and the returning phase.
*/
WORKING(true, false, true),
/**
* Player has been released with temporary protection.
* Cannot be targeted by kidnappers until protection expires.
* Duration: 5 minutes (6000 ticks).
*/
PROTECTED(false, true, false);
private final boolean imprisoned;
private final boolean protected_;
private final boolean captive;
PrisonerState(boolean imprisoned, boolean protected_, boolean captive) {
this.imprisoned = imprisoned;
this.protected_ = protected_;
this.captive = captive;
}
/**
* @return true if player is in any imprisonment state (in cell or working)
*/
public boolean isImprisoned() {
return imprisoned;
}
/**
* @return true if player is protected from being captured/recaptured
*/
public boolean isProtected() {
return protected_;
}
/**
* @return true if player is under active captivity (being transported or imprisoned)
*/
public boolean isCaptive() {
return captive;
}
/**
* @return true if player can be targeted by kidnappers
*/
public boolean isTargetable() {
return this == FREE;
}
/**
* @return true if player is in a cell (IMPRISONED state)
*/
public boolean isInCell() {
return this == IMPRISONED;
}
/**
* @return true if player is actively working
*/
public boolean isWorking() {
return this == WORKING;
}
}

View File

@@ -0,0 +1,364 @@
package com.tiedup.remake.prison;
import com.tiedup.remake.core.TiedUpMod;
import java.util.EnumSet;
import java.util.Map;
import java.util.Set;
/**
* Defines valid state transitions for prisoners.
*
* All transitions must go through this class to ensure consistency.
* Invalid transitions are rejected with logging.
*/
public class PrisonerTransition {
/**
* Valid transitions from each state.
*
* State diagram:
*
* ┌─────────┐ capture ┌──────────┐ imprison ┌────────────┐
* │ FREE │──────────────►│ CAPTURED │───────────────►│ IMPRISONED │
* └─────────┘ └──────────┘ └────────────┘
* ▲ │ │ ▲
* │ extract│ │ │return
* │ expire ▼ │ │
* ┌────────────┐◄───────────────────────────────────┌─────────┐ │
* │ PROTECTED │ release │ WORKING │───┘
* └────────────┘ └─────────┘
* │ │
* │ expire │escape
* ▼ ▼
* ┌─────────┐◄───────────────────────────────────────────┘
* │ FREE │ escape (distance/timeout)
* └─────────┘
*/
private static final Map<
PrisonerState,
Set<PrisonerState>
> VALID_TRANSITIONS = Map.of(
PrisonerState.FREE,
EnumSet.of(
PrisonerState.CAPTURED, // Kidnapper captures player
PrisonerState.PROTECTED // Admin command or special case
),
PrisonerState.CAPTURED,
EnumSet.of(
PrisonerState.IMPRISONED, // Delivered to cell
PrisonerState.FREE // Escape during transport
),
PrisonerState.IMPRISONED,
EnumSet.of(
PrisonerState.WORKING, // Extracted for labor
PrisonerState.PROTECTED, // Released with grace period
PrisonerState.FREE // Escape (distance, collar removed)
),
PrisonerState.WORKING,
EnumSet.of(
PrisonerState.IMPRISONED, // Returned to cell after task
PrisonerState.PROTECTED, // Ransom paid during work
PrisonerState.FREE // Escape (distance, timeout)
),
PrisonerState.PROTECTED,
EnumSet.of(
PrisonerState.FREE // Protection expired
)
);
/**
* Attempt a state transition.
*
* @param record The prisoner record to modify
* @param newState The target state
* @param currentTime Current game time
* @param playerId Player UUID (for logging)
* @return true if transition was valid and applied
*/
public static boolean transition(
PrisonerRecord record,
PrisonerState newState,
long currentTime,
java.util.UUID playerId
) {
PrisonerState oldState = record.getState();
// Same state is a no-op
if (oldState == newState) {
return true;
}
// Check if transition is valid
Set<PrisonerState> validTargets = VALID_TRANSITIONS.get(oldState);
if (validTargets == null || !validTargets.contains(newState)) {
TiedUpMod.LOGGER.warn(
"[PrisonerTransition] Invalid transition {} -> {} for player {}",
oldState,
newState,
playerId != null ? playerId.toString().substring(0, 8) : "null"
);
return false;
}
// Apply transition
record.setState(newState, currentTime);
// Clean up state-specific data on certain transitions
switch (newState) {
case FREE:
// Clear all ownership data
record.setCaptorId(null);
record.setCampId(null);
record.setCellId(null);
record.setProtectionExpiry(0);
break;
case PROTECTED:
// Keep camp/cell data in case protection expires and they're recaptured
// Protection expiry should be set by caller
break;
default:
// No automatic cleanup for other states
break;
}
TiedUpMod.LOGGER.debug(
"[PrisonerTransition] {} -> {} for player {}",
oldState,
newState,
playerId != null ? playerId.toString().substring(0, 8) : "null"
);
return true;
}
/**
* Force a state transition without validation.
* Use sparingly - only for admin commands or initialization.
*
* @param record The prisoner record to modify
* @param newState The target state
* @param currentTime Current game time
*/
public static void forceTransition(
PrisonerRecord record,
PrisonerState newState,
long currentTime
) {
PrisonerState oldState = record.getState();
record.setState(newState, currentTime);
// Still clean up on FREE
if (newState == PrisonerState.FREE) {
record.setCaptorId(null);
record.setCampId(null);
record.setCellId(null);
record.setProtectionExpiry(0);
}
TiedUpMod.LOGGER.info(
"[PrisonerTransition] FORCED {} -> {}",
oldState,
newState
);
}
/**
* Check if a transition is valid without applying it.
*
* @param fromState Current state
* @param toState Target state
* @return true if transition would be valid
*/
public static boolean isValidTransition(
PrisonerState fromState,
PrisonerState toState
) {
if (fromState == toState) {
return true;
}
Set<PrisonerState> validTargets = VALID_TRANSITIONS.get(fromState);
return validTargets != null && validTargets.contains(toState);
}
/**
* Get all valid target states from a given state.
*
* @param fromState Current state
* @return Set of valid target states
*/
public static Set<PrisonerState> getValidTargets(PrisonerState fromState) {
Set<PrisonerState> targets = VALID_TRANSITIONS.get(fromState);
return targets != null
? EnumSet.copyOf(targets)
: EnumSet.noneOf(PrisonerState.class);
}
// ==================== CONVENIENCE METHODS ====================
/**
* Capture a free player.
* Sets captor ID and transitions to CAPTURED.
*/
public static boolean capture(
PrisonerRecord record,
java.util.UUID captorId,
long currentTime,
java.util.UUID playerId
) {
if (record.getState() != PrisonerState.FREE) {
TiedUpMod.LOGGER.warn(
"[PrisonerTransition] Cannot capture - not FREE"
);
return false;
}
record.setCaptorId(captorId);
return transition(
record,
PrisonerState.CAPTURED,
currentTime,
playerId
);
}
/**
* Imprison a captured player in a cell.
* Sets camp and cell IDs and transitions to IMPRISONED.
*/
public static boolean imprison(
PrisonerRecord record,
java.util.UUID campId,
java.util.UUID cellId,
long currentTime,
java.util.UUID playerId
) {
if (record.getState() != PrisonerState.CAPTURED) {
TiedUpMod.LOGGER.warn(
"[PrisonerTransition] Cannot imprison - not CAPTURED"
);
return false;
}
record.setCampId(campId);
record.setCellId(cellId);
return transition(
record,
PrisonerState.IMPRISONED,
currentTime,
playerId
);
}
/**
* Extract prisoner from cell for labor.
* Transitions to WORKING.
*/
public static boolean extract(
PrisonerRecord record,
long currentTime,
java.util.UUID playerId
) {
if (record.getState() != PrisonerState.IMPRISONED) {
TiedUpMod.LOGGER.warn(
"[PrisonerTransition] Cannot extract - not IMPRISONED"
);
return false;
}
return transition(record, PrisonerState.WORKING, currentTime, playerId);
}
/**
* Return prisoner to cell after labor.
* Transitions to IMPRISONED.
*/
public static boolean returnToCell(
PrisonerRecord record,
long currentTime,
java.util.UUID playerId
) {
if (record.getState() != PrisonerState.WORKING) {
TiedUpMod.LOGGER.warn(
"[PrisonerTransition] Cannot return - not WORKING"
);
return false;
}
return transition(
record,
PrisonerState.IMPRISONED,
currentTime,
playerId
);
}
/**
* Release prisoner with grace period.
* Sets protection expiry and transitions to PROTECTED.
*
* @param gracePeriodTicks Duration of protection in ticks (default: 6000 = 5 min)
*/
public static boolean release(
PrisonerRecord record,
long currentTime,
long gracePeriodTicks,
java.util.UUID playerId
) {
PrisonerState current = record.getState();
if (
current != PrisonerState.IMPRISONED &&
current != PrisonerState.WORKING
) {
TiedUpMod.LOGGER.warn(
"[PrisonerTransition] Cannot release - not IMPRISONED or WORKING"
);
return false;
}
if (gracePeriodTicks <= 0) {
// No grace period — go straight to FREE
record.setProtectionExpiry(0);
return transition(
record,
PrisonerState.FREE,
currentTime,
playerId
);
}
record.setProtectionExpiry(currentTime + gracePeriodTicks);
return transition(
record,
PrisonerState.PROTECTED,
currentTime,
playerId
);
}
/**
* Escape from captivity.
* Transitions to FREE, clearing all ownership data.
*/
public static boolean escape(
PrisonerRecord record,
long currentTime,
java.util.UUID playerId,
String reason
) {
PrisonerState current = record.getState();
if (!current.isCaptive() && current != PrisonerState.PROTECTED) {
TiedUpMod.LOGGER.warn(
"[PrisonerTransition] Cannot escape - not captive"
);
return false;
}
TiedUpMod.LOGGER.info(
"[PrisonerTransition] Player {} escaped: {}",
playerId != null ? playerId.toString().substring(0, 8) : "null",
reason
);
return transition(record, PrisonerState.FREE, currentTime, playerId);
}
}

View File

@@ -0,0 +1,471 @@
package com.tiedup.remake.prison;
import java.util.UUID;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtUtils;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Items;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.registries.ForgeRegistries;
import com.mojang.logging.LogUtils;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
/**
* Phase 2: Data class representing a single ransom demand.
*
* Contains:
* - Captive and captor IDs
* - Demanded item and amount
* - Deadline tick
* - State (PENDING, PAID, EXPIRED, CANCELLED)
* - Optional cell/prison association
*/
public class RansomData {
private static final Logger LOGGER = LogUtils.getLogger();
/**
* Ransom state enumeration
*/
public enum RansomState {
/**
* Ransom is active and awaiting payment
*/
PENDING("pending"),
/**
* Ransom has been paid, captive can be released
*/
PAID("paid"),
/**
* Deadline passed without payment
*/
EXPIRED("expired"),
/**
* Ransom was cancelled (captive escaped, captor died, etc.)
*/
CANCELLED("cancelled");
private final String serializedName;
RansomState(String serializedName) {
this.serializedName = serializedName;
}
public String getSerializedName() {
return serializedName;
}
public static RansomState fromString(String name) {
for (RansomState state : values()) {
if (state.serializedName.equalsIgnoreCase(name)) {
return state;
}
}
return PENDING;
}
}
/**
* Ransom difficulty level (determines item type and amount)
*/
public enum RansomDifficulty {
EASY(Items.IRON_INGOT, 8, 16, 24000L), // 1 MC day = 20 min real time
NORMAL(Items.GOLD_INGOT, 4, 8, 48000L), // 2 MC days
HARD(Items.DIAMOND, 1, 3, 72000L); // 3 MC days
private final Item demandItem;
private final int minAmount;
private final int maxAmount;
private final long deadlineTicks;
RansomDifficulty(
Item demandItem,
int minAmount,
int maxAmount,
long deadlineTicks
) {
this.demandItem = demandItem;
this.minAmount = minAmount;
this.maxAmount = maxAmount;
this.deadlineTicks = deadlineTicks;
}
public Item getDemandItem() {
return demandItem;
}
public int getMinAmount() {
return minAmount;
}
public int getMaxAmount() {
return maxAmount;
}
public long getDeadlineTicks() {
return deadlineTicks;
}
}
private final UUID ransomId;
private final UUID captiveId;
private final UUID captorId;
private final Item demandItem;
private final int demandAmount;
private final long createdTick;
private final long deadlineTick;
private final RansomDifficulty difficulty;
private RansomState state;
private int amountPaid;
@Nullable
private UUID cellId;
@Nullable
private BlockPos dropChestPos;
public RansomData(
UUID captiveId,
UUID captorId,
RansomDifficulty difficulty,
long currentTick
) {
this.ransomId = UUID.randomUUID();
this.captiveId = captiveId;
this.captorId = captorId;
this.difficulty = difficulty;
this.demandItem = difficulty.getDemandItem();
this.demandAmount =
difficulty.getMinAmount() +
(int) (Math.random() *
(difficulty.getMaxAmount() - difficulty.getMinAmount() + 1));
this.createdTick = currentTick;
this.deadlineTick = currentTick + difficulty.getDeadlineTicks();
this.state = RansomState.PENDING;
this.amountPaid = 0;
}
/**
* Constructor for custom item/amount ransoms (used by camp labor system).
*
* @param captiveId The captive's UUID
* @param captorId The captor's UUID (trader/maid)
* @param demandItem The item type demanded (e.g., EMERALD)
* @param demandAmount The amount demanded
* @param deadlineTicks How long until deadline (0 = no deadline for labor)
* @param currentTick The current game tick
*/
public RansomData(
UUID captiveId,
UUID captorId,
Item demandItem,
int demandAmount,
long deadlineTicks,
long currentTick
) {
this.ransomId = UUID.randomUUID();
this.captiveId = captiveId;
this.captorId = captorId;
this.difficulty = RansomDifficulty.NORMAL; // Default for serialization
this.demandItem = demandItem;
this.demandAmount = demandAmount;
this.createdTick = currentTick;
// If deadlineTicks is 0 or negative, set very far in the future (labor has no deadline)
this.deadlineTick =
deadlineTicks > 0
? currentTick + deadlineTicks
: currentTick + (365L * 24L * 60L * 60L * 20L); // 1 year in ticks
this.state = RansomState.PENDING;
this.amountPaid = 0;
}
// Private constructor for loading
private RansomData(
UUID ransomId,
UUID captiveId,
UUID captorId,
Item demandItem,
int demandAmount,
long createdTick,
long deadlineTick,
RansomDifficulty difficulty,
RansomState state,
int amountPaid,
@Nullable UUID cellId,
@Nullable BlockPos dropChestPos
) {
this.ransomId = ransomId;
this.captiveId = captiveId;
this.captorId = captorId;
this.demandItem = demandItem;
this.demandAmount = demandAmount;
this.createdTick = createdTick;
this.deadlineTick = deadlineTick;
this.difficulty = difficulty;
this.state = state;
this.amountPaid = amountPaid;
this.cellId = cellId;
this.dropChestPos = dropChestPos;
}
// ==================== GETTERS ====================
public UUID getRansomId() {
return ransomId;
}
public UUID getCaptiveId() {
return captiveId;
}
public UUID getCaptorId() {
return captorId;
}
public Item getDemandItem() {
return demandItem;
}
public int getDemandAmount() {
return demandAmount;
}
public long getCreatedTick() {
return createdTick;
}
public long getDeadlineTick() {
return deadlineTick;
}
public RansomDifficulty getDifficulty() {
return difficulty;
}
public RansomState getState() {
return state;
}
public int getAmountPaid() {
return amountPaid;
}
@Nullable
public UUID getCellId() {
return cellId;
}
@Nullable
public BlockPos getDropChestPos() {
return dropChestPos;
}
// ==================== SETTERS ====================
public void setState(RansomState state) {
this.state = state;
}
public void setCellId(@Nullable UUID cellId) {
this.cellId = cellId;
}
public void setDropChestPos(@Nullable BlockPos pos) {
this.dropChestPos = pos;
}
// ==================== LOGIC ====================
/**
* Check if the ransom is still active (pending).
*/
public boolean isActive() {
return state == RansomState.PENDING;
}
/**
* Check if the deadline has passed.
*/
public boolean isExpired(long currentTick) {
return currentTick >= deadlineTick;
}
/**
* Get remaining time in ticks.
*/
public long getRemainingTicks(long currentTick) {
return Math.max(0, deadlineTick - currentTick);
}
/**
* Get remaining time as formatted string (e.g., "1d 12h 30m").
*/
public String getRemainingTimeFormatted(long currentTick) {
long remaining = getRemainingTicks(currentTick);
long seconds = remaining / 20;
long minutes = seconds / 60;
long hours = minutes / 60;
long days = hours / 24;
minutes %= 60;
hours %= 24;
if (days > 0) {
return String.format("%dd %dh %dm", days, hours, minutes);
} else if (hours > 0) {
return String.format("%dh %dm", hours, minutes);
} else {
return String.format("%dm", minutes);
}
}
/**
* Add payment towards the ransom.
*
* @param amount Amount of items paid
* @return true if ransom is now fully paid
*/
public boolean addPayment(int amount) {
if (state != RansomState.PENDING) {
return false;
}
amountPaid += amount;
if (amountPaid >= demandAmount) {
state = RansomState.PAID;
return true;
}
return false;
}
/**
* Increase the debt by reducing the amount paid (used for punishments).
* Can make amountPaid negative, effectively increasing the total debt.
*
* @param amount Amount to increase the debt by
*/
public void increaseDebt(int amount) {
if (state != RansomState.PENDING) {
return;
}
amountPaid -= amount;
}
/**
* Get remaining amount to pay.
*/
public int getRemainingAmount() {
return Math.max(0, demandAmount - amountPaid);
}
/**
* Get payment progress as percentage.
*/
public float getPaymentProgress() {
return ((float) amountPaid / demandAmount) * 100f;
}
// ==================== SERIALIZATION ====================
public CompoundTag save() {
CompoundTag tag = new CompoundTag();
tag.putUUID("ransomId", ransomId);
tag.putUUID("captiveId", captiveId);
tag.putUUID("captorId", captorId);
ResourceLocation demandItemKey = ForgeRegistries.ITEMS.getKey(demandItem);
if (demandItemKey != null) {
tag.putString("demandItem", demandItemKey.toString());
} else {
LOGGER.warn("[RansomData] Unregistered demand item {}, falling back to iron_ingot", demandItem);
tag.putString("demandItem", "minecraft:iron_ingot");
}
tag.putInt("demandAmount", demandAmount);
tag.putLong("createdTick", createdTick);
tag.putLong("deadlineTick", deadlineTick);
tag.putString("difficulty", difficulty.name());
tag.putString("state", state.getSerializedName());
tag.putInt("amountPaid", amountPaid);
if (cellId != null) {
tag.putUUID("cellId", cellId);
}
if (dropChestPos != null) {
tag.put("dropChestPos", NbtUtils.writeBlockPos(dropChestPos));
}
return tag;
}
public static RansomData load(CompoundTag tag) {
UUID ransomId = tag.getUUID("ransomId");
UUID captiveId = tag.getUUID("captiveId");
UUID captorId = tag.getUUID("captorId");
String itemKey = tag.getString("demandItem");
Item demandItem;
try {
demandItem = ForgeRegistries.ITEMS.getValue(
net.minecraft.resources.ResourceLocation.parse(itemKey)
);
if (demandItem == null) demandItem = Items.IRON_INGOT;
} catch (Exception e) {
LOGGER.warn("[RansomData] Failed to parse demand item key '{}', falling back to iron_ingot", itemKey, e);
demandItem = Items.IRON_INGOT;
}
int demandAmount = tag.getInt("demandAmount");
long createdTick = tag.getLong("createdTick");
long deadlineTick = tag.getLong("deadlineTick");
RansomDifficulty difficulty;
try {
difficulty = RansomDifficulty.valueOf(tag.getString("difficulty"));
} catch (IllegalArgumentException e) {
LOGGER.warn("[RansomData] Unknown difficulty '{}', falling back to NORMAL", tag.getString("difficulty"));
difficulty = RansomDifficulty.NORMAL;
}
RansomState state = RansomState.fromString(tag.getString("state"));
int amountPaid = tag.getInt("amountPaid");
UUID cellId = tag.contains("cellId") ? tag.getUUID("cellId") : null;
BlockPos dropChestPos = tag.contains("dropChestPos")
? NbtUtils.readBlockPos(tag.getCompound("dropChestPos"))
: null;
return new RansomData(
ransomId,
captiveId,
captorId,
demandItem,
demandAmount,
createdTick,
deadlineTick,
difficulty,
state,
amountPaid,
cellId,
dropChestPos
);
}
@Override
public String toString() {
ResourceLocation demandItemKey = ForgeRegistries.ITEMS.getKey(demandItem);
return String.format(
"RansomData{id=%s, captive=%s, demand=%dx%s, state=%s, paid=%d/%d}",
ransomId.toString().substring(0, 8),
captiveId.toString().substring(0, 8),
demandAmount,
demandItemKey != null ? demandItemKey : "unknown",
state,
amountPaid,
demandAmount
);
}
}

View File

@@ -0,0 +1,263 @@
package com.tiedup.remake.prison;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.annotation.Nullable;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.Items;
import net.minecraftforge.registries.ForgeRegistries;
/**
* Ransom/debt tracking for a prisoner.
*
* Simplified from RansomData - focused on debt tracking for the labor system.
* Uses emeralds as the standard currency (matches LaborTask value system).
*/
public class RansomRecord {
// ==================== CORE DATA ====================
/** Total debt amount (in emeralds) */
private int totalDebt;
/** Amount already paid */
private int amountPaid;
/** When the ransom was created (game time) */
private long createdTime;
/** Item type for payment (default: emeralds) */
private Item paymentItem;
// ==================== CONTRIBUTIONS ====================
/** Tracking who paid what (playerUUID -> amount) */
private final Map<UUID, Integer> contributions;
// ==================== CONSTRUCTOR ====================
public RansomRecord() {
this.totalDebt = 0;
this.amountPaid = 0;
this.createdTime = 0;
this.paymentItem = Items.EMERALD;
this.contributions = new HashMap<>();
}
public RansomRecord(int totalDebt, long createdTime) {
this.totalDebt = totalDebt;
this.amountPaid = 0;
this.createdTime = createdTime;
this.paymentItem = Items.EMERALD;
this.contributions = new HashMap<>();
}
public RansomRecord(int totalDebt, Item paymentItem, long createdTime) {
this.totalDebt = totalDebt;
this.amountPaid = 0;
this.createdTime = createdTime;
this.paymentItem = paymentItem;
this.contributions = new HashMap<>();
}
// ==================== GETTERS ====================
public int getTotalDebt() {
return totalDebt;
}
public int getAmountPaid() {
return amountPaid;
}
public int getRemainingDebt() {
return Math.max(0, totalDebt - amountPaid);
}
public long getCreatedTime() {
return createdTime;
}
public Item getPaymentItem() {
return paymentItem;
}
public Map<UUID, Integer> getContributions() {
return new HashMap<>(contributions);
}
// ==================== PAYMENT ====================
/**
* Add a payment toward the ransom.
*
* @param amount Amount to pay
* @return true if ransom is now fully paid
*/
public boolean addPayment(int amount) {
return addPayment(amount, null);
}
/**
* Add a payment toward the ransom with contributor tracking.
*
* @param amount Amount to pay
* @param contributorId UUID of player who paid (null for labor earnings)
* @return true if ransom is now fully paid
*/
public boolean addPayment(int amount, @Nullable UUID contributorId) {
if (amount <= 0) {
return isPaid();
}
this.amountPaid += amount;
if (contributorId != null) {
contributions.merge(contributorId, amount, Integer::sum);
}
return isPaid();
}
/**
* Increase the debt (used for punishments).
* Can make effective debt go into negative paid amount.
*
* @param amount Amount to increase debt by
*/
public void increaseDebt(int amount) {
if (amount > 0) {
this.totalDebt += amount;
}
}
/**
* Set the total debt directly.
* Used when initializing ransom.
*/
public void setTotalDebt(int totalDebt) {
this.totalDebt = Math.max(0, totalDebt);
}
// ==================== QUERY ====================
/**
* Check if ransom is fully paid.
*/
public boolean isPaid() {
return amountPaid >= totalDebt;
}
/**
* Get payment progress as percentage (0-100).
*/
public int getProgressPercent() {
if (totalDebt <= 0) return 100;
return Math.min(100, (amountPaid * 100) / totalDebt);
}
/**
* Get contribution from a specific player.
*/
public int getContribution(UUID playerId) {
return contributions.getOrDefault(playerId, 0);
}
/**
* Check if this record is active (has unpaid debt).
*/
public boolean isActive() {
return totalDebt > 0 && !isPaid();
}
// ==================== SERIALIZATION ====================
public CompoundTag save() {
CompoundTag tag = new CompoundTag();
tag.putInt("totalDebt", totalDebt);
tag.putInt("amountPaid", amountPaid);
tag.putLong("createdTime", createdTime);
var itemKey = ForgeRegistries.ITEMS.getKey(paymentItem);
if (itemKey != null) {
tag.putString("paymentItem", itemKey.toString());
}
// Save contributions
if (!contributions.isEmpty()) {
ListTag contribList = new ListTag();
for (Map.Entry<UUID, Integer> entry : contributions.entrySet()) {
CompoundTag contribTag = new CompoundTag();
contribTag.putUUID("playerId", entry.getKey());
contribTag.putInt("amount", entry.getValue());
contribList.add(contribTag);
}
tag.put("contributions", contribList);
}
return tag;
}
public static RansomRecord load(CompoundTag tag) {
RansomRecord record = new RansomRecord();
record.totalDebt = tag.getInt("totalDebt");
record.amountPaid = tag.getInt("amountPaid");
record.createdTime = tag.getLong("createdTime");
if (tag.contains("paymentItem")) {
String itemKeyStr = tag.getString("paymentItem");
var itemKey = net.minecraft.resources.ResourceLocation.tryParse(
itemKeyStr
);
if (itemKey != null) {
Item item = ForgeRegistries.ITEMS.getValue(itemKey);
if (item != null && item != Items.AIR) {
record.paymentItem = item;
}
}
}
// Load contributions
if (tag.contains("contributions")) {
ListTag contribList = tag.getList(
"contributions",
Tag.TAG_COMPOUND
);
for (int i = 0; i < contribList.size(); i++) {
CompoundTag contribTag = contribList.getCompound(i);
UUID playerId = contribTag.getUUID("playerId");
int amount = contribTag.getInt("amount");
record.contributions.put(playerId, amount);
}
}
return record;
}
/**
* Reset to default state.
*/
public void reset() {
this.totalDebt = 0;
this.amountPaid = 0;
this.createdTime = 0;
this.paymentItem = Items.EMERALD;
this.contributions.clear();
}
@Override
public String toString() {
return String.format(
"RansomRecord{debt=%d, paid=%d, remaining=%d}",
totalDebt,
amountPaid,
getRemainingDebt()
);
}
}

View File

@@ -0,0 +1,255 @@
package com.tiedup.remake.prison.service;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.v2.BodyRegionV2;
import com.tiedup.remake.state.IBondageState;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.ItemStack;
/**
* Centralized bondage state management.
*
* Handles saving/restoring bondage state for labor extraction.
* When a prisoner is extracted for labor, their restraints are temporarily removed.
* When they return, the original restraints are restored.
*/
public class BondageService {
// Singleton instance
private static final BondageService INSTANCE = new BondageService();
public static BondageService get() {
return INSTANCE;
}
private BondageService() {}
// ==================== SNAPSHOT ====================
/**
* Save the prisoner's current bondage state before extraction.
* Captures all restraints EXCEPT the collar (which must never be removed).
*
* @param cap The prisoner's kidnapped capability
* @return CompoundTag containing the snapshot
*/
public CompoundTag saveSnapshot(IBondageState cap) {
CompoundTag tag = new CompoundTag();
// Save bind/tie state
if (cap.isTiedUp()) {
ItemStack bind = cap.getEquipment(BodyRegionV2.ARMS);
if (!bind.isEmpty()) {
tag.put("BindItem", bind.save(new CompoundTag()));
TiedUpMod.LOGGER.debug(
"[BondageService] SAVE: BindItem = {}",
bind.getItem().getDescriptionId()
);
}
}
// Save gag state
if (cap.isGagged()) {
ItemStack gag = cap.getEquipment(BodyRegionV2.MOUTH);
if (!gag.isEmpty()) {
tag.put("GagItem", gag.save(new CompoundTag()));
TiedUpMod.LOGGER.debug(
"[BondageService] SAVE: GagItem = {}",
gag.getItem().getDescriptionId()
);
}
}
// Save blindfold state
tag.putBoolean("Blindfolded", cap.isBlindfolded());
if (cap.isBlindfolded()) {
ItemStack blindfold = cap.getEquipment(BodyRegionV2.EYES);
if (!blindfold.isEmpty()) {
tag.put("BlindfoldItem", blindfold.save(new CompoundTag()));
}
}
// Save mittens state
tag.putBoolean("HasMittens", cap.hasMittens());
if (cap.hasMittens()) {
ItemStack mittens = cap.getEquipment(BodyRegionV2.HANDS);
if (!mittens.isEmpty()) {
tag.put("MittensItem", mittens.save(new CompoundTag()));
}
}
// Save earplugs state
tag.putBoolean("HasEarplugs", cap.hasEarplugs());
if (cap.hasEarplugs()) {
ItemStack earplugs = cap.getEquipment(BodyRegionV2.EARS);
if (!earplugs.isEmpty()) {
tag.put("EarplugsItem", earplugs.save(new CompoundTag()));
}
}
TiedUpMod.LOGGER.debug(
"[BondageService] SAVE complete: keys={}",
tag.getAllKeys()
);
// NOTE: Collar is NEVER saved - it's invariant and must stay on
return tag;
}
/**
* Restore the prisoner's bondage state after labor completion.
* Reapplies all restraints that were saved EXCEPT the collar.
*
* @param cap The prisoner's kidnapped capability
* @param snapshot The saved bondage snapshot
*/
public void restoreSnapshot(IBondageState cap, CompoundTag snapshot) {
if (snapshot == null || snapshot.isEmpty()) {
TiedUpMod.LOGGER.warn(
"[BondageService] RESTORE: snapshot is NULL or empty - nothing to restore!"
);
return;
}
TiedUpMod.LOGGER.debug(
"[BondageService] RESTORE: Starting restore, keys={}",
snapshot.getAllKeys()
);
// Restore bind/tie
if (snapshot.contains("BindItem")) {
ItemStack bind = ItemStack.of(snapshot.getCompound("BindItem"));
if (!bind.isEmpty()) {
cap.equip(BodyRegionV2.ARMS, bind);
TiedUpMod.LOGGER.debug(
"[BondageService] RESTORE: Applied BindItem = {}",
bind.getItem().getDescriptionId()
);
}
}
// Restore gag
if (snapshot.contains("GagItem")) {
ItemStack gag = ItemStack.of(snapshot.getCompound("GagItem"));
if (!gag.isEmpty()) {
cap.equip(BodyRegionV2.MOUTH, gag);
TiedUpMod.LOGGER.debug(
"[BondageService] RESTORE: Applied GagItem = {}",
gag.getItem().getDescriptionId()
);
}
}
// Restore blindfold
if (
snapshot.getBoolean("Blindfolded") &&
snapshot.contains("BlindfoldItem")
) {
ItemStack blindfold = ItemStack.of(
snapshot.getCompound("BlindfoldItem")
);
if (!blindfold.isEmpty()) {
cap.equip(BodyRegionV2.EYES, blindfold);
}
}
// Restore mittens
if (
snapshot.getBoolean("HasMittens") &&
snapshot.contains("MittensItem")
) {
ItemStack mittens = ItemStack.of(
snapshot.getCompound("MittensItem")
);
if (!mittens.isEmpty()) {
cap.equip(BodyRegionV2.HANDS, mittens);
}
}
// Restore earplugs
if (
snapshot.getBoolean("HasEarplugs") &&
snapshot.contains("EarplugsItem")
) {
ItemStack earplugs = ItemStack.of(
snapshot.getCompound("EarplugsItem")
);
if (!earplugs.isEmpty()) {
cap.equip(BodyRegionV2.EARS, earplugs);
}
}
// NOTE: Collar is NEVER restored - it's invariant and already on
}
/**
* Remove temporary bondage restraints for labor.
* Called after saveSnapshot() to free the prisoner's hands for work.
*
* @param cap The prisoner's kidnapped capability
*/
public void removeForLabor(IBondageState cap) {
// Remove bind (so they can use hands)
if (cap.isTiedUp()) {
cap.unequip(BodyRegionV2.ARMS);
}
// Remove gag (so they can communicate if needed)
if (cap.isGagged()) {
cap.unequip(BodyRegionV2.MOUTH);
}
// Remove blindfold (so they can see)
if (cap.isBlindfolded()) {
cap.unequip(BodyRegionV2.EYES);
}
// Remove mittens (so they can use hands)
if (cap.hasMittens()) {
cap.unequip(BodyRegionV2.HANDS);
}
// Remove earplugs (guards need to communicate with the prisoner)
if (cap.hasEarplugs()) {
cap.unequip(BodyRegionV2.EARS);
}
// NOTE: Collar ALWAYS stays on - never remove it
TiedUpMod.LOGGER.debug("[BondageService] Removed restraints for labor");
}
/**
* Check if a player has any restraints that would impede labor.
*/
public boolean hasLaborImpedingRestraints(IBondageState cap) {
return cap.isTiedUp() || cap.isBlindfolded() || cap.hasMittens();
}
/**
* Apply basic prisoner restraints (bind only).
* Used when initially imprisoning a captive.
*/
public void applyBasicRestraints(IBondageState cap, ItemStack rope) {
if (!cap.isTiedUp() && !rope.isEmpty()) {
cap.equip(BodyRegionV2.ARMS, rope.copy());
}
}
/**
* Apply full restraints for cell confinement.
* Typically includes bind + gag.
*/
public void applyFullRestraints(
IBondageState cap,
ItemStack bind,
ItemStack gag
) {
if (!cap.isTiedUp() && !bind.isEmpty()) {
cap.equip(BodyRegionV2.ARMS, bind.copy());
}
if (!cap.isGagged() && gag != null && !gag.isEmpty()) {
cap.equip(BodyRegionV2.MOUTH, gag.copy());
}
}
}

View File

@@ -0,0 +1,525 @@
package com.tiedup.remake.prison.service;
import com.tiedup.remake.blocks.entity.MarkerBlockEntity;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.cells.MarkerType;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.labor.LaborTask;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import org.jetbrains.annotations.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
/**
* Centralized item management for the prison system.
*
* Handles:
* - Confiscating valuables from new prisoners
* - Confiscating contraband from working prisoners
* - Collecting task items from completed labor
* - Finding and managing camp storage chests
*/
public class ItemService {
// Singleton instance
private static final ItemService INSTANCE = new ItemService();
public static ItemService get() {
return INSTANCE;
}
private ItemService() {}
// ==================== ITEM CATEGORIES ====================
/** Items considered "valuables" to confiscate on imprisonment */
private static final Set<Item> VALUABLE_ITEMS = Set.of(
Items.DIAMOND,
Items.EMERALD,
Items.GOLD_INGOT,
Items.IRON_INGOT,
Items.NETHERITE_INGOT,
Items.NETHERITE_SCRAP,
Items.DIAMOND_BLOCK,
Items.EMERALD_BLOCK,
Items.GOLD_BLOCK,
Items.IRON_BLOCK,
Items.NETHERITE_BLOCK,
Items.LAPIS_LAZULI,
Items.LAPIS_BLOCK,
Items.AMETHYST_SHARD
);
/** Items considered "contraband" to confiscate during labor */
private static final Set<Item> CONTRABAND_ITEMS = Set.of(
// Weapons
Items.DIAMOND_SWORD,
Items.IRON_SWORD,
Items.GOLDEN_SWORD,
Items.STONE_SWORD,
Items.WOODEN_SWORD,
Items.NETHERITE_SWORD,
Items.BOW,
Items.CROSSBOW,
Items.TRIDENT,
// Tools that can be weapons
Items.DIAMOND_AXE,
Items.IRON_AXE,
Items.GOLDEN_AXE,
Items.STONE_AXE,
Items.WOODEN_AXE,
Items.NETHERITE_AXE,
Items.DIAMOND_PICKAXE,
Items.NETHERITE_PICKAXE,
// Escape items
Items.ENDER_PEARL,
Items.CHORUS_FRUIT,
Items.ELYTRA,
// Dangerous items
Items.FLINT_AND_STEEL,
Items.FIRE_CHARGE,
Items.TNT
);
// ==================== CONFISCATION ====================
/**
* Confiscate valuable items from a new prisoner.
* Called when a prisoner is first imprisoned.
*
* @param player The prisoner
* @return List of confiscated items
*/
public List<ItemStack> confiscateValuables(ServerPlayer player) {
List<ItemStack> confiscated = new ArrayList<>();
var inventory = player.getInventory();
for (int i = 0; i < inventory.getContainerSize(); i++) {
ItemStack stack = inventory.getItem(i);
if (!stack.isEmpty() && isValuable(stack.getItem())) {
confiscated.add(stack.copy());
inventory.setItem(i, ItemStack.EMPTY);
}
}
if (!confiscated.isEmpty()) {
TiedUpMod.LOGGER.info(
"[ItemService] Confiscated {} valuable stacks from {}",
confiscated.size(),
player.getName().getString()
);
}
return confiscated;
}
/**
* Confiscate contraband items from a working prisoner.
* Called periodically during labor or when returning to cell.
*
* @param player The prisoner
* @return List of confiscated items
*/
public List<ItemStack> confiscateContraband(ServerPlayer player) {
List<ItemStack> confiscated = new ArrayList<>();
var inventory = player.getInventory();
for (int i = 0; i < inventory.getContainerSize(); i++) {
ItemStack stack = inventory.getItem(i);
if (!stack.isEmpty() && isContraband(stack)) {
confiscated.add(stack.copy());
inventory.setItem(i, ItemStack.EMPTY);
}
}
if (!confiscated.isEmpty()) {
TiedUpMod.LOGGER.info(
"[ItemService] Confiscated {} contraband stacks from {}",
confiscated.size(),
player.getName().getString()
);
}
return confiscated;
}
/**
* Check if an item is considered a valuable.
*/
public boolean isValuable(Item item) {
return VALUABLE_ITEMS.contains(item);
}
/**
* Check if an item stack is contraband.
* Excludes items tagged as LaborTool.
*/
public boolean isContraband(ItemStack stack) {
if (stack.isEmpty()) return false;
// Labor tools are allowed
if (isLaborTool(stack)) return false;
return CONTRABAND_ITEMS.contains(stack.getItem());
}
/**
* Check if an item is a labor tool (from the camp).
*/
private boolean isLaborTool(ItemStack stack) {
if (!stack.hasTag()) return false;
var tag = stack.getTag();
return tag != null && tag.getBoolean("LaborTool");
}
// ==================== TASK ITEM COLLECTION ====================
/**
* Collect task items from a worker's inventory.
* Called by maid when task is complete.
*
* @param player The worker
* @param task The completed task
* @return List of collected item stacks
*/
public List<ItemStack> collectTaskItems(
ServerPlayer player,
LaborTask task
) {
return task.collectItems(player);
}
/**
* Reclaim labor equipment from a worker.
* Called when returning prisoner to cell.
*
* @param player The worker
* @param task The task (for tool identification)
* @return List of reclaimed tools (for storage)
*/
public List<ItemStack> reclaimEquipment(
ServerPlayer player,
LaborTask task
) {
return task.reclaimEquipment(player);
}
// ==================== CHEST MANAGEMENT ====================
/**
* Find the storage chest for a camp.
*
* Lookup order:
* 1. CampData registered loot positions (fast path)
* 2. Lazy discovery: scan 30-block radius around camp center for chests with LOOT markers above
* — registers found chests in CampData so future lookups are instant
*
* @param level The server level
* @param campId The camp UUID
* @return BlockPos of chest, or null if not found
*/
@Nullable
public BlockPos findCampChest(ServerLevel level, UUID campId) {
com.tiedup.remake.cells.CampOwnership ownership =
com.tiedup.remake.cells.CampOwnership.get(level);
com.tiedup.remake.cells.CampOwnership.CampData camp = ownership.getCamp(
campId
);
// Fast path: CampData has registered loot positions
if (camp != null && !camp.getLootChestPositions().isEmpty()) {
for (BlockPos pos : camp.getLootChestPositions()) {
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof ChestBlockEntity) {
return pos;
}
}
}
// Lazy discovery: scan around camp center for chests with LOOT markers above
if (camp != null && camp.getCenter() != null) {
List<BlockPos> discovered = discoverLootChests(level, camp, 30);
if (!discovered.isEmpty()) {
return discovered.get(0);
}
}
return null;
}
/**
* Find all storage chests for a camp.
*
* @param level The server level
* @param campId The camp UUID
* @return List of chest positions
*/
public List<BlockPos> findAllCampChests(ServerLevel level, UUID campId) {
com.tiedup.remake.cells.CampOwnership ownership =
com.tiedup.remake.cells.CampOwnership.get(level);
com.tiedup.remake.cells.CampOwnership.CampData camp = ownership.getCamp(
campId
);
if (camp == null) return List.of();
// Fast path: CampData has registered loot positions
if (!camp.getLootChestPositions().isEmpty()) {
List<BlockPos> valid = new ArrayList<>();
for (BlockPos pos : camp.getLootChestPositions()) {
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof ChestBlockEntity) {
valid.add(pos);
}
}
if (!valid.isEmpty()) return valid;
}
// Lazy discovery
if (camp.getCenter() != null) {
return discoverLootChests(level, camp, 30);
}
return List.of();
}
/**
* Find the chest for a specific cell.
*
* @param level The server level
* @param cellId The cell UUID
* @return BlockPos of chest, or null if not found
*/
@Nullable
public BlockPos findCellChest(ServerLevel level, UUID cellId) {
CellRegistryV2 registry = CellRegistryV2.get(level);
CellDataV2 cell = registry.getCell(cellId);
if (cell == null) return null;
// V2: scan interior blocks for chests (no LOOT markers in V2)
for (BlockPos pos : cell.getInteriorBlocks()) {
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof ChestBlockEntity) {
return pos;
}
}
return null;
}
/**
* Scan around camp center for chests with LOOT markers above them.
* Registers found chests in CampData for future fast lookups.
*
* @param level The server level
* @param camp The camp data
* @param radius Horizontal search radius
* @return List of discovered chest positions
*/
private List<BlockPos> discoverLootChests(
ServerLevel level,
com.tiedup.remake.cells.CampOwnership.CampData camp,
int radius
) {
BlockPos center = camp.getCenter();
List<BlockPos> found = new ArrayList<>();
int yRange = 15;
for (int x = -radius; x <= radius; x++) {
for (int z = -radius; z <= radius; z++) {
for (int y = -yRange; y <= yRange; y++) {
BlockPos pos = center.offset(x, y, z);
BlockEntity be = level.getBlockEntity(pos);
if (!(be instanceof ChestBlockEntity)) continue;
// Check for LOOT marker above
BlockPos above = pos.above();
BlockEntity markerBe = level.getBlockEntity(above);
if (
markerBe instanceof MarkerBlockEntity marker &&
marker.getMarkerType() == MarkerType.LOOT
) {
found.add(pos);
// Register in CampData for future fast lookups
camp.addLootChestPosition(pos);
}
}
}
}
if (!found.isEmpty()) {
com.tiedup.remake.cells.CampOwnership.get(level).setDirty();
TiedUpMod.LOGGER.info(
"[ItemService] Lazy discovery: found {} LOOT chests for camp {} (scanned {}r around {})",
found.size(),
camp.getCampId().toString().substring(0, 8),
radius,
center.toShortString()
);
}
return found;
}
/**
* Store items in a chest.
*
* @param level The server level
* @param chestPos Position of the chest
* @param items Items to store
* @return Number of items that couldn't be stored (chest full)
*/
public int storeInChest(
ServerLevel level,
BlockPos chestPos,
List<ItemStack> items
) {
BlockEntity be = level.getBlockEntity(chestPos);
if (!(be instanceof Container container)) {
return items.stream().mapToInt(ItemStack::getCount).sum();
}
int notStored = 0;
for (ItemStack stack : items) {
ItemStack remaining = insertIntoContainer(container, stack.copy());
notStored += remaining.getCount();
}
return notStored;
}
/**
* Retrieve a tool from a chest.
*
* @param level The server level
* @param chestPos Position of the chest
* @param toolType The type of tool to find
* @return The tool stack, or empty if not found
*/
public ItemStack retrieveToolFromChest(
ServerLevel level,
BlockPos chestPos,
Item toolType
) {
BlockEntity be = level.getBlockEntity(chestPos);
if (!(be instanceof Container container)) {
return ItemStack.EMPTY;
}
for (int i = 0; i < container.getContainerSize(); i++) {
ItemStack stack = container.getItem(i);
if (
!stack.isEmpty() &&
stack.getItem() == toolType &&
isLaborTool(stack)
) {
ItemStack retrieved = stack.copy();
container.setItem(i, ItemStack.EMPTY);
container.setChanged();
return retrieved;
}
}
return ItemStack.EMPTY;
}
/**
* Insert an item stack into a container.
*
* @param container The container
* @param stack The stack to insert
* @return Remaining items that couldn't be inserted
*/
private ItemStack insertIntoContainer(
Container container,
ItemStack stack
) {
if (stack.isEmpty()) return ItemStack.EMPTY;
// First try to merge with existing stacks
for (
int i = 0;
i < container.getContainerSize() && !stack.isEmpty();
i++
) {
ItemStack slotStack = container.getItem(i);
if (
!slotStack.isEmpty() &&
ItemStack.isSameItemSameTags(slotStack, stack) &&
slotStack.getCount() < slotStack.getMaxStackSize()
) {
int space = slotStack.getMaxStackSize() - slotStack.getCount();
int toAdd = Math.min(space, stack.getCount());
slotStack.grow(toAdd);
stack.shrink(toAdd);
}
}
// Then try empty slots
for (
int i = 0;
i < container.getContainerSize() && !stack.isEmpty();
i++
) {
if (container.getItem(i).isEmpty()) {
container.setItem(i, stack.copy());
stack.setCount(0);
}
}
if (container instanceof BlockEntity blockEntity) {
blockEntity.setChanged();
}
return stack;
}
/**
* Clear prisoner's entire inventory except for armor.
* Used for maximum security confinement.
*
* @param player The prisoner
* @return All cleared items
*/
public List<ItemStack> clearInventory(ServerPlayer player) {
List<ItemStack> cleared = new ArrayList<>();
var inventory = player.getInventory();
// Clear main inventory and hotbar (slots 0-35)
for (int i = 0; i < 36; i++) {
ItemStack stack = inventory.getItem(i);
if (!stack.isEmpty()) {
cleared.add(stack.copy());
inventory.setItem(i, ItemStack.EMPTY);
}
}
// Clear offhand (slot 40)
ItemStack offhand = inventory.offhand.get(0);
if (!offhand.isEmpty()) {
cleared.add(offhand.copy());
inventory.offhand.set(0, ItemStack.EMPTY);
}
TiedUpMod.LOGGER.info(
"[ItemService] Cleared {} stacks from {}'s inventory",
cleared.size(),
player.getName().getString()
);
return cleared;
}
}

File diff suppressed because it is too large Load Diff