Remove build artifacts, dev tool configs, unused dependencies, and third-party source dumps. Add proper README, update .gitignore, clean up Makefile.
642 lines
22 KiB
Java
642 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;
|
|
|
|
/**
|
|
* 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 <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;
|
|
}
|
|
}
|