package ar.com.companeros.runtime; import ar.com.companeros.action.ToolResult; import ar.com.companeros.body.PlayerSessions; import ar.com.companeros.decision.*; import ar.com.companeros.identity.*; import ar.com.companeros.memory.*; import ar.com.companeros.social.AgentInbox; import ar.com.companeros.tools.*; import ar.com.companeros.world.*; import com.google.gson.*; import com.mojang.logging.LogUtils; import java.util.*; import net.minecraft.commands.CommandSourceStack; import net.minecraft.core.*; import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.monster.Monster; import net.minecraft.world.item.ItemStack; import net.minecraft.world.phys.Vec3; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.TickEvent; import net.minecraftforge.event.entity.living.LivingDeathEvent; import net.minecraftforge.event.server.*; import net.minecraftforge.fml.loading.FMLPaths; /** Percepción → decisión → plan → herramientas → resultados. No contiene un guion de vida. */ public final class AgentRuntime implements AutoCloseable { private static final Map SERVERS = new WeakHashMap<>(); private static final Set PERMISSIONS = Set.copyOf(EnumSet.allOf(ToolPermission.class)); private static boolean installed; private final MinecraftServer server; private final AgentConfig config; private final ToolRegistry tools; private final ToolLearningService learning; private final DecisionGateway gateway; private final ParasiteReasoning parasiteReasoning; private final String operatorInstructions; private final PersistentMemoryArchive archive; private final java.util.concurrent.ThreadPoolExecutor archiveWriter = new java.util.concurrent.ThreadPoolExecutor( 1, 1, 0, java.util.concurrent.TimeUnit.MILLISECONDS, new java.util.concurrent.ArrayBlockingQueue<>(1), runnable -> { Thread thread = new Thread(runnable, "raps-memory"); thread.setDaemon(true); return thread; }); private final Map minds = new HashMap<>(); private boolean closed; private static final class Mind { long epoch = 1, seq, lastDecisionTick = -1200, lastProgressTick; ServerPlayer body; String dimension = ""; ToolExecution execution; final ArrayDeque plan = new ArrayDeque<>(); final ArrayDeque recent = new ArrayDeque<>(); String goal, trigger = "INITIAL_OBSERVATION", activity = "Evaluando entorno", failedFingerprint = ""; UUID lastAdminMessage; long lastAdminOrder = -1; String currentFingerprint = ""; String webQuery = ""; int repeatedFailure; double progress; boolean pending; Mind(String goal) { this.goal = goal; } } private AgentRuntime(MinecraftServer server) throws java.io.IOException { this.server = server; config = AgentConfig.load(FMLPaths.CONFIGDIR.get()); operatorInstructions = AgentInstructions.load(FMLPaths.CONFIGDIR.get().resolve("raps")); archive = new PersistentMemoryArchive(FMLPaths.CONFIGDIR.get().resolve("raps").resolve("memory")); tools = new ToolRegistry(FMLPaths.CONFIGDIR.get().resolve("raps").resolve("Tools")); new ar.com.companeros.suggestions.SuggestionStore(FMLPaths.CONFIGDIR.get().resolve("raps/Suggestions")); BuiltinTools.install(tools); learning = new ToolLearningService(tools); var providerConfig = config.decisionConfig(); java.util.function.Supplier credential = providerConfig.enabled() ? new ApiCredential(FMLPaths.CONFIGDIR.get().resolve("raps")) : () -> ""; gateway = new DecisionGateway(providerConfig, credential, new StrategicGate(providerConfig.solEnabled()), BudgetLedger.file(FMLPaths.CONFIGDIR.get().resolve("raps/budget.json"))); parasiteReasoning = new ParasiteReasoning(server, gateway, providerConfig, archive); } // Vida del servicio: el banco de GameTests controla sus propios fixtures. public static synchronized void install() { if (installed) return; installed = true; MinecraftForge.EVENT_BUS.addListener(AgentRuntime::started); MinecraftForge.EVENT_BUS.addListener(AgentRuntime::tick); MinecraftForge.EVENT_BUS.addListener(AgentRuntime::stopping); MinecraftForge.EVENT_BUS.addListener(AgentRuntime::death); } private static void started(ServerStartedEvent event) { if (event.getServer() instanceof net.minecraft.gametest.framework.GameTestServer) return; AgentRuntime runtime; try { runtime = get(event.getServer()); } catch (RuntimeException failure) { LogUtils.getLogger().error("No se pudo iniciar el controlador de agentes; revisá config/raps", failure); return; } if (runtime.config.autoConnect()) for (AgentIdentity identity : AgentRegistry.get(runtime.server).entries()) { if (identity.autoLogin()) try { runtime.open(identity); } catch (RuntimeException failure) { LogUtils.getLogger().warn("No se pudo conectar al agente {}: {}", identity.name(), failure.getMessage()); } } } public static java.util.concurrent.CompletionStage appraiseUpgrade(MinecraftServer server, DecisionRequest request) { if (!server.isSameThread() || request.kind() != DecisionRequest.Kind.UPGRADE_APPRAISAL) throw new IllegalArgumentException("La tasación pertenece al servicio del servidor"); DecisionGateway gateway = get(server).gateway; gateway.updateSession(request.actorUUID(), request.sessionEpoch(), request.observationSeq()); return gateway.decide(request); } private static AgentRuntime get(MinecraftServer server) { if (!server.isSameThread()) throw new IllegalStateException("El runtime exige el hilo del servidor"); AgentRuntime current = SERVERS.get(server); if (current == null) try { current = new AgentRuntime(server); SERVERS.put(server, current); } catch (java.io.IOException failure) { throw new IllegalStateException("No se pudo cargar la biblioteca de herramientas", failure); } return current; } private static void stopping(ServerStoppingEvent event) { AgentRuntime runtime = SERVERS.remove(event.getServer()); if (runtime != null) runtime.close(); } private static void tick(TickEvent.ServerTickEvent event) { if (event.phase != TickEvent.Phase.END) return; AgentRuntime runtime = SERVERS.get(event.getServer()); if (runtime == null || runtime.closed) return; for (AgentIdentity identity : AgentRegistry.get(runtime.server).entries()) { if (identity.state() != AgentState.ACTIVE) continue; ServerPlayer body = PlayerSessions.find(runtime.server, identity.id()); if (body == null) continue; try { runtime.advance(identity, body); } catch (RuntimeException failure) { LogUtils.getLogger().error("Se pausó el controlador de {}", identity.name(), failure); Mind mind = runtime.minds.get(identity.id()); if (mind != null) runtime.invalidate(identity.id(), mind, "CONTROLLER_ERROR"); AgentRegistry.get(runtime.server).markState(identity.id(), AgentState.PAUSED); } } runtime.advanceParasite(); if (runtime.server.getTickCount() % 1200 == 0) try { runtime.tools.saveStatistics(); runtime.learning.save(); runtime.queueMemories(); } catch (java.io.IOException failure) { LogUtils.getLogger().warn("No se pudo guardar estadísticas de herramientas", failure); } } // Un tick local sigue el plan. La observación para el modelo sólo se prepara por eventos y cupo. private void advanceParasite() { try { parasiteReasoning.tick(); } catch (RuntimeException failure) { LogUtils.getLogger().error("No se pudo evaluar la intención del parásito", failure); } } private void advance(AgentIdentity identity, ServerPlayer body) { Mind mind = minds.computeIfAbsent(identity.id(), id -> new Mind(AgentJournal.get(server).entry(id).goal())); if (mind.body != body) { invalidate(identity.id(), mind, "BODY_CHANGED"); mind.body = body; } String dimension = body.level().dimension().location().toString(); if (!mind.dimension.equals(dimension)) { invalidate(identity.id(), mind, "DIMENSION_CHANGED"); mind.dimension = dimension; } if (!body.isAlive()) return; long tick = server.getTickCount(); if (tick % 20 == 0) { AgentInbox.Message admin = AgentInbox.get(server).pending(identity.id(), 64).stream() .filter(message -> message.kind() == AgentInbox.Kind.ADMIN).findFirst().orElse(null); long adminOrder = admin == null ? -1 : AgentInbox.get(server).deliveryOrder(identity.id(), admin.id()); if (admin != null && adminOrder > mind.lastAdminOrder) { mind.lastAdminMessage = admin.id(); mind.lastAdminOrder = adminOrder; invalidate(identity.id(), mind, "ADMIN_MESSAGE_RECEIVED"); } if (mind.execution != null && body.getHealth() <= 8 && !body.serverLevel().getEntitiesOfClass(Monster.class, body.getBoundingBox().inflate(6), monster -> monster.isAlive() && !(monster instanceof ar.com.companeros.horror.ParasiteEntity) && body.hasLineOfSight(monster)).isEmpty()) invalidate(identity.id(), mind, "DANGER_LOW_HEALTH"); } if (mind.execution != null) { ToolExecution.Status status = mind.execution.tick(tick); if (status == ToolExecution.Status.RUNNING) { if (mind.execution.progress() > mind.progress) { mind.progress = mind.execution.progress(); mind.lastProgressTick = tick; } if (tick - mind.lastProgressTick >= 2400) invalidate(identity.id(), mind, "NO_PROGRESS"); return; } String outcome = mind.execution.toolId() + ":" + status + ":" + mind.execution.failureReason(); remember(mind, outcome); if (status == ToolExecution.Status.SUCCEEDED) { mind.repeatedFailure = 0; mind.failedFingerprint = ""; } else { mind.repeatedFailure = mind.failedFingerprint.equals(mind.currentFingerprint) ? mind.repeatedFailure + 1 : 1; mind.failedFingerprint = mind.currentFingerprint; mind.plan.clear(); mind.trigger = "ACTION_FAILED_RECONSIDER"; } mind.execution = null; AgentJournal.get(server).checkpoint(identity.id(), mind.goal, outcome, mind.repeatedFailure, false); } // Reflejos breves no eligen proyectos: comer un alimento que ya se posee. if (body.getFoodData().getFoodLevel() <= 12 && PlayerOperations.find(body, stack -> stack.isEdible() && !stack.is(net.minecraft.world.item.Items.ROTTEN_FLESH) && !stack.is(net.minecraft.world.item.Items.PUFFERFISH) && !stack.is(net.minecraft.world.item.Items.SPIDER_EYE) && !stack.is(net.minecraft.world.item.Items.POISONOUS_POTATO)) >= 0) { start(identity.id(), mind, new ToolDecision.Step("eat", 1, Map.of())); return; } // Una amenaza visible cercana interrumpe el trabajo antes de replantearlo. if (tick % 20 == 0 && body.getHealth() <= 8 && !body.serverLevel().getEntitiesOfClass(Monster.class, body.getBoundingBox().inflate(6), monster -> monster.isAlive() && !(monster instanceof ar.com.companeros.horror.ParasiteEntity) && body.hasLineOfSight(monster)).isEmpty()) { mind.plan.clear(); mind.trigger = "DANGER_LOW_HEALTH"; } if (!identity.mode().equals("autonomo")) { mind.activity = "En pausa"; return; } if (!mind.plan.isEmpty()) { start(identity.id(), mind, mind.plan.removeFirst()); return; } if (gateway.disabled()) { mind.activity = "Esperando decisión"; return; } if (mind.pending || tick - mind.lastDecisionTick < config.decisionConfig().minAgentIntervalSeconds() * 20L) return; request(identity, body, mind); } private void start(UUID id, Mind mind, ToolDecision.Step step) { String fingerprint = step.toolId() + ":" + step.version() + ":" + new TreeMap<>(step.arguments()); if (mind.repeatedFailure >= 3 && fingerprint.equals(mind.failedFingerprint)) { mind.plan.clear(); mind.trigger = "REPEATED_FAILURE_REQUIRE_NEW_STRATEGY"; remember(mind, "Tres fallos de la misma acción: elegir otra estrategia o volver a verificar condiciones."); return; } JsonObject inputs = inputs(step.arguments()); defaults(step.toolId(), inputs); try { if (!AgentRoles.get(server).allows(id, step.toolId())) { mind.plan.clear(); remember(mind, "El rol actual delega esa tarea a otro compañero."); return; } mind.execution = startExecution(id, step.toolId(), step.version(), inputs, result -> { remember(mind, result.reason().name() + " " + String.join(" ", result.actualEffects())); for (String effect : result.actualEffects()) if (effect.startsWith("web_query:")) { mind.webQuery = effect.substring("web_query:".length()); mind.plan.clear(); } }); } catch (IllegalArgumentException rejected) { mind.plan.clear(); mind.trigger = "INVALID_TOOL_ARGUMENT_RECONSIDER"; mind.repeatedFailure = mind.failedFingerprint.equals(fingerprint) ? mind.repeatedFailure + 1 : 1; mind.failedFingerprint = fingerprint; remember(mind, "La herramienta rechazó los argumentos de " + step.toolId()); return; } mind.currentFingerprint = fingerprint; mind.activity = step.toolId(); mind.progress = 0; mind.lastProgressTick = server.getTickCount(); AgentJournal.get(server).checkpoint(id, mind.goal, "RUNNING:" + step.toolId(), mind.repeatedFailure, true); } private void request(AgentIdentity identity, ServerPlayer body, Mind mind) { mind.pending = true; mind.lastDecisionTick = server.getTickCount(); mind.seq++; gateway.updateSession(identity.id(), mind.epoch, mind.seq); List messages = AgentInbox.get(server).pending(identity.id(), 8); UUID observedAdmin = latestAdminId(server, identity.id()); boolean research = !mind.webQuery.isBlank(); String query = mind.webQuery; mind.webQuery = ""; ObservationSnapshot observation = Perception.snapshot(server, body, research ? query : mind.goal, List.copyOf(mind.recent), archive); Map capabilities = new LinkedHashMap<>(); List available = new ArrayList<>(); tools.certifiedTools().stream().filter(tool -> tool.author().equals(new UUID(0, 1))).forEach(available::add); available.addAll(learning.trialTools(identity.id())); tools.certifiedTools().stream().filter(tool -> !tool.author().equals(new UUID(0, 1))).forEach(available::add); for (ToolManifest tool : available) { if (!AgentRoles.get(server).allows(identity.id(), tool.id()) || tool.id().equals("research_web") && (!config.webEnabled() || research)) continue; if (research && !Set.of("inspect", "research_recipe", "message_internal", "message_global").contains(tool.id())) continue; if (AgentRoles.get(server).role(identity.id()) == AgentRoles.Role.INVESTIGADOR && tool.permissions().stream().anyMatch(p -> Set.of(ToolPermission.MINE, ToolPermission.PLACE, ToolPermission.CRAFT).contains(p))) continue; if (capabilities.size() >= 32 && !capabilities.containsKey(tool.id())) continue; ToolCapability previous = capabilities.get(tool.id()); if (previous != null && previous.version() > tool.version()) continue; capabilities.put(tool.id(), new ToolCapability(tool.id(), tool.version(), capabilityDescription(tool), tool.permissions().stream().map(Enum::name).collect(java.util.stream.Collectors.toSet()))); } Set allowed = PERMISSIONS.stream().map(Enum::name).collect(java.util.stream.Collectors.toSet()); String trigger = messages.isEmpty() ? mind.trigger : "MESSAGE_RECEIVED_RECONSIDER"; DecisionRequest request = new DecisionRequest(UUID.randomUUID(), identity.id(), mind.epoch, mind.seq, trigger, observation, capabilities, allowed, research ? DecisionRequest.Kind.WEB_RESEARCH : DecisionRequest.Kind.ORDINARY, null, operatorInstructions); if (research) request = new DecisionRequest(request.requestId(), identity.id(), mind.epoch, mind.seq, "Investigar Minecraft 1.20.1: " + query, observation, capabilities, allowed, DecisionRequest.Kind.WEB_RESEARCH, null, operatorInstructions); final DecisionRequest sentRequest = request; gateway.decide(sentRequest).whenComplete((answer, failure) -> server.execute(() -> { if (closed || minds.get(identity.id()) != mind || sentRequest.sessionEpoch() != mind.epoch || sentRequest.observationSeq() != mind.seq) return; mind.pending = false; if (!Objects.equals(observedAdmin, latestAdminId(server, identity.id()))) { invalidate(identity.id(), mind, "ADMIN_MESSAGE_CHANGED_REOBSERVE"); return; } if (failure != null || answer == null || !answer.validFor(identity.id(), mind.epoch, mind.seq)) { remember(mind, "DECISION:" + (answer == null ? "ERROR" : answer.status())); return; } ServerPlayer current = PlayerSessions.find(server, identity.id()); if (current != body || !current.isAlive() || !current.level().dimension().location().toString().equals(observation.dimension()) || AgentRegistry.get(server).find(identity.id()).orElseThrow().state() != AgentState.ACTIVE) return; mind.goal = answer.decision().goal(); mind.plan.addAll(answer.decision().steps()); mind.trigger = "PLAN_FINISHED"; if (research) { String report = answer.decision().briefRationale() + "\nFuentes: " + String.join(" ", answer.webSources()); if (report.length() > 2048) report = report.substring(0, 2048); long now = System.currentTimeMillis(); MemoryStore.get(server).record(new MemoryRecord(sentRequest.requestId(), identity.id(), AgentRegistry.get(server).worldId(), GlobalPos.of(body.level().dimension(), body.blockPosition()), now, body.serverLevel().getGameTime(), MemoryType.STRATEGY, .6, .5, 0, 0, MemoryOrigin.REPORTED, "web:" + sentRequest.requestId(), List.of(), report, Set.of("investigacion", "web"), Set.of(), null, false)); remember(mind, "Hallazgo web por verificar: " + report.substring(0, Math.min(480, report.length()))); } for (AgentInbox.Message message : messages) { if (storeMessage(identity.id(), body, message)) AgentInbox.get(server).acknowledge(identity.id(), message.id()); } AgentJournal.get(server).checkpoint(identity.id(), mind.goal, "PLAN_ACCEPTED", mind.repeatedFailure, !mind.plan.isEmpty()); })); } private void invalidate(UUID id, Mind mind, String reason) { if (mind.execution != null) { mind.execution.cancel(); mind.execution = null; } mind.plan.clear(); mind.pending = false; mind.epoch++; mind.seq = 0; gateway.updateSession(id, mind.epoch, mind.seq); mind.trigger = reason; remember(mind, reason); AgentJournal.get(server).checkpoint(id, mind.goal, reason, mind.repeatedFailure, true); if (PlayerSessions.find(server, id) != null) PlayerSessions.setMovementIntent(server, id, 0, 0, false); } private static UUID latestAdminId(MinecraftServer server, UUID agent) { return AgentInbox.get(server).pending(agent, 64).stream().filter(m -> m.kind() == AgentInbox.Kind.ADMIN).map(AgentInbox.Message::id).findFirst().orElse(null); } private boolean storeMessage(UUID agent, ServerPlayer body, AgentInbox.Message message) { MemoryStore memory = MemoryStore.get(server); MemoryRecord existing = memory.find(message.id()).orElse(null); if (existing != null) return existing.agentId().equals(agent) && existing.content().equals(message.body()) && existing.sourceId().equals(message.kind() + ":" + message.senderId()); return memory.record(new MemoryRecord(message.id(), agent, memory.worldId(), null, message.createdAtUtc(), server.overworld().getGameTime(), MemoryType.EXPERIENCE, .5, .5, 0, 0, MemoryOrigin.REPORTED, message.kind() + ":" + message.senderId(), List.of(), message.body(), Set.of("message"), Set.of(message.senderId()), null, false)); } private static void remember(Mind mind, String text) { if (text.length() > 512) text = text.substring(0, 512); mind.recent.addLast(text); while (mind.recent.size() > 8) mind.recent.removeFirst(); } private static void death(LivingDeathEvent event) { if (!(event.getEntity() instanceof ServerPlayer body) || body.level().isClientSide) return; AgentRuntime runtime = SERVERS.get(body.server); if (runtime == null || !PlayerSessions.isAgent(body.server, body.getUUID())) return; Mind mind = runtime.minds.get(body.getUUID()); if (mind != null) runtime.invalidate(body.getUUID(), mind, "DEATH_RECOVERY"); MemoryStore memory = MemoryStore.get(body.server); long now = System.currentTimeMillis(); var attacker = event.getSource().getEntity(); String who = attacker == null ? "" : " · atacante " + attacker.getName().getString() + " " + attacker.getUUID(); memory.record(new MemoryRecord(UUID.randomUUID(), body.getUUID(), memory.worldId(), GlobalPos.of(body.level().dimension(), body.blockPosition()), now, body.server.overworld().getGameTime(), MemoryType.DEATH, .9, 1, now, body.server.overworld().getGameTime(), MemoryOrigin.OBSERVED, event.getSource().getMsgId(), List.of(), "Muerte: " + event.getSource().getMsgId() + " en " + body.blockPosition().toShortString() + who, Set.of("death", "danger"), attacker == null ? Set.of() : Set.of(attacker.getUUID()), null, true)); } private void open(AgentIdentity identity) { if (ar.com.companeros.horror.HorrorModule.isInfected(server, identity.id())) throw new IllegalStateException("Ese jugador es el huésped del parásito."); if (PlayerSessions.find(server, identity.id()) != null) { AgentRegistry.get(server).markState(identity.id(), AgentState.ACTIVE); return; } if (PlayerSessions.players(server).size() >= config.populationLimit()) throw new IllegalStateException("Se alcanzó la población habilitada"); GlobalPos camp = identity.camp(); var level = camp == null ? server.overworld() : server.getLevel(camp.dimension()); if (level == null) throw new IllegalStateException("La dimensión del campamento no está disponible"); BlockPos pos = camp == null ? level.getSharedSpawnPos() : camp.pos(); PlayerSessions.create(server, identity.id(), identity.name(), level, pos); AgentRegistry.get(server).markState(identity.id(), AgentState.ACTIVE); Mind mind = minds.computeIfAbsent(identity.id(), id -> new Mind(AgentJournal.get(server).entry(id).goal())); mind.trigger = AgentJournal.get(server).entry(identity.id()).interrupted() ? "RESTART_REOBSERVE" : "INITIAL_OBSERVATION"; } @Override public void close() { if (closed) return; for (var item : minds.entrySet()) invalidate(item.getKey(), item.getValue(), "SERVER_STOPPED"); closed = true; gateway.close(); archiveWriter.shutdown(); try { if (archiveWriter.awaitTermination(30, java.util.concurrent.TimeUnit.SECONDS)) writeMemories(memorySnapshot()); else LogUtils.getLogger().warn("El archivado sigue ocupado; SavedData conserva el estado del mundo"); tools.saveStatistics(); learning.save(); } catch (InterruptedException failure) { Thread.currentThread().interrupt(); } catch (java.io.IOException failure) { LogUtils.getLogger().warn("No se guardaron estadísticas o memoria persistente", failure); } } private Map> memorySnapshot() { MemoryStore memory = MemoryStore.get(server); Map> snapshot = new HashMap<>(); for (AgentIdentity identity : AgentRegistry.get(server).entries()) if (identity.state() != AgentState.LEGACY) snapshot.put(identity.id(), memory.recordsFor(identity.id())); return Map.copyOf(snapshot); } private void writeMemories(Map> snapshot) throws java.io.IOException { for (var entry : snapshot.entrySet()) archive.sync(entry.getKey(), entry.getValue()); } private void queueMemories() { Map> snapshot = memorySnapshot(); try { archiveWriter.execute(() -> { try { writeMemories(snapshot); } catch (java.io.IOException failure) { LogUtils.getLogger().error("No se pudo archivar memoria fuera del mundo", failure); } }); } catch (java.util.concurrent.RejectedExecutionException busy) { LogUtils.getLogger().warn("Archivador ocupado; se conserva SavedData para la siguiente sincronización"); } } // Controles humanos. Un agente nunca hereda los permisos de su administrador. public static boolean isRegisteredPlayer(MinecraftServer server, String name) { return find(server, name) != null; } private static AgentIdentity find(MinecraftServer server, String name) { return AgentRegistry.get(server).entries().stream().filter(a -> a.state() != AgentState.LEGACY && a.state() != AgentState.RETIRED && a.name().equalsIgnoreCase(name)).findFirst().orElse(null); } private static boolean authorized(CommandSourceStack source, AgentIdentity identity, boolean admin) { if (source.getEntity() != null && (!(source.getEntity() instanceof ServerPlayer player) || !AgentRegistry.get(source.getServer()).isHuman(player) || PlayerSessions.isAgent(source.getServer(), player.getUUID()))) return false; return source.hasPermission(2) || !admin && identity != null && source.getEntity() != null && source.getEntity().getUUID().equals(identity.legacyOwner()); } private static int reject(CommandSourceStack source, String text) { source.sendFailure(Component.literal(text)); return 0; } private static int reply(CommandSourceStack source, String text) { source.sendSuccess(() -> Component.literal(text), false); return 1; } public static int create(CommandSourceStack source, String name) { if (!authorized(source, null, true)) return reject(source, "Necesitás permiso de administrador."); if (!name.matches("[A-Za-z0-9_]{1,16}") || name.equalsIgnoreCase("all")) return reject(source, "Usá un nombre de hasta 16 letras, números o guiones bajos."); AgentRuntime runtime = get(source.getServer()); AgentRegistry registry = AgentRegistry.get(source.getServer()); AgentIdentity previous = registry.entries().stream().filter(a -> a.name().equalsIgnoreCase(name)).findFirst().orElse(null); if (previous != null) return reject(source, previous.state() == AgentState.RETIRED || previous.state() == AgentState.LEGACY ? "Ese nombre tiene datos de un bot retirado o del prototipo. Elegí otro nombre." : "Ese nombre ya tiene datos guardados. Usá /Raps join bot " + previous.name() + "."); if (registry.entries().stream().filter(a -> a.state() != AgentState.LEGACY && a.state() != AgentState.RETIRED).count() >= runtime.config.populationLimit()) return reject(source, "Se alcanzó la población habilitada."); UUID id = PlayerProfileIds.offline(name); BlockPos pos; if (source.getServer().getPlayerList().getPlayerByName(name) != null || java.nio.file.Files.exists(source.getServer().getWorldPath(net.minecraft.world.level.storage.LevelResource.PLAYER_DATA_DIR).resolve(id + ".dat"))) return reject(source, "Ese nombre ya pertenece a un jugador del mundo. Elegí otro para el bot."); if (source.getEntity() == null) { BlockPos spawn = source.getLevel().getSharedSpawnPos(); int x = spawn.getX() + 2, z = spawn.getZ(); // getHeight devuelve la altura mínima cuando el chunk todavía no está cargado. source.getLevel().getChunk(x >> 4, z >> 4); pos = new BlockPos(x, source.getLevel().getHeight(net.minecraft.world.level.levelgen.Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z), z); } else pos = BlockPos.containing(source.getPosition().add(1.5, 0, 0)); AgentIdentity identity = registry.register(id, name, source.getEntity() == null ? null : source.getEntity().getUUID(), GlobalPos.of(source.getLevel().dimension(), pos), "autonomo"); try { runtime.open(identity); return reply(source, name + " se sumó a la aventura."); } catch (RuntimeException failure) { registry.markState(id, AgentState.RETIRED); return reject(source, "No se pudo conectar a " + name + ": " + failure.getMessage()); } } public static int connect(CommandSourceStack source, String name) { if (!authorized(source, null, true)) return reject(source, "Necesitás permiso de administrador."); AgentIdentity identity = find(source.getServer(), name); if (identity == null) return reject(source, "No hay un bot disponible con ese nombre. Usá /Raps estado o /Raps crear ."); try { get(source.getServer()).open(identity); return reply(source, name + " conectado."); } catch (RuntimeException failure) { return reject(source, "No se pudo conectar: " + failure.getMessage()); } } public static List joinableNames(MinecraftServer server) { return AgentRegistry.get(server).entries().stream() .filter(a -> a.state() != AgentState.LEGACY && a.state() != AgentState.RETIRED) .filter(a -> !ar.com.companeros.horror.HorrorModule.isInfected(server, a.id())) .map(AgentIdentity::name).toList(); } public static int connectAll(CommandSourceStack source) { if (!authorized(source, null, true)) return reject(source, "Necesitás permiso de administrador."); AgentRuntime runtime = get(source.getServer()); List names = joinableNames(source.getServer()); if (names.isEmpty()) return reject(source, "No hay bots guardados. Creá uno con /Raps crear ."); int joined = 0; for (String name : names) { AgentIdentity identity = find(source.getServer(), name); if (PlayerSessions.find(source.getServer(), identity.id()) != null) continue; try { runtime.open(identity); joined++; } catch (RuntimeException failure) { reject(source, name + ": " + failure.getMessage()); } } reply(source, "Se conectaron " + joined + " bot(s). Conectados: " + PlayerSessions.players(source.getServer()).size() + "."); return joined; } public static int pause(CommandSourceStack source, String name) { AgentIdentity identity = find(source.getServer(), name); if (identity == null || !authorized(source, identity, false)) return reject(source, "No encontré un bot que puedas pausar."); if (ar.com.companeros.horror.HorrorModule.isInfected(source.getServer(), identity.id())) return reject(source, "El huésped se controla mediante /Raps mimicer."); AgentRuntime runtime = get(source.getServer()); Mind mind = runtime.minds.get(identity.id()); if (mind != null) runtime.invalidate(identity.id(), mind, "ADMIN_PAUSED"); AgentRegistry.get(source.getServer()).markState(identity.id(), AgentState.PAUSED); PlayerSessions.remove(source.getServer(), identity.id()); return reply(source, name + " pausado. Su inventario está guardado."); } public static int retire(CommandSourceStack source, String name) { AgentIdentity identity = find(source.getServer(), name); if (identity == null || !authorized(source, identity, true)) return reject(source, "No encontré un bot que puedas retirar."); if (ar.com.companeros.horror.HorrorModule.isInfected(source.getServer(), identity.id())) return reject(source, "El huésped conserva su identidad durante la infección."); pause(source, name); AgentRegistry.get(source.getServer()).markState(identity.id(), AgentState.RETIRED); return reply(source, name + " retirado."); } public static int setMode(CommandSourceStack source, String name, String mode) { AgentIdentity identity = find(source.getServer(), name); if (identity == null || !authorized(source, identity, false)) return reject(source, "No encontré un compañero tuyo con ese nombre."); if (mode.equals("seguir")) return reject(source, "Usá /botmalo para coordinar una tarea con " + name + "."); AgentRuntime runtime = get(source.getServer()); Mind mind = runtime.minds.get(identity.id()); if (mind != null) runtime.invalidate(identity.id(), mind, "MODE_CHANGED"); AgentRegistry.get(source.getServer()).updateSettings(identity.id(), identity.camp(), mode); return reply(source, name + ": " + mode); } public static int setRole(CommandSourceStack source, String name, AgentRoles.Role role) { AgentIdentity identity = find(source.getServer(), name); if (identity == null || !authorized(source, identity, true)) return reject(source, "Necesitás un bot registrado y permiso de administrador."); AgentRoles.get(source.getServer()).assign(identity.id(), role); AgentRuntime runtime = get(source.getServer()); Mind mind = runtime.minds.get(identity.id()); if (mind != null) runtime.invalidate(identity.id(), mind, "ROLE_CHANGED"); return reply(source, name + " · Rol: " + role.name().toLowerCase(Locale.ROOT)); } public static int setCamp(CommandSourceStack source, String name) { AgentIdentity identity = find(source.getServer(), name); if (identity == null || !authorized(source, identity, false)) return reject(source, "No encontré un compañero tuyo con ese nombre."); GlobalPos camp = GlobalPos.of(source.getLevel().dimension(), BlockPos.containing(source.getPosition())); AgentRuntime runtime = get(source.getServer()); Mind mind = runtime.minds.get(identity.id()); if (mind != null) runtime.invalidate(identity.id(), mind, "CAMP_CHANGED"); AgentRegistry.get(source.getServer()).updateSettings(identity.id(), camp, identity.mode()); return reply(source, "Campamento de " + name + " marcado."); } public static int inventory(CommandSourceStack source, String name) { AgentIdentity identity = find(source.getServer(), name); if (identity == null || !authorized(source, identity, false)) return reject(source, "No encontré un compañero tuyo con ese nombre."); ServerPlayer player = PlayerSessions.find(source.getServer(), identity.id()); if (player == null) return reject(source, "Conectá al bot para consultar su inventario."); List entries = new ArrayList<>(); for (int i = 0; i < player.getInventory().getContainerSize(); i++) { ItemStack stack = player.getInventory().getItem(i); if (!stack.isEmpty()) entries.add(stack.getCount() + " " + stack.getHoverName().getString()); } return reply(source, name + ": " + (entries.isEmpty() ? "Inventario vacío" : String.join(", ", entries))); } public static int handOver(CommandSourceStack source, String name) { AgentIdentity identity = find(source.getServer(), name); if (identity == null || !authorized(source, identity, false)) return reject(source, "No encontré un compañero tuyo con ese nombre."); ServerPlayer player = PlayerSessions.find(source.getServer(), identity.id()); if (player == null || source.getEntity() == null || player.distanceToSqr(source.getEntity()) > 16) return reject(source, "Acercate al compañero para recibir sus objetos."); for (int i = 0; i < 36; i++) { ItemStack stack = player.getInventory().removeItemNoUpdate(i); if (!stack.isEmpty()) player.drop(stack, false, true); } player.getInventory().setChanged(); return reply(source, "Objetos entregados."); } public static List describe(MinecraftServer server) { AgentRuntime runtime = get(server); List rows = new ArrayList<>(); for (AgentIdentity identity : AgentRegistry.get(server).entries()) { if (identity.state() == AgentState.LEGACY || identity.state() == AgentState.RETIRED) continue; ServerPlayer player = PlayerSessions.find(server, identity.id()); Mind mind = runtime.minds.get(identity.id()); boolean infected = ar.com.companeros.horror.HorrorModule.isInfected(server, identity.id()); String activity = infected ? "Huésped · Etapa " + ar.com.companeros.horror.HorrorState.get(server).stage(server.overworld().getGameTime(), ar.com.companeros.horror.HorrorModule.config(server)) : player == null ? "Desconectado" : mind == null ? "Evaluando entorno" : mind.activity; rows.add(identity.name() + " · " + activity + (player == null ? "" : " · Salud " + (int)player.getHealth() + "/20 · Comida " + player.getFoodData().getFoodLevel() + "/20 · " + player.blockPosition().toShortString()) + " · Rol " + AgentRoles.get(server).role(identity.id()).name().toLowerCase(Locale.ROOT) + " · UUID " + identity.id() + " · Mensajes " + AgentInbox.get(server).pending(identity.id(), 64).size()); } return List.copyOf(rows); } public static List toolNames(MinecraftServer server) { return get(server).tools.certifiedTools().stream().map(t -> t.id() + " v" + t.version() + " · " + t.description()).toList(); } public static int proposeTool(CommandSourceStack source, String name, String json) { AgentIdentity identity = find(source.getServer(), name); if (identity == null || !authorized(source, identity, true)) return reject(source, "Necesitás un bot registrado y permiso de administrador."); if (ar.com.companeros.horror.HorrorModule.isInfected(source.getServer(), identity.id())) return reject(source, "El huésped ya no propone herramientas de jugador."); try { var proposal = get(source.getServer()).learning.propose(identity.id(), json); return reply(source, name + " propuso " + proposal.manifest().id() + " v" + proposal.manifest().version() + ". Pendiente de pruebas."); } catch (java.io.IOException | IllegalArgumentException failure) { return reject(source, "La propuesta no pudo validarse o guardarse."); } } public static int toolState(CommandSourceStack source, String name) { AgentIdentity identity = find(source.getServer(), name); if (identity == null || !authorized(source, identity, true)) return reject(source, "Necesitás un bot registrado y permiso de administrador."); AgentRuntime runtime = get(source.getServer()); List all = new ArrayList<>(runtime.tools.certifiedTools()); runtime.tools.pendingProposals().stream().map(ToolRegistry.Proposal::manifest).forEach(all::add); List own = all.stream().filter(tool -> tool.author().equals(identity.id())).limit(64).toList(); if (own.isEmpty()) return reply(source, name + " todavía no creó herramientas."); for (ToolManifest tool : own) { var progress = runtime.learning.statistics(tool.sha256()); reply(source, tool.id() + " v" + tool.version() + " · " + (progress.certified() ? "Certificada" : "En pruebas") + " · Éxitos " + progress.successes() + " · Fallos " + progress.failures() + " · Cancelaciones " + progress.cancellations()); } return own.size(); } public static int runTool(CommandSourceStack source, String name, String tool, String json) { AgentIdentity identity = find(source.getServer(), name); if (identity == null || !authorized(source, identity, true)) return reject(source, "Necesitás un bot registrado y permiso de administrador."); AgentRuntime runtime = get(source.getServer()); ServerPlayer player = PlayerSessions.find(source.getServer(), identity.id()); if (player == null) return reject(source, "Conectá al bot primero."); if (ar.com.companeros.horror.HorrorModule.isInfected(source.getServer(), identity.id())) return reject(source, "El huésped ya no usa herramientas de jugador."); try { if (json.length() > 4096) throw new IllegalArgumentException(); JsonObject parsed = JsonParser.parseString(json).getAsJsonObject(); defaults(tool, parsed); Mind mind = runtime.minds.computeIfAbsent(identity.id(), id -> new Mind(AgentJournal.get(runtime.server).entry(id).goal())); runtime.invalidate(identity.id(), mind, "ADMIN_TOOL_TEST"); mind.body = player; mind.dimension = player.level().dimension().location().toString(); int version = runtime.toolVersion(identity.id(), tool); mind.execution = runtime.startExecution(identity.id(), tool, version, parsed, r -> remember(mind, r.status() + ":" + r.reason())); mind.activity = tool; mind.lastProgressTick = runtime.server.getTickCount(); mind.progress = 0; AgentJournal.get(runtime.server).checkpoint(identity.id(), mind.goal, "ADMIN_TOOL:" + tool, mind.repeatedFailure, true); return reply(source, name + " inició " + tool + "."); } catch (RuntimeException failure) { return reject(source, "La herramienta o sus argumentos no son válidos."); } } public static int setPopulationLimit(CommandSourceStack source, int value) { if (!authorized(source, null, true)) return reject(source, "Necesitás permiso de administrador."); AgentRuntime runtime = get(source.getServer()); if (PlayerSessions.players(source.getServer()).size() > value) return reject(source, "Pausá bots antes de reducir la población."); runtime.config.setPopulationLimit(value); return reply(source, "Población habilitada: " + value + "."); } private static void defaults(String tool, JsonObject inputs) { if ((tool.equals("craft") || tool.equals("store")) && !inputs.has("count")) inputs.addProperty("count", tool.equals("craft") ? 1 : 64); if (tool.equals("collect") && !inputs.has("radius")) inputs.addProperty("radius", 1); if (tool.equals("create_banner") && !inputs.has("symbol")) inputs.addProperty("symbol", "none"); if (tool.equals("create_banner") && !inputs.has("symbolColor")) inputs.addProperty("symbolColor", "black"); } // Una propuesta sólo se prueba por su autor; las certificadas se comparten entre jugadores. private ToolExecution startExecution(UUID actor, String tool, int version, JsonObject inputs, java.util.function.Consumer completed) { var context = new ToolRegistry.ExecutionContext(actor, server.getTickCount(), PERMISSIONS, inputs); var candidate = learning.pending(actor, tool, version); ToolManifest manifest = candidate.orElseGet(() -> tools.certifiedTools().stream() .filter(value -> value.id().equals(tool) && value.version() == version).findFirst() .orElseThrow(() -> new IllegalArgumentException("Herramienta desconocida"))); if (AgentRoles.get(server).role(actor) == AgentRoles.Role.INVESTIGADOR && manifest.permissions().stream().anyMatch(permission -> Set.of(ToolPermission.MINE, ToolPermission.PLACE, ToolPermission.CRAFT).contains(permission))) throw new IllegalArgumentException("El investigador delega esa producción"); return candidate.isPresent() ? learning.startTrial(candidate.get().sha256(), context, server, completed) : tools.start(tool, version, context, new WorldPrimitiveExecutor(server, completed, learning)); } private static String capabilityDescription(ToolManifest tool) { String description = tool.description(); return description.substring(0, Math.min(410, description.length())) + " sha256=" + tool.sha256(); } private int toolVersion(UUID actor, String tool) { return java.util.stream.Stream.concat(tools.certifiedTools().stream(), learning.trialTools(actor).stream()) .filter(manifest -> manifest.id().equals(tool)).mapToInt(ToolManifest::version).max().orElse(1); } private static JsonObject inputs(Map arguments) { JsonObject inputs = new JsonObject(); for (var entry : arguments.entrySet()) { if (Set.of("x", "y", "z", "count", "radius").contains(entry.getKey())) inputs.addProperty(entry.getKey(), Double.parseDouble(entry.getValue())); else inputs.addProperty(entry.getKey(), entry.getValue()); } return inputs; } }