Volver al índice

src/main/java/ar/com/companeros/social/AgentInbox.java

package ar.com.companeros.social;

import com.mojang.logging.LogUtils;
import java.util.*;
import net.minecraft.nbt.*;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.saveddata.SavedData;

/** Mensajes pendientes persistentes; la autoridad del remitente la asigna el servidor. */
public final class AgentInbox extends SavedData {
    public enum Kind { ADMIN, HUMAN, AGENT }
    public record Message(UUID id, UUID agentId, UUID senderId, String senderName, Kind kind, String body, long createdAtUtc) { }
    private final List<Message> pending = new ArrayList<>();
    private static final int PER_AGENT_LIMIT = 64;
    private static final int TOTAL_LIMIT = 1024;
    private static final int ORDINARY_PER_AGENT_LIMIT = 56;
    private static final int ORDINARY_TOTAL_LIMIT = 896;
    private final Map<UUID, Integer> perAgentCounts = new HashMap<>();
    private final Map<UUID, Integer> ordinaryPerAgentCounts = new HashMap<>();
    private int ordinaryCount;
    // Secuencia del servidor: la fecha informa al agente, pero no decide qué pedido es nuevo.
    private record DeliveryOrder(UUID agentId, long sequence) { }
    private final Map<UUID, DeliveryOrder> deliveryOrders = new HashMap<>();
    private long nextDeliveryOrder = 1;

    public static AgentInbox get(MinecraftServer server) {
        if (server == null || !server.isSameThread())
            throw new IllegalStateException("El buzón se usa en el hilo del servidor");
        return server.overworld().getDataStorage().computeIfAbsent(AgentInbox::load, AgentInbox::new, "companeros_buzon");
    }

    // Solo servicios confiables crean mensajes; el agente no elige su etiqueta ADMIN.
    public boolean deliver(UUID agentId, UUID senderId, String senderName, Kind kind, String body) {
        Objects.requireNonNull(agentId); Objects.requireNonNull(senderId); Objects.requireNonNull(kind);
        String text = Objects.requireNonNull(body).strip();
        if (senderName == null || senderName.isBlank() || senderName.length() > 128
            || senderName.chars().anyMatch(Character::isISOControl)
            || text.isEmpty() || text.length() > 2000 || text.chars().anyMatch(Character::isISOControl) || pending.size() >= TOTAL_LIMIT
            || perAgentCounts.getOrDefault(agentId, 0) >= PER_AGENT_LIMIT || nextDeliveryOrder == Long.MAX_VALUE) return false;
        if (kind != Kind.ADMIN && (!canAcceptOrdinary()
            || ordinaryPerAgentCounts.getOrDefault(agentId, 0) >= ORDINARY_PER_AGENT_LIMIT)) return false;
        Message message = new Message(UUID.randomUUID(), agentId, senderId, senderName, kind, text, System.currentTimeMillis());
        append(message); setDirty();
        LogUtils.getLogger().info("[Agentes:{}] message={} sender={} recipient={} text={}", kind, message.id(), senderId, agentId, text);
        return true;
    }

