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; }
}