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. * * Commands: * /blockplayer - Block a player from interacting with you * /unblockplayer - Unblock a player * /checkblocked - Check if a player has blocked you * /norp - Announce non-consent to current RP (cooldown 45s) * /me - Roleplay action message in local area * /pm - 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 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 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 context) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); Optional 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.translatable("command.tiedup.social.cannot_block_self")); return 0; } SocialData data = SocialData.get(player.serverLevel()); if (data.isBlocked(player.getUUID(), target.getUUID())) { source.sendFailure( Component.translatable("command.tiedup.social.already_blocked", target.getName().getString()) ); return 0; } data.addBlock(player.getUUID(), target.getUUID()); source.sendSuccess( () -> Component.translatable("command.tiedup.social.blocked", target.getName().getString()).withStyle(ChatFormatting.GREEN), false ); TiedUpMod.LOGGER.info( "[SOCIAL] {} blocked {}", player.getName().getString(), target.getName().getString() ); return 1; } private static int unblockPlayer(CommandContext context) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); Optional 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.translatable("command.tiedup.social.not_blocked", target.getName().getString()) ); return 0; } data.removeBlock(player.getUUID(), target.getUUID()); source.sendSuccess( () -> Component.translatable("command.tiedup.social.unblocked", target.getName().getString()).withStyle(ChatFormatting.GREEN), false ); return 1; } private static int checkBlocked(CommandContext context) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); Optional 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.translatable("command.tiedup.social.has_blocked_you", target.getName().getString()).withStyle(ChatFormatting.RED), false ); } else { source.sendSuccess( () -> Component.translatable("command.tiedup.social.has_not_blocked_you", target.getName().getString()).withStyle(ChatFormatting.GREEN), 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 context) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); Optional 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.translatable("command.tiedup.social.norp_cooldown", remaining) ); return 0; } // Set cooldown NORP_COOLDOWNS.put(player.getUUID(), now); // Broadcast to all players Component message = Component.literal("") .append( Component.translatable("command.tiedup.social.norp_prefix").withStyle( ChatFormatting.RED, ChatFormatting.BOLD ) ) .append( Component.literal(player.getName().getString()).withStyle( ChatFormatting.YELLOW ) ) .append( Component.translatable( "command.tiedup.social.norp_announcement" ).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 context) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); Optional 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 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 context ) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); Optional 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.translatable("command.tiedup.social.pm_blocked") ); return 0; } // Send to target (earplug-aware) Component toTarget = Component.literal("") .append( Component.translatable( "command.tiedup.social.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.translatable( "command.tiedup.social.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 context, int distance ) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); Optional 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.translatable("command.tiedup.social.talkarea_disabled").withStyle(ChatFormatting.GREEN), false ); } else { source.sendSuccess( () -> Component.translatable("command.tiedup.social.talkarea_set", distance).withStyle(ChatFormatting.GREEN), false ); } return 1; } private static int talkInfo(CommandContext context) throws CommandSyntaxException { CommandSourceStack source = context.getSource(); Optional 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.translatable("command.tiedup.social.talkinfo_disabled").withStyle(ChatFormatting.YELLOW), false ); } else { source.sendSuccess( () -> Component.translatable("command.tiedup.social.talkinfo_distance", talkArea).withStyle(ChatFormatting.YELLOW), false ); } return 1; } }