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.
This commit is contained in:
NotEvil
2026-04-12 00:51:22 +02:00
parent 2e7a1d403b
commit f6466360b6
1947 changed files with 238025 additions and 1 deletions

View File

@@ -0,0 +1,176 @@
package com.tiedup.remake.commands;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.bounty.Bounty;
import com.tiedup.remake.bounty.BountyManager;
import com.tiedup.remake.core.TiedUpMod;
import com.tiedup.remake.state.IBondageState;
import com.tiedup.remake.util.KidnappedHelper;
import com.tiedup.remake.core.SettingsAccessor;
import java.util.Optional;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
/**
* Command: /bounty <player>
*
* Phase 17: Bounty System
*
* Creates a bounty on a target player using the held item as reward.
*
* Requirements:
* - Must hold an item (the reward)
* - Cannot put bounty on yourself
* - Respects max bounties limit
* - Cannot be tied up when creating bounty
*/
public class BountyCommand {
public static void register(
CommandDispatcher<CommandSourceStack> dispatcher
) {
dispatcher.register(createBountyCommand());
TiedUpMod.LOGGER.info("Registered /bounty command");
}
/**
* Create the bounty command builder (for use as subcommand of /tiedup).
* @return The command builder
*/
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createBountyCommand() {
return Commands.literal("bounty").then(
Commands.argument("target", EntityArgument.player()).executes(
BountyCommand::execute
)
);
}
private static int execute(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
// Must be a player
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
ServerPlayer target = EntityArgument.getPlayer(context, "target");
// Cannot bounty yourself
if (player.getUUID().equals(target.getUUID())) {
source.sendFailure(
Component.literal(
"You cannot put a bounty on yourself!"
).withStyle(ChatFormatting.RED)
);
return 0;
}
// Check if player is tied up
IBondageState playerState = KidnappedHelper.getKidnappedState(player);
if (playerState != null && playerState.isTiedUp()) {
source.sendFailure(
Component.literal(
"You cannot create bounties while tied up!"
).withStyle(ChatFormatting.RED)
);
return 0;
}
// Get bounty manager
BountyManager manager = BountyManager.get(player.serverLevel());
// Check bounty limit
if (!manager.canCreateBounty(player, player.serverLevel())) {
int max = SettingsAccessor.getMaxBounties(
player.serverLevel().getGameRules()
);
source.sendFailure(
Component.literal(
"Maximum number (" + max + ") of active bounties reached!"
).withStyle(ChatFormatting.RED)
);
return 0;
}
// Must hold an item as reward
ItemStack heldItem = player.getMainHandItem();
if (heldItem.isEmpty()) {
source.sendFailure(
Component.literal(
"You must hold an item as the reward!"
).withStyle(ChatFormatting.RED)
);
return 0;
}
// Get bounty duration
int duration = SettingsAccessor.getBountyDuration(
player.serverLevel().getGameRules()
);
// SECURITY FIX: Create reward with count=1 to prevent item duplication
// Bug: If player held 64 diamonds, bounty would be 64 diamonds but only cost 1
ItemStack rewardItem = heldItem.copy();
rewardItem.setCount(1);
// Create the bounty
Bounty bounty = new Bounty(
player.getUUID(),
player.getName().getString(),
target.getUUID(),
target.getName().getString(),
rewardItem,
duration
);
// Consume one item from the stack (not the entire stack!)
player.getMainHandItem().shrink(1);
// Add bounty
manager.addBounty(bounty);
// Notify player
source.sendSuccess(
() ->
Component.literal(
"Bounty created on " + target.getName().getString() + "!"
).withStyle(ChatFormatting.GREEN),
false
);
// Broadcast to all players
player.server
.getPlayerList()
.broadcastSystemMessage(
Component.literal(
"[Bounty] " +
player.getName().getString() +
" has put a bounty on " +
target.getName().getString() +
"!"
).withStyle(ChatFormatting.GOLD),
false
);
TiedUpMod.LOGGER.info(
"[BOUNTY] {} created bounty on {} with reward {}",
player.getName().getString(),
target.getName().getString(),
bounty.getRewardDescription()
);
return 1;
}
}

View File

@@ -0,0 +1,332 @@
package com.tiedup.remake.commands;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.tiedup.remake.cells.CampOwnership;
import com.tiedup.remake.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.PrisonerRecord;
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.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
/**
* Debug commands for the unified captivity system.
*
* Commands:
* /tiedup debug prisoner <player> - Show captivity state for a player
* /tiedup debug validate - Validate captivity system consistency
* /tiedup debug repair - Repair inconsistencies (WARNING: modifies data)
* /tiedup debug camp <campIdPrefix> - Show camp info and indexed cells
*/
public class CaptivityDebugCommand {
/**
* Create the /tiedup debug command tree.
*/
public static LiteralArgumentBuilder<
CommandSourceStack
> createDebugCommand() {
return Commands.literal("debug")
.requires(CommandHelper.REQUIRES_OP) // Admin only
// /tiedup debug prisoner <player>
.then(
Commands.literal("prisoner").then(
Commands.argument(
"player",
EntityArgument.player()
).executes(CaptivityDebugCommand::debugPrisoner)
)
)
// /tiedup debug validate
.then(
Commands.literal("validate").executes(
CaptivityDebugCommand::validateSystem
)
)
// /tiedup debug repair
.then(
Commands.literal("repair").executes(
CaptivityDebugCommand::repairSystem
)
)
// /tiedup debug camp <campIdPrefix>
.then(
Commands.literal("camp").then(
Commands.argument(
"campIdPrefix",
StringArgumentType.string()
).executes(CaptivityDebugCommand::debugCamp)
)
);
}
/**
* Show captivity state for a player.
* /tiedup debug prisoner <player>
*/
private static int debugPrisoner(CommandContext<CommandSourceStack> ctx) {
try {
ServerPlayer target = EntityArgument.getPlayer(ctx, "player");
ServerLevel level = target.serverLevel();
PrisonerManager manager = PrisonerManager.get(level);
PrisonerRecord record = manager.getRecord(target.getUUID());
StringBuilder debugInfo = new StringBuilder();
debugInfo
.append("Player: ")
.append(target.getName().getString())
.append("\n");
debugInfo.append("State: ").append(record.getState()).append("\n");
debugInfo
.append("Camp ID: ")
.append(record.getCampId())
.append("\n");
debugInfo
.append("Cell ID: ")
.append(record.getCellId())
.append("\n");
debugInfo
.append("Captor ID: ")
.append(record.getCaptorId())
.append("\n");
debugInfo
.append("Protection Expiry: ")
.append(record.getProtectionExpiry())
.append("\n");
debugInfo
.append("Is Protected: ")
.append(record.isProtected(level.getGameTime()))
.append("\n");
debugInfo
.append("Is Captive: ")
.append(record.isCaptive())
.append("\n");
// Send debug info to command executor
ctx
.getSource()
.sendSuccess(
() ->
Component.literal(
"=== Captivity Debug Info ===\n" + debugInfo
).withStyle(ChatFormatting.YELLOW),
false
);
return 1; // Success
} catch (Exception e) {
ctx
.getSource()
.sendFailure(
Component.literal("Error: " + e.getMessage()).withStyle(
ChatFormatting.RED
)
);
return 0; // Failure
}
}
/**
* Validate captivity system consistency.
* /tiedup debug validate
* NOTE: CaptivitySystemValidator was removed. This now shows a summary of the prison system.
*/
private static int validateSystem(CommandContext<CommandSourceStack> ctx) {
try {
ServerLevel level = ctx.getSource().getLevel();
ctx
.getSource()
.sendSuccess(
() ->
Component.literal(
"Checking captivity system..."
).withStyle(ChatFormatting.YELLOW),
true
);
// Show PrisonerManager stats instead
PrisonerManager manager = PrisonerManager.get(level);
String debugInfo = manager.toDebugString();
ctx
.getSource()
.sendSuccess(
() ->
Component.literal(debugInfo).withStyle(
ChatFormatting.GREEN
),
true
);
return 1; // Success
} catch (Exception e) {
ctx
.getSource()
.sendFailure(
Component.literal(
"Error during validation: " + e.getMessage()
).withStyle(ChatFormatting.RED)
);
return 0; // Failure
}
}
/**
* Repair captivity system inconsistencies.
* /tiedup debug repair
* NOTE: CaptivitySystemValidator was removed. This command is now a placeholder.
*/
private static int repairSystem(CommandContext<CommandSourceStack> ctx) {
try {
ctx
.getSource()
.sendSuccess(
() ->
Component.literal(
"Repair functionality has been simplified with the new PrisonerManager system."
).withStyle(ChatFormatting.YELLOW),
true
);
ctx
.getSource()
.sendSuccess(
() ->
Component.literal(
"The new system maintains consistency automatically."
).withStyle(ChatFormatting.GREEN),
true
);
return 1; // Success
} catch (Exception e) {
ctx
.getSource()
.sendFailure(
Component.literal("Error: " + e.getMessage()).withStyle(
ChatFormatting.RED
)
);
return 0; // Failure
}
}
/**
* Debug camp information and indexed cells.
* /tiedup debug camp <campIdPrefix>
*/
private static int debugCamp(CommandContext<CommandSourceStack> ctx) {
try {
ServerLevel level = ctx.getSource().getLevel();
String campIdPrefix = StringArgumentType.getString(
ctx,
"campIdPrefix"
);
CampOwnership ownership = CampOwnership.get(level);
CellRegistryV2 cellRegistry = CellRegistryV2.get(level);
// Find camps matching prefix
UUID matchingCamp = null;
for (CampOwnership.CampData camp : ownership.getAllCamps()) {
String campIdStr = camp.getCampId().toString();
if (campIdStr.startsWith(campIdPrefix)) {
matchingCamp = camp.getCampId();
break;
}
}
if (matchingCamp == null) {
ctx
.getSource()
.sendFailure(
Component.literal(
"No camp found with ID prefix: " + campIdPrefix
).withStyle(ChatFormatting.RED)
);
return 0;
}
// Get camp info
CampOwnership.CampData campData = ownership.getCamp(matchingCamp);
StringBuilder sb = new StringBuilder();
sb.append(
String.format(
"=== Camp %s ===\n",
matchingCamp.toString().substring(0, 8)
)
);
sb.append(String.format("Trader: %s\n", campData.getTraderUUID()));
sb.append(String.format("Maid: %s\n", campData.getMaidUUID()));
sb.append(String.format("Alive: %s\n", campData.isAlive()));
sb.append(String.format("Center: %s\n", campData.getCenter()));
// Get indexed cells
List<CellDataV2> cells = cellRegistry.getCellsByCamp(matchingCamp);
sb.append(
String.format("\n=== Indexed Cells (%d) ===\n", cells.size())
);
if (cells.isEmpty()) {
sb.append("⚠ NO CELLS INDEXED for this camp!\n");
} else {
for (CellDataV2 cell : cells.subList(
0,
Math.min(cells.size(), 10)
)) {
sb.append(
String.format(
"- Cell %s at %s (type=%s, owner=%s, interior=%d, walls=%d)\n",
cell.getId().toString().substring(0, 8),
cell.getCorePos().toShortString(),
cell.isCampOwned() ? "CAMP" : "PLAYER",
cell.getOwnerId() != null
? cell.getOwnerId().toString().substring(0, 8)
: "null",
cell.getInteriorBlocks().size(),
cell.getWallBlocks().size()
)
);
}
if (cells.size() > 10) {
sb.append(
String.format("... and %d more\n", cells.size() - 10)
);
}
}
final String campInfo = sb.toString();
ctx
.getSource()
.sendSuccess(
() ->
Component.literal(campInfo).withStyle(
ChatFormatting.YELLOW
),
false
);
return 1; // Success
} catch (Exception e) {
ctx
.getSource()
.sendFailure(
Component.literal("Error: " + e.getMessage()).withStyle(
ChatFormatting.RED
)
);
return 0; // Failure
}
}
}

View File

