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