package ar.com.companeros.world; import ar.com.companeros.action.ToolResult; import ar.com.companeros.body.PlayerSessions; import ar.com.companeros.memory.MemoryStore; import ar.com.companeros.social.AgentCommunication; import ar.com.companeros.tools.PrimitiveExecutor; import com.google.gson.JsonObject; import java.util.*; import java.util.function.Consumer; import net.minecraft.core.*; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.item.ItemEntity; import net.minecraft.world.item.Item; import net.minecraftforge.registries.ForgeRegistries; /** Adaptación de herramientas: sólo entidades, inventarios y acciones del servidor. */ public final class WorldPrimitiveExecutor implements PrimitiveExecutor { private final MinecraftServer server; private final Consumer completed; private final ar.com.companeros.tools.ToolLearningService learning; public WorldPrimitiveExecutor(MinecraftServer server, Consumer completed) { this(server, completed, null); } public WorldPrimitiveExecutor(MinecraftServer server, Consumer completed, ar.com.companeros.tools.ToolLearningService learning) { this.server = server; this.completed = completed; this.learning = learning; } // Entrada única. La biblioteca ya comprobó argumentos y permisos; se valida el cuerpo otra vez. @Override public PrimitiveTask begin(UUID actor, UUID invocation, String primitive, JsonObject args) { ServerPlayer player = PlayerSessions.find(server, actor); if (player == null || !player.isAlive() || ar.com.companeros.horror.HorrorModule.isInfected(server, actor)) return failed("BODY_UNAVAILABLE"); if (!ar.com.companeros.identity.AgentRoles.get(server).allows(actor, primitive)) return failed("ROLE_RESTRICTED"); return switch (primitive) { case "sneak" -> once(player, invocation, () -> { player.setShiftKeyDown(args.get("enabled").getAsBoolean()); return immediate(player, invocation, ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, Map.of(), "sneaking:" + player.isShiftKeyDown()); }); case "suggest_change" -> once(player, invocation, () -> { try { var store = new ar.com.companeros.suggestions.SuggestionStore(net.minecraftforge.fml.loading.FMLPaths.CONFIGDIR.get().resolve("raps/Suggestions")); var folder = store.submit(player.getUUID(), player.getGameProfile().getName(), player.level().dimension().location() + " " + player.blockPosition().toShortString(), args.get("manifest").getAsString(), System.currentTimeMillis()); return immediate(player, invocation, ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, Map.of(), "Suggestions/" + folder.getFileName()); } catch (java.io.IOException | RuntimeException rejected) { return immediate(player, invocation, ToolResult.Status.BLOCKED, ToolResult.Reason.NO_CHANGE, Map.of(), "Sugerencia rechazada: " + rejected.getMessage()); } }); case "move_to" -> navigation(player, invocation, position(args)); case "mine_block" -> operation(player, PlayerOperations.mine(player, invocation, position(args))); case "place_block" -> operation(player, PlayerOperations.place(player, invocation, position(args), item(args))); case "eat" -> operation(player, PlayerOperations.eat(player, invocation)); case "attack" -> operation(player, PlayerOperations.attack(player, invocation, UUID.fromString(args.get("target").getAsString()))); case "store" -> operation(player, PlayerOperations.store(player, invocation, position(args), item(args), number(args, "count", 64))); case "craft" -> crafting(player, invocation, item(args), number(args, "count", 1)); case "collect" -> once(player, invocation, () -> { Map before = PlayerCrafting.counts(player); for (ItemEntity drop : player.serverLevel().getEntitiesOfClass(ItemEntity.class, player.getBoundingBox().inflate(1, .5, 1)).stream().limit(16).toList()) drop.playerTouch(player); Map after = PlayerCrafting.counts(player); boolean changed = !after.equals(before); return immediate(player, invocation, changed ? ToolResult.Status.SUCCEEDED : ToolResult.Status.BLOCKED, changed ? ToolResult.Reason.NONE : ToolResult.Reason.NO_CHANGE, PlayerCrafting.delta(before, after), changed ? "collected" : ""); }); case "inspect" -> once(player, invocation, () -> immediate(player, invocation, ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, Map.of(), "observed:" + player.blockPosition().toShortString())); case "research_recipe" -> once(player, invocation, () -> { long now = System.currentTimeMillis(); return new ToolResult(invocation, ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, Map.of(), RecipeResearch.inspect(player, args.get("item").getAsString()), List.of(), GlobalPos.of(player.level().dimension(), player.blockPosition()), now, now); }); case "research_web" -> once(player, invocation, () -> immediate(player, invocation, ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, Map.of(), "web_query:" + args.get("query").getAsString())); case "choose_skin" -> skinSelection(player, invocation, args.get("profile").getAsString()); case "parasite_appearance_catalog" -> once(player, invocation, () -> immediate(player, invocation, ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, Map.of(), "prefabs=obsidian,iron,diamond,copper,none; slots=chest,arms,legs; fabricar visual:prefab mediante tasación y pago antes de montar; config/raps/Appearance/prefabs.json")); case "parasite_appearance" -> once(player, invocation, () -> { String outcome = ar.com.companeros.horror.ParasiteUpgrades.get(server) .appearance(player, args.get("prefab").getAsString(), args.get("slot").getAsString()); boolean success = outcome.startsWith("Aspecto montado"); return immediate(player, invocation, success ? ToolResult.Status.SUCCEEDED : ToolResult.Status.BLOCKED, success ? ToolResult.Reason.NONE : ToolResult.Reason.NO_CHANGE, Map.of(), outcome); }); case "parasite_upgrade_status" -> once(player, invocation, () -> immediate(player, invocation, ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, Map.of(), ar.com.companeros.horror.ParasiteUpgrades.get(server).describe())); case "parasite_upgrade_catalog" -> once(player, invocation, () -> { List matches = ar.com.companeros.horror.ParasiteUpgrades.catalog(args.get("query").getAsString()); long now = System.currentTimeMillis(); return new ToolResult(invocation, ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, Map.of(), matches, List.of(), GlobalPos.of(player.level().dimension(), player.blockPosition()), now, now); }); case "parasite_upgrade_propose", "parasite_upgrade_apply" -> once(player, invocation, () -> { var upgrades = ar.com.companeros.horror.ParasiteUpgrades.get(server); Map before = PlayerCrafting.counts(player); String outcome = primitive.equals("parasite_upgrade_propose") ? upgrades.propose(player, args.get("capacity").getAsString(), args.get("goal").getAsString()) : upgrades.apply(player, args.get("quote").getAsString()); boolean success = outcome.startsWith("Tasación solicitada") || outcome.startsWith("Cotización disponible") || outcome.startsWith("Mejora montada"); return immediate(player, invocation, success ? ToolResult.Status.SUCCEEDED : ToolResult.Status.BLOCKED, success ? ToolResult.Reason.NONE : ToolResult.Reason.NO_CHANGE, PlayerCrafting.delta(before, PlayerCrafting.counts(player)), outcome); }); case "propose_tool" -> once(player, invocation, () -> { if (learning == null) return immediate(player, invocation, ToolResult.Status.BLOCKED, ToolResult.Reason.NO_CHANGE, Map.of(), "tool_learning_unavailable"); try { var proposal = learning.propose(actor, args.get("manifest").getAsString()); return immediate(player, invocation, ToolResult.Status.SUCCEEDED, ToolResult.Reason.NONE, Map.of(), "tool_proposed:" + proposal.manifest().id() + ":" + proposal.manifest().version() + ":" + proposal.manifest().sha256()); } catch (java.io.IOException | IllegalArgumentException rejected) { return immediate(player, invocation, ToolResult.Status.BLOCKED, ToolResult.Reason.NO_CHANGE, Map.of(), "tool_proposal_rejected"); } }); case "create_banner" -> once(player, invocation, () -> ar.com.companeros.flags.BannerActions.create(player, invocation, args)); case "place_banner" -> once(player, invocation, () -> ar.com.companeros.flags.BannerActions.place(player, invocation, args)); case "message_internal" -> once(player, invocation, () -> { int delivered = AgentCommunication.sendInternal(server, actor, args.get("recipient").getAsString(), args.get("text").getAsString()); return immediate(player, invocation, delivered > 0 ? ToolResult.Status.SUCCEEDED : ToolResult.Status.BLOCKED, delivered > 0 ? ToolResult.Reason.NONE : ToolResult.Reason.NO_CHANGE, Map.of(), delivered > 0 ? "internal_message:" + delivered : ""); }); case "message_global" -> once(player, invocation, () -> { boolean published = AgentCommunication.sendGlobal(player, args.get("text").getAsString()); return immediate(player, invocation, published ? ToolResult.Status.SUCCEEDED : ToolResult.Status.BLOCKED, published ? ToolResult.Reason.NONE : ToolResult.Reason.NO_CHANGE, Map.of(), published ? "global_published" : ""); }); default -> failed("UNSUPPORTED"); }; } // Resultados terminales: una memoria y una notificación por invocación, nunca por tick. private PrimitiveTask skinSelection(ServerPlayer player, UUID invocation, String profile) { java.util.concurrent.CompletableFuture selected = ar.com.companeros.identity.SkinProfiles.get(server).select(player.getUUID(), profile); return new PrimitiveTask() { Result terminal; public Result tick(long tick) { if (terminal != null) return terminal; if (PlayerSessions.find(server, player.getUUID()) != player || !player.isAlive()) { selected.cancel(true); return terminal = Result.failure("BODY_CHANGED"); } if (!selected.isDone()) return Result.running(0); boolean success = !selected.isCancelled() && !selected.isCompletedExceptionally() && Boolean.TRUE.equals(selected.getNow(false)); record(player, immediate(player, invocation, success ? ToolResult.Status.SUCCEEDED : ToolResult.Status.BLOCKED, success ? ToolResult.Reason.NONE : ToolResult.Reason.NO_CHANGE, Map.of(), success ? "skin_selected:" + profile : "skin_unavailable")); return terminal = success ? Result.success() : Result.failure("SKIN_UNAVAILABLE"); } public void cancel() { selected.cancel(true); if (terminal == null) terminal = Result.failure("CANCELLED"); } }; } private void record(ServerPlayer player, ToolResult result) { MemoryStore.get(server).recordResult(player.getUUID(), result, server.overworld().getGameTime(), result.status() + ":" + result.reason() + " " + String.join(" ", result.actualEffects())); completed.accept(result); } private PrimitiveTask operation(ServerPlayer player, PlayerOperations.Operation operation) { var dimension = player.level().dimension(); return new PrimitiveTask() { Result finalResult; @Override public Result tick(long tick) { if (finalResult != null) return finalResult; if (PlayerSessions.find(server, player.getUUID()) != player || !player.level().dimension().equals(dimension)) { operation.cancel(); return finalResult = Result.failure("BODY_CHANGED"); } ToolResult result = operation.tick(tick); if (!result.finished()) return Result.running(0); record(player, result); return finalResult = result.succeeded() ? Result.success() : Result.failure(result.reason().name()); } @Override public void cancel() { operation.cancel(); if (finalResult == null) finalResult = Result.failure("CANCELLED"); } }; } private PrimitiveTask navigation(ServerPlayer player, UUID id, BlockPos target) { var dimension = player.level().dimension(); NavigationTask task = new NavigationTask(player, target, .8, server.getTickCount()); return new PrimitiveTask() { Result finalResult; @Override public Result tick(long tick) { if (finalResult != null) return finalResult; if (PlayerSessions.find(server, player.getUUID()) != player || !player.level().dimension().equals(dimension)) { task.cancel(); return finalResult = Result.failure("BODY_CHANGED"); } NavigationTask.State state = task.tick(tick); if (state == NavigationTask.State.RUNNING) return Result.running(0); boolean success = state == NavigationTask.State.SUCCEEDED; record(player, immediate(player, id, success ? ToolResult.Status.SUCCEEDED : ToolResult.Status.BLOCKED, success ? ToolResult.Reason.NONE : ToolResult.Reason.PATH_BLOCKED, Map.of(), "move:" + target.toShortString())); return finalResult = success ? Result.success() : Result.failure("PATH_BLOCKED"); } @Override public void cancel() { task.cancel(); if (finalResult == null) finalResult = Result.failure("CANCELLED"); } }; } private PrimitiveTask crafting(ServerPlayer player, UUID id, Item item, int wanted) { var dimension = player.level().dimension(); Map before = PlayerCrafting.counts(player); String key = ForgeRegistries.ITEMS.getKey(item).toString(); return new PrimitiveTask() { int attempts; Result result; @Override public Result tick(long tick) { if (result != null) return result; if (PlayerSessions.find(server, player.getUUID()) != player || !player.isAlive() || !player.level().dimension().equals(dimension)) return result = Result.failure("BODY_CHANGED"); ToolResult part = PlayerCrafting.craft(player, id, item); int produced = PlayerCrafting.counts(player).getOrDefault(key, 0) - before.getOrDefault(key, 0); if (!part.succeeded() || produced >= wanted || ++attempts >= 64) { boolean complete = produced >= wanted; ToolResult done = immediate(player, id, complete ? ToolResult.Status.SUCCEEDED : part.succeeded() ? ToolResult.Status.BLOCKED : part.status(), complete ? ToolResult.Reason.NONE : part.succeeded() ? ToolResult.Reason.TIMEOUT : part.reason(), PlayerCrafting.delta(before, PlayerCrafting.counts(player)), "crafted:" + produced + ":" + key); record(player, done); return result = done.succeeded() ? Result.success() : Result.failure(done.reason().name()); } return Result.running(Math.min(.99, (double)produced / wanted)); } @Override public void cancel() { if (result == null) result = Result.failure("CANCELLED"); } }; } private PrimitiveTask once(ServerPlayer player, UUID id, java.util.function.Supplier action) { var dimension = player.level().dimension(); return new PrimitiveTask() { Result result; @Override public Result tick(long tick) { if (result != null) return result; if (PlayerSessions.find(server, player.getUUID()) != player || !player.isAlive() || !player.level().dimension().equals(dimension)) return result = Result.failure("BODY_CHANGED"); ToolResult done = action.get(); record(player, done); return result = done.succeeded() ? Result.success() : Result.failure(done.reason().name()); } @Override public void cancel() { if (result == null) result = Result.failure("CANCELLED"); } }; } private static ToolResult immediate(ServerPlayer p, UUID id, ToolResult.Status status, ToolResult.Reason reason, Map delta, String effect) { long now = System.currentTimeMillis(); return new ToolResult(id, status, reason, delta, effect == null || effect.isBlank() ? List.of() : List.of(effect), List.of(), GlobalPos.of(p.level().dimension(), p.blockPosition()), now, now); } private static PrimitiveTask failed(String reason) { return new PrimitiveTask() { public Result tick(long tick) { return Result.failure(reason); } public void cancel() { } }; } private static BlockPos position(JsonObject args) { return BlockPos.containing(args.get("x").getAsDouble(), args.get("y").getAsDouble(), args.get("z").getAsDouble()); } private static Item item(JsonObject args) { ResourceLocation id = new ResourceLocation(args.get("item").getAsString()); if (!ForgeRegistries.ITEMS.containsKey(id)) throw new IllegalArgumentException("Item desconocido"); return ForgeRegistries.ITEMS.getValue(id); } private static int number(JsonObject args, String key, int fallback) { return args.has(key) ? args.get(key).getAsInt() : fallback; } }