Volver al índice
src/main/java/ar/com/companeros/memory/PersistentMemoryArchive.java
package ar.com.companeros.memory;
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import net.minecraft.nbt.*;
/** Memoria por jugador fuera del mundo. Ubicaciones antiguas no se recuperan como recursos actuales. */
public final class PersistentMemoryArchive {
private static final int MAX_RECORDS = 8192;
private final Path directory;
private final java.util.concurrent.ConcurrentMap<UUID, Map<UUID, MemoryRecord>> loaded = new java.util.concurrent.ConcurrentHashMap<>();
public PersistentMemoryArchive(Path directory) { this.directory = directory.toAbsolutePath().normalize(); }
// Lectura perezosa y acotada: cada jugador carga únicamente su propio archivo.
private Map<UUID, MemoryRecord> records(UUID agent) throws IOException {
Map<UUID, MemoryRecord> existing = loaded.get(agent); if (existing != null) return existing;
Path path = directory.resolve(agent + ".dat"); LinkedHashMap<UUID, MemoryRecord> records = new LinkedHashMap<>();
if (Files.exists(path)) {
if (Files.isSymbolicLink(path) || Files.size(path) > 16_777_216) throw new IOException("Archivo de memoria fuera de límite");
CompoundTag root = NbtIo.readCompressed(path.toFile());
if (root.getInt("Schema") != 1 || !root.getUUID("Agent").equals(agent)) throw new IOException("Identidad de memoria incorrecta");
ListTag entries = root.getList("Records", Tag.TAG_COMPOUND);
if (entries.size() > MAX_RECORDS) throw new IOException("Demasiados recuerdos archivados");
for (Tag entry : entries) {
MemoryRecord record = MemoryStore.readRecord((CompoundTag)entry);
if (!record.agentId().equals(agent) || records.putIfAbsent(record.id(), record) != null)
throw new IOException("Archivo de memoria contiene identidad ajena o duplicada");
}
}
Map<UUID, MemoryRecord> immutable = Collections.unmodifiableMap(records);
Map<UUID, MemoryRecord> previous = loaded.putIfAbsent(agent, immutable); return previous == null ? immutable : previous;
}
// Mezcla idempotente: no borra recuerdos de otros mundos al guardar el actual.
public synchronized void sync(UUID agent, List<MemoryRecord> current) throws IOException {
var records = new LinkedHashMap<>(records(agent)); boolean changed = !Files.exists(directory.resolve(agent + ".dat"));
for (MemoryRecord memory : current) {
if (!memory.agentId().equals(agent)) throw new IllegalArgumentException("Memoria de otro jugador");
if (memory.equals(records.get(memory.id()))) continue;
if (!records.containsKey(memory.id()) && records.size() >= MAX_RECORDS) {
MemoryRecord removable = records.values().stream().filter(record -> !record.protectedFromEviction())
.min(Comparator.comparingDouble((MemoryRecord record) -> record.importance() * record.confidence())
.thenComparingLong(MemoryRecord::occurredAtUtc)).orElse(null);
if (removable == null) throw new IOException("Archivo lleno de recuerdos protegidos; se conserva el original");
records.remove(removable.id());
}
records.put(memory.id(), memory); changed = true;
}
if (!changed) return;
Files.createDirectories(directory); Path file = directory.resolve(agent + ".dat"), temporary = directory.resolve(agent + ".tmp");
if (Files.isSymbolicLink(file) || Files.isSymbolicLink(temporary)) throw new IOException("Enlace de memoria no permitido");
CompoundTag root = new CompoundTag(); root.putInt("Schema", 1); root.putUUID("Agent", agent);
ListTag entries = new ListTag(); records.values().forEach(record -> entries.add(MemoryStore.writeRecord(record))); root.put("Records", entries);
try {
NbtIo.writeCompressed(root, temporary.toFile());
try { Files.move(temporary, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); }
catch (AtomicMoveNotSupportedException unsupported) { Files.move(temporary, file, StandardCopyOption.REPLACE_EXISTING); }
loaded.put(agent, Collections.unmodifiableMap(records));
} finally { Files.deleteIfExists(temporary); }
}
// Experiencias transferibles: no son evidencia de recursos, posiciones o condiciones del mundo nuevo.
public List<MemoryRecord> historical(UUID agent, UUID currentWorld, String query) throws IOException {
Set<String> words = new HashSet<>(Arrays.asList(query.toLowerCase(Locale.ROOT).split("[^\\p{L}\\p{N}_]+")));
long now = System.currentTimeMillis();
return records(agent).values().stream().filter(memory -> !memory.worldId().equals(currentWorld)
&& memory.type() != MemoryType.PLACE && memory.type() != MemoryType.RESOURCE)
.sorted(Comparator.comparingDouble((MemoryRecord memory) -> historicalScore(memory, words, now)).reversed())
.limit(3).toList();
}
private static double historicalScore(MemoryRecord memory, Set<String> words, long now) {
double relevant = words.stream().filter(word -> word.length() > 2 && memory.content().toLowerCase(Locale.ROOT).contains(word)).count();
double ageDays = Math.max(0, now - memory.occurredAtUtc()) / 86_400_000.0;
return relevant + memory.importance() * 3 + memory.confidence() / (1 + ageDays / 60)
+ (memory.protectedFromEviction() ? 2 : 0);
}
}