package ar.com.companeros.social; import ar.com.companeros.identity.*; import com.mojang.brigadier.arguments.StringArgumentType; import java.util.*; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.MinecraftServer; import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.event.ServerChatEvent; import static net.minecraft.commands.Commands.*; /** Canal explícito de administración: un mensaje inline o el siguiente mensaje de chat. */ public final class AdminChannel { private static final Map armed = new java.util.concurrent.ConcurrentHashMap<>(); private static final UUID CONSOLE = new UUID(0, 0); private static boolean lifecycleInstalled; private record Administrator(MinecraftServer server, UUID id) { } private record Selection(List recipients, long expiresAt) { private Selection { recipients = List.copyOf(recipients); } } private AdminChannel() { } public static void register(RegisterCommandsEvent event) { installLifecycle(); var root = literal("botmalo").requires(AdminChannel::allowed); root.then(literal("cancelar").executes(ctx -> { clearSelection(ctx.getSource()); ctx.getSource().sendSuccess(() -> Component.literal("Canal de bots cancelado."), false); return 1; })); root.then(argument("destino", StringArgumentType.word()) .executes(ctx -> { var source = ctx.getSource(); String target = StringArgumentType.getString(ctx, "destino"); List selected = recipients(source, target); if (selected.isEmpty()) return missing(source); ServerPlayer admin = source.getPlayerOrException(); armed.put(new Administrator(source.getServer(), admin.getUUID()), new Selection(selected.stream().map(AgentIdentity::id).toList(), System.currentTimeMillis() + 120_000)); source.sendSuccess(() -> Component.literal("Tu próximo mensaje de chat será enviado a " + target + "."), false); return 1; }) .then(argument("mensaje", StringArgumentType.greedyString()).executes(ctx -> send(ctx.getSource(), StringArgumentType.getString(ctx, "destino"), StringArgumentType.getString(ctx, "mensaje"))))); event.getDispatcher().register(root); } // Solo un humano con permisos o la consola puede generar una intervención de administrador. private static boolean allowed(CommandSourceStack source) { return source != null && source.getServer().isSameThread() && source.hasPermission(2) && (source.getEntity() == null || source.getEntity() instanceof ServerPlayer player && !(player instanceof net.minecraftforge.common.util.FakePlayer) && !ar.com.companeros.body.PlayerSessions.isAgent(source.getServer(), player.getUUID()) && !AgentRegistry.get(source.getServer()).isAgent(player.getUUID())); } public static void chat(ServerChatEvent event) { ServerPlayer admin = event.getPlayer(); Selection selection = armed.remove(new Administrator(admin.server, admin.getUUID())); if (selection == null) return; // Aunque haya vencido o perdido OP, este texto privado no vuelve al chat global. event.setCanceled(true); if (selection.expiresAt() < System.currentTimeMillis()) { admin.server.execute(() -> admin.sendSystemMessage(Component.literal("El canal de bots venció. Volvé a usar /botmalo."))); return; } String text = event.getRawText(); admin.server.execute(() -> { if (admin.server.getPlayerList().getPlayer(admin.getUUID()) != admin || admin.hasDisconnected()) return; CommandSourceStack source = admin.createCommandSourceStack(); if (!allowed(source)) { source.sendFailure(Component.literal("El canal de bots ya no está autorizado.")); return; } sendSelected(source, selection.recipients(), text); }); } public static int send(CommandSourceStack source, String target, String message) { if (!allowed(source)) return 0; if (target == null || target.isBlank() || target.length() > 48 || message == null) return 0; clearSelection(source); var agents = recipients(source, target); if (agents.isEmpty()) return missing(source); return sendSelected(source, agents.stream().map(AgentIdentity::id).toList(), message); } private static int sendSelected(CommandSourceStack source, List selected, String message) { if (!allowed(source)) return 0; AgentRegistry registry = AgentRegistry.get(source.getServer()); List agents = selected.stream().distinct().map(registry::find).flatMap(Optional::stream) .filter(agent -> agent.state() != AgentState.RETIRED).toList(); if (agents.isEmpty()) return missing(source); UUID adminId = source.getEntity() == null ? CONSOLE : source.getEntity().getUUID(); int delivered = 0; for (AgentIdentity agent : agents) if (AgentInbox.get(source.getServer()).deliver(agent.id(), adminId, source.getTextName(), AgentInbox.Kind.ADMIN, message)) delivered++; int count = delivered; if (count == 0) source.sendFailure(Component.literal("No se pudo entregar: el mensaje está vacío, es demasiado largo o el buzón está lleno.")); else source.sendSuccess(() -> Component.literal("Mensaje del administrador enviado a " + count + " bot(s)."), false); return count; } private static List recipients(CommandSourceStack source, String target) { return AgentRegistry.get(source.getServer()).entries().stream().filter(agent -> agent.state() != AgentState.RETIRED) .filter(agent -> target.equalsIgnoreCase("all") || agent.name().equalsIgnoreCase(target)).toList(); } private static void clearSelection(CommandSourceStack source) { if (source.getEntity() != null) armed.remove(new Administrator(source.getServer(), source.getEntity().getUUID())); } private static synchronized void installLifecycle() { if (lifecycleInstalled) return; lifecycleInstalled = true; net.minecraftforge.common.MinecraftForge.EVENT_BUS.addListener(AdminChannel::loggedOut); net.minecraftforge.common.MinecraftForge.EVENT_BUS.addListener(AdminChannel::stopping); } private static void loggedOut(net.minecraftforge.event.entity.player.PlayerEvent.PlayerLoggedOutEvent event) { if (event.getEntity() instanceof ServerPlayer player) armed.remove(new Administrator(player.server, player.getUUID())); } private static void stopping(net.minecraftforge.event.server.ServerStoppingEvent event) { armed.keySet().removeIf(key -> key.server() == event.getServer()); } private static int missing(CommandSourceStack source) { source.sendFailure(Component.literal("No hay bots registrados con ese nombre.")); return 0; } }