Volver al índice
src/main/java/ar/com/companeros/runtime/Perception.java
package ar.com.companeros.runtime;
import ar.com.companeros.decision.ObservationSnapshot;
import ar.com.companeros.identity.AgentRegistry;
import ar.com.companeros.memory.MemoryQuery;
import ar.com.companeros.memory.MemoryRecord;
import ar.com.companeros.memory.MemoryStore;
import ar.com.companeros.social.AgentInbox;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.BlockTags;
import net.minecraft.tags.FluidTags;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.monster.Enemy;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.CropBlock;
import net.minecraft.world.level.block.SweetBerryBushBlock;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.common.Tags;
import net.minecraftforge.registries.ForgeRegistries;
/** Captura local y con presupuesto; la consulta al modelo decide cuándo solicitar otra observación. */
public final class Perception {
private static final int RADIUS = 8, BLOCK_SAMPLES = 96;
private Perception() { }
// Entrada única desde el hilo lógico; ninguna entidad viva pasa al proveedor asíncrono.
public static ObservationSnapshot snapshot(MinecraftServer server, ServerPlayer player,
String goal, List<String> recent) {
return snapshot(server, player, goal, recent, null);
}
public static ObservationSnapshot snapshot(MinecraftServer server, ServerPlayer player,
String goal, List<String> recent, ar.com.companeros.memory.PersistentMemoryArchive archive) {
if (!server.isSameThread()) throw new IllegalStateException("La percepción se captura en el hilo servidor");
ServerLevel level = player.serverLevel();
AgentRegistry registry = AgentRegistry.get(server);
GlobalPos location = GlobalPos.of(level.dimension(), player.blockPosition());
List<String> facts = new ArrayList<>();
facts.add("role=" + ar.com.companeros.identity.AgentRoles.get(server).role(player.getUUID()));
facts.add("skin_profile=" + ar.com.companeros.identity.SkinProfiles.get(server).source(player.getUUID()));
// Versiones relevantes al objetivo; el catálogo de recetas es la autoridad del modpack.
String lowerGoal = goal == null ? "" : goal.toLowerCase(java.util.Locale.ROOT);
net.minecraftforge.fml.ModList.get().getMods().stream()
.filter(mod -> lowerGoal.contains(mod.getModId()) || mod.getModId().equals("minecraft") || mod.getModId().equals("forge") || mod.getModId().equals("create"))
.limit(8).forEach(mod -> facts.add("mod_installed=" + mod.getModId() + " version=" + mod.getVersion()));
floorFacts(player, facts);
nearbyEntities(player, registry, facts);
visibleResources(player, facts);
if (recent != null) recent.stream().filter(java.util.Objects::nonNull).limit(6)
.forEach(value -> facts.add("recent=" + limit(value, 480)));
List<String> memories = new ArrayList<>(MemoryStore.get(server)
.query(player.getUUID(), MemoryQuery.relevant(limit(goal, 512), location, level.getGameTime()))
.stream().map(Perception::compactMemory).toList());
if (archive != null) try {
for (MemoryRecord previous : archive.historical(player.getUUID(), registry.worldId(), limit(goal, 512)))
memories.add(limit("HISTORICAL_OTHER_WORLD world=" + previous.worldId() + " type=" + previous.type()
+ " date=" + Instant.ofEpochMilli(previous.occurredAtUtc()) + " confidence=" + previous.confidence()
+ " origin=" + previous.origin() + " source=" + previous.sourceId()
+ " no_current_location_or_resources=true content=" + previous.content(), 2048));
} catch (java.io.IOException invalid) {
com.mojang.logging.LogUtils.getLogger().error("No se pudo recuperar memoria archivada de {}", player.getGameProfile().getName(), invalid);
}
List<String> messages = AgentInbox.get(server).pending(player.getUUID(), 8).stream()
.map(Perception::inboxObservation).toList();
Map<String, Integer> inventory = new HashMap<>();
for (int slot = 0; slot < 36; slot++) {
var stack = player.getInventory().getItem(slot);
if (!stack.isEmpty()) inventory.merge(ForgeRegistries.ITEMS.getKey(stack.getItem()).toString(), stack.getCount(), Integer::sum);
}
BlockPos pos = player.blockPosition();
return new ObservationSnapshot(registry.worldId(), level.dimension().location().toString(), pos.getX(), pos.getY(), pos.getZ(),
player.getHealth(), player.getFoodData().getFoodLevel(), inventory, limit(goal, 512),
facts.stream().limit(32).map(value -> limit(value, 512)).toList(), memories, messages);
}
// El buzón valida 2000 caracteres de cuerpo; el DTO reserva además su cabecera sin truncar instrucciones.
public static String inboxObservation(AgentInbox.Message message) {
java.util.Objects.requireNonNull(message);
return "id=" + message.id() + " sender=" + message.senderId() + " kind=" + message.kind()
+ " createdAtUtc=" + message.createdAtUtc() + " name=" + message.senderName() + " body=" + message.body();
}
// Estado propio y soporte inmediato: evita afirmar que un vacío equivale a una caída letal.
private static void floorFacts(ServerPlayer player, List<String> facts) {
ServerLevel level = player.serverLevel(); BlockPos feet = player.blockPosition();
if (!level.hasChunkAt(feet) || !level.hasChunkAt(feet.below())) return;
var below = level.getBlockState(feet.below()); var standing = level.getBlockState(feet);
if (standing.getFluidState().is(FluidTags.LAVA) || below.getFluidState().is(FluidTags.LAVA)) facts.add("hazard=lava_at_feet");
else if (below.getCollisionShape(level, feet.below()).isEmpty()) facts.add("floor_support=false");
else facts.add("floor_support=true");
facts.add("day_time=" + level.getDayTime() % 24000 + " rain=" + level.isRaining());
}
// Visión cercana: entidades cargadas, alcance esférico, campo de vista y línea sin chunks desconocidos.
private static void nearbyEntities(ServerPlayer player, AgentRegistry registry, List<String> facts) {
player.serverLevel().getEntitiesOfClass(Entity.class, player.getBoundingBox().inflate(RADIUS),
entity -> entity != player && entity.isAlive() && entity.distanceToSqr(player) <= RADIUS * RADIUS)
.stream().sorted(Comparator.comparingDouble(player::distanceToSqr))
.filter(entity -> inView(player, entity.getEyePosition()) && loadedLine(player.serverLevel(), player.getEyePosition(), entity.getEyePosition())
&& player.hasLineOfSight(entity)).limit(12).forEach(entity -> {
String kind = entity instanceof ServerPlayer other ? registry.isAgent(other.getUUID()) ? "agent" : "human"
: entity instanceof ar.com.companeros.horror.ParasiteEntity ? "parasite_ignores_agents"
: entity instanceof Enemy ? "hostile" : "entity";
BlockPos pos = entity.blockPosition();
facts.add(kind + "=" + ForgeRegistries.ENTITY_TYPES.getKey(entity.getType())
+ " id=" + entity.getUUID() + " name=" + limit(entity.getName().getString(), 64)
+ " pos=" + pos.getX() + "," + pos.getY() + "," + pos.getZ());
});
}
// Solo recursos con evidencia de visión; máximo 96 posiciones consultadas y 12 resultados.
private static void visibleResources(ServerPlayer player, List<String> facts) {
ServerLevel level = player.serverLevel(); BlockPos center = player.blockPosition();
Set<BlockPos> examined = new HashSet<>(); int found = 0;
for (int sample = 0; sample < BLOCK_SAMPLES && found < 12; sample++) {
double angle = sample * 2.399963229728653; int radius = 1 + sample % RADIUS;
BlockPos pos = center.offset((int) Math.round(Math.cos(angle) * radius), (sample / RADIUS) % 5 - 2,
(int) Math.round(Math.sin(angle) * radius));
Vec3 point = Vec3.atCenterOf(pos);
if (!examined.add(pos) || !level.hasChunkAt(pos) || point.distanceToSqr(player.position()) > RADIUS * RADIUS
|| !inView(player, point)) continue;
var state = level.getBlockState(pos);
if (!state.is(BlockTags.LOGS) && !state.is(Tags.Blocks.ORES) && !state.is(Blocks.STONE)
&& !(state.getBlock() instanceof CropBlock) && !(state.getBlock() instanceof SweetBerryBushBlock)) continue;
if (!loadedLine(level, player.getEyePosition(), point)) continue;
var hit = level.clip(new ClipContext(player.getEyePosition(), point,
ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, player));
if (hit.getType() != HitResult.Type.MISS && !hit.getBlockPos().equals(pos)) continue;
String maturity = state.getBlock() instanceof CropBlock crop ? " mature=" + crop.isMaxAge(state)
: state.getBlock() instanceof SweetBerryBushBlock ? " mature=" + (state.getValue(SweetBerryBushBlock.AGE) >= 2) : "";
facts.add("visible_block=" + ForgeRegistries.BLOCKS.getKey(state.getBlock())
+ " pos=" + pos.getX() + "," + pos.getY() + "," + pos.getZ() + maturity); found++;
}
}
private static boolean inView(ServerPlayer player, Vec3 point) {
Vec3 delta = point.subtract(player.getEyePosition());
return delta.lengthSqr() < .25 || player.getLookAngle().dot(delta.normalize()) >= Math.cos(Math.toRadians(70));
}
private static boolean loadedLine(ServerLevel level, Vec3 from, Vec3 to) {
// Revisar el rectángulo de chunks completo evita omitir cruces muy cortos cerca de una esquina.
BlockPos start = BlockPos.containing(from), end = BlockPos.containing(to);
for (int x = Math.min(start.getX() >> 4, end.getX() >> 4); x <= Math.max(start.getX() >> 4, end.getX() >> 4); x++)
for (int z = Math.min(start.getZ() >> 4, end.getZ() >> 4); z <= Math.max(start.getZ() >> 4, end.getZ() >> 4); z++)
if (!level.hasChunkAt(new BlockPos(x << 4, 0, z << 4))) return false;
return true;
}
private static String compactMemory(MemoryRecord memory) {
String location = memory.location() == null ? "unknown" : memory.location().dimension().location() + ":" + memory.location().pos().toShortString();
return limit("id=" + memory.id() + " type=" + memory.type() + " origin=" + memory.origin()
+ " importance=" + memory.importance() + " confidence=" + memory.confidence()
+ " date=" + Instant.ofEpochMilli(memory.occurredAtUtc()) + " place=" + location
+ " last_verified=" + memory.lastVerifiedAtUtc() + " source=" + memory.sourceId()
+ " content=" + limit(memory.content(), 1200), 2048);
}
private static String limit(String value, int max) {
if (value == null) return ""; return value.length() > max ? value.substring(0, max) : value;
}
}