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 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 perAgentCounts = new HashMap<>(); private final Map 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 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 pending(UUID agentId, int limit) { int maximum = Math.max(0, Math.min(64, limit)); List 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 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 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 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; } }