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