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:
455
src/main/java/com/tiedup/remake/prison/LaborRecord.java
Normal file
455
src/main/java/com/tiedup/remake/prison/LaborRecord.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
800
src/main/java/com/tiedup/remake/prison/PrisonerManager.java
Normal file
800
src/main/java/com/tiedup/remake/prison/PrisonerManager.java
Normal 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();
|
||||
}
|
||||
}
|
||||
239
src/main/java/com/tiedup/remake/prison/PrisonerRecord.java
Normal file
239
src/main/java/com/tiedup/remake/prison/PrisonerRecord.java
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
94
src/main/java/com/tiedup/remake/prison/PrisonerState.java
Normal file
94
src/main/java/com/tiedup/remake/prison/PrisonerState.java
Normal 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;
|
||||
}
|
||||
}
|
||||
364
src/main/java/com/tiedup/remake/prison/PrisonerTransition.java
Normal file
364
src/main/java/com/tiedup/remake/prison/PrisonerTransition.java
Normal 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);
|
||||
}
|
||||
}
|
||||
471
src/main/java/com/tiedup/remake/prison/RansomData.java
Normal file
471
src/main/java/com/tiedup/remake/prison/RansomData.java
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
263
src/main/java/com/tiedup/remake/prison/RansomRecord.java
Normal file
263
src/main/java/com/tiedup/remake/prison/RansomRecord.java
Normal 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
525
src/main/java/com/tiedup/remake/prison/service/ItemService.java
Normal file
525
src/main/java/com/tiedup/remake/prison/service/ItemService.java
Normal 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;
|
||||
}
|
||||
}
|
||||
1046
src/main/java/com/tiedup/remake/prison/service/PrisonerService.java
Normal file
1046
src/main/java/com/tiedup/remake/prison/service/PrisonerService.java
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user