    public List<Message> pending(UUID agentId, int limit) {
        int maximum = Math.max(0, Math.min(64, limit));
        List<Message> selected = new ArrayList<>();
        // Orden de recepción persistido: dos pedidos en el mismo milisegundo siguen siendo distintos.
        for (int index = pending.size() - 1; index >= 0 && selected.size() < maximum; index--) {
            Message message = pending.get(index);
            if (message.agentId().equals(agentId) && message.kind() == Kind.ADMIN) selected.add(message);
        }
        for (Message message : pending) {
            if (selected.size() >= maximum) break;
            if (message.agentId().equals(agentId) && message.kind() != Kind.ADMIN) selected.add(message);
        }
        return List.copyOf(selected);
    }
    public Optional<Message> latestAdmin(UUID agentId) {
        for (int index = pending.size() - 1; index >= 0; index--) {
            Message message = pending.get(index);
            if (message.agentId().equals(agentId) && message.kind() == Kind.ADMIN) return Optional.of(message);
        }
        return Optional.empty();
    }
    public long deliveryOrder(UUID agentId, UUID messageId) {
        DeliveryOrder order = deliveryOrders.get(messageId);
        return order != null && order.agentId().equals(agentId) ? order.sequence() : -1;
    }
    public long latestAdminOrder(UUID agentId) {
        return latestAdmin(agentId).map(message -> deliveryOrder(agentId, message.id())).orElse(-1L);
    }
    public boolean acknowledge(UUID agentId, UUID messageId) {
        for (var iterator = pending.iterator(); iterator.hasNext();) {
            Message message = iterator.next();
            if (!message.agentId().equals(agentId) || !message.id().equals(messageId)) continue;
            iterator.remove();
            deliveryOrders.remove(message.id());
            decrement(perAgentCounts, agentId);
            if (message.kind() != Kind.ADMIN) { ordinaryCount--; decrement(ordinaryPerAgentCounts, agentId); }
            setDirty(); return true;
        }
        return false;
    }
    public boolean canAcceptOrdinary() { return ordinaryCount < ORDINARY_TOTAL_LIMIT && pending.size() < TOTAL_LIMIT; }
    private void append(Message message) {
        append(message, nextDeliveryOrder);
    }
    private void append(Message message, long order) {
        pending.add(message);
        deliveryOrders.put(message.id(), new DeliveryOrder(message.agentId(), order));
        nextDeliveryOrder = order + 1;
        perAgentCounts.merge(message.agentId(), 1, Integer::sum);
        if (message.kind() != Kind.ADMIN) { ordinaryCount++; ordinaryPerAgentCounts.merge(message.agentId(), 1, Integer::sum); }
    }
    private static void decrement(Map<UUID, Integer> counts, UUID id) {
        int next = counts.getOrDefault(id, 1) - 1;
        if (next == 0) counts.remove(id); else counts.put(id, next);
    }

    // Guardado versionado y carga acotada de mensajes no confirmados.
    @Override public CompoundTag save(CompoundTag tag) {
        tag.putInt("Schema", 1);
        tag.putLong("NextDeliveryOrder", nextDeliveryOrder);
        ListTag messages = new ListTag();
        for (Message message : pending) {
            CompoundTag entry = new CompoundTag();
            entry.putUUID("Id", message.id()); entry.putUUID("Agent", message.agentId()); entry.putUUID("Sender", message.senderId());
            entry.putString("SenderName", message.senderName()); entry.putString("Kind", message.kind().name());
            entry.putString("Body", message.body()); entry.putLong("Created", message.createdAtUtc());
            entry.putLong("DeliveryOrder", deliveryOrder(message.agentId(), message.id())); messages.add(entry);
        }
        tag.put("Messages", messages); return tag;
    }
    public static AgentInbox load(CompoundTag tag) {
        if (tag.getInt("Schema") > 1) throw new IllegalStateException("Formato de buzón más nuevo que el mod");
        if (tag.getInt("Schema") == 1 && !tag.contains("Messages", Tag.TAG_LIST))
            throw new IllegalStateException("Buzón incompleto");
        AgentInbox inbox = new AgentInbox();
        Set<UUID> seen = new HashSet<>();
        for (Tag value : tag.getList("Messages", Tag.TAG_COMPOUND)) {
            if (inbox.pending.size() >= TOTAL_LIMIT) break;
            CompoundTag entry = (CompoundTag) value;
            try {
                String body = entry.getString("Body");
                UUID agent = entry.getUUID("Agent");
                String senderName = entry.getString("SenderName");
                UUID id = entry.getUUID("Id");
                if (body.isBlank() || body.length() > 2000 || body.chars().anyMatch(Character::isISOControl)
                    || senderName.isBlank() || senderName.length() > 128 || senderName.chars().anyMatch(Character::isISOControl)
                    || entry.getLong("Created") < 0 || seen.contains(id)
                    || inbox.perAgentCounts.getOrDefault(agent, 0) >= PER_AGENT_LIMIT) continue;
                long storedOrder = entry.getLong("DeliveryOrder");
                // Guardados anteriores no tenían secuencia. El orden de su lista reconstruye la recepción.
                long order = storedOrder >= inbox.nextDeliveryOrder && storedOrder < Long.MAX_VALUE
                    ? storedOrder : inbox.nextDeliveryOrder;
                if (order == Long.MAX_VALUE) continue;
                inbox.append(new Message(id, agent, entry.getUUID("Sender"), senderName,
                    Kind.valueOf(entry.getString("Kind")), body, entry.getLong("Created")), order);
                seen.add(id);
            } catch (IllegalArgumentException ignored) { }
        }
        long storedNext = tag.getLong("NextDeliveryOrder");
        if (storedNext > inbox.nextDeliveryOrder) inbox.nextDeliveryOrder = storedNext;
        return inbox;
    }
}