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; /** * Phase 2: SavedData registry for confiscated player inventories. * * 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 */ 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 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 items, List chestPositions, ServerLevel level ) { if (items.isEmpty() || chestPositions.isEmpty()) { return 0; } int deposited = 0; List 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; } }