Volver al índice
src/main/java/ar/com/companeros/runtime/AgentRuntime.java
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<MinecraftServer, AgentRuntime> SERVERS = new WeakHashMap<>();
private static final Set<ToolPermission> 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<UUID, Mind> 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<ToolDecision.Step> plan = new ArrayDeque<>();
final ArrayDeque<String> 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<String> 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<DecisionResponse> 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<AgentInbox.Message> 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<String, ToolCapability> capabilities = new LinkedHashMap<>();
List<ToolManifest> 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<String> 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<UUID, List<MemoryRecord>> memorySnapshot() {
MemoryStore memory = MemoryStore.get(server);
Map<UUID, List<MemoryRecord>> 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<UUID, List<MemoryRecord>> snapshot) throws java.io.IOException {
for (var entry : snapshot.entrySet()) archive.sync(entry.getKey(), entry.getValue());
}
private void queueMemories() {
Map<UUID, List<MemoryRecord>> 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 <nombre>.");
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<String> 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<String> names = joinableNames(source.getServer());
if (names.isEmpty()) return reject(source, "No hay bots guardados. Creá uno con /Raps crear <nombre>.");
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<String> 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<String> describe(MinecraftServer server) {
AgentRuntime runtime = get(server); List<String> 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<String> 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<ToolManifest> all = new ArrayList<>(runtime.tools.certifiedTools());
runtime.tools.pendingProposals().stream().map(ToolRegistry.Proposal::manifest).forEach(all::add);
List<ToolManifest> 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<ToolResult> 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<String, String> 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;
}
}