@@ -0,0 +1,678 @@
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.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> - 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 <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>
*
* Name the selected cell (from wand) and link it to the executing player.
*/
private static int setName(CommandContext<CommandSourceStack> 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.literal("Must be a 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.literal(
"No cell selected. Use the Admin Wand on a Cell Core first."
)
);
return 0;
}
CellDataV2 cell = registry.getCell(selectedCellId);
if (cell == null) {
source.sendFailure(
Component.literal("Selected cell no longer exists")
);
return 0;
}
// Check name uniqueness
CellDataV2 existingCell = registry.getCellByName(name);
if (
existingCell != null && !existingCell.getId().equals(selectedCellId)
) {
source.sendFailure(
Component.literal("Cell name '" + name + "' already exists")
);
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.literal(
"Named cell '" + name + "' and linked to you"
),
true
);
return 1;
}
/**
* /tiedup cell list
*
* List all registered cells.
*/
private static int listAll(CommandContext<CommandSourceStack> context) {
CommandSourceStack source = context.getSource();
ServerLevel serverLevel = source.getLevel();
CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
Collection<CellDataV2> cells = registry.getAllCells();
if (cells.isEmpty()) {
source.sendSuccess(
() -> Component.literal("No cells registered"),
false
);
return 1;
}
source.sendSuccess(
() -> Component.literal("=== Cells (" + cells.size() + ") ==="),
false
);
for (CellDataV2 cell : cells) {
String info = formatCellInfo(cell, serverLevel);
source.sendSuccess(() -> Component.literal(info), false);
}
return 1;
}
/**
* /tiedup cell list <owner>
*
* List cells owned by a specific player.
*/
private static int listByOwner(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
ServerPlayer owner = EntityArgument.getPlayer(context, "owner");
ServerLevel serverLevel = source.getLevel();
CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
List<CellDataV2> cells = registry.getCellsByOwner(owner.getUUID());
if (cells.isEmpty()) {
source.sendSuccess(
() ->
Component.literal(
owner.getName().getString() + " has no cells"
),
false
);
return 1;
}
source.sendSuccess(
() ->
Component.literal(
"=== Cells owned by " +
owner.getName().getString() +
" (" +
cells.size() +
") ==="
),
false
);
for (CellDataV2 cell : cells) {
String info = formatCellInfo(cell, serverLevel);
source.sendSuccess(() -> Component.literal(info), false);
}
return 1;
}
/**
* /tiedup cell info
*
* Show info about the selected cell (from wand).
*/
private static int infoSelected(
CommandContext<CommandSourceStack> context
) {
CommandSourceStack source = context.getSource();
// Must be a player
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Must be a player"));
return 0;
}
ServerLevel serverLevel = player.serverLevel();
CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
UUID selectedCellId = getSelectedCellFromWand(player);
if (selectedCellId == null) {
source.sendFailure(
Component.literal(
"No cell selected. Use the Admin Wand on a Cell Core first."
)
);
return 0;
}
CellDataV2 cell = registry.getCell(selectedCellId);
if (cell == null) {
source.sendFailure(
Component.literal("Selected cell no longer exists")
);
return 0;
}
displayCellInfo(source, cell, serverLevel);
return 1;
}
/**
* /tiedup cell info <name>
*
* Show info about a cell by name.
*/
private static int infoByName(CommandContext<CommandSourceStack> 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.literal("Cell '" + name + "' not found")
);
return 0;
}
displayCellInfo(source, cell, serverLevel);
return 1;
}
/**
* /tiedup cell delete
*
* Delete the selected cell (from wand).
*/
private static int deleteSelected(
CommandContext<CommandSourceStack> context
) {
CommandSourceStack source = context.getSource();
// Must be a player
if (!(source.getEntity() instanceof ServerPlayer player)) {
source.sendFailure(Component.literal("Must be a player"));
return 0;
}
ServerLevel serverLevel = player.serverLevel();
CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
UUID selectedCellId = getSelectedCellFromWand(player);
if (selectedCellId == null) {
source.sendFailure(
Component.literal(
"No cell selected. Use the Admin Wand on a Cell Core first."
)
);
return 0;
}
CellDataV2 cell = registry.getCell(selectedCellId);
if (cell == null) {
source.sendFailure(
Component.literal("Selected 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.literal("Deleted cell '" + cellName + "'"),
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<CommandSourceStack> 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.literal(
"Reset " +
finalResetCount +
" spawn markers (found " +
finalSpawnMarkerCount +
" total spawn markers in " +
radius +
" block radius)"
),
true
);
if (resetCount > 0) {
source.sendSuccess(
() ->
Component.literal(
"You can now save the structure - NPCs will spawn when it's placed."
),
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.literal("=== Cell: " + nameDisplay + " ==="),
false
);
source.sendSuccess(
() -> Component.literal("ID: " + cell.getId().toString()),
false
);
source.sendSuccess(
() -> Component.literal("State: " + cell.getState()),
false
);
source.sendSuccess(
() ->
Component.literal(
"Core Position: " + cell.getCorePos().toShortString()
),
false
);
// Spawn point (may be null in V2)
if (cell.getSpawnPoint() != null) {
source.sendSuccess(
() ->
Component.literal(
"Spawn Point: " + cell.getSpawnPoint().toShortString()
),
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.literal(
"Owner: " +
ownerName +
" (" +
cell.getOwnerId().toString().substring(0, 8) +
"...)"
),
false
);
} else {
source.sendSuccess(
() -> Component.literal("Owner: (world-generated)"),
false
);
}
// Geometry
source.sendSuccess(
() ->
Component.literal(
"Interior blocks: " + cell.getInteriorBlocks().size()
),
false
);
source.sendSuccess(
() ->
Component.literal(
"Wall blocks: " + cell.getWallBlocks().size()
),
false
);
// Breach info
if (!cell.getBreachedPositions().isEmpty()) {
source.sendSuccess(
() ->
Component.literal(
"Breaches: " +
cell.getBreachedPositions().size() +
" (" +
String.format(
"%.1f",
cell.getBreachPercentage() * 100
) +
"%)"
),
false
);
}
// Features
if (!cell.getBeds().isEmpty()) {
source.sendSuccess(
() -> Component.literal("Beds: " + cell.getBeds().size()),
false
);
}
if (!cell.getAnchors().isEmpty()) {
source.sendSuccess(
() -> Component.literal("Anchors: " + cell.getAnchors().size()),
false
);
}
if (!cell.getDoors().isEmpty()) {
source.sendSuccess(
() -> Component.literal("Doors: " + cell.getDoors().size()),
false
);
}
// Prisoners
source.sendSuccess(
() ->
Component.literal(
"Prisoners: " + cell.getPrisonerCount() + "/4"
),
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),
false
);
}
}
}

View File

@@ -0,0 +1,311 @@
package com.tiedup.remake.commands;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.items.clothes.GenericClothes;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
/**
* Command handler for clothes configuration.
*
* Subcommands:
* /tiedup clothes url set <url> - Set dynamic texture URL on held clothes
* /tiedup clothes url reset - Remove dynamic texture URL
* /tiedup clothes fullskin - Toggle full-skin mode
* /tiedup clothes smallarms - Toggle small arms forcing
* /tiedup clothes keephead - Toggle keep head mode (preserves wearer's head)
* /tiedup clothes layer <part> - Toggle layer visibility
*
* All commands operate on the clothes item held in main hand.
*/
public class ClothesCommand {
/**
* Create the /tiedup clothes ... command tree.
*
* @return The command builder
*/
public static LiteralArgumentBuilder<
CommandSourceStack
> createClothesCommand() {
return Commands.literal("clothes")
// /tiedup clothes url set <url>
// /tiedup clothes url reset
.then(
Commands.literal("url")
.then(
Commands.literal("set").then(
Commands.argument(
"url",
StringArgumentType.greedyString()
).executes(ClothesCommand::setUrl)
)
)
.then(
Commands.literal("reset").executes(
ClothesCommand::resetUrl
)
)
)
// /tiedup clothes fullskin
.then(
Commands.literal("fullskin").executes(
ClothesCommand::toggleFullSkin
)
)
// /tiedup clothes smallarms
.then(
Commands.literal("smallarms").executes(
ClothesCommand::toggleSmallArms
)
)
// /tiedup clothes keephead
.then(
Commands.literal("keephead").executes(
ClothesCommand::toggleKeepHead
)
)
// /tiedup clothes layer <part>
.then(
Commands.literal("layer").then(
Commands.argument("part", StringArgumentType.word())
.suggests((ctx, builder) -> {
builder.suggest("head");
builder.suggest("body");
builder.suggest("leftarm");
builder.suggest("rightarm");
builder.suggest("leftleg");
builder.suggest("rightleg");
return builder.buildFuture();
})
.executes(ClothesCommand::toggleLayer)
)
);
}
/**
* Get the clothes ItemStack from player's main hand.
* Returns null and sends error message if not holding clothes.
*/
private static ItemStack getHeldClothes(
CommandContext<CommandSourceStack> ctx
) throws CommandSyntaxException {
ServerPlayer player = ctx.getSource().getPlayerOrException();
ItemStack held = player.getMainHandItem();
if (held.isEmpty() || !(held.getItem() instanceof GenericClothes)) {
ctx
.getSource()
.sendFailure(
Component.translatable("command.tiedup.clothes.not_holding")
);
return null;
}
return held;
}
/**
* /tiedup clothes url set <url>
*/
private static int setUrl(CommandContext<CommandSourceStack> ctx)
throws CommandSyntaxException {
ItemStack clothes = getHeldClothes(ctx);
if (clothes == null) return 0;
String url = StringArgumentType.getString(ctx, "url");
// Basic URL validation
if (!url.startsWith("https://")) {
ctx
.getSource()
.sendFailure(
Component.translatable(
"command.tiedup.clothes.url_must_https"
)
);
return 0;
}
// Check URL length
if (url.length() > 2048) {
ctx
.getSource()
.sendFailure(
Component.translatable(
"command.tiedup.clothes.url_too_long"
)
);
return 0;
}
GenericClothes item = (GenericClothes) clothes.getItem();
item.setDynamicTextureUrl(clothes, url);
ctx
.getSource()
.sendSuccess(
() -> Component.translatable("command.tiedup.clothes.url_set"),
false
);
return 1;
}
/**
* /tiedup clothes url reset
*/
private static int resetUrl(CommandContext<CommandSourceStack> ctx)
throws CommandSyntaxException {
ItemStack clothes = getHeldClothes(ctx);
if (clothes == null) return 0;
GenericClothes item = (GenericClothes) clothes.getItem();
item.removeDynamicTextureUrl(clothes);
ctx
.getSource()
.sendSuccess(
() ->
Component.translatable("command.tiedup.clothes.url_reset"),
false
);
return 1;
}
/**
* /tiedup clothes fullskin
*/
private static int toggleFullSkin(CommandContext<CommandSourceStack> ctx)
throws CommandSyntaxException {
ItemStack clothes = getHeldClothes(ctx);
if (clothes == null) return 0;
GenericClothes item = (GenericClothes) clothes.getItem();
boolean newState = !item.isFullSkinEnabled(clothes);
item.setFullSkinEnabled(clothes, newState);
String stateKey = newState ? "enabled" : "disabled";
ctx
.getSource()
.sendSuccess(
() ->
Component.translatable(
"command.tiedup.clothes.fullskin_" + stateKey
),
false
);
return 1;
}
/**
* /tiedup clothes smallarms
*/
private static int toggleSmallArms(CommandContext<CommandSourceStack> ctx)
throws CommandSyntaxException {
ItemStack clothes = getHeldClothes(ctx);
if (clothes == null) return 0;
GenericClothes item = (GenericClothes) clothes.getItem();
boolean newState = !item.shouldForceSmallArms(clothes);
item.setForceSmallArms(clothes, newState);
String stateKey = newState ? "enabled" : "disabled";
ctx
.getSource()
.sendSuccess(
() ->
Component.translatable(
"command.tiedup.clothes.smallarms_" + stateKey
),
false
);
return 1;
}
/**
* /tiedup clothes keephead
* When enabled, the wearer's head/hat is preserved instead of being replaced by clothes.
*/
private static int toggleKeepHead(CommandContext<CommandSourceStack> ctx)
throws CommandSyntaxException {
ItemStack clothes = getHeldClothes(ctx);
if (clothes == null) return 0;
GenericClothes item = (GenericClothes) clothes.getItem();
boolean newState = !item.isKeepHeadEnabled(clothes);
item.setKeepHeadEnabled(clothes, newState);
String stateKey = newState ? "enabled" : "disabled";
ctx
.getSource()
.sendSuccess(
() ->
Component.translatable(
"command.tiedup.clothes.keephead_" + stateKey
),
false
);
return 1;
}
/**
* /tiedup clothes layer <part>
*/
private static int toggleLayer(CommandContext<CommandSourceStack> ctx)
throws CommandSyntaxException {
ItemStack clothes = getHeldClothes(ctx);
if (clothes == null) return 0;
String part = StringArgumentType.getString(ctx, "part").toLowerCase();
String layerKey = mapPartToLayerKey(part);
if (layerKey == null) {
ctx
.getSource()
.sendFailure(
Component.translatable(
"command.tiedup.clothes.unknown_layer",
part
)
);
return 0;
}
GenericClothes item = (GenericClothes) clothes.getItem();
boolean newState = !item.isLayerEnabled(clothes, layerKey);
item.setLayerEnabled(clothes, layerKey, newState);
String stateKey = newState ? "visible" : "hidden";
ctx
.getSource()
.sendSuccess(
() ->
Component.translatable(
"command.tiedup.clothes.layer_" + stateKey,
part
),
false
);
return 1;
}
/**
* Map user-friendly part name to NBT layer key.
*/
private static String mapPartToLayerKey(String part) {
return switch (part) {
case "head" -> GenericClothes.LAYER_HEAD;
case "body" -> GenericClothes.LAYER_BODY;
case "leftarm" -> GenericClothes.LAYER_LEFT_ARM;
case "rightarm" -> GenericClothes.LAYER_RIGHT_ARM;
case "leftleg" -> GenericClothes.LAYER_LEFT_LEG;
case "rightleg" -> GenericClothes.LAYER_RIGHT_LEG;
default -> null;
};
}
}

