Strip all Phase references, TODO/FUTURE roadmap notes, and internal planning comments from the codebase. Run Prettier for consistent formatting across all Java files.
641 lines
22 KiB
Java
641 lines
22 KiB
Java
package com.tiedup.remake.cells;
|
|
|
|
import com.tiedup.remake.core.TiedUpMod;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.UUID;
|
|
import net.minecraft.core.BlockPos;
|
|
import net.minecraft.nbt.CompoundTag;
|
|
import net.minecraft.nbt.ListTag;
|
|
import net.minecraft.nbt.NbtUtils;
|
|
import net.minecraft.nbt.Tag;
|
|
import net.minecraft.server.level.ServerLevel;
|
|
import net.minecraft.server.level.ServerPlayer;
|
|
import net.minecraft.world.Container;
|
|
import net.minecraft.world.entity.player.Inventory;
|
|
import net.minecraft.world.item.ItemStack;
|
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
|
import net.minecraft.world.level.block.entity.ChestBlockEntity;
|
|
import net.minecraft.world.level.saveddata.SavedData;
|
|
import org.jetbrains.annotations.Nullable;
|
|
|
|
/**
|
|
*
|
|
* When a player is imprisoned:
|
|
* 1. Their inventory is saved to NBT
|
|
* 2. Items are transferred to a LOOT chest in the cell
|
|
* 3. Player's inventory is cleared
|
|
* 4. Data is persisted for recovery after server restart
|
|
*
|
|
* Recovery options:
|
|
* - Player finds and opens the chest manually
|
|
* - Player escapes and returns to chest
|
|
* - Admin command /tiedup returnstuff <player>
|
|
*/
|
|
public class ConfiscatedInventoryRegistry extends SavedData {
|
|
|
|
private static final String DATA_NAME =
|
|
TiedUpMod.MOD_ID + "_confiscated_inventories";
|
|
|
|
/**
|
|
* Map of prisoner UUID to their confiscated inventory data
|
|
*/
|
|
private final Map<UUID, ConfiscatedData> confiscatedInventories =
|
|
new HashMap<>();
|
|
|
|
/**
|
|
* Data class for a single confiscated inventory
|
|
*/
|
|
public static class ConfiscatedData {
|
|
|
|
public final UUID prisonerId;
|
|
public final CompoundTag inventoryNbt;
|
|
public final BlockPos chestPos;
|
|
public final UUID cellId;
|
|
public final long confiscatedTime;
|
|
|
|
public ConfiscatedData(
|
|
UUID prisonerId,
|
|
CompoundTag inventoryNbt,
|
|
BlockPos chestPos,
|
|
@Nullable UUID cellId,
|
|
long confiscatedTime
|
|
) {
|
|
this.prisonerId = prisonerId;
|
|
this.inventoryNbt = inventoryNbt;
|
|
this.chestPos = chestPos;
|
|
this.cellId = cellId;
|
|
this.confiscatedTime = confiscatedTime;
|
|
}
|
|
|
|
public CompoundTag save() {
|
|
CompoundTag tag = new CompoundTag();
|
|
tag.putUUID("prisonerId", prisonerId);
|
|
tag.put("inventory", inventoryNbt);
|
|
tag.put("chestPos", NbtUtils.writeBlockPos(chestPos));
|
|
if (cellId != null) {
|
|
tag.putUUID("cellId", cellId);
|
|
}
|
|
tag.putLong("time", confiscatedTime);
|
|
return tag;
|
|
}
|
|
|
|
public static ConfiscatedData load(CompoundTag tag) {
|
|
UUID prisonerId = tag.getUUID("prisonerId");
|
|
CompoundTag inventoryNbt = tag.getCompound("inventory");
|
|
BlockPos chestPos = NbtUtils.readBlockPos(
|
|
tag.getCompound("chestPos")
|
|
);
|
|
UUID cellId = tag.contains("cellId") ? tag.getUUID("cellId") : null;
|
|
long time = tag.getLong("time");
|
|
return new ConfiscatedData(
|
|
prisonerId,
|
|
inventoryNbt,
|
|
chestPos,
|
|
cellId,
|
|
time
|
|
);
|
|
}
|
|
}
|
|
|
|
public ConfiscatedInventoryRegistry() {}
|
|
|
|
// ==================== STATIC ACCESSORS ====================
|
|
|
|
/**
|
|
* Get or create the registry for a server level.
|
|
*/
|
|
public static ConfiscatedInventoryRegistry get(ServerLevel level) {
|
|
return level
|
|
.getDataStorage()
|
|
.computeIfAbsent(
|
|
ConfiscatedInventoryRegistry::load,
|
|
ConfiscatedInventoryRegistry::new,
|
|
DATA_NAME
|
|
);
|
|
}
|
|
|
|
// ==================== CONFISCATION METHODS ====================
|
|
|
|
/**
|
|
* Confiscate a player's inventory.
|
|
*
|
|
* @param player The player whose inventory to confiscate
|
|
* @param chestPos The position of the LOOT chest
|
|
* @param cellId The cell ID (optional)
|
|
* @return true if confiscation was successful
|
|
*/
|
|
public boolean confiscate(
|
|
ServerPlayer player,
|
|
BlockPos chestPos,
|
|
@Nullable UUID cellId
|
|
) {
|
|
Inventory inventory = player.getInventory();
|
|
|
|
// CRITICAL FIX: Transaction safety - save record BEFORE any state changes
|
|
// This ensures that if the server crashes, we have a backup to restore from
|
|
|
|
// Save inventory to NBT (backup in case of issues)
|
|
CompoundTag inventoryNbt = savePlayerInventory(player);
|
|
|
|
// Create and persist record FIRST (before modifying game state)
|
|
ConfiscatedData data = new ConfiscatedData(
|
|
player.getUUID(),
|
|
inventoryNbt,
|
|
chestPos,
|
|
cellId,
|
|
System.currentTimeMillis()
|
|
);
|
|
confiscatedInventories.put(player.getUUID(), data);
|
|
setDirty(); // Persist immediately before state changes
|
|
|
|
// Now attempt transfer to chest
|
|
boolean transferred = transferToChest(
|
|
player.serverLevel(),
|
|
chestPos,
|
|
inventory
|
|
);
|
|
|
|
if (!transferred) {
|
|
// Transfer failed - rollback: remove record and DO NOT clear inventory
|
|
confiscatedInventories.remove(player.getUUID());
|
|
setDirty(); // Persist the rollback
|
|
|
|
TiedUpMod.LOGGER.error(
|
|
"[ConfiscatedInventoryRegistry] Failed to transfer items for {} - items NOT confiscated, record rolled back",
|
|
player.getName().getString()
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// Transfer succeeded - now safe to clear inventory
|
|
inventory.clearContent();
|
|
|
|
TiedUpMod.LOGGER.info(
|
|
"[ConfiscatedInventoryRegistry] Confiscated inventory from {} (chest: {}, cell: {})",
|
|
player.getName().getString(),
|
|
chestPos.toShortString(),
|
|
cellId != null ? cellId.toString().substring(0, 8) : "none"
|
|
);
|
|
|
|
// Final persist to save cleared inventory state
|
|
setDirty();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Dump a player's inventory to a chest WITHOUT creating a backup record.
|
|
* Used for daily labor returns - items gathered during work are transferred to camp storage.
|
|
* Does NOT clear the player's inventory - caller should clear labor tools separately.
|
|
*
|
|
* @param player The player whose inventory to dump
|
|
* @param chestPos The position of the chest
|
|
* @return true if transfer was successful
|
|
*/
|
|
public boolean dumpInventoryToChest(
|
|
ServerPlayer player,
|
|
BlockPos chestPos
|
|
) {
|
|
Inventory inventory = player.getInventory();
|
|
|
|
// Transfer items to chest
|
|
boolean transferred = transferToChest(
|
|
player.serverLevel(),
|
|
chestPos,
|
|
inventory
|
|
);
|
|
|
|
if (!transferred) {
|
|
TiedUpMod.LOGGER.warn(
|
|
"[ConfiscatedInventoryRegistry] Failed to dump labor inventory for {} - chest issue",
|
|
player.getName().getString()
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// Clear player inventory (items are now in chest)
|
|
inventory.clearContent();
|
|
|
|
TiedUpMod.LOGGER.debug(
|
|
"[ConfiscatedInventoryRegistry] Dumped labor inventory from {} to chest at {}",
|
|
player.getName().getString(),
|
|
chestPos.toShortString()
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Deposits items across multiple LOOT chests with smart rotation.
|
|
* Distributes items evenly to prevent single chest from filling too quickly.
|
|
*
|
|
* @param items List of items to deposit
|
|
* @param chestPositions List of chest positions to use (in priority order)
|
|
* @param level Server level
|
|
* @return Number of items successfully deposited (remainder dropped)
|
|
*/
|
|
public int depositItemsInChests(
|
|
List<ItemStack> items,
|
|
List<BlockPos> chestPositions,
|
|
ServerLevel level
|
|
) {
|
|
if (items.isEmpty() || chestPositions.isEmpty()) {
|
|
return 0;
|
|
}
|
|
|
|
int deposited = 0;
|
|
List<ItemStack> overflow = new ArrayList<>();
|
|
|
|
// Try to deposit each item
|
|
for (ItemStack stack : items) {
|
|
if (stack.isEmpty()) continue;
|
|
|
|
boolean placed = false;
|
|
|
|
// Try all chests in order
|
|
for (BlockPos chestPos : chestPositions) {
|
|
BlockEntity be = level.getBlockEntity(chestPos);
|
|
if (!(be instanceof ChestBlockEntity chest)) continue;
|
|
|
|
// Try to stack with existing items first
|
|
for (int i = 0; i < chest.getContainerSize(); i++) {
|
|
ItemStack slot = chest.getItem(i);
|
|
|
|
// Stack with existing
|
|
if (
|
|
!slot.isEmpty() &&
|
|
ItemStack.isSameItemSameTags(slot, stack) &&
|
|
slot.getCount() < slot.getMaxStackSize()
|
|
) {
|
|
int spaceInSlot =
|
|
slot.getMaxStackSize() - slot.getCount();
|
|
int toAdd = Math.min(spaceInSlot, stack.getCount());
|
|
|
|
slot.grow(toAdd);
|
|
chest.setChanged();
|
|
stack.shrink(toAdd);
|
|
deposited += toAdd;
|
|
|
|
if (stack.isEmpty()) {
|
|
placed = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (placed) break;
|
|
|
|
// Try to place in empty slot
|
|
if (!stack.isEmpty()) {
|
|
for (int i = 0; i < chest.getContainerSize(); i++) {
|
|
if (chest.getItem(i).isEmpty()) {
|
|
chest.setItem(i, stack.copy());
|
|
chest.setChanged();
|
|
deposited += stack.getCount();
|
|
placed = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (placed) break;
|
|
}
|
|
|
|
// If not placed, add to overflow
|
|
if (!placed && !stack.isEmpty()) {
|
|
overflow.add(stack);
|
|
}
|
|
}
|
|
|
|
// Drop overflow items at first chest location
|
|
if (!overflow.isEmpty() && !chestPositions.isEmpty()) {
|
|
BlockPos dropPos = chestPositions.get(0);
|
|
for (ItemStack stack : overflow) {
|
|
net.minecraft.world.entity.item.ItemEntity itemEntity =
|
|
new net.minecraft.world.entity.item.ItemEntity(
|
|
level,
|
|
dropPos.getX() + 0.5,
|
|
dropPos.getY() + 1.0,
|
|
dropPos.getZ() + 0.5,
|
|
stack
|
|
);
|
|
level.addFreshEntity(itemEntity);
|
|
}
|
|
|
|
TiedUpMod.LOGGER.warn(
|
|
"[ConfiscatedInventoryRegistry] {} items overflowed - dropped at {}",
|
|
overflow.stream().mapToInt(ItemStack::getCount).sum(),
|
|
dropPos.toShortString()
|
|
);
|
|
}
|
|
|
|
return deposited;
|
|
}
|
|
|
|
/**
|
|
* Save player inventory to NBT.
|
|
*/
|
|
private CompoundTag savePlayerInventory(ServerPlayer player) {
|
|
CompoundTag tag = new CompoundTag();
|
|
tag.put("Items", player.getInventory().save(new ListTag()));
|
|
return tag;
|
|
}
|
|
|
|
/**
|
|
* Transfer inventory contents to a chest.
|
|
* Handles ALL inventory slots:
|
|
* - Slots 0-35: Main inventory (hotbar 0-8, backpack 9-35)
|
|
* - Slots 36-39: Armor (boots, leggings, chestplate, helmet)
|
|
* - Slot 40: Offhand
|
|
*/
|
|
private boolean transferToChest(
|
|
ServerLevel level,
|
|
BlockPos chestPos,
|
|
Inventory inventory
|
|
) {
|
|
// Find an existing chest near the LOOT marker position
|
|
BlockPos actualChestPos = findExistingChestNear(level, chestPos);
|
|
if (actualChestPos == null) {
|
|
TiedUpMod.LOGGER.warn(
|
|
"[ConfiscatedInventoryRegistry] No existing chest found near {} - structure may be damaged",
|
|
chestPos.toShortString()
|
|
);
|
|
return false;
|
|
}
|
|
|
|
BlockEntity be = level.getBlockEntity(actualChestPos);
|
|
if (!(be instanceof Container chest)) {
|
|
TiedUpMod.LOGGER.warn(
|
|
"[ConfiscatedInventoryRegistry] Block at {} is not a container",
|
|
actualChestPos.toShortString()
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// Transfer ALL items including armor and offhand
|
|
int mainItems = 0;
|
|
int armorItems = 0;
|
|
int offhandItems = 0;
|
|
int droppedItems = 0;
|
|
|
|
// getContainerSize() returns 41: 36 main + 4 armor + 1 offhand
|
|
for (int i = 0; i < inventory.getContainerSize(); i++) {
|
|
ItemStack stack = inventory.getItem(i);
|
|
if (!stack.isEmpty()) {
|
|
ItemStack remaining = addToContainer(chest, stack.copy());
|
|
if (remaining.isEmpty()) {
|
|
// Track which slot type was transferred
|
|
if (i < 36) {
|
|
mainItems++;
|
|
} else if (i < 40) {
|
|
armorItems++;
|
|
} else {
|
|
offhandItems++;
|
|
}
|
|
} else {
|
|
// Couldn't fit all items - drop them on the ground near the chest
|
|
// This prevents permanent item loss
|
|
net.minecraft.world.entity.item.ItemEntity itemEntity =
|
|
new net.minecraft.world.entity.item.ItemEntity(
|
|
level,
|
|
actualChestPos.getX() + 0.5,
|
|
actualChestPos.getY() + 1.0,
|
|
actualChestPos.getZ() + 0.5,
|
|
remaining
|
|
);
|
|
// Add slight random velocity to spread items
|
|
itemEntity.setDeltaMovement(
|
|
(level.random.nextDouble() - 0.5) * 0.2,
|
|
0.2,
|
|
(level.random.nextDouble() - 0.5) * 0.2
|
|
);
|
|
level.addFreshEntity(itemEntity);
|
|
droppedItems += remaining.getCount();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (droppedItems > 0) {
|
|
TiedUpMod.LOGGER.info(
|
|
"[ConfiscatedInventoryRegistry] Dropped {} overflow items at {}",
|
|
droppedItems,
|
|
actualChestPos.toShortString()
|
|
);
|
|
}
|
|
|
|
int totalTransferred = mainItems + armorItems + offhandItems;
|
|
TiedUpMod.LOGGER.info(
|
|
"[ConfiscatedInventoryRegistry] Confiscated {} items (inventory: {}, armor: {}, offhand: {})",
|
|
totalTransferred,
|
|
mainItems,
|
|
armorItems,
|
|
offhandItems
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Add an item stack to a container, returning any remainder.
|
|
*/
|
|
private ItemStack addToContainer(Container container, ItemStack stack) {
|
|
ItemStack remaining = stack.copy();
|
|
|
|
for (
|
|
int i = 0;
|
|
i < container.getContainerSize() && !remaining.isEmpty();
|
|
i++
|
|
) {
|
|
ItemStack slotStack = container.getItem(i);
|
|
|
|
if (slotStack.isEmpty()) {
|
|
container.setItem(i, remaining.copy());
|
|
remaining = ItemStack.EMPTY;
|
|
} else if (ItemStack.isSameItemSameTags(slotStack, remaining)) {
|
|
int space = slotStack.getMaxStackSize() - slotStack.getCount();
|
|
int toTransfer = Math.min(space, remaining.getCount());
|
|
if (toTransfer > 0) {
|
|
slotStack.grow(toTransfer);
|
|
remaining.shrink(toTransfer);
|
|
}
|
|
}
|
|
}
|
|
|
|
container.setChanged();
|
|
return remaining;
|
|
}
|
|
|
|
/**
|
|
* Find an existing chest near the given position (typically a LOOT marker).
|
|
* The LOOT marker is placed ABOVE the physical chest in the structure,
|
|
* so we check below first, then at the position, then in a small radius.
|
|
*
|
|
* Does NOT spawn new chests - structures must have chests placed by their templates.
|
|
*
|
|
* @return The position of an existing chest, or null if none found
|
|
*/
|
|
@Nullable
|
|
private static BlockPos findExistingChestNear(
|
|
ServerLevel level,
|
|
BlockPos pos
|
|
) {
|
|
// Check below the marker (chest is usually under the LOOT marker)
|
|
BlockPos below = pos.below();
|
|
if (level.getBlockEntity(below) instanceof ChestBlockEntity) {
|
|
return below;
|
|
}
|
|
|
|
// Check at the marker position itself
|
|
if (level.getBlockEntity(pos) instanceof ChestBlockEntity) {
|
|
return pos;
|
|
}
|
|
|
|
// Search in a small radius (3 blocks)
|
|
for (int radius = 1; radius <= 3; radius++) {
|
|
for (int dx = -radius; dx <= radius; dx++) {
|
|
for (int dy = -2; dy <= 1; dy++) {
|
|
for (int dz = -radius; dz <= radius; dz++) {
|
|
if (dx == 0 && dy == 0 && dz == 0) continue;
|
|
|
|
BlockPos testPos = pos.offset(dx, dy, dz);
|
|
if (
|
|
level.getBlockEntity(testPos) instanceof
|
|
ChestBlockEntity
|
|
) {
|
|
TiedUpMod.LOGGER.debug(
|
|
"[ConfiscatedInventoryRegistry] Found existing chest at {} (offset from marker {})",
|
|
testPos.toShortString(),
|
|
pos.toShortString()
|
|
);
|
|
return testPos;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TiedUpMod.LOGGER.warn(
|
|
"[ConfiscatedInventoryRegistry] No existing chest found near marker at {}",
|
|
pos.toShortString()
|
|
);
|
|
return null;
|
|
}
|
|
|
|
// ==================== RECOVERY METHODS ====================
|
|
|
|
/**
|
|
* Check if a player has confiscated inventory.
|
|
*/
|
|
public boolean hasConfiscatedInventory(UUID playerId) {
|
|
return confiscatedInventories.containsKey(playerId);
|
|
}
|
|
|
|
/**
|
|
* Get confiscated data for a player.
|
|
*/
|
|
@Nullable
|
|
public ConfiscatedData getConfiscatedData(UUID playerId) {
|
|
return confiscatedInventories.get(playerId);
|
|
}
|
|
|
|
/**
|
|
* Get the chest position for a player's confiscated inventory.
|
|
*/
|
|
@Nullable
|
|
public BlockPos getChestPosition(UUID playerId) {
|
|
ConfiscatedData data = confiscatedInventories.get(playerId);
|
|
return data != null ? data.chestPos : null;
|
|
}
|
|
|
|
/**
|
|
* Restore a player's confiscated inventory from the NBT backup.
|
|
* This gives items directly to the player, bypassing the chest.
|
|
*
|
|
* @param player The player to restore inventory to
|
|
* @return true if restoration was successful, false if no confiscated data found
|
|
*/
|
|
public boolean restoreInventory(ServerPlayer player) {
|
|
ConfiscatedData data = confiscatedInventories.get(player.getUUID());
|
|
if (data == null) {
|
|
TiedUpMod.LOGGER.debug(
|
|
"[ConfiscatedInventoryRegistry] No confiscated inventory for {}",
|
|
player.getName().getString()
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// Load inventory from NBT backup
|
|
if (data.inventoryNbt.contains("Items")) {
|
|
ListTag items = data.inventoryNbt.getList(
|
|
"Items",
|
|
Tag.TAG_COMPOUND
|
|
);
|
|
player.getInventory().load(items);
|
|
|
|
TiedUpMod.LOGGER.info(
|
|
"[ConfiscatedInventoryRegistry] Restored {} inventory slots to {}",
|
|
items.size(),
|
|
player.getName().getString()
|
|
);
|
|
}
|
|
|
|
// Remove the confiscation record
|
|
confiscatedInventories.remove(player.getUUID());
|
|
setDirty();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Remove the confiscation record for a player without restoring items.
|
|
* Used when the player has already retrieved items from the chest manually.
|
|
*
|
|
* @param playerId The player's UUID
|
|
*/
|
|
public void clearConfiscationRecord(UUID playerId) {
|
|
if (confiscatedInventories.remove(playerId) != null) {
|
|
TiedUpMod.LOGGER.debug(
|
|
"[ConfiscatedInventoryRegistry] Cleared confiscation record for {}",
|
|
playerId.toString().substring(0, 8)
|
|
);
|
|
setDirty();
|
|
}
|
|
}
|
|
|
|
// ==================== SERIALIZATION ====================
|
|
|
|
@Override
|
|
public CompoundTag save(CompoundTag tag) {
|
|
ListTag list = new ListTag();
|
|
for (ConfiscatedData data : confiscatedInventories.values()) {
|
|
list.add(data.save());
|
|
}
|
|
tag.put("confiscated", list);
|
|
return tag;
|
|
}
|
|
|
|
public static ConfiscatedInventoryRegistry load(CompoundTag tag) {
|
|
ConfiscatedInventoryRegistry registry =
|
|
new ConfiscatedInventoryRegistry();
|
|
|
|
if (tag.contains("confiscated")) {
|
|
ListTag list = tag.getList("confiscated", Tag.TAG_COMPOUND);
|
|
for (int i = 0; i < list.size(); i++) {
|
|
ConfiscatedData data = ConfiscatedData.load(
|
|
list.getCompound(i)
|
|
);
|
|
registry.confiscatedInventories.put(data.prisonerId, data);
|
|
}
|
|
}
|
|
|
|
TiedUpMod.LOGGER.info(
|
|
"[ConfiscatedInventoryRegistry] Loaded {} confiscated inventory records",
|
|
registry.confiscatedInventories.size()
|
|
);
|
|
|
|
return registry;
|
|
}
|
|
}
|