package com.tiedup.remake.commands; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.tiedup.remake.blocks.entity.MarkerBlockEntity; import com.tiedup.remake.cells.CellDataV2; import com.tiedup.remake.cells.CellOwnerType; import com.tiedup.remake.cells.CellRegistryV2; import com.tiedup.remake.items.ItemAdminWand; import java.util.Collection; import java.util.List; import java.util.UUID; import net.minecraft.ChatFormatting; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.commands.arguments.EntityArgument; import net.minecraft.core.BlockPos; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.block.entity.BlockEntity; /** * Cell management commands. * * Phase: Kidnapper Revamp - Cell System * * Commands: * /tiedup cell name - Name the selected cell (from wand) and link to player * /tiedup cell list [owner] - List all cells, optionally filtered by owner * /tiedup cell info [name] - Show info about selected cell or by name * /tiedup cell delete - Delete the selected cell * * Requires OP level 2. */ public class CellCommand { /** * Create the cell command builder (for use as subcommand of /tiedup). */ public static com.mojang.brigadier.builder.LiteralArgumentBuilder< CommandSourceStack > createCellCommand() { return Commands.literal("cell") .requires(CommandHelper.REQUIRES_OP) // /tiedup cell name .then( Commands.literal("name").then( Commands.argument( "name", StringArgumentType.word() ).executes(CellCommand::setName) ) ) // /tiedup cell list [owner] .then( Commands.literal("list") .executes(CellCommand::listAll) .then( Commands.argument( "owner", EntityArgument.player() ).executes(CellCommand::listByOwner) ) ) // /tiedup cell info [name] .then( Commands.literal("info") .executes(CellCommand::infoSelected) .then( Commands.argument( "name", StringArgumentType.word() ).executes(CellCommand::infoByName) ) ) // /tiedup cell delete .then( Commands.literal("delete").executes(CellCommand::deleteSelected) ) // /tiedup cell resetspawns [radius] .then( Commands.literal("resetspawns") .executes(ctx -> resetSpawns(ctx, 100)) // Default 100 block radius .then( Commands.argument( "radius", IntegerArgumentType.integer(1, 500) ).executes(ctx -> resetSpawns( ctx, IntegerArgumentType.getInteger(ctx, "radius") ) ) ) ); } /** * /tiedup cell name * * Name the selected cell (from wand) and link it to the executing player. */ private static int setName(CommandContext context) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); String name = StringArgumentType.getString(context, "name"); // Must be a player if (!(source.getEntity() instanceof ServerPlayer player)) { source.sendFailure(Component.translatable("command.tiedup.error.must_be_player")); return 0; } ServerLevel serverLevel = player.serverLevel(); CellRegistryV2 registry = CellRegistryV2.get(serverLevel); // Get selected cell from wand UUID selectedCellId = getSelectedCellFromWand(player); if (selectedCellId == null) { source.sendFailure( Component.translatable("command.tiedup.cell.no_selection") ); return 0; } CellDataV2 cell = registry.getCell(selectedCellId); if (cell == null) { source.sendFailure( Component.translatable("command.tiedup.cell.no_longer_exists") ); return 0; } // Check name uniqueness CellDataV2 existingCell = registry.getCellByName(name); if ( existingCell != null && !existingCell.getId().equals(selectedCellId) ) { source.sendFailure( Component.translatable("command.tiedup.cell.name_exists", name) ); return 0; } // Set name and owner cell.setName(name); // MEDIUM FIX: Update camp index when changing ownership // Store old ownerId before changing (in case it was camp-owned) UUID oldOwnerId = cell.isCampOwned() ? cell.getOwnerId() : null; cell.setOwnerId(player.getUUID()); cell.setOwnerType(CellOwnerType.PLAYER); registry.updateCampIndex(cell, oldOwnerId); registry.setDirty(); source.sendSuccess( () -> Component.translatable("command.tiedup.cell.named", name).withStyle(ChatFormatting.GREEN), true ); return 1; } /** * /tiedup cell list * * List all registered cells. */ private static int listAll(CommandContext context) { CommandSourceStack source = context.getSource(); ServerLevel serverLevel = source.getLevel(); CellRegistryV2 registry = CellRegistryV2.get(serverLevel); Collection cells = registry.getAllCells(); if (cells.isEmpty()) { source.sendSuccess( () -> Component.translatable("command.tiedup.cell.none_registered"), false ); return 1; } source.sendSuccess( () -> Component.translatable("command.tiedup.cell.list_header", cells.size()).withStyle(ChatFormatting.GOLD), false ); for (CellDataV2 cell : cells) { String info = formatCellInfo(cell, serverLevel); source.sendSuccess(() -> Component.literal(info).withStyle(ChatFormatting.GRAY), false); } return 1; } /** * /tiedup cell list * * List cells owned by a specific player. */ private static int listByOwner(CommandContext context) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); ServerPlayer owner = EntityArgument.getPlayer(context, "owner"); ServerLevel serverLevel = source.getLevel(); CellRegistryV2 registry = CellRegistryV2.get(serverLevel); List cells = registry.getCellsByOwner(owner.getUUID()); if (cells.isEmpty()) { source.sendSuccess( () -> Component.translatable("command.tiedup.cell.no_cells_for_owner", owner.getName().getString()), false ); return 1; } source.sendSuccess( () -> Component.translatable( "command.tiedup.cell.list_owner_header", owner.getName().getString(), cells.size() ).withStyle(ChatFormatting.GOLD), false ); for (CellDataV2 cell : cells) { String info = formatCellInfo(cell, serverLevel); source.sendSuccess(() -> Component.literal(info).withStyle(ChatFormatting.GRAY), false); } return 1; } /** * /tiedup cell info * * Show info about the selected cell (from wand). */ private static int infoSelected( CommandContext context ) { CommandSourceStack source = context.getSource(); // Must be a player if (!(source.getEntity() instanceof ServerPlayer player)) { source.sendFailure(Component.translatable("command.tiedup.error.must_be_player")); return 0; } ServerLevel serverLevel = player.serverLevel(); CellRegistryV2 registry = CellRegistryV2.get(serverLevel); UUID selectedCellId = getSelectedCellFromWand(player); if (selectedCellId == null) { source.sendFailure( Component.translatable("command.tiedup.cell.no_selection") ); return 0; } CellDataV2 cell = registry.getCell(selectedCellId); if (cell == null) { source.sendFailure( Component.translatable("command.tiedup.cell.no_longer_exists") ); return 0; } displayCellInfo(source, cell, serverLevel); return 1; } /** * /tiedup cell info * * Show info about a cell by name. */ private static int infoByName(CommandContext context) { CommandSourceStack source = context.getSource(); String name = StringArgumentType.getString(context, "name"); ServerLevel serverLevel = source.getLevel(); CellRegistryV2 registry = CellRegistryV2.get(serverLevel); CellDataV2 cell = registry.getCellByName(name); if (cell == null) { source.sendFailure( Component.translatable("command.tiedup.cell.not_found", name) ); return 0; } displayCellInfo(source, cell, serverLevel); return 1; } /** * /tiedup cell delete * * Delete the selected cell (from wand). */ private static int deleteSelected( CommandContext context ) { CommandSourceStack source = context.getSource(); // Must be a player if (!(source.getEntity() instanceof ServerPlayer player)) { source.sendFailure(Component.translatable("command.tiedup.error.must_be_player")); return 0; } ServerLevel serverLevel = player.serverLevel(); CellRegistryV2 registry = CellRegistryV2.get(serverLevel); UUID selectedCellId = getSelectedCellFromWand(player); if (selectedCellId == null) { source.sendFailure( Component.translatable("command.tiedup.cell.no_selection") ); return 0; } CellDataV2 cell = registry.getCell(selectedCellId); if (cell == null) { source.sendFailure( Component.translatable("command.tiedup.cell.no_longer_exists") ); return 0; } String cellName = cell.getName() != null ? cell.getName() : cell.getId().toString().substring(0, 8) + "..."; // Remove cell from registry registry.removeCell(selectedCellId); // Clear selection from wand ItemStack mainHand = player.getMainHandItem(); if (mainHand.getItem() instanceof ItemAdminWand) { ItemAdminWand.setActiveCellId(mainHand, null); } ItemStack offHand = player.getOffhandItem(); if (offHand.getItem() instanceof ItemAdminWand) { ItemAdminWand.setActiveCellId(offHand, null); } source.sendSuccess( () -> Component.translatable("command.tiedup.cell.deleted", cellName).withStyle(ChatFormatting.GREEN), true ); return 1; } /** * /tiedup cell resetspawns [radius] * * Reset the hasSpawned flag on all spawn markers within radius. * Use this before saving a structure to ensure NPCs will spawn when placed. * * Performance: Iterates over loaded chunks and their BlockEntities instead of * individual block positions to avoid server crash on large radii. */ private static int resetSpawns( CommandContext context, int radius ) { CommandSourceStack source = context.getSource(); ServerLevel serverLevel = source.getLevel(); BlockPos center = BlockPos.containing(source.getPosition()); int resetCount = 0; int spawnMarkerCount = 0; // Convert block radius to chunk radius (16 blocks per chunk) int chunkRadius = (radius + 15) >> 4; int centerChunkX = center.getX() >> 4; int centerChunkZ = center.getZ() >> 4; // Iterate over chunks instead of individual blocks (much faster) for ( int cx = centerChunkX - chunkRadius; cx <= centerChunkX + chunkRadius; cx++ ) { for ( int cz = centerChunkZ - chunkRadius; cz <= centerChunkZ + chunkRadius; cz++ ) { // Only process loaded chunks net.minecraft.world.level.chunk.LevelChunk chunk = serverLevel .getChunkSource() .getChunkNow(cx, cz); if (chunk == null) continue; // Iterate over all BlockEntities in this chunk for (BlockEntity be : chunk.getBlockEntities().values()) { if (!(be instanceof MarkerBlockEntity marker)) continue; // Check if within radius (squared distance for performance) BlockPos pos = be.getBlockPos(); double distSq = center.distSqr(pos); if (distSq > (double) radius * radius) continue; if (marker.isSpawnMarker()) { spawnMarkerCount++; if (marker.hasSpawned()) { marker.resetHasSpawned(); resetCount++; } } } } } final int finalResetCount = resetCount; final int finalSpawnMarkerCount = spawnMarkerCount; source.sendSuccess( () -> Component.translatable( "command.tiedup.cell.reset_spawns", finalResetCount, finalSpawnMarkerCount, radius ).withStyle(ChatFormatting.GREEN), true ); if (resetCount > 0) { source.sendSuccess( () -> Component.translatable("command.tiedup.cell.reset_spawns_hint").withStyle(ChatFormatting.GRAY), false ); } return 1; } // ==================== HELPERS ==================== /** * Get the selected cell ID from the player's admin wand. */ private static UUID getSelectedCellFromWand(ServerPlayer player) { // Check main hand ItemStack mainHand = player.getMainHandItem(); if (mainHand.getItem() instanceof ItemAdminWand) { UUID cellId = ItemAdminWand.getActiveCellId(mainHand); if (cellId != null) return cellId; } // Check offhand ItemStack offHand = player.getOffhandItem(); if (offHand.getItem() instanceof ItemAdminWand) { return ItemAdminWand.getActiveCellId(offHand); } return null; } /** * Format cell info for list display. */ private static String formatCellInfo(CellDataV2 cell, ServerLevel level) { StringBuilder sb = new StringBuilder(); // Name or ID if (cell.getName() != null) { sb.append(" ").append(cell.getName()); } else { sb .append(" ") .append(cell.getId().toString().substring(0, 8)) .append("..."); } // Position sb.append(" @ ").append(cell.getCorePos().toShortString()); // Owner if (cell.hasOwner()) { ServerPlayer owner = level .getServer() .getPlayerList() .getPlayer(cell.getOwnerId()); if (owner != null) { sb.append(" (").append(owner.getName().getString()).append(")"); } else { sb.append(" (offline)"); } } else { sb.append(" (world)"); } // Prisoners if (cell.isOccupied()) { sb .append(" [") .append(cell.getPrisonerCount()) .append(" prisoners]"); } return sb.toString(); } /** * Display detailed cell info (V2). */ private static void displayCellInfo( CommandSourceStack source, CellDataV2 cell, ServerLevel level ) { String nameDisplay = cell.getName() != null ? cell.getName() : "(unnamed)"; source.sendSuccess( () -> Component.translatable("command.tiedup.cell.info_header", nameDisplay).withStyle(ChatFormatting.GOLD), false ); source.sendSuccess( () -> Component.translatable("command.tiedup.cell.info_id", cell.getId().toString()).withStyle(ChatFormatting.GRAY), false ); source.sendSuccess( () -> Component.translatable("command.tiedup.cell.info_state", cell.getState().toString()).withStyle(ChatFormatting.GRAY), false ); source.sendSuccess( () -> Component.translatable( "command.tiedup.cell.info_core_pos", cell.getCorePos().toShortString() ).withStyle(ChatFormatting.GRAY), false ); // Spawn point (may be null in V2) if (cell.getSpawnPoint() != null) { source.sendSuccess( () -> Component.translatable( "command.tiedup.cell.info_spawn_point", cell.getSpawnPoint().toShortString() ).withStyle(ChatFormatting.GRAY), false ); } // Owner info if (cell.hasOwner()) { ServerPlayer owner = level .getServer() .getPlayerList() .getPlayer(cell.getOwnerId()); String ownerName = owner != null ? owner.getName().getString() : "(offline)"; source.sendSuccess( () -> Component.translatable( "command.tiedup.cell.info_owner", ownerName, cell.getOwnerId().toString().substring(0, 8) + "..." ).withStyle(ChatFormatting.GRAY), false ); } else { source.sendSuccess( () -> Component.translatable("command.tiedup.cell.info_owner_world").withStyle(ChatFormatting.GRAY), false ); } // Geometry source.sendSuccess( () -> Component.translatable( "command.tiedup.cell.info_interior", cell.getInteriorBlocks().size() ).withStyle(ChatFormatting.GRAY), false ); source.sendSuccess( () -> Component.translatable( "command.tiedup.cell.info_walls", cell.getWallBlocks().size() ).withStyle(ChatFormatting.GRAY), false ); // Breach info if (!cell.getBreachedPositions().isEmpty()) { source.sendSuccess( () -> Component.translatable( "command.tiedup.cell.info_breaches", cell.getBreachedPositions().size(), String.format("%.1f", cell.getBreachPercentage() * 100) ).withStyle(ChatFormatting.RED), false ); } // Features if (!cell.getBeds().isEmpty()) { source.sendSuccess( () -> Component.translatable("command.tiedup.cell.info_beds", cell.getBeds().size()).withStyle(ChatFormatting.GRAY), false ); } if (!cell.getAnchors().isEmpty()) { source.sendSuccess( () -> Component.translatable("command.tiedup.cell.info_anchors", cell.getAnchors().size()).withStyle(ChatFormatting.GRAY), false ); } if (!cell.getDoors().isEmpty()) { source.sendSuccess( () -> Component.translatable("command.tiedup.cell.info_doors", cell.getDoors().size()).withStyle(ChatFormatting.GRAY), false ); } // Prisoners source.sendSuccess( () -> Component.translatable( "command.tiedup.cell.info_prisoners", cell.getPrisonerCount() ).withStyle(ChatFormatting.GRAY), false ); for (UUID prisonerId : cell.getPrisonerIds()) { ServerPlayer prisoner = level .getServer() .getPlayerList() .getPlayer(prisonerId); String prisonerName = prisoner != null ? prisoner.getName().getString() : "(offline)"; source.sendSuccess( () -> Component.literal(" - " + prisonerName).withStyle(ChatFormatting.GRAY), false ); } } }