View File

@@ -0,0 +1,526 @@
package com.tiedup.remake.commands;
import com.mojang.brigadier.CommandDispatcher;
import com.tiedup.remake.v2.BodyRegionV2;
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.cells.CellDataV2;
import com.tiedup.remake.cells.CellRegistryV2;
import com.tiedup.remake.items.base.ItemCollar;
import com.tiedup.remake.state.PlayerBindState;
import com.tiedup.remake.util.teleport.Position;
import com.tiedup.remake.util.teleport.TeleportHelper;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
/**
* Collar management commands for Phase 18.
*
* Commands:
* /collar claim <player> - Claim ownership of a player's collar
* /collar unclaim <player> - Remove your ownership
* /collar rename <player> <name> - Set alias for collar
* /collar addowner <target> <owner> - Add another owner
* /collar removeowner <target> <owner> - Remove an owner
* /collar sethome <player> - Set home location
* /collar setprison <player> - Set prison location (legacy)
* /collar setcell <player> <cellname> - Assign cell to collar (preferred)
* /collar prisonradius <player> <radius> - Set prison fence radius (legacy)
* /collar prisonfence <player> [on/off] - Toggle prison fence (legacy)
* /collar backhome <player> - Teleport slave back home
* /collar info <player> - Show collar info
*
* Requires OP level 2.
*/
public class CollarCommand {
public static void register(
CommandDispatcher<CommandSourceStack> dispatcher
) {
dispatcher.register(createCollarCommand());
}
/**
* Create the collar command builder (for use as subcommand of /tiedup).
* @return The command builder
*/
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createCollarCommand() {
return Commands.literal("collar")
.requires(CommandHelper.REQUIRES_OP)
// /collar claim <player>
.then(
Commands.literal("claim").then(
Commands.argument(
"player",
EntityArgument.player()
).executes(CollarCommand::claim)
)
)
// /collar unclaim <player>
.then(
Commands.literal("unclaim").then(
Commands.argument(
"player",
EntityArgument.player()
).executes(CollarCommand::unclaim)
)
)
// /collar rename <player> <name>
.then(
Commands.literal("rename").then(
Commands.argument("player", EntityArgument.player()).then(
Commands.argument(
"name",
StringArgumentType.greedyString()
).executes(CollarCommand::rename)
)
)
)
// /collar addowner <target> <owner>
.then(
Commands.literal("addowner").then(
Commands.argument("target", EntityArgument.player()).then(
Commands.argument(
"owner",
EntityArgument.player()
).executes(CollarCommand::addOwner)
)
)
)
// /collar removeowner <target> <owner>
.then(
Commands.literal("removeowner").then(
Commands.argument("target", EntityArgument.player()).then(
Commands.argument(
"owner",
EntityArgument.player()
).executes(CollarCommand::removeOwner)
)
)
)
// /collar setcell <player> <cellname> (assigns cell)
.then(
Commands.literal("setcell").then(
Commands.argument("player", EntityArgument.player()).then(
Commands.argument(
"cellname",
StringArgumentType.word()
).executes(CollarCommand::setCell)
)
)
)
// /collar tocell <player> (teleport to assigned cell)
.then(
Commands.literal("tocell").then(
Commands.argument(
"player",
EntityArgument.player()
).executes(CollarCommand::teleportToCell)
)
)
// /collar info <player>
.then(
Commands.literal("info").then(
Commands.argument(
"player",
EntityArgument.player()
).executes(CollarCommand::info)
)
);
}
private static ItemStack getPlayerCollar(ServerPlayer player) {
PlayerBindState state = PlayerBindState.getInstance(player);
if (state == null || !state.hasCollar()) return ItemStack.EMPTY;
return state.getEquipment(BodyRegionV2.NECK);
}
private static int claim(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
ServerPlayer target = EntityArgument.getPlayer(context, "player");
ItemStack collar = getPlayerCollar(target);
if (collar.isEmpty()) {
source.sendFailure(
Component.literal(
target.getName().getString() + " does not have a collar"
)
);
return 0;
}
if (source.getEntity() instanceof ServerPlayer executor) {
if (collar.getItem() instanceof ItemCollar collarItem) {
collarItem.addOwner(collar, executor);
source.sendSuccess(
() ->
Component.literal(
"§aClaimed " +
target.getName().getString() +
"'s collar"
),
true
);
return 1;
}
}
source.sendFailure(Component.literal("Failed to claim collar"));
return 0;
}
private static int unclaim(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
ServerPlayer target = EntityArgument.getPlayer(context, "player");
ItemStack collar = getPlayerCollar(target);
if (collar.isEmpty()) {
source.sendFailure(
Component.literal(
target.getName().getString() + " does not have a collar"
)
);
return 0;
}
if (source.getEntity() instanceof ServerPlayer executor) {
if (collar.getItem() instanceof ItemCollar collarItem) {
collarItem.removeOwner(collar, executor.getUUID());
source.sendSuccess(
() ->
Component.literal(
"§aRemoved your ownership from " +
target.getName().getString() +
"'s collar"
),
true
);
return 1;
}
}
source.sendFailure(Component.literal("Failed to unclaim collar"));
return 0;
}
private static int rename(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
ServerPlayer target = EntityArgument.getPlayer(context, "player");
String name = StringArgumentType.getString(context, "name");
ItemStack collar = getPlayerCollar(target);
if (collar.isEmpty()) {
source.sendFailure(
Component.literal(
target.getName().getString() + " does not have a collar"
)
);
return 0;
}
if (collar.getItem() instanceof ItemCollar collarItem) {
collarItem.setNickname(collar, name);
source.sendSuccess(
() ->
Component.literal(
"§aSet collar nickname to '" + name + "'"
),
true
);
return 1;
}
return 0;
}
private static int addOwner(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
ServerPlayer target = EntityArgument.getPlayer(context, "target");
ServerPlayer owner = EntityArgument.getPlayer(context, "owner");
ItemStack collar = getPlayerCollar(target);
if (collar.isEmpty()) {
source.sendFailure(
Component.literal(
target.getName().getString() + " does not have a collar"
)
);
return 0;
}
if (collar.getItem() instanceof ItemCollar collarItem) {
collarItem.addOwner(collar, owner);
source.sendSuccess(
() ->
Component.literal(
"§aAdded " +
owner.getName().getString() +
" as owner of " +
target.getName().getString() +
"'s collar"
),
true
);
return 1;
}
return 0;
}
private static int removeOwner(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
ServerPlayer target = EntityArgument.getPlayer(context, "target");
ServerPlayer owner = EntityArgument.getPlayer(context, "owner");
ItemStack collar = getPlayerCollar(target);
if (collar.isEmpty()) {
source.sendFailure(
Component.literal(
target.getName().getString() + " does not have a collar"
)
);
return 0;
}
if (collar.getItem() instanceof ItemCollar collarItem) {
collarItem.removeOwner(collar, owner.getUUID());
source.sendSuccess(
() ->
Component.literal(
"§aRemoved " +
owner.getName().getString() +
" as owner of " +
target.getName().getString() +
"'s collar"
),
true
);
return 1;
}
return 0;
}
/**
* /collar setcell <player> <cellname>
*
* Assign a named cell to a player's collar.
*/
private static int setCell(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
ServerPlayer target = EntityArgument.getPlayer(context, "player");
String cellName = StringArgumentType.getString(context, "cellname");
ItemStack collar = getPlayerCollar(target);
if (collar.isEmpty()) {
source.sendFailure(
Component.literal(
target.getName().getString() + " does not have a collar"
)
);
return 0;
}
// Get the cell by name
ServerLevel serverLevel = source.getLevel();
CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
CellDataV2 cell = registry.getCellByName(cellName);
if (cell == null) {
source.sendFailure(
Component.literal("Cell '" + cellName + "' not found")
);
return 0;
}
if (collar.getItem() instanceof ItemCollar collarItem) {
collarItem.setCellId(collar, cell.getId());
source.sendSuccess(
() ->
Component.literal(
"§aAssigned cell '" +
cellName +
"' to " +
target.getName().getString() +
"'s collar"
),
true
);
return 1;
}
return 0;
}
/**
* /collar tocell <player>
*
* Teleport player to their assigned cell.
*/
private static int teleportToCell(
CommandContext<CommandSourceStack> context
) throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
ServerPlayer target = EntityArgument.getPlayer(context, "player");
ItemStack collar = getPlayerCollar(target);
if (collar.isEmpty()) {
source.sendFailure(
Component.literal(
target.getName().getString() + " does not have a collar"
)
);
return 0;
}
if (collar.getItem() instanceof ItemCollar collarItem) {
if (!collarItem.hasCellAssigned(collar)) {
source.sendFailure(
Component.literal("No cell assigned to collar")
);
return 0;
}
// Get cell position and teleport
java.util.UUID cellId = collarItem.getCellId(collar);
ServerLevel serverLevel = source.getLevel();
CellDataV2 cell = CellRegistryV2.get(serverLevel).getCell(cellId);
if (cell == null) {
source.sendFailure(
Component.literal("Assigned cell no longer exists")
);
return 0;
}
net.minecraft.core.BlockPos teleportTarget =
cell.getSpawnPoint() != null
? cell.getSpawnPoint()
: cell.getCorePos().above();
Position cellPos = new Position(
teleportTarget,
serverLevel.dimension()
);
TeleportHelper.teleportEntity(target, cellPos);
source.sendSuccess(
() ->
Component.literal(
"§aTeleported " +
target.getName().getString() +
" to cell at " +
cell.getCorePos().toShortString()
),
true
);
return 1;
}
return 0;
}
private static int info(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
ServerPlayer target = EntityArgument.getPlayer(context, "player");
ItemStack collar = getPlayerCollar(target);
if (collar.isEmpty()) {
source.sendFailure(
Component.literal(
target.getName().getString() + " does not have a collar"
)
);
return 0;
}
if (collar.getItem() instanceof ItemCollar collarItem) {
source.sendSuccess(
() ->
Component.literal(
"§6=== Collar Info for " +
target.getName().getString() +
" ==="
),
false
);
String nickname = collarItem.getNickname(collar);
source.sendSuccess(
() ->
Component.literal(
"§7Nickname: §f" +
(nickname.isEmpty() ? "None" : nickname)
),
false
);
source.sendSuccess(
() ->
Component.literal(
"§7Has Owner: §f" + collarItem.hasOwner(collar)
),
false
);
// Cell assignment
java.util.UUID cellId = collarItem.getCellId(collar);
if (cellId != null) {
ServerLevel serverLevel = source.getLevel();
CellRegistryV2 registry = CellRegistryV2.get(serverLevel);
CellDataV2 cell = registry.getCell(cellId);
if (cell != null) {
String cellDisplay =
cell.getName() != null
? cell.getName()
: cellId.toString().substring(0, 8) + "...";
source.sendSuccess(
() ->
Component.literal(
"§7Assigned Cell: §a" +
cellDisplay +
" §7@ " +
cell.getCorePos().toShortString()
),
false
);
} else {
source.sendSuccess(
() -> Component.literal("§7Assigned Cell: §c(deleted)"),
false
);
}
} else {
source.sendSuccess(
() -> Component.literal("§7Assigned Cell: §fNone"),
false
);
}
source.sendSuccess(
() ->
Component.literal(
"§7Locked: §f" + collarItem.isLocked(collar)
),
false
);
return 1;
}
return 0;
}
}

