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> 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 records(UUID agent) throws IOException { Map existing = loaded.get(agent); if (existing != null) return existing; Path path = directory.resolve(agent + ".dat"); LinkedHashMap 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 immutable = Collections.unmodifiableMap(records); Map 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 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 historical(UUID agent, UUID currentWorld, String query) throws IOException { Set 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 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); } }