Files
TiedUp-/src/main/java/com/tiedup/remake/cells/CellRegistryV2.java
NotEvil f6466360b6 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.
2026-04-12 00:51:22 +02:00

904 lines
28 KiB
Java

package com.tiedup.remake.cells;
import com.tiedup.remake.core.SystemMessageManager;
import com.tiedup.remake.core.TiedUpMod;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import net.minecraft.core.BlockPos;
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.ChunkPos;
import net.minecraft.world.level.saveddata.SavedData;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Global registry for Cell System V2 data.
*
* Named CellRegistryV2 to coexist with v1 CellRegistry during migration.
* Uses "tiedup_cell_registry_v2" as the SavedData name.
*
* Provides spatial indices for fast lookups by wall position, interior position,
* core position, chunk, and camp.
*/
public class CellRegistryV2 extends SavedData {
private static final String DATA_NAME = "tiedup_cell_registry_v2";
/** Reservation timeout in ticks (30 seconds = 600 ticks) */
private static final long RESERVATION_TIMEOUT_TICKS = 600L;
// ==================== RESERVATION ====================
private static class CellReservation {
private final UUID kidnapperUUID;
private final long expiryTime;
public CellReservation(UUID kidnapperUUID, long expiryTime) {
this.kidnapperUUID = kidnapperUUID;
this.expiryTime = expiryTime;
}
public UUID getKidnapperUUID() {
return kidnapperUUID;
}
public boolean isExpired(long currentTime) {
return currentTime >= expiryTime;
}
}
// ==================== STORAGE ====================
// Primary storage
private final Map<UUID, CellDataV2> cells = new ConcurrentHashMap<>();
// Indices (rebuilt on load)
private final Map<BlockPos, UUID> wallToCell = new ConcurrentHashMap<>();
private final Map<BlockPos, UUID> interiorToCell =
new ConcurrentHashMap<>();
private final Map<BlockPos, UUID> coreToCell = new ConcurrentHashMap<>();
// Spatial + camp indices
private final Map<ChunkPos, Set<UUID>> cellsByChunk =
new ConcurrentHashMap<>();
private final Map<UUID, Set<UUID>> cellsByCamp = new ConcurrentHashMap<>();
// Breach tracking index (breached wall position → cell ID)
private final Map<BlockPos, UUID> breachedToCell =
new ConcurrentHashMap<>();
// Reservations (not persisted)
private final Map<UUID, CellReservation> reservations =
new ConcurrentHashMap<>();
// ==================== STATIC ACCESS ====================
public static CellRegistryV2 get(ServerLevel level) {
return level
.getDataStorage()
.computeIfAbsent(
CellRegistryV2::load,
CellRegistryV2::new,
DATA_NAME
);
}
public static CellRegistryV2 get(MinecraftServer server) {
return get(server.overworld());
}
// ==================== CELL LIFECYCLE ====================
/**
* Create a new cell from a flood-fill result.
*/
public CellDataV2 createCell(
BlockPos corePos,
FloodFillResult result,
@Nullable UUID ownerId
) {
CellDataV2 cell = new CellDataV2(corePos, result);
if (ownerId != null) {
cell.setOwnerId(ownerId);
}
cells.put(cell.getId(), cell);
// Register in indices
coreToCell.put(corePos.immutable(), cell.getId());
for (BlockPos pos : cell.getWallBlocks()) {
wallToCell.put(pos.immutable(), cell.getId());
}
for (BlockPos pos : cell.getInteriorBlocks()) {
interiorToCell.put(pos.immutable(), cell.getId());
}
addToSpatialIndex(cell);
setDirty();
return cell;
}
/**
* Register a pre-constructed CellDataV2 (used by migration and structure loading).
* The cell must already have its ID and corePos set.
*/
public void registerExistingCell(CellDataV2 cell) {
cells.put(cell.getId(), cell);
coreToCell.put(cell.getCorePos().immutable(), cell.getId());
for (BlockPos pos : cell.getWallBlocks()) {
wallToCell.put(pos.immutable(), cell.getId());
}
for (BlockPos pos : cell.getInteriorBlocks()) {
interiorToCell.put(pos.immutable(), cell.getId());
}
addToSpatialIndex(cell);
setDirty();
}
/**
* Remove a cell from the registry and all indices.
*/
public void removeCell(UUID cellId) {
CellDataV2 cell = cells.remove(cellId);
if (cell == null) return;
coreToCell.remove(cell.getCorePos());
for (BlockPos pos : cell.getWallBlocks()) {
wallToCell.remove(pos);
}
for (BlockPos pos : cell.getBreachedPositions()) {
breachedToCell.remove(pos);
}
for (BlockPos pos : cell.getInteriorBlocks()) {
interiorToCell.remove(pos);
}
removeFromSpatialIndex(cell);
reservations.remove(cellId);
setDirty();
}
/**
* Rescan a cell with a new flood-fill result.
* Clears old indices and repopulates with new geometry.
*/
public void rescanCell(UUID cellId, FloodFillResult newResult) {
CellDataV2 cell = cells.get(cellId);
if (cell == null) return;
// Clear old indices for this cell
for (BlockPos pos : cell.getWallBlocks()) {
wallToCell.remove(pos);
}
for (BlockPos pos : cell.getBreachedPositions()) {
wallToCell.remove(pos);
breachedToCell.remove(pos);
}
for (BlockPos pos : cell.getInteriorBlocks()) {
interiorToCell.remove(pos);
}
removeFromSpatialIndex(cell);
// Update geometry
cell.updateGeometry(newResult);
// Rebuild indices
for (BlockPos pos : cell.getWallBlocks()) {
wallToCell.put(pos.immutable(), cellId);
}
for (BlockPos pos : cell.getInteriorBlocks()) {
interiorToCell.put(pos.immutable(), cellId);
}
addToSpatialIndex(cell);
setDirty();
}
// ==================== QUERIES ====================
@Nullable
public CellDataV2 getCell(UUID cellId) {
return cells.get(cellId);
}
@Nullable
public CellDataV2 getCellAtCore(BlockPos corePos) {
UUID cellId = coreToCell.get(corePos);
return cellId != null ? cells.get(cellId) : null;
}
@Nullable
public CellDataV2 getCellContaining(BlockPos pos) {
UUID cellId = interiorToCell.get(pos);
return cellId != null ? cells.get(cellId) : null;
}
@Nullable
public CellDataV2 getCellByWall(BlockPos pos) {
UUID cellId = wallToCell.get(pos);
return cellId != null ? cells.get(cellId) : null;
}
@Nullable
public UUID getCellIdAtWall(BlockPos pos) {
return wallToCell.get(pos);
}
public boolean isInsideAnyCell(BlockPos pos) {
return interiorToCell.containsKey(pos);
}
public Collection<CellDataV2> getAllCells() {
return Collections.unmodifiableCollection(cells.values());
}
public int getCellCount() {
return cells.size();
}
public List<CellDataV2> getCellsByCamp(UUID campId) {
Set<UUID> cellIds = cellsByCamp.get(campId);
if (cellIds == null) return Collections.emptyList();
List<CellDataV2> result = new ArrayList<>();
for (UUID cellId : cellIds) {
CellDataV2 cell = cells.get(cellId);
if (cell != null) {
result.add(cell);
}
}
return result;
}
public List<CellDataV2> findCellsNear(BlockPos center, double radius) {
List<CellDataV2> nearby = new ArrayList<>();
double radiusSq = radius * radius;
int chunkRadius = (int) Math.ceil(radius / 16.0) + 1;
ChunkPos centerChunk = new ChunkPos(center);
for (int dx = -chunkRadius; dx <= chunkRadius; dx++) {
for (int dz = -chunkRadius; dz <= chunkRadius; dz++) {
ChunkPos checkChunk = new ChunkPos(
centerChunk.x + dx,
centerChunk.z + dz
);
Set<UUID> cellsInChunk = cellsByChunk.get(checkChunk);
if (cellsInChunk != null) {
for (UUID cellId : cellsInChunk) {
CellDataV2 cell = cells.get(cellId);
if (
cell != null &&
cell.getCorePos().distSqr(center) <= radiusSq
) {
nearby.add(cell);
}
}
}
}
}
return nearby;
}
@Nullable
public CellDataV2 findCellByPrisoner(UUID prisonerId) {
for (CellDataV2 cell : cells.values()) {
if (cell.hasPrisoner(prisonerId)) {
return cell;
}
}
return null;
}
@Nullable
public CellDataV2 getCellByName(String name) {
if (name == null || name.isEmpty()) return null;
for (CellDataV2 cell : cells.values()) {
if (name.equals(cell.getName())) {
return cell;
}
}
return null;
}
public List<CellDataV2> getCellsByOwner(UUID ownerId) {
if (ownerId == null) return Collections.emptyList();
return cells
.values()
.stream()
.filter(c -> ownerId.equals(c.getOwnerId()))
.collect(Collectors.toList());
}
public int getCellCountOwnedBy(UUID ownerId) {
if (ownerId == null) return 0;
return (int) cells
.values()
.stream()
.filter(c -> ownerId.equals(c.getOwnerId()))
.count();
}
@Nullable
public UUID getNextCellId(@Nullable UUID currentId) {
if (cells.isEmpty()) return null;
List<UUID> ids = new ArrayList<>(cells.keySet());
if (currentId == null) return ids.get(0);
int index = ids.indexOf(currentId);
if (index < 0 || index >= ids.size() - 1) return ids.get(0);
return ids.get(index + 1);
}
// ==================== CAMP QUERIES ====================
public List<UUID> getPrisonersInCamp(UUID campId) {
if (campId == null) return Collections.emptyList();
return getCellsByCamp(campId)
.stream()
.flatMap(cell -> cell.getPrisonerIds().stream())
.collect(Collectors.toList());
}
public int getPrisonerCountInCamp(UUID campId) {
if (campId == null) return 0;
return getCellsByCamp(campId)
.stream()
.mapToInt(CellDataV2::getPrisonerCount)
.sum();
}
@Nullable
public CellDataV2 findAvailableCellInCamp(UUID campId) {
if (campId == null) return null;
for (CellDataV2 cell : getCellsByCamp(campId)) {
if (!cell.isFull()) {
return cell;
}
}
return null;
}
public boolean hasCampCells(UUID campId) {
if (campId == null) return false;
Set<UUID> cellIds = cellsByCamp.get(campId);
return cellIds != null && !cellIds.isEmpty();
}
/**
* Update the camp index for a cell after ownership change.
* Removes from old camp index, adds to new if camp-owned.
*/
public void updateCampIndex(CellDataV2 cell, @Nullable UUID oldOwnerId) {
if (oldOwnerId != null) {
Set<UUID> oldCampCells = cellsByCamp.get(oldOwnerId);
if (oldCampCells != null) {
oldCampCells.remove(cell.getId());
if (oldCampCells.isEmpty()) {
cellsByCamp.remove(oldOwnerId);
}
}
}
if (cell.isCampOwned() && cell.getOwnerId() != null) {
cellsByCamp
.computeIfAbsent(cell.getOwnerId(), k ->
ConcurrentHashMap.newKeySet()
)
.add(cell.getId());
}
setDirty();
}
// ==================== PRISONER MANAGEMENT ====================
public synchronized boolean assignPrisoner(UUID cellId, UUID prisonerId) {
CellDataV2 cell = cells.get(cellId);
if (cell == null || cell.isFull()) return false;
// Ensure prisoner uniqueness across cells
CellDataV2 existingCell = findCellByPrisoner(prisonerId);
if (existingCell != null) {
if (existingCell.getId().equals(cellId)) {
return true; // Already in this cell
}
return false; // Already in another cell
}
if (cell.addPrisoner(prisonerId)) {
setDirty();
return true;
}
return false;
}
public boolean releasePrisoner(
UUID cellId,
UUID prisonerId,
MinecraftServer server
) {
CellDataV2 cell = cells.get(cellId);
if (cell == null) return false;
if (cell.removePrisoner(prisonerId)) {
// Synchronize with PrisonerManager
if (cell.isCampOwned() && cell.getCampId() != null) {
com.tiedup.remake.prison.PrisonerManager manager =
com.tiedup.remake.prison.PrisonerManager.get(
server.overworld()
);
com.tiedup.remake.prison.PrisonerState currentState =
manager.getState(prisonerId);
boolean isBeingExtracted =
currentState ==
com.tiedup.remake.prison.PrisonerState.WORKING;
if (
!isBeingExtracted &&
currentState ==
com.tiedup.remake.prison.PrisonerState.IMPRISONED
) {
manager.release(
prisonerId,
server.overworld().getGameTime()
);
}
}
setDirty();
return true;
}
return false;
}
public boolean assignPrisonerWithNotification(
UUID cellId,
UUID prisonerId,
MinecraftServer server,
String prisonerName
) {
CellDataV2 cell = cells.get(cellId);
if (cell == null || cell.isFull()) return false;
if (cell.addPrisoner(prisonerId)) {
setDirty();
if (cell.hasOwner()) {
notifyOwner(
server,
cell.getOwnerId(),
SystemMessageManager.MessageCategory.PRISONER_ARRIVED,
prisonerName
);
}
return true;
}
return false;
}
public boolean releasePrisonerWithNotification(
UUID cellId,
UUID prisonerId,
MinecraftServer server,
String prisonerName,
boolean escaped
) {
CellDataV2 cell = cells.get(cellId);
if (cell == null) return false;
if (cell.removePrisoner(prisonerId)) {
// Synchronize with PrisonerManager
if (cell.isCampOwned() && cell.getCampId() != null) {
com.tiedup.remake.prison.PrisonerManager manager =
com.tiedup.remake.prison.PrisonerManager.get(
server.overworld()
);
com.tiedup.remake.prison.PrisonerState currentState =
manager.getState(prisonerId);
if (
currentState ==
com.tiedup.remake.prison.PrisonerState.IMPRISONED
) {
manager.release(
prisonerId,
server.overworld().getGameTime()
);
}
}
setDirty();
if (cell.hasOwner()) {
SystemMessageManager.MessageCategory category = escaped
? SystemMessageManager.MessageCategory.PRISONER_ESCAPED
: SystemMessageManager.MessageCategory.PRISONER_RELEASED;
notifyOwner(server, cell.getOwnerId(), category, prisonerName);
}
return true;
}
return false;
}
public int releasePrisonerFromAllCells(UUID prisonerId) {
int count = 0;
for (CellDataV2 cell : cells.values()) {
if (cell.removePrisoner(prisonerId)) {
count++;
}
}
if (count > 0) {
setDirty();
}
return count;
}
/** Offline timeout for cleanup: 30 minutes */
private static final long OFFLINE_TIMEOUT_MS = 30 * 60 * 1000L;
public int cleanupEscapedPrisoners(
ServerLevel level,
com.tiedup.remake.state.CollarRegistry collarRegistry,
double maxDistance
) {
int removed = 0;
for (CellDataV2 cell : cells.values()) {
List<UUID> toRemove = new ArrayList<>();
for (UUID prisonerId : cell.getPrisonerIds()) {
boolean shouldRemove = false;
String reason = null;
ServerPlayer prisoner = level
.getServer()
.getPlayerList()
.getPlayer(prisonerId);
if (prisoner == null) {
Long timestamp = cell.getPrisonerTimestamp(prisonerId);
long ts =
timestamp != null
? timestamp
: System.currentTimeMillis();
long offlineDuration = System.currentTimeMillis() - ts;
if (offlineDuration > OFFLINE_TIMEOUT_MS) {
shouldRemove = true;
reason =
"offline for too long (" +
(offlineDuration / 60000) +
" minutes)";
} else {
continue;
}
} else {
// Use corePos for distance check (V2 uses core position, not spawnPoint)
double distSq = prisoner
.blockPosition()
.distSqr(cell.getCorePos());
if (distSq > maxDistance * maxDistance) {
shouldRemove = true;
reason =
"too far from cell (" +
(int) Math.sqrt(distSq) +
" blocks)";
}
if (
!shouldRemove && !collarRegistry.hasOwners(prisonerId)
) {
shouldRemove = true;
reason = "no collar registered";
}
if (!shouldRemove) {
com.tiedup.remake.state.IBondageState state =
com.tiedup.remake.util.KidnappedHelper.getKidnappedState(
prisoner
);
if (state == null || !state.isCaptive()) {
shouldRemove = true;
reason = "no longer captive";
}
}
}
if (shouldRemove) {
toRemove.add(prisonerId);
TiedUpMod.LOGGER.info(
"[CellRegistryV2] Removing escaped prisoner {} from cell {} - reason: {}",
prisonerId.toString().substring(0, 8),
cell.getId().toString().substring(0, 8),
reason
);
}
}
for (UUID id : toRemove) {
cell.removePrisoner(id);
if (cell.isCampOwned() && cell.getCampId() != null) {
com.tiedup.remake.prison.PrisonerManager manager =
com.tiedup.remake.prison.PrisonerManager.get(
level.getServer().overworld()
);
com.tiedup.remake.prison.PrisonerState currentState =
manager.getState(id);
if (
currentState ==
com.tiedup.remake.prison.PrisonerState.IMPRISONED
) {
com.tiedup.remake.prison.service.PrisonerService.get().escape(
level,
id,
"offline_cleanup"
);
}
}
removed++;
}
}
if (removed > 0) {
setDirty();
}
return removed;
}
// ==================== NOTIFICATIONS ====================
private void notifyOwner(
MinecraftServer server,
UUID ownerId,
SystemMessageManager.MessageCategory category,
String prisonerName
) {
if (server == null || ownerId == null) return;
ServerPlayer owner = server.getPlayerList().getPlayer(ownerId);
if (owner != null) {
String template = SystemMessageManager.getTemplate(category);
String formattedMessage = String.format(template, prisonerName);
SystemMessageManager.sendToPlayer(
owner,
category,
formattedMessage
);
}
}
// ==================== BREACH MANAGEMENT ====================
/**
* Record a wall breach: updates CellDataV2 and indices atomically.
*/
public void addBreach(UUID cellId, BlockPos pos) {
CellDataV2 cell = cells.get(cellId);
if (cell == null) return;
cell.addBreach(pos);
wallToCell.remove(pos);
breachedToCell.put(pos.immutable(), cellId);
setDirty();
}
/**
* Repair a wall breach: updates CellDataV2 and indices atomically.
*/
public void repairBreach(UUID cellId, BlockPos pos) {
CellDataV2 cell = cells.get(cellId);
if (cell == null) return;
cell.repairBreach(pos);
breachedToCell.remove(pos);
wallToCell.put(pos.immutable(), cellId);
setDirty();
}
/**
* Get the cell ID for a breached wall position.
*/
@Nullable
public UUID getCellIdAtBreach(BlockPos pos) {
return breachedToCell.get(pos);
}
// ==================== RESERVATIONS ====================
public boolean reserveCell(UUID cellId, UUID kidnapperUUID, long gameTime) {
cleanupExpiredReservations(gameTime);
long expiryTime = gameTime + RESERVATION_TIMEOUT_TICKS;
CellReservation existing = reservations.get(cellId);
if (existing != null) {
if (existing.getKidnapperUUID().equals(kidnapperUUID)) {
reservations.put(
cellId,
new CellReservation(kidnapperUUID, expiryTime)
);
return true;
}
if (!existing.isExpired(gameTime)) {
return false;
}
}
reservations.put(
cellId,
new CellReservation(kidnapperUUID, expiryTime)
);
return true;
}
public boolean consumeReservation(UUID cellId, UUID kidnapperUUID) {
CellReservation reservation = reservations.remove(cellId);
if (reservation == null) return false;
return reservation.getKidnapperUUID().equals(kidnapperUUID);
}
public boolean isReservedByOther(
UUID cellId,
@Nullable UUID kidnapperUUID,
long gameTime
) {
CellReservation reservation = reservations.get(cellId);
if (reservation == null) return false;
if (reservation.isExpired(gameTime)) {
reservations.remove(cellId);
return false;
}
return (
kidnapperUUID == null ||
!reservation.getKidnapperUUID().equals(kidnapperUUID)
);
}
public void cancelReservation(UUID cellId, UUID kidnapperUUID) {
CellReservation reservation = reservations.get(cellId);
if (
reservation != null &&
reservation.getKidnapperUUID().equals(kidnapperUUID)
) {
reservations.remove(cellId);
}
}
private void cleanupExpiredReservations(long gameTime) {
reservations
.entrySet()
.removeIf(entry -> entry.getValue().isExpired(gameTime));
}
// ==================== SPATIAL INDEX ====================
private void addToSpatialIndex(CellDataV2 cell) {
ChunkPos chunkPos = new ChunkPos(cell.getCorePos());
cellsByChunk
.computeIfAbsent(chunkPos, k -> ConcurrentHashMap.newKeySet())
.add(cell.getId());
// Add to camp index if camp-owned
if (
cell.getOwnerType() == CellOwnerType.CAMP &&
cell.getOwnerId() != null
) {
cellsByCamp
.computeIfAbsent(cell.getOwnerId(), k ->
ConcurrentHashMap.newKeySet()
)
.add(cell.getId());
}
}
private void removeFromSpatialIndex(CellDataV2 cell) {
ChunkPos chunkPos = new ChunkPos(cell.getCorePos());
Set<UUID> cellsInChunk = cellsByChunk.get(chunkPos);
if (cellsInChunk != null) {
cellsInChunk.remove(cell.getId());
if (cellsInChunk.isEmpty()) {
cellsByChunk.remove(chunkPos);
}
}
if (
cell.getOwnerType() == CellOwnerType.CAMP &&
cell.getOwnerId() != null
) {
Set<UUID> cellsInCamp = cellsByCamp.get(cell.getOwnerId());
if (cellsInCamp != null) {
cellsInCamp.remove(cell.getId());
if (cellsInCamp.isEmpty()) {
cellsByCamp.remove(cell.getOwnerId());
}
}
}
}
// ==================== INDEX REBUILD ====================
private void rebuildIndices() {
wallToCell.clear();
interiorToCell.clear();
coreToCell.clear();
breachedToCell.clear();
cellsByChunk.clear();
cellsByCamp.clear();
for (CellDataV2 cell : cells.values()) {
coreToCell.put(cell.getCorePos(), cell.getId());
for (BlockPos pos : cell.getWallBlocks()) {
wallToCell.put(pos, cell.getId());
}
for (BlockPos pos : cell.getBreachedPositions()) {
breachedToCell.put(pos, cell.getId());
}
for (BlockPos pos : cell.getInteriorBlocks()) {
interiorToCell.put(pos, cell.getId());
}
addToSpatialIndex(cell);
}
}
// ==================== PERSISTENCE ====================
@Override
public @NotNull CompoundTag save(@NotNull CompoundTag tag) {
ListTag cellList = new ListTag();
for (CellDataV2 cell : cells.values()) {
cellList.add(cell.save());
}
tag.put("cells", cellList);
return tag;
}
public static CellRegistryV2 load(CompoundTag tag) {
CellRegistryV2 registry = new CellRegistryV2();
if (tag.contains("cells")) {
ListTag cellList = tag.getList("cells", Tag.TAG_COMPOUND);
for (int i = 0; i < cellList.size(); i++) {
CellDataV2 cell = CellDataV2.load(cellList.getCompound(i));
if (cell != null) {
registry.cells.put(cell.getId(), cell);
}
}
}
registry.rebuildIndices();
return registry;
}
// ==================== DEBUG ====================
public String toDebugString() {
StringBuilder sb = new StringBuilder();
sb.append("CellRegistryV2:\n");
sb.append(" Total cells: ").append(cells.size()).append("\n");
sb.append(" Wall index: ").append(wallToCell.size()).append("\n");
sb
.append(" Interior index: ")
.append(interiorToCell.size())
.append("\n");
sb.append(" Core index: ").append(coreToCell.size()).append("\n");
for (CellDataV2 cell : cells.values()) {
sb.append(" ").append(cell.toString()).append("\n");
}
return sb.toString();
}
}