View File

@@ -0,0 +1,91 @@
package com.tiedup.remake.commands;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.sync.PacketSyncBindState;
import com.tiedup.remake.state.PlayerBindState;
import java.util.Optional;
import java.util.function.Predicate;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
/**
* Utility methods for command handling.
*/
public final class CommandHelper {
/**
* Permission level 2 (OP) requirement for commands.
* Use with: .requires(CommandHelper.REQUIRES_OP)
*/
public static final Predicate<CommandSourceStack> REQUIRES_OP = source ->
source.hasPermission(2);
private CommandHelper() {}
/**
* Get the player from command source, or send failure message and return empty.
*
* Usage:
* <pre>
* var playerOpt = CommandHelper.getPlayerOrFail(source);
* if (playerOpt.isEmpty()) return 0;
* ServerPlayer player = playerOpt.get();
* </pre>
*
* @param source The command source
* @return Optional containing the player, or empty if source is not a player
*/
public static Optional<ServerPlayer> getPlayerOrFail(
CommandSourceStack source
) {
if (source.getEntity() instanceof ServerPlayer player) {
return Optional.of(player);
}
source.sendFailure(Component.literal("Must be a player"));
return Optional.empty();
}
/**
* Get the player from command source without sending failure message.
*
* @param source The command source
* @return Optional containing the player, or empty if source is not a player
*/
public static Optional<ServerPlayer> getPlayer(CommandSourceStack source) {
if (source.getEntity() instanceof ServerPlayer player) {
return Optional.of(player);
}
return Optional.empty();
}
/**
* Check if source is a player.
*
* @param source The command source
* @return true if source is a player
*/
public static boolean isPlayer(CommandSourceStack source) {
return source.getEntity() instanceof ServerPlayer;
}
/**
* Sync player state to client after command changes.
*
* @param player The player to sync
* @param state The player's bind state
*/
public static void syncPlayerState(
ServerPlayer player,
PlayerBindState state
) {
// Sync V2 equipment
com.tiedup.remake.v2.bondage.capability.V2EquipmentHelper.sync(player);
// Sync bind state
PacketSyncBindState statePacket = PacketSyncBindState.fromPlayer(player);
if (statePacket != null) {
ModNetwork.sendToPlayer(statePacket, player);
}
}
}

View File

@@ -0,0 +1,294 @@
package com.tiedup.remake.commands;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.items.ModItems;
import java.util.Optional;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
/**
* Key management commands for Phase 18.
*
* Commands:
* /key claim - Claim the key you're holding
* /key unclaim - Remove ownership from your key
* /key assign <player> - Assign your key to a player
* /key public - Make key publicly usable
* /key info - Show key info
*
* Must hold a collar key in main hand.
*/
public class KeyCommand {
private static final String TAG_OWNER = "Owner";
private static final String TAG_OWNER_NAME = "OwnerName";
private static final String TAG_TARGET = "Target";
private static final String TAG_TARGET_NAME = "TargetName";
private static final String TAG_PUBLIC = "Public";
public static void register(
CommandDispatcher<CommandSourceStack> dispatcher
) {
dispatcher.register(createKeyCommand());
}
/**
* Create the key command builder (for use as subcommand of /tiedup).
* @return The command builder
*/
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createKeyCommand() {
return Commands.literal("key")
// /key claim
.then(Commands.literal("claim").executes(KeyCommand::claim))
// /key unclaim
.then(Commands.literal("unclaim").executes(KeyCommand::unclaim))
// /key assign <player>
.then(
Commands.literal("assign").then(
Commands.argument(
"player",
EntityArgument.player()
).executes(KeyCommand::assign)
)
)
// /key public
.then(Commands.literal("public").executes(KeyCommand::togglePublic))
// /key info
.then(Commands.literal("info").executes(KeyCommand::info));
}
private static ItemStack getHeldKey(ServerPlayer player) {
ItemStack held = player.getMainHandItem();
if (
held.is(ModItems.COLLAR_KEY.get()) ||
held.is(ModItems.MASTER_KEY.get())
) {
return held;
}
return ItemStack.EMPTY;
}
private static int claim(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
ItemStack key = getHeldKey(player);
if (key.isEmpty()) {
source.sendFailure(Component.literal("You must hold a collar key"));
return 0;
}
CompoundTag tag = key.getOrCreateTag();
// Check if already claimed by someone else
if (
tag.hasUUID(TAG_OWNER) &&
!tag.getUUID(TAG_OWNER).equals(player.getUUID())
) {
source.sendFailure(
Component.literal("This key is already claimed by someone else")
);
return 0;
}
tag.putUUID(TAG_OWNER, player.getUUID());
tag.putString(TAG_OWNER_NAME, player.getName().getString());
source.sendSuccess(
() -> Component.literal("§aYou have claimed this key"),
false
);
return 1;
}
private static int unclaim(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
ItemStack key = getHeldKey(player);
if (key.isEmpty()) {
source.sendFailure(Component.literal("You must hold a collar key"));
return 0;
}
CompoundTag tag = key.getOrCreateTag();
if (!tag.hasUUID(TAG_OWNER)) {
source.sendFailure(Component.literal("This key is not claimed"));
return 0;
}
if (!tag.getUUID(TAG_OWNER).equals(player.getUUID())) {
source.sendFailure(Component.literal("You do not own this key"));
return 0;
}
tag.remove(TAG_OWNER);
tag.remove(TAG_OWNER_NAME);
source.sendSuccess(
() -> Component.literal("§aYou have unclaimed this key"),
false
);
return 1;
}
private static int assign(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
ServerPlayer target = EntityArgument.getPlayer(context, "player");
ItemStack key = getHeldKey(player);
if (key.isEmpty()) {
source.sendFailure(Component.literal("You must hold a collar key"));
return 0;
}
CompoundTag tag = key.getOrCreateTag();
// Must be owner to assign
if (
tag.hasUUID(TAG_OWNER) &&
!tag.getUUID(TAG_OWNER).equals(player.getUUID())
) {
source.sendFailure(Component.literal("You do not own this key"));
return 0;
}
tag.putUUID(TAG_TARGET, target.getUUID());
tag.putString(TAG_TARGET_NAME, target.getName().getString());
source.sendSuccess(
() ->
Component.literal(
"§aAssigned key to " + target.getName().getString()
),
false
);
return 1;
}
private static int togglePublic(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
ItemStack key = getHeldKey(player);
if (key.isEmpty()) {
source.sendFailure(Component.literal("You must hold a collar key"));
return 0;
}
CompoundTag tag = key.getOrCreateTag();
// Must be owner to toggle
if (
tag.hasUUID(TAG_OWNER) &&
!tag.getUUID(TAG_OWNER).equals(player.getUUID())
) {
source.sendFailure(Component.literal("You do not own this key"));
return 0;
}
boolean isPublic = !tag.getBoolean(TAG_PUBLIC);
tag.putBoolean(TAG_PUBLIC, isPublic);
source.sendSuccess(
() ->
Component.literal(
"§aKey is now " + (isPublic ? "public" : "private")
),
false
);
return 1;
}
private static int info(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
ItemStack key = getHeldKey(player);
if (key.isEmpty()) {
source.sendFailure(Component.literal("You must hold a collar key"));
return 0;
}
CompoundTag tag = key.getOrCreateTag();
source.sendSuccess(
() -> Component.literal("§6=== Key Info ==="),
false
);
String ownerName = tag.getString(TAG_OWNER_NAME);
source.sendSuccess(
() ->
Component.literal(
"§7Owner: §f" +
(ownerName.isEmpty() ? "Not claimed" : ownerName)
),
false
);
String targetName = tag.getString(TAG_TARGET_NAME);
source.sendSuccess(
() ->
Component.literal(
"§7Assigned to: §f" +
(targetName.isEmpty() ? "Not assigned" : targetName)
),
false
);
boolean isPublic = tag.getBoolean(TAG_PUBLIC);
source.sendSuccess(
() -> Component.literal("§7Public: §f" + (isPublic ? "Yes" : "No")),
false
);
return 1;
}
}

View File

@@ -0,0 +1,234 @@
package com.tiedup.remake.commands;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.items.base.BlindfoldVariant;
import com.tiedup.remake.items.base.EarplugsVariant;
import com.tiedup.remake.items.base.GagVariant;
import com.tiedup.remake.items.base.KnifeVariant;
import java.util.Optional;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
/**
* Utility commands for Phase 18.
*
* Commands:
* /kidnapset - Get a starter kit of mod items
* /kidnapreload [type] - Reload data files (jobs, sales, gagtalk)
*/
public class KidnapSetCommand {
public static void register(
CommandDispatcher<CommandSourceStack> dispatcher
) {
dispatcher.register(createKidnapSetCommand());
dispatcher.register(createKidnapReloadCommand());
}
/**
* Create the kidnapset command builder (for use as subcommand of /tiedup).
* @return The command builder
*/
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createKidnapSetCommand() {
return Commands.literal("kidnapset")
.requires(CommandHelper.REQUIRES_OP)
.executes(KidnapSetCommand::giveSet);
}
/**
* Create the kidnapreload command builder (for use as subcommand of /tiedup).
* @return The command builder
*/
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createKidnapReloadCommand() {
return Commands.literal("kidnapreload")
.requires(CommandHelper.REQUIRES_OP)
.executes(ctx -> reload(ctx, "all"))
.then(Commands.literal("jobs").executes(ctx -> reload(ctx, "jobs")))
.then(
Commands.literal("sales").executes(ctx -> reload(ctx, "sales"))
)
.then(
Commands.literal("gagtalk").executes(ctx ->
reload(ctx, "gagtalk")
)
)
.then(Commands.literal("all").executes(ctx -> reload(ctx, "all")));
}
/**
* Give a starter kit of mod items to the player.
*/
private static int giveSet(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
int given = 0;
// Binds
given += giveItem(
player,
new ItemStack(ModItems.getBind(BindVariant.ROPES), 8)
);
given += giveItem(
player,
new ItemStack(ModItems.getBind(BindVariant.CHAIN), 4)
);
given += giveItem(
player,
new ItemStack(ModItems.getBind(BindVariant.LEATHER_STRAPS), 4)
);
// Gags
given += giveItem(
player,
new ItemStack(ModItems.getGag(GagVariant.CLOTH_GAG), 4)
);
given += giveItem(
player,
new ItemStack(ModItems.getGag(GagVariant.BALL_GAG), 4)
);
given += giveItem(
player,
new ItemStack(ModItems.getGag(GagVariant.TAPE_GAG), 4)
);
// Blindfolds
given += giveItem(
player,
new ItemStack(ModItems.getBlindfold(BlindfoldVariant.CLASSIC), 4)
);
given += giveItem(
player,
new ItemStack(ModItems.getBlindfold(BlindfoldVariant.MASK), 2)
);
// Collars
given += giveItem(
player,
new ItemStack(ModItems.CLASSIC_COLLAR.get(), 4)
);
given += giveItem(
player,
new ItemStack(ModItems.SHOCK_COLLAR.get(), 2)
);
given += giveItem(player, new ItemStack(ModItems.GPS_COLLAR.get(), 2));
// Tools
given += giveItem(
player,
new ItemStack(ModItems.getKnife(KnifeVariant.IRON), 2)
);
given += giveItem(
player,
new ItemStack(ModItems.getKnife(KnifeVariant.GOLDEN), 1)
);
given += giveItem(player, new ItemStack(ModItems.WHIP.get(), 1));
given += giveItem(player, new ItemStack(ModItems.PADDLE.get(), 1));
// Controllers
given += giveItem(
player,
new ItemStack(ModItems.SHOCKER_CONTROLLER.get(), 1)
);
given += giveItem(player, new ItemStack(ModItems.GPS_LOCATOR.get(), 1));
// Keys and locks
given += giveItem(player, new ItemStack(ModItems.PADLOCK.get(), 4));
given += giveItem(player, new ItemStack(ModItems.COLLAR_KEY.get(), 2));
given += giveItem(player, new ItemStack(ModItems.MASTER_KEY.get(), 1));
// Earplugs
given += giveItem(
player,
new ItemStack(ModItems.getEarplugs(EarplugsVariant.CLASSIC), 4)
);
// Rope arrows
given += giveItem(player, new ItemStack(ModItems.ROPE_ARROW.get(), 16));
// Chloroform
given += giveItem(
player,
new ItemStack(ModItems.CHLOROFORM_BOTTLE.get(), 2)
);
given += giveItem(player, new ItemStack(ModItems.RAG.get(), 4));
int finalGiven = given;
source.sendSuccess(
() ->
Component.literal(
"§aGave kidnap set (" + finalGiven + " item stacks)"
),
true
);
return finalGiven;
}
private static int giveItem(ServerPlayer player, ItemStack stack) {
if (!player.getInventory().add(stack)) {
// Drop on ground if inventory full
player.drop(stack, false);
}
return 1;
}
/**
* Reload data files.
*/
private static int reload(
CommandContext<CommandSourceStack> context,
String type
) {
CommandSourceStack source = context.getSource();
int reloaded = 0;
if (type.equals("all") || type.equals("jobs")) {
com.tiedup.remake.util.tasks.JobLoader.init();
reloaded++;
}
if (type.equals("all") || type.equals("sales")) {
com.tiedup.remake.util.tasks.SaleLoader.init();
reloaded++;
}
if (type.equals("all") || type.equals("gagtalk")) {
// GagTalkManager is code-based (no external data files)
// No reload needed - materials/logic defined in GagMaterial enum
reloaded++;
}
int finalReloaded = reloaded;
source.sendSuccess(
() ->
Component.literal(
"§aReloaded " +
(type.equals("all") ? "all data files" : type) +
" (" +
finalReloaded +
" files)"
),
true
);
return reloaded;
}
}

View File

@@ -0,0 +1,754 @@
package com.tiedup.remake.commands;
import com.mojang.brigadier.CommandDispatcher;
import com.tiedup.remake.v2.BodyRegionV2;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.entities.*;
import com.tiedup.remake.entities.skins.EliteKidnapperSkinManager;
import com.tiedup.remake.items.ModItems;
import com.tiedup.remake.items.base.BindVariant;
import com.tiedup.remake.items.base.BlindfoldVariant;
import com.tiedup.remake.items.base.EarplugsVariant;
import com.tiedup.remake.items.base.GagVariant;
import com.tiedup.remake.state.IBondageState;
import java.util.List;
import java.util.Optional;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.MobSpawnType;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.AABB;
/**
* NPC management commands for Phase 18.
*
* Commands:
* /npc spawn kidnapper [player] - Spawn a kidnapper at location
* /npc spawn elite <name> [player] - Spawn an elite kidnapper
* /npc spawn archer [player] - Spawn an archer kidnapper
* /npc spawn damsel [player] - Spawn a damsel NPC
* /npc kill [radius] - Kill all mod NPCs in radius
* /npc tie - Tie the nearest NPC
* /npc gag - Gag the nearest NPC
* /npc blindfold - Blindfold the nearest NPC
* /npc collar - Collar the nearest NPC
* /npc untie - Untie the nearest NPC (remove all)
* /npc state - Show state of nearest NPC
* /npc full - Apply full bondage to nearest NPC
*
* All commands require OP level 2.
*/
public class NPCCommand {
public static void register(
CommandDispatcher<CommandSourceStack> dispatcher
) {
dispatcher.register(createNPCCommand());
}
/**
* Create the NPC command builder (for use as subcommand of /tiedup).
* @return The command builder
*/
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createNPCCommand() {
return Commands.literal("npc")
.requires(CommandHelper.REQUIRES_OP)
.then(
Commands.literal("spawn")
// /npc spawn kidnapper [player]
.then(
Commands.literal("kidnapper")
.executes(ctx -> spawnKidnapper(ctx, null))
.then(
Commands.argument(
"player",
EntityArgument.player()
).executes(ctx ->
spawnKidnapper(
ctx,
EntityArgument.getPlayer(ctx, "player")
)
)
)
)
// /npc spawn elite <name> [player]
.then(
Commands.literal("elite").then(
Commands.argument("name", StringArgumentType.word())
.suggests((ctx, builder) -> {
for (KidnapperVariant v : EliteKidnapperSkinManager.CORE.getAllVariants()) {
builder.suggest(v.id());
}
return builder.buildFuture();
})
.executes(ctx -> spawnElite(ctx, null))
.then(
Commands.argument(
"player",
EntityArgument.player()
).executes(ctx ->
spawnElite(
ctx,
EntityArgument.getPlayer(
ctx,
"player"
)
)
)
)
)
)
// /npc spawn archer [player]
.then(
Commands.literal("archer")
.executes(ctx -> spawnArcher(ctx, null))
.then(
Commands.argument(
"player",
EntityArgument.player()
).executes(ctx ->
spawnArcher(
ctx,
EntityArgument.getPlayer(ctx, "player")
)
)
)
)
// /npc spawn damsel [player]
.then(
Commands.literal("damsel")
.executes(ctx -> spawnDamsel(ctx, null))
.then(
Commands.argument(
"player",
EntityArgument.player()
).executes(ctx ->
spawnDamsel(
ctx,
EntityArgument.getPlayer(ctx, "player")
)
)
)
)
)
// /npc kill <radius> - Kill all mod NPCs in radius
.then(
Commands.literal("kill")
.executes(ctx -> killNPCs(ctx, 10))
.then(
Commands.argument(
"radius",
com.mojang.brigadier.arguments.IntegerArgumentType.integer(
1,
100
)
).executes(ctx ->
killNPCs(
ctx,
com.mojang.brigadier.arguments.IntegerArgumentType.getInteger(
ctx,
"radius"
)
)
)
)
)
// /npc tie - Tie nearest NPC
.then(Commands.literal("tie").executes(NPCCommand::tieNPC))
// /npc gag - Gag nearest NPC
.then(Commands.literal("gag").executes(NPCCommand::gagNPC))
// /npc blindfold - Blindfold nearest NPC
.then(
Commands.literal("blindfold").executes(NPCCommand::blindfoldNPC)
)
// /npc collar - Collar nearest NPC
.then(Commands.literal("collar").executes(NPCCommand::collarNPC))
// /npc untie - Untie nearest NPC
.then(Commands.literal("untie").executes(NPCCommand::untieNPC))
// /npc state - Show NPC state
.then(Commands.literal("state").executes(NPCCommand::showNPCState))
// /npc full - Full bondage on nearest NPC
.then(
Commands.literal("full").executes(NPCCommand::fullBondageNPC)
);
}
private static int spawnKidnapper(
CommandContext<CommandSourceStack> context,
ServerPlayer targetPlayer
) throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
ServerLevel level = source.getLevel();
// Get spawn location
double x, y, z;
if (targetPlayer != null) {
x = targetPlayer.getX();
y = targetPlayer.getY();
z = targetPlayer.getZ();
} else if (source.getEntity() instanceof ServerPlayer player) {
x = player.getX();
y = player.getY();
z = player.getZ();
} else {
source.sendFailure(
Component.literal("Must specify a player or be a player")
);
return 0;
}
// Spawn the kidnapper
EntityKidnapper kidnapper = ModEntities.KIDNAPPER.get().create(level);
if (kidnapper != null) {
kidnapper.moveTo(x, y, z, level.random.nextFloat() * 360F, 0.0F);
kidnapper.finalizeSpawn(
level,
level.getCurrentDifficultyAt(kidnapper.blockPosition()),
MobSpawnType.COMMAND,
null,
null
);
level.addFreshEntity(kidnapper);
source.sendSuccess(
() ->
Component.literal(
"§aSpawned Kidnapper at " + formatPos(x, y, z)
),
true
);
return 1;
}
source.sendFailure(Component.literal("Failed to spawn Kidnapper"));
return 0;
}
private static int spawnElite(
CommandContext<CommandSourceStack> context,
ServerPlayer targetPlayer
) throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
ServerLevel level = source.getLevel();
String name = StringArgumentType.getString(context, "name");
// Parse variant
KidnapperVariant variant = EliteKidnapperSkinManager.CORE.getVariant(
name
);
if (variant == null) {
source.sendFailure(
Component.literal(
"Unknown elite variant: " +
name +
". Available: suki, carol, athena, evelyn"
)
);
return 0;
}
// Get spawn location
double x, y, z;
if (targetPlayer != null) {
x = targetPlayer.getX();
y = targetPlayer.getY();
z = targetPlayer.getZ();
} else if (source.getEntity() instanceof ServerPlayer player) {
x = player.getX();
y = player.getY();
z = player.getZ();
} else {
source.sendFailure(
Component.literal("Must specify a player or be a player")
);
return 0;
}
// Spawn the elite kidnapper
EntityKidnapperElite elite = ModEntities.KIDNAPPER_ELITE.get().create(
level
);
if (elite != null) {
elite.moveTo(x, y, z, level.random.nextFloat() * 360F, 0.0F);
elite.setKidnapperVariant(variant);
elite.finalizeSpawn(
level,
level.getCurrentDifficultyAt(elite.blockPosition()),
MobSpawnType.COMMAND,
null,
null
);
level.addFreshEntity(elite);
source.sendSuccess(
() ->
Component.literal(
"§aSpawned Elite Kidnapper '" +
variant.defaultName() +
"' at " +
formatPos(x, y, z)
),
true
);
return 1;
}
source.sendFailure(
Component.literal("Failed to spawn Elite Kidnapper")
);
return 0;
}
private static int spawnArcher(
CommandContext<CommandSourceStack> context,
ServerPlayer targetPlayer
) throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
ServerLevel level = source.getLevel();
// Get spawn location
double x, y, z;
if (targetPlayer != null) {
x = targetPlayer.getX();
y = targetPlayer.getY();
z = targetPlayer.getZ();
} else if (source.getEntity() instanceof ServerPlayer player) {
x = player.getX();
y = player.getY();
z = player.getZ();
} else {
source.sendFailure(
Component.literal("Must specify a player or be a player")
);
return 0;
}
// Spawn the archer
EntityKidnapperArcher archer =
ModEntities.KIDNAPPER_ARCHER.get().create(level);
if (archer != null) {
archer.moveTo(x, y, z, level.random.nextFloat() * 360F, 0.0F);
archer.finalizeSpawn(
level,
level.getCurrentDifficultyAt(archer.blockPosition()),
MobSpawnType.COMMAND,
null,
null
);
level.addFreshEntity(archer);
source.sendSuccess(
() ->
Component.literal(
"§aSpawned Archer Kidnapper at " + formatPos(x, y, z)
),
true
);
return 1;
}
source.sendFailure(
Component.literal("Failed to spawn Archer Kidnapper")
);
return 0;
}
private static int spawnDamsel(
CommandContext<CommandSourceStack> context,
ServerPlayer targetPlayer
) throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
ServerLevel level = source.getLevel();
// Get spawn location
double x, y, z;
if (targetPlayer != null) {
x = targetPlayer.getX();
y = targetPlayer.getY();
z = targetPlayer.getZ();
} else if (source.getEntity() instanceof ServerPlayer player) {
x = player.getX();
y = player.getY();
z = player.getZ();
} else {
source.sendFailure(
Component.literal("Must specify a player or be a player")
);
return 0;
}
// Spawn the damsel
EntityDamsel damsel = ModEntities.DAMSEL.get().create(level);
if (damsel != null) {
damsel.moveTo(x, y, z, level.random.nextFloat() * 360F, 0.0F);
damsel.finalizeSpawn(
level,
level.getCurrentDifficultyAt(damsel.blockPosition()),
MobSpawnType.COMMAND,
null,
null
);
level.addFreshEntity(damsel);
source.sendSuccess(
() ->
Component.literal(
"§aSpawned Damsel at " + formatPos(x, y, z)
),
true
);
return 1;
}
source.sendFailure(Component.literal("Failed to spawn Damsel"));
return 0;
}
private static int killNPCs(
CommandContext<CommandSourceStack> context,
int radius
) throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
ServerLevel level = player.serverLevel();
int killed = 0;
// Find and kill all mod NPCs in radius
var entities = level.getEntitiesOfClass(
net.minecraft.world.entity.LivingEntity.class,
player.getBoundingBox().inflate(radius),
e ->
e instanceof com.tiedup.remake.entities.AbstractTiedUpNpc
);
for (var entity : entities) {
entity.discard();
killed++;
}
int finalKilled = killed;
source.sendSuccess(
() ->
Component.literal(
"§aKilled " + finalKilled + " mod NPCs in radius " + radius
),
true
);
return killed;
}
private static String formatPos(double x, double y, double z) {
return String.format("(%.1f, %.1f, %.1f)", x, y, z);
}
// ========================================
// NPC Bondage Commands (from DamselTestCommand)
// ========================================
/**
* Find the nearest mod NPC (Damsel or Kidnapper) within 10 blocks.
*/
private static IBondageState findNearestNPC(
CommandContext<CommandSourceStack> context
) {
Entity source = context.getSource().getEntity();
if (source == null) return null;
AABB searchBox = source.getBoundingBox().inflate(10.0);
// Search for any IBondageState entity (EntityDamsel implements this)
List<EntityDamsel> npcs = source
.level()
.getEntitiesOfClass(EntityDamsel.class, searchBox);
if (npcs.isEmpty()) {
return null;
}
// Return closest NPC
return npcs
.stream()
.min((a, b) ->
Double.compare(a.distanceToSqr(source), b.distanceToSqr(source))
)
.orElse(null);
}
private static int tieNPC(CommandContext<CommandSourceStack> context) {
IBondageState npc = findNearestNPC(context);
if (npc == null) {
context
.getSource()
.sendFailure(
Component.literal("No mod NPC found within 10 blocks")
);
return 0;
}
if (npc.isTiedUp()) {
context
.getSource()
.sendFailure(Component.literal("NPC is already tied up"));
return 0;
}
npc.equip(BodyRegionV2.ARMS, new ItemStack(ModItems.getBind(BindVariant.ROPES)));
context
.getSource()
.sendSuccess(
() -> Component.literal("§aTied up " + npc.getKidnappedName()),
true
);
return 1;
}
private static int gagNPC(CommandContext<CommandSourceStack> context) {
IBondageState npc = findNearestNPC(context);
if (npc == null) {
context
.getSource()
.sendFailure(
Component.literal("No mod NPC found within 10 blocks")
);
return 0;
}
if (npc.isGagged()) {
context
.getSource()
.sendFailure(Component.literal("NPC is already gagged"));
return 0;
}
npc.equip(BodyRegionV2.MOUTH, new ItemStack(ModItems.getGag(GagVariant.CLOTH_GAG)));
context
.getSource()
.sendSuccess(
() -> Component.literal("§aGagged " + npc.getKidnappedName()),
true
);
return 1;
}
private static int blindfoldNPC(
CommandContext<CommandSourceStack> context
) {
IBondageState npc = findNearestNPC(context);
if (npc == null) {
context
.getSource()
.sendFailure(
Component.literal("No mod NPC found within 10 blocks")
);
return 0;
}
if (npc.isBlindfolded()) {
context
.getSource()
.sendFailure(Component.literal("NPC is already blindfolded"));
return 0;
}
npc.equip(BodyRegionV2.EYES,
new ItemStack(ModItems.getBlindfold(BlindfoldVariant.CLASSIC))
);
context
.getSource()
.sendSuccess(
() ->
Component.literal(
"§aBlindfolded " + npc.getKidnappedName()
),
true
);
return 1;
}
private static int collarNPC(CommandContext<CommandSourceStack> context) {
IBondageState npc = findNearestNPC(context);
if (npc == null) {
context
.getSource()
.sendFailure(
Component.literal("No mod NPC found within 10 blocks")
);
return 0;
}
if (npc.hasCollar()) {
context
.getSource()
.sendFailure(Component.literal("NPC already has a collar"));
return 0;
}
npc.equip(BodyRegionV2.NECK, new ItemStack(ModItems.CLASSIC_COLLAR.get()));
context
.getSource()
.sendSuccess(
() -> Component.literal("§aCollared " + npc.getKidnappedName()),
true
);
return 1;
}
private static int untieNPC(CommandContext<CommandSourceStack> context) {
IBondageState npc = findNearestNPC(context);
if (npc == null) {
context
.getSource()
.sendFailure(
Component.literal("No mod NPC found within 10 blocks")
);
return 0;
}
npc.untie(true); // Drop items
context
.getSource()
.sendSuccess(
() -> Component.literal("§aUntied " + npc.getKidnappedName()),
true
);
return 1;
}
private static int fullBondageNPC(
CommandContext<CommandSourceStack> context
) {
IBondageState npc = findNearestNPC(context);
if (npc == null) {
context
.getSource()
.sendFailure(
Component.literal("No mod NPC found within 10 blocks")
);
return 0;
}
// Apply full bondage using AbstractTiedUpNpc method
if (npc instanceof com.tiedup.remake.entities.AbstractTiedUpNpc npcEntity) {
npcEntity.applyBondage(
new ItemStack(ModItems.getBind(BindVariant.ROPES)),
new ItemStack(ModItems.getGag(GagVariant.CLOTH_GAG)),
new ItemStack(ModItems.getBlindfold(BlindfoldVariant.CLASSIC)),
new ItemStack(ModItems.getEarplugs(EarplugsVariant.CLASSIC)),
new ItemStack(ModItems.CLASSIC_COLLAR.get()),
ItemStack.EMPTY // No clothes
);
}
context
.getSource()
.sendSuccess(
() ->
Component.literal(
"§aFully restrained " + npc.getKidnappedName()
),
true
);
return 1;
}
private static int showNPCState(
CommandContext<CommandSourceStack> context
) {
IBondageState npc = findNearestNPC(context);
if (npc == null) {
context
.getSource()
.sendFailure(
Component.literal("No mod NPC found within 10 blocks")
);
return 0;
}
CommandSourceStack source = context.getSource();
source.sendSuccess(
() ->
Component.literal(
"§6=== NPC State: " + npc.getKidnappedName() + " ==="
),
false
);
if (npc instanceof EntityDamsel damsel) {
source.sendSuccess(
() ->
Component.literal("§eVariant: §f" + damsel.getVariantId()),
false
);
source.sendSuccess(
() ->
Component.literal(
"§eSlim Arms: " +
(damsel.hasSlimArms() ? "§aYes" : "§7No")
),
false
);
}
source.sendSuccess(
() ->
Component.literal(
"§eTied Up: " + (npc.isTiedUp() ? "§aYes" : "§7No")
),
false
);
source.sendSuccess(
() ->
Component.literal(
"§eGagged: " + (npc.isGagged() ? "§aYes" : "§7No")
),
false
);
source.sendSuccess(
() ->
Component.literal(
"§eBlindfolded: " + (npc.isBlindfolded() ? "§aYes" : "§7No")
),
false
);
source.sendSuccess(
() ->
Component.literal(
"§eHas Collar: " + (npc.hasCollar() ? "§aYes" : "§7No")
),
false
);
source.sendSuccess(
() ->
Component.literal(
"§eHas Earplugs: " + (npc.hasEarplugs() ? "§aYes" : "§7No")
),
false
);
source.sendSuccess(
() ->
Component.literal(
"§eIs Captive: " + (npc.isCaptive() ? "§aYes" : "§7No")
),
false
);
return 1;
}
}

View File

@@ -0,0 +1,511 @@
package com.tiedup.remake.commands;
import com.mojang.brigadier.CommandDispatcher;
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.core.TiedUpMod;
import com.tiedup.remake.state.SocialData;
import com.tiedup.remake.util.MessageDispatcher;
import java.util.*;
import java.util.Optional;
import net.minecraft.ChatFormatting;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.phys.AABB;
/**
* Social and RP commands for Phase 18.
*
* Commands:
* /blockplayer <player> - Block a player from interacting with you
* /unblockplayer <player> - Unblock a player
* /checkblocked <player> - Check if a player has blocked you
* /norp - Announce non-consent to current RP (cooldown 45s)
* /me <action> - Roleplay action message in local area
* /pm <player> <message> - Private message to a player
* /talkarea [distance] - Set local chat area distance
*
* These commands are usable while tied up.
*
* Data persistence: Block lists and talk area settings are stored in SocialData
* (SavedData) and persist across server restarts.
*/
public class SocialCommand {
// Cooldowns for /norp (UUID -> last use timestamp)
// Note: Cooldowns are intentionally NOT persisted - they reset on server restart
private static final Map<UUID, Long> NORP_COOLDOWNS = new HashMap<>();
private static final long NORP_COOLDOWN_MS = 45000; // 45 seconds
/** Remove player cooldown on disconnect to prevent memory leak. */
public static void cleanupPlayer(UUID playerId) {
NORP_COOLDOWNS.remove(playerId);
}
public static void register(
CommandDispatcher<CommandSourceStack> dispatcher
) {
dispatcher.register(createBlockPlayerCommand());
dispatcher.register(createUnblockPlayerCommand());
dispatcher.register(createCheckBlockedCommand());
dispatcher.register(createNoRPCommand());
dispatcher.register(createMeCommand());
dispatcher.register(createPMCommand());
dispatcher.register(createTalkAreaCommand());
dispatcher.register(createTalkInfoCommand());
}
// === Command Builders (for use as subcommands of /tiedup) ===
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createBlockPlayerCommand() {
return Commands.literal("blockplayer").then(
Commands.argument("player", EntityArgument.player()).executes(
SocialCommand::blockPlayer
)
);
}
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createUnblockPlayerCommand() {
return Commands.literal("unblockplayer").then(
Commands.argument("player", EntityArgument.player()).executes(
SocialCommand::unblockPlayer
)
);
}
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createCheckBlockedCommand() {
return Commands.literal("checkblocked").then(
Commands.argument("player", EntityArgument.player()).executes(
SocialCommand::checkBlocked
)
);
}
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createNoRPCommand() {
return Commands.literal("norp").executes(SocialCommand::noRP);
}
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createMeCommand() {
return Commands.literal("me").then(
Commands.argument(
"action",
StringArgumentType.greedyString()
).executes(SocialCommand::meAction)
);
}
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createPMCommand() {
return Commands.literal("pm").then(
Commands.argument("player", EntityArgument.player()).then(
Commands.argument(
"message",
StringArgumentType.greedyString()
).executes(SocialCommand::privateMessage)
)
);
}
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createTalkAreaCommand() {
return Commands.literal("talkarea")
.executes(ctx -> setTalkArea(ctx, 0)) // Disable
.then(
Commands.argument(
"distance",
IntegerArgumentType.integer(1, 100)
).executes(ctx ->
setTalkArea(
ctx,
IntegerArgumentType.getInteger(ctx, "distance")
)
)
);
}
public static com.mojang.brigadier.builder.LiteralArgumentBuilder<
CommandSourceStack
> createTalkInfoCommand() {
return Commands.literal("talkinfo").executes(SocialCommand::talkInfo);
}
// ========================================
// Block System
// ========================================
private static int blockPlayer(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
ServerPlayer target = EntityArgument.getPlayer(context, "player");
if (player.getUUID().equals(target.getUUID())) {
source.sendFailure(Component.literal("You cannot block yourself"));
return 0;
}
SocialData data = SocialData.get(player.serverLevel());
if (data.isBlocked(player.getUUID(), target.getUUID())) {
source.sendFailure(
Component.literal(
target.getName().getString() + " is already blocked"
)
);
return 0;
}
data.addBlock(player.getUUID(), target.getUUID());
source.sendSuccess(
() ->
Component.literal("§aBlocked " + target.getName().getString()),
false
);
TiedUpMod.LOGGER.info(
"[SOCIAL] {} blocked {}",
player.getName().getString(),
target.getName().getString()
);
return 1;
}
private static int unblockPlayer(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
ServerPlayer target = EntityArgument.getPlayer(context, "player");
SocialData data = SocialData.get(player.serverLevel());
if (!data.isBlocked(player.getUUID(), target.getUUID())) {
source.sendFailure(
Component.literal(
target.getName().getString() + " is not blocked"
)
);
return 0;
}
data.removeBlock(player.getUUID(), target.getUUID());
source.sendSuccess(
() ->
Component.literal(
"§aUnblocked " + target.getName().getString()
),
false
);
return 1;
}
private static int checkBlocked(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
ServerPlayer target = EntityArgument.getPlayer(context, "player");
SocialData data = SocialData.get(player.serverLevel());
boolean blocked = data.isBlocked(target.getUUID(), player.getUUID());
if (blocked) {
source.sendSuccess(
() ->
Component.literal(
"§c" + target.getName().getString() + " has blocked you"
),
false
);
} else {
source.sendSuccess(
() ->
Component.literal(
"§a" +
target.getName().getString() +
" has not blocked you"
),
false
);
}
return 1;
}
/**
* Check if a player is blocked by another.
* Can be used by other systems to check interaction permissions.
*
* @param level The server level to get SocialData from
* @param blocker The player who may have blocked
* @param blocked The player who may be blocked
* @return true if blocker has blocked blocked
*/
public static boolean isBlocked(
ServerLevel level,
UUID blocker,
UUID blocked
) {
return SocialData.get(level).isBlocked(blocker, blocked);
}
// ========================================
// RP Commands
// ========================================
private static int noRP(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
// Check cooldown
long now = System.currentTimeMillis();
Long lastUse = NORP_COOLDOWNS.get(player.getUUID());
if (lastUse != null && now - lastUse < NORP_COOLDOWN_MS) {
long remaining = (NORP_COOLDOWN_MS - (now - lastUse)) / 1000;
source.sendFailure(
Component.literal(
"Please wait " +
remaining +
" seconds before using /norp again"
)
);
return 0;
}
// Set cooldown
NORP_COOLDOWNS.put(player.getUUID(), now);
// Broadcast to all players
Component message = Component.literal("")
.append(
Component.literal("[NoRP] ").withStyle(
ChatFormatting.RED,
ChatFormatting.BOLD
)
)
.append(
Component.literal(player.getName().getString()).withStyle(
ChatFormatting.YELLOW
)
)
.append(
Component.literal(
" has announced non-consent to current RP"
).withStyle(ChatFormatting.RED)
);
// Broadcast to all players (earplug-aware)
for (ServerPlayer p : player.server.getPlayerList().getPlayers()) {
MessageDispatcher.sendTo(p, message);
}
TiedUpMod.LOGGER.info(
"[SOCIAL] {} used /norp",
player.getName().getString()
);
return 1;
}
private static int meAction(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
String action = StringArgumentType.getString(context, "action");
// Get talk area for local chat
SocialData data = SocialData.get(player.serverLevel());
int talkArea = data.getTalkArea(player.getUUID());
Component message = Component.literal("")
.append(
Component.literal("* ").withStyle(ChatFormatting.LIGHT_PURPLE)
)
.append(
Component.literal(player.getName().getString()).withStyle(
ChatFormatting.LIGHT_PURPLE
)
)
.append(
Component.literal(" " + action).withStyle(
ChatFormatting.LIGHT_PURPLE
)
);
if (talkArea > 0) {
// Local chat - send to nearby players (earplug-aware)
AABB area = player.getBoundingBox().inflate(talkArea);
List<ServerPlayer> nearby = player
.serverLevel()
.getEntitiesOfClass(ServerPlayer.class, area);
for (ServerPlayer p : nearby) {
MessageDispatcher.sendTo(p, message);
}
} else {
// Global chat (earplug-aware)
for (ServerPlayer p : player.server.getPlayerList().getPlayers()) {
MessageDispatcher.sendTo(p, message);
}
}
return 1;
}
private static int privateMessage(
CommandContext<CommandSourceStack> context
) throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> senderOpt = CommandHelper.getPlayerOrFail(
source
);
if (senderOpt.isEmpty()) return 0;
ServerPlayer sender = senderOpt.get();
ServerPlayer target = EntityArgument.getPlayer(context, "player");
String message = StringArgumentType.getString(context, "message");
// Check if blocked
SocialData data = SocialData.get(sender.serverLevel());
if (data.isBlocked(target.getUUID(), sender.getUUID())) {
source.sendFailure(
Component.literal("This player has blocked you")
);
return 0;
}
// Send to target (earplug-aware)
Component toTarget = Component.literal("")
.append(
Component.literal(
"[PM from " + sender.getName().getString() + "] "
).withStyle(ChatFormatting.LIGHT_PURPLE)
)
.append(Component.literal(message).withStyle(ChatFormatting.WHITE));
MessageDispatcher.sendFrom(sender, target, toTarget);
// Confirm to sender (always show - they're the one sending)
Component toSender = Component.literal("")
.append(
Component.literal(
"[PM to " + target.getName().getString() + "] "
).withStyle(ChatFormatting.GRAY)
)
.append(Component.literal(message).withStyle(ChatFormatting.WHITE));
MessageDispatcher.sendSystemMessage(sender, toSender);
return 1;
}
// ========================================
// Talk Area
// ========================================
private static int setTalkArea(
CommandContext<CommandSourceStack> context,
int distance
) throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
SocialData data = SocialData.get(player.serverLevel());
data.setTalkArea(player.getUUID(), distance);
if (distance == 0) {
source.sendSuccess(
() -> Component.literal("§aTalk area disabled (global chat)"),
false
);
} else {
source.sendSuccess(
() ->
Component.literal(
"§aTalk area set to " + distance + " blocks"
),
false
);
}
return 1;
}
private static int talkInfo(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
CommandSourceStack source = context.getSource();
Optional<ServerPlayer> playerOpt = CommandHelper.getPlayerOrFail(
source
);
if (playerOpt.isEmpty()) return 0;
ServerPlayer player = playerOpt.get();
SocialData data = SocialData.get(player.serverLevel());
int talkArea = data.getTalkArea(player.getUUID());
if (talkArea == 0) {
source.sendSuccess(
() ->
Component.literal("Talk area: §edisabled §7(global chat)"),
false
);
} else {
source.sendSuccess(
() -> Component.literal("Talk area: §e" + talkArea + " blocks"),
false
);
}
return 1;
}
}

View File

@@ -0,0 +1,80 @@
package com.tiedup.remake.commands;
import com.mojang.brigadier.CommandDispatcher;
import com.tiedup.remake.commands.subcommands.BondageSubCommand;
import com.tiedup.remake.commands.subcommands.DebtSubCommand;
import com.tiedup.remake.commands.subcommands.InventorySubCommand;
import com.tiedup.remake.commands.subcommands.MasterTestSubCommand;
import com.tiedup.remake.commands.subcommands.TestAnimSubCommand;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
/**
* Main TiedUp! command suite — registration hub.
*
* All command implementations are delegated to domain-specific sub-command classes
* in the {@code subcommands/} package. This class only builds the root node and
* wires together the sub-trees.
*
* Commands are grouped by domain:
* - Bondage: tie, untie, gag, ungag, blindfold, unblind, collar, takecollar,
* takeearplugs, putearplugs, takeclothes, putclothes, fullyrestrain, enslave, free, adjust
* - Debt: debt show/set/add/remove
* - Master test: mastertest, masterchair, mastertask
* - Test animation: testanim <animId>/stop
* - Inventory: returnstuff
* - Plus existing delegated commands: bounty, npc, key, collar, clothes,
* kidnapset, kidnapreload, cell, social, debug
*/
@SuppressWarnings("null")
public class TiedUpCommand {
/**
* Register all TiedUp commands with the command dispatcher.
*
* @param dispatcher The command dispatcher
*/
public static void register(
CommandDispatcher<CommandSourceStack> dispatcher
) {
var root = Commands.literal("tiedup");
// === Domain sub-commands (implemented in subcommands/ package) ===
BondageSubCommand.register(root);
DebtSubCommand.register(root);
MasterTestSubCommand.register(root);
TestAnimSubCommand.register(root);
InventorySubCommand.register(root);
// === Existing delegated commands (each has its own class) ===
// /tiedup bounty <player> - Bounty system
root.then(BountyCommand.createBountyCommand());
// /tiedup npc ... - NPC management
root.then(NPCCommand.createNPCCommand());
// /tiedup key ... - Key management
root.then(KeyCommand.createKeyCommand());
// /tiedup collar ... - Collar management
root.then(CollarCommand.createCollarCommand());
// /tiedup clothes ... - Clothes configuration
root.then(ClothesCommand.createClothesCommand());
// /tiedup kidnapset - Configuration reload
root.then(KidnapSetCommand.createKidnapSetCommand());
// /tiedup kidnapreload - Configuration reload (alias)
root.then(KidnapSetCommand.createKidnapReloadCommand());
// /tiedup cell ... - Cell management
root.then(CellCommand.createCellCommand());
// === Social Commands ===
root.then(SocialCommand.createBlockPlayerCommand());
root.then(SocialCommand.createUnblockPlayerCommand());
root.then(SocialCommand.createCheckBlockedCommand());
root.then(SocialCommand.createNoRPCommand());
root.then(SocialCommand.createMeCommand());
root.then(SocialCommand.createPMCommand());
root.then(SocialCommand.createTalkAreaCommand());
root.then(SocialCommand.createTalkInfoCommand());
// /tiedup debug ... - Captivity system debugging
root.then(CaptivityDebugCommand.createDebugCommand());
dispatcher.register(root);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,223 @@
package com.tiedup.remake.commands.subcommands;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.commands.CommandHelper;
import com.tiedup.remake.prison.PrisonerManager;
import com.tiedup.remake.prison.RansomRecord;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
/**
* Debt management sub-commands for /tiedup.
* Handles: debt show, debt set, debt add, debt remove
*/
@SuppressWarnings("null")
public class DebtSubCommand {
public static void register(LiteralArgumentBuilder<CommandSourceStack> root) {
// /tiedup debt <player> [set|add|remove <amount>]
root.then(
Commands.literal("debt")
.requires(CommandHelper.REQUIRES_OP)
.then(
Commands.argument("player", EntityArgument.player())
// /tiedup debt <player> -> show debt
.executes(DebtSubCommand::debtShow)
// /tiedup debt <player> set <amount>
.then(
Commands.literal("set").then(
Commands.argument(
"amount",
IntegerArgumentType.integer(0)
).executes(DebtSubCommand::debtSet)
)
)
// /tiedup debt <player> add <amount>
.then(
Commands.literal("add").then(
Commands.argument(
"amount",
IntegerArgumentType.integer(0)
).executes(DebtSubCommand::debtAdd)
)
)
// /tiedup debt <player> remove <amount>
.then(
Commands.literal("remove").then(
Commands.argument(
"amount",
IntegerArgumentType.integer(0)
).executes(DebtSubCommand::debtRemove)
)
)
)
);
}
// ========================================
// Command Implementations
// ========================================
private static int debtShow(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
ServerPlayer target = EntityArgument.getPlayer(context, "player");
ServerLevel level = context.getSource().getLevel();
PrisonerManager manager = PrisonerManager.get(level);
RansomRecord ransom = manager.getRansomRecord(target.getUUID());
if (ransom == null) {
context
.getSource()
.sendSuccess(
() ->
Component.literal(
target.getName().getString() +
" has no debt record."
),
false
);
return 1;
}
int total = ransom.getTotalDebt();
int paid = ransom.getAmountPaid();
int remaining = ransom.getRemainingDebt();
context
.getSource()
.sendSuccess(
() ->
Component.literal(
target.getName().getString() +
" \u2014 Debt: " +
total +
" | Paid: " +
paid +
" | Remaining: " +
remaining +
" emeralds"
),
false
);
return 1;
}
private static int debtSet(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
ServerPlayer target = EntityArgument.getPlayer(context, "player");
int amount = IntegerArgumentType.getInteger(context, "amount");
ServerLevel level = context.getSource().getLevel();
PrisonerManager manager = PrisonerManager.get(level);
RansomRecord ransom = manager.getRansomRecord(target.getUUID());
if (ransom == null) {
context
.getSource()
.sendFailure(
Component.literal(
target.getName().getString() + " has no debt record."
)
);
return 0;
}
ransom.setTotalDebt(amount);
context
.getSource()
.sendSuccess(
() ->
Component.literal(
"Set " +
target.getName().getString() +
"'s total debt to " +
amount +
" emeralds."
),
true
);
return 1;
}
private static int debtAdd(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
ServerPlayer target = EntityArgument.getPlayer(context, "player");
int amount = IntegerArgumentType.getInteger(context, "amount");
ServerLevel level = context.getSource().getLevel();
PrisonerManager manager = PrisonerManager.get(level);
RansomRecord ransom = manager.getRansomRecord(target.getUUID());
if (ransom == null) {
context
.getSource()
.sendFailure(
Component.literal(
target.getName().getString() + " has no debt record."
)
);
return 0;
}
ransom.increaseDebt(amount);
context
.getSource()
.sendSuccess(
() ->
Component.literal(
"Added " +
amount +
" emeralds to " +
target.getName().getString() +
"'s debt. Remaining: " +
ransom.getRemainingDebt()
),
true
);
return 1;
}
private static int debtRemove(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
ServerPlayer target = EntityArgument.getPlayer(context, "player");
int amount = IntegerArgumentType.getInteger(context, "amount");
ServerLevel level = context.getSource().getLevel();
PrisonerManager manager = PrisonerManager.get(level);
RansomRecord ransom = manager.getRansomRecord(target.getUUID());
if (ransom == null) {
context
.getSource()
.sendFailure(
Component.literal(
target.getName().getString() + " has no debt record."
)
);
return 0;
}
ransom.addPayment(amount, null);
boolean paid = ransom.isPaid();
context
.getSource()
.sendSuccess(
() ->
Component.literal(
"Removed " +
amount +
" emeralds from " +
target.getName().getString() +
"'s debt. Remaining: " +
ransom.getRemainingDebt() +
(paid ? " (PAID OFF!)" : "")
),
true
);
return 1;
}
}

View File

@@ -0,0 +1,96 @@
package com.tiedup.remake.commands.subcommands;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.cells.ConfiscatedInventoryRegistry;
import com.tiedup.remake.commands.CommandHelper;
import com.tiedup.remake.core.SystemMessageManager;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
/**
* Inventory management sub-commands for /tiedup.
* Handles: returnstuff
*/
@SuppressWarnings("null")
public class InventorySubCommand {
public static void register(LiteralArgumentBuilder<CommandSourceStack> root) {
// /tiedup returnstuff <player>
root.then(
Commands.literal("returnstuff")
.requires(CommandHelper.REQUIRES_OP)
.then(
Commands.argument("player", EntityArgument.player())
.executes(InventorySubCommand::returnstuff)
)
);
}
// ========================================
// Command Implementations
// ========================================
/**
* /tiedup returnstuff <player>
*
* Restore a player's confiscated inventory from the NBT backup.
* Items are given directly to the player, bypassing the chest.
*/
private static int returnstuff(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
ServerPlayer targetPlayer = EntityArgument.getPlayer(context, "player");
ServerLevel level = context.getSource().getLevel();
ConfiscatedInventoryRegistry registry =
ConfiscatedInventoryRegistry.get(level);
if (!registry.hasConfiscatedInventory(targetPlayer.getUUID())) {
context
.getSource()
.sendFailure(
Component.literal(
targetPlayer.getName().getString() +
" has no confiscated inventory to restore"
)
);
return 0;
}
boolean restored = registry.restoreInventory(targetPlayer);
if (restored) {
context
.getSource()
.sendSuccess(
() ->
Component.literal(
"\u00a7aRestored confiscated inventory to " +
targetPlayer.getName().getString()
),
true
);
SystemMessageManager.sendToPlayer(
targetPlayer,
SystemMessageManager.MessageCategory.INFO,
"Your confiscated items have been returned!"
);
return 1;
}
context
.getSource()
.sendFailure(
Component.literal(
"Failed to restore inventory for " +
targetPlayer.getName().getString()
)
);
return 0;
}
}

View File

@@ -0,0 +1,243 @@
package com.tiedup.remake.commands.subcommands;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.commands.CommandHelper;
import com.tiedup.remake.entities.EntityMaster;
import com.tiedup.remake.entities.ModEntities;
import com.tiedup.remake.entities.ai.master.MasterState;
import com.tiedup.remake.state.PlayerBindState;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.MobSpawnType;
/**
* Master/pet-play test sub-commands for /tiedup.
* Handles: mastertest, masterchair, mastertask
*/
@SuppressWarnings("null")
public class MasterTestSubCommand {
public static void register(LiteralArgumentBuilder<CommandSourceStack> root) {
// /tiedup mastertest
root.then(
Commands.literal("mastertest")
.requires(CommandHelper.REQUIRES_OP)
.executes(MasterTestSubCommand::mastertest)
);
// /tiedup masterchair
root.then(
Commands.literal("masterchair")
.requires(CommandHelper.REQUIRES_OP)
.executes(MasterTestSubCommand::masterchair)
);
// /tiedup mastertask <task>
root.then(
Commands.literal("mastertask")
.requires(CommandHelper.REQUIRES_OP)
.then(
Commands.argument("task", StringArgumentType.word())
.suggests((ctx, builder) -> {
for (MasterState s : MasterState.values()) {
builder.suggest(s.name().toLowerCase());
}
return builder.buildFuture();
})
.executes(MasterTestSubCommand::mastertask)
)
);
}
// ========================================
// Command Implementations
// ========================================
/**
* /tiedup mastertest
*
* Spawn a Master NPC nearby and immediately become its pet.
* For testing the pet play system without going through capture.
*/
private static int mastertest(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
ServerPlayer player = context.getSource().getPlayerOrException();
ServerLevel level = context.getSource().getLevel();
double x = player.getX() + player.getLookAngle().x * 2;
double y = player.getY();
double z = player.getZ() + player.getLookAngle().z * 2;
EntityMaster master = ModEntities.MASTER.get().create(level);
if (master == null) {
context
.getSource()
.sendFailure(
Component.literal("Failed to create Master entity")
);
return 0;
}
master.moveTo(x, y, z, player.getYRot() + 180F, 0.0F);
master.finalizeSpawn(
level,
level.getCurrentDifficultyAt(master.blockPosition()),
MobSpawnType.COMMAND,
null,
null
);
level.addFreshEntity(master);
master.setPetPlayer(player);
master.putPetCollar(player);
CommandHelper.syncPlayerState(player, PlayerBindState.getInstance(player));
String masterName = master.getNpcName();
if (masterName == null || masterName.isEmpty()) masterName = "Master";
String finalName = masterName;
context
.getSource()
.sendSuccess(
() ->
Component.literal(
"Spawned Master '" +
finalName +
"' \u2014 you are now their pet."
),
true
);
return 1;
}
/**
* /tiedup masterchair
*
* Force the nearest Master NPC into HUMAN_CHAIR state.
* Requires the player to be that Master's pet.
*/
private static int masterchair(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
ServerPlayer player = context.getSource().getPlayerOrException();
EntityMaster master = findNearestMaster(player);
if (master == null) {
context
.getSource()
.sendFailure(
Component.literal("No Master NPC found within 20 blocks")
);
return 0;
}
if (!master.hasPet()) {
master.setPetPlayer(player);
master.putPetCollar(player);
CommandHelper.syncPlayerState(player, PlayerBindState.getInstance(player));
}
master.setMasterState(MasterState.HUMAN_CHAIR);
String masterName = master.getNpcName();
if (masterName == null || masterName.isEmpty()) masterName = "Master";
String finalName = masterName;
context
.getSource()
.sendSuccess(
() ->
Component.literal(
"Forced " + finalName + " into HUMAN_CHAIR state"
),
true
);
return 1;
}
/**
* /tiedup mastertask <state>
*
* Force the nearest Master NPC into any MasterState.
* Useful for testing specific master behaviors.
*/
private static int mastertask(CommandContext<CommandSourceStack> context)
throws CommandSyntaxException {
ServerPlayer player = context.getSource().getPlayerOrException();
String taskName = StringArgumentType.getString(context, "task");
MasterState targetState;
try {
targetState = MasterState.valueOf(taskName.toUpperCase());
} catch (IllegalArgumentException e) {
context
.getSource()
.sendFailure(
Component.literal("Unknown MasterState: " + taskName)
);
return 0;
}
EntityMaster master = findNearestMaster(player);
if (master == null) {
context
.getSource()
.sendFailure(
Component.literal("No Master NPC found within 20 blocks")
);
return 0;
}
if (!master.hasPet()) {
master.setPetPlayer(player);
master.putPetCollar(player);
CommandHelper.syncPlayerState(player, PlayerBindState.getInstance(player));
}
master.setMasterState(targetState);
String masterName = master.getNpcName();
if (masterName == null || masterName.isEmpty()) masterName = "Master";
String finalName = masterName;
context
.getSource()
.sendSuccess(
() ->
Component.literal(
"Forced " +
finalName +
" into " +
targetState.name() +
" state"
),
true
);
return 1;
}
/**
* Find the nearest EntityMaster within 20 blocks of a player.
*/
@javax.annotation.Nullable
private static EntityMaster findNearestMaster(ServerPlayer player) {
var masters = player
.level()
.getEntitiesOfClass(
EntityMaster.class,
player.getBoundingBox().inflate(20.0),
m -> m.isAlive()
);
if (masters.isEmpty()) return null;
return masters
.stream()
.min((a, b) ->
Double.compare(a.distanceToSqr(player), b.distanceToSqr(player))
)
.orElse(null);
}
}

View File

@@ -0,0 +1,120 @@
package com.tiedup.remake.commands.subcommands;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.tiedup.remake.commands.CommandHelper;
import com.tiedup.remake.network.ModNetwork;
import com.tiedup.remake.network.sync.PacketPlayTestAnimation;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.commands.arguments.EntityArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
/**
* Test animation sub-commands for /tiedup.
* Handles: testanim <animId> [player], testanim stop [player]
*/
@SuppressWarnings("null")
public class TestAnimSubCommand {
public static void register(LiteralArgumentBuilder<CommandSourceStack> root) {
// /tiedup testanim <animId> [player]
// /tiedup testanim stop [player]
root.then(
Commands.literal("testanim")
.requires(CommandHelper.REQUIRES_OP)
.then(
Commands.literal("stop")
.executes(ctx -> testAnimStop(ctx, null))
.then(
Commands.argument("player", EntityArgument.player())
.executes(ctx ->
testAnimStop(
ctx,
EntityArgument.getPlayer(ctx, "player")
)
)
)
)
.then(
Commands.argument("animId", StringArgumentType.string())
.executes(ctx -> testAnim(ctx, null))
.then(
Commands.argument("player", EntityArgument.player())
.executes(ctx ->
testAnim(
ctx,
EntityArgument.getPlayer(ctx, "player")
)
)
)
)
);
}
// ========================================
// Command Implementations
// ========================================
/**
* /tiedup testanim <animId> [player]
* Play an animation from player_animation/ on a player.
*/
private static int testAnim(
CommandContext<CommandSourceStack> context,
ServerPlayer target
) throws CommandSyntaxException {
if (target == null) {
target = context.getSource().getPlayerOrException();
}
String animId = StringArgumentType.getString(context, "animId");
ModNetwork.sendToAllTrackingAndSelf(
new PacketPlayTestAnimation(target.getUUID(), animId),
target
);
final String name = target.getName().getString();
final String anim = animId;
context
.getSource()
.sendSuccess(
() ->
Component.literal(
"Playing animation '" + anim + "' on " + name
),
false
);
return 1;
}
/**
* /tiedup testanim stop [player]
* Stop animation on a player.
*/
private static int testAnimStop(
CommandContext<CommandSourceStack> context,
ServerPlayer target
) throws CommandSyntaxException {
if (target == null) {
target = context.getSource().getPlayerOrException();
}
ModNetwork.sendToAllTrackingAndSelf(
new PacketPlayTestAnimation(target.getUUID(), ""),
target
);
final String name = target.getName().getString();
context
.getSource()
.sendSuccess(
() -> Component.literal("Stopped animation on " + name),
false
);
return 1;
}
}