Volver al índice

src/main/java/ar/com/companeros/world/WorldPrimitiveExecutor.java

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<ToolResult> completed;
    private final ar.com.companeros.tools.ToolLearningService learning;
    public WorldPrimitiveExecutor(MinecraftServer server, Consumer<ToolResult> completed) {
        this(server, completed, null);
    }
    public WorldPrimitiveExecutor(MinecraftServer server, Consumer<ToolResult> 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<String, Integer> 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<String, Integer> 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<String> 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<String, Integer> 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<Boolean> 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<String, Integer> 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<ToolResult> 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<String, Integer> 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; }
}