package ar.com.companeros.horror.tools; import ar.com.companeros.decision.ToolCapability; import ar.com.companeros.horror.*; import ar.com.companeros.tools.ToolManifest; import com.google.gson.*; import com.mojang.logging.LogUtils; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.security.*; import java.time.Instant; import java.util.*; import java.util.concurrent.CompletableFuture; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.phys.Vec3; import net.minecraftforge.fml.loading.FMLPaths; /** Aprendizaje corporal: JSON validado, acciones del cuerpo y evidencia real antes de compartir. */ public final class InfectedTools { public enum Outcome { SUCCEEDED, FAILED, CANCELLED } private static final Map SERVICES = new WeakHashMap<>(); private static final int MAX_ENTRIES = 64, MAX_LEDGER_BYTES = 1_048_576, AUTO_COOLDOWN = 1200; private static final Gson JSON = new GsonBuilder().setPrettyPrinting().create(); private final Path directory; private final byte[] signingKey; private final Map entries = new LinkedHashMap<>(); private Execution active; private UUID observedBody, lastVisibleTarget; private Vec3 lastPosition; private float lastHealth; private int blockedTicks; private long nextAutomaticAt; // En producción la ruta es fija. El constructor con ruta es únicamente para pruebas internas. InfectedTools(Path folder) throws IOException { directory = folder.toAbsolutePath().normalize(); checkDirectory(); Files.createDirectories(directory); checkDirectory(); signingKey = readOrCreateKey(); restore(); } public static InfectedTools get(MinecraftServer server) { if (!server.isSameThread()) throw new IllegalStateException("Las herramientas del huésped usan el hilo del servidor"); InfectedTools found = SERVICES.get(server); if (found != null) return found; try { found = new InfectedTools(FMLPaths.CONFIGDIR.get().resolve("raps/Tools/infected")); SERVICES.put(server, found); return found; } catch (IOException failure) { throw new IllegalStateException("No se pudo cargar Tools/infected; no se concedieron capacidades", failure); } } public static void tick(MinecraftServer server) { InfectedTools service = SERVICES.get(server); if (service == null && HorrorModule.findParasite(server).filter(ParasiteEntity::isAlive).isPresent()) service = get(server); if (service != null) service.advance(server); } public static void close(MinecraftServer server) { InfectedTools service = SERVICES.remove(server); if (service == null) return; if (service.active != null) service.finish(service.active, Outcome.CANCELLED, "SERVER_STOPPING"); } public boolean busy() { return active != null; } public void cancelCurrent() { if (active != null) finish(active, Outcome.CANCELLED, "CANCELLED_BY_OPERATOR"); } // El autor proviene del cuerpo canónico, nunca de un UUID enviado por el modelo. public InfectedToolManifest propose(ParasiteEntity body, String source) { canonical(body); try { return proposeForAuthor(body.hostId().orElseThrow(), source); } catch (IOException failure) { throw new IllegalStateException("No se guardó la propuesta corporal", failure); } } InfectedToolManifest proposeForAuthor(UUID author, String source) throws IOException { InfectedToolManifest manifest = InfectedToolManifest.parse(source, author); validateComposition(manifest, new HashSet<>(), 0); String key = key(manifest.id(), manifest.version()); Learned old = entries.get(key); if (old != null) { if (!old.manifest.sha256().equals(manifest.sha256())) throw invalid("La versión ya existe con otro contenido o autor"); return old.manifest; } if (entries.size() >= MAX_ENTRIES) throw invalid("Tools/infected alcanzó el límite de herramientas"); Learned candidate = new Learned(manifest); entries.put(key, candidate); try { save(); } catch (IOException failure) { entries.remove(key); throw failure; } JsonObject readable = new JsonObject(); readable.addProperty("author", author.toString()); readable.addProperty("sha256", manifest.sha256()); readable.add("manifest", manifest.toJson()); try { write(directory.resolve(manifest.id() + "_v" + manifest.version() + ".json"), JSON.toJson(readable)); } catch (IOException failure) { LogUtils.getLogger().warn("El manifiesto corporal quedó guardado en el ledger, pero no se pudo exportar su copia {}", manifest.id(), failure); } log("PROPOSED", manifest, author, "Sin certificación ni ejecuciones inventadas"); return manifest; } public List available(UUID actor) { return entries.values().stream().filter(row -> row.certified || row.manifest.author().equals(actor) && row.failures < 8) .map(row -> row.manifest).toList(); } public Map catalog(UUID actor) { Map result = new LinkedHashMap<>(); for (InfectedToolManifest manifest : available(actor)) { ToolCapability capability = new ToolCapability(manifest.id(), manifest.version(), manifest.description().substring(0, Math.min(390, manifest.description().length())) + " sha256=" + manifest.sha256() + (entries.get(key(manifest.id(), manifest.version())).certified ? " CERTIFIED" : " AUTHOR_TRIAL"), manifest.permissions().stream().map(Enum::name).collect(java.util.stream.Collectors.toSet())); ToolCapability old = result.get(manifest.id()); if (old == null || capability.version() > old.version()) result.put(manifest.id(), capability); } return Map.copyOf(result); } public Statistics statistics(String id, int version) { Learned row = entries.get(key(id, version)); if (row == null) throw invalid("Herramienta corporal desconocida"); return new Statistics(row.successes, row.failures, row.cancellations, row.reuseSuccesses, row.reuseFailures, row.certified, row.lastResult); } // Sólo una ejecución corporal. La composición se compila por completo antes de tocar el mundo. public CompletableFuture start(ParasiteEntity body, String id, int version, Map inputs) { MinecraftServer server = canonical(body); UUID actor = body.hostId().orElseThrow(); if (active != null) throw invalid("El cuerpo ya está ejecutando una herramienta"); Learned row = entries.get(key(id, version)); if (row == null || !row.certified && !row.manifest.author().equals(actor)) throw invalid("La herramienta no está disponible para este huésped"); if (!row.certified && row.failures >= 8) throw invalid("La herramienta requiere una versión corregida tras ocho fallos"); if (inputs.size() > 16) throw invalid("Demasiadas entradas corporales"); JsonObject values = new JsonObject(); for (var entry : inputs.entrySet()) { if (!entry.getKey().matches("[a-z][a-zA-Z0-9_]{0,47}") || entry.getValue().length() > 512) throw invalid("Entrada corporal inválida"); values.addProperty(entry.getKey(), entry.getValue()); } List plan = new ArrayList<>(); compile(row.manifest, values, plan, new HashSet<>(), 0); active = new Execution(row, body, actor, server.overworld().getGameTime(), List.copyOf(plan), !row.certified); log("STARTED", row.manifest, actor, active.id.toString()); return active.result; } public CompletableFuture startLatest(ParasiteEntity body, String id) { InfectedToolManifest manifest = available(body.hostId().orElseThrow()).stream().filter(value -> value.id().equals(id)) .max(Comparator.comparingInt(InfectedToolManifest::version)).orElseThrow(() -> invalid("Herramienta corporal desconocida")); return start(body, id, manifest.version(), Map.of()); } private static MinecraftServer canonical(ParasiteEntity body) { if (!(body.level() instanceof ServerLevel level) || !level.getServer().isSameThread() || !body.isAlive()) throw invalid("Falta un huésped vivo del servidor"); MinecraftServer server = level.getServer(); HorrorState state = HorrorState.get(server); if (!HorrorModule.config(server).enabled() || body.hostId().isEmpty() || !state.host().filter(body.hostId().get()::equals).isPresent() || !state.entityId().filter(body.getUUID()::equals).isPresent() || HorrorModule.findParasite(server).filter(entity -> entity == body).isEmpty()) throw invalid("Sólo el cuerpo canónico del bot infectado puede usar Tools/infected"); return server; } private Counts validateComposition(InfectedToolManifest manifest, Set visiting, int depth) { if (depth > 4 || !visiting.add(manifest.sha256())) throw invalid("Dependencia circular o demasiado profunda"); int steps = 0, ticks = 0; try { for (ToolManifest.Dependency dependency : manifest.dependencies()) certified(dependency); for (ToolManifest.Step step : manifest.steps()) { if (step.primitive() != null) { InfectedToolManifest.Permission needed = step.primitive().equals("speak") ? InfectedToolManifest.Permission.BODY_VOICE : InfectedToolManifest.Permission.BODY_MOVE; if (!manifest.permissions().contains(needed)) throw invalid("La primitiva amplía permisos corporales"); steps++; ticks += step.timeoutTicks(); } else { InfectedToolManifest child = certified(step.tool()); if (!manifest.permissions().containsAll(child.permissions())) throw invalid("La dependencia amplía permisos corporales"); Counts nested = validateComposition(child, visiting, depth + 1); if (nested.ticks > step.timeoutTicks()) throw invalid("El límite del paso no alcanza para la dependencia"); steps += nested.steps; ticks += nested.ticks; } } if (steps > manifest.maxSteps() || ticks > manifest.maxTicks()) throw invalid("La composición expandida excede sus límites"); return new Counts(steps, ticks); } finally { visiting.remove(manifest.sha256()); } } private InfectedToolManifest certified(ToolManifest.Dependency dependency) { Learned child = entries.get(key(dependency.id(), dependency.version())); if (child == null || !child.certified || !child.manifest.sha256().equals(dependency.sha256())) throw invalid("Dependencia corporal no certificada o hash distinto"); return child.manifest; } private void compile(InfectedToolManifest manifest, JsonObject inputs, List output, Set visiting, int depth) { validateComposition(manifest, new HashSet<>(), 0); if (depth > 4 || !visiting.add(manifest.sha256())) throw invalid("Dependencia circular"); try { for (ToolManifest.Step step : manifest.steps()) { JsonObject args = InfectedToolManifest.resolve(step.arguments(), inputs); if (step.primitive() != null) { InfectedToolManifest.validatePrimitive(step.primitive(), args, step.timeoutTicks(), false); output.add(new Compiled(step.primitive(), args, step.timeoutTicks())); } else compile(certified(step.tool()), args, output, visiting, depth + 1); } } finally { visiting.remove(manifest.sha256()); } if (output.size() > InfectedToolManifest.MAX_STEPS) throw invalid("Demasiados pasos corporales expandidos"); } // Tick local independiente de Luna. Cada paso conserva su plazo y un testigo del efecto real. void advance(MinecraftServer server) { if (!server.isSameThread()) throw new IllegalStateException("Ejecución corporal fuera del hilo del servidor"); if (active != null) advanceExecution(server); if (active == null) automatic(server); } private void advanceExecution(MinecraftServer server) { Execution run = active; long time = server.overworld().getGameTime(); try { if (canonical(run.body) != server || !run.actor.equals(run.body.hostId().orElse(null))) throw invalid("El cuerpo cambió"); } catch (RuntimeException missing) { finish(run, Outcome.CANCELLED, "BODY_UNAVAILABLE"); return; } if (time - run.started > run.row.manifest.maxTicks()) { finish(run, Outcome.FAILED, "TOOL_TIMEOUT"); return; } if (run.step != null) { long elapsed = time - run.stepStarted; if (elapsed > run.step.timeoutTicks) { finish(run, Outcome.FAILED, "STEP_TIMEOUT"); return; } boolean done; if (run.voice != null) { if (!run.voice.isDone()) return; boolean delivered; try { delivered = Boolean.TRUE.equals(run.voice.join()); } catch (RuntimeException failure) { delivered = false; } if (!delivered) { finish(run, Outcome.FAILED, "VOICE_NOT_DELIVERED"); return; } run.useful++; done = true; } else done = elapsed >= run.duration; if (!done) return; if (InfectedToolManifest.isIntent(run.step.primitive) && usefulIntent(run)) run.useful++; run.completed++; run.step = null; run.voice = null; } if (run.completed == run.plan.size()) { finish(run, run.useful > 0 ? Outcome.SUCCEEDED : Outcome.FAILED, run.useful > 0 ? "SUCCEEDED" : "NO_USEFUL_EFFECT"); return; } if (time - run.started >= run.row.manifest.maxTicks()) { finish(run, Outcome.FAILED, "TOOL_TIMEOUT"); return; } run.step = run.plan.get(run.completed); run.stepStarted = time; run.before = run.body.position(); run.targetBefore = run.body.getTarget() == null ? null : run.body.getTarget().position(); run.attacksBefore = run.body.successfulAttackCount(); run.scaresBefore = run.body.deliveredScareCount(); if (InfectedToolManifest.isIntent(run.step.primitive)) { run.duration = InfectedToolManifest.integer(run.step.args.get("durationTicks"), 20, 1200); run.body.setIntent(ParasiteEntity.Intent.valueOf(run.step.primitive.toUpperCase(Locale.ROOT)), run.duration); } else if (run.step.primitive.equals("wait")) { run.duration = InfectedToolManifest.integer(run.step.args.get("durationTicks"), 1, 1200); run.body.pauseForTool(run.duration); } else if (run.step.primitive.equals("posture")) { int duration = run.step.args.has("durationTicks") ? InfectedToolManifest.integer(run.step.args.get("durationTicks"), 20, ParasiteEntity.MAX_POSTURE_TICKS) : ParasiteEntity.DEFAULT_POSTURE_TICKS; if (!run.body.requestBodyPosture(run.step.args.get("mode").getAsString(), duration)) { finish(run, Outcome.FAILED, "POSTURE_NO_REAL_SUPPORT"); return; } run.duration = 1; } else { run.duration = 0; run.voice = HorrorSpeech.speak(run.body, InfectedToolManifest.text(run.step.args.get("text"))); } } private static boolean usefulIntent(Execution run) { if (run.body.successfulAttackCount() > run.attacksBefore || run.body.deliveredScareCount() > run.scaresBefore) return true; Vec3 after = run.body.position(); if (after.distanceToSqr(run.before) <= .04) return false; if (run.step.primitive.equals("climb")) return after.y - run.before.y > .2; if (run.step.primitive.equals("retreat") || run.step.primitive.equals("ambush")) { if (run.targetBefore == null) return false; double beforeDistance = run.before.distanceTo(run.targetBefore), afterDistance = after.distanceTo(run.targetBefore); return run.step.primitive.equals("retreat") ? afterDistance - beforeDistance > .2 : beforeDistance - afterDistance > .2; } return true; } private void finish(Execution run, Outcome outcome, String reason) { if (active != run) return; active = null; if (run.voice != null && !run.voice.isDone()) run.voice.cancel(false); if (run.body.isAlive()) { run.body.pauseForTool(0); if (outcome != Outcome.SUCCEEDED) { run.body.getNavigation().stop(); run.body.setIntent(ParasiteEntity.Intent.STALK, 20); } } Learned row = run.row; if (run.trial) { if (outcome == Outcome.SUCCEEDED) row.successes++; else if (outcome == Outcome.CANCELLED) row.cancellations++; else row.failures++; row.receipts.add(new Receipt(run.id, run.actor, run.started, outcome.name(), run.completed, run.useful)); if (row.receipts.size() > 32) { Receipt removable = row.receipts.stream().filter(receipt -> !receipt.result.equals("SUCCEEDED")).findFirst().orElse(row.receipts.get(0)); row.receipts.remove(removable); } row.certified = row.successes >= 3 && (double) row.successes / (row.successes + row.failures) >= .8; } else if (outcome == Outcome.SUCCEEDED) row.reuseSuccesses++; else row.reuseFailures++; row.lastResult = reason; try { save(); log(outcome.name(), row.manifest, run.actor, run.id + " steps=" + run.completed + " useful=" + run.useful + " " + reason); } catch (IOException failure) { // Una persistencia fallida nunca entrega una nueva certificación. row.certified = false; outcome = Outcome.FAILED; row.lastResult = "PERSISTENCE_FAILED"; LogUtils.getLogger().error("No se pudo guardar la ejecución corporal {}", row.manifest.id(), failure); } run.result.complete(outcome); } // Políticas retenidas y certificadas: sólo eventos reales, sin nuevas llamadas al modelo. private void automatic(MinecraftServer server) { ParasiteEntity body = HorrorModule.findParasite(server).orElse(null); if (body == null || !body.isAlive()) return; try { canonical(body); } catch (RuntimeException unavailable) { return; } ServerPlayer target = body.getTarget() instanceof ServerPlayer human && HorrorModule.mayTarget(server, human) && body.hasLineOfSight(human) ? human : null; UUID visible = target == null ? null : target.getUUID(); EnumSet events = EnumSet.noneOf(InfectedToolManifest.Trigger.class); if (!body.getUUID().equals(observedBody)) { observedBody = body.getUUID(); lastVisibleTarget = null; lastPosition = body.position(); lastHealth = body.getHealth(); blockedTicks = 0; } if (visible != null && !visible.equals(lastVisibleTarget)) events.add(InfectedToolManifest.Trigger.ON_TARGET_VISIBLE); if (body.getHealth() + .1 < lastHealth) events.add(InfectedToolManifest.Trigger.ON_DAMAGE); boolean blocked = target != null && (body.horizontalCollision || !body.getNavigation().isDone() && body.position().distanceToSqr(lastPosition) < .0025); blockedTicks = blocked ? blockedTicks + 1 : 0; if (blockedTicks == 40) events.add(InfectedToolManifest.Trigger.ON_BLOCKED); lastVisibleTarget = visible; lastPosition = body.position(); lastHealth = body.getHealth(); long time = server.overworld().getGameTime(); if (time < nextAutomaticAt || events.isEmpty()) return; for (Learned row : entries.values()) if (row.certified && row.manifest.triggers().stream().anyMatch(events::contains)) { nextAutomaticAt = time + AUTO_COOLDOWN; try { start(body, row.manifest.id(), row.manifest.version(), Map.of()); log("AUTOMATIC", row.manifest, body.hostId().orElseThrow(), events.toString()); } catch (RuntimeException missingInputs) { LogUtils.getLogger().debug("No se inició la herramienta corporal automática {}: {}", row.manifest.id(), missingInputs.getMessage()); } return; } } // Registro autenticado. Editar contadores o manifiestos no puede aprobar herramientas. private void save() throws IOException { checkDirectory(); JsonObject content = new JsonObject(); content.addProperty("schema", 1); JsonArray tools = new JsonArray(); for (Learned row : entries.values()) { JsonObject tool = new JsonObject(); tool.addProperty("author", row.manifest.author().toString()); tool.addProperty("sha256", row.manifest.sha256()); tool.add("manifest", row.manifest.toJson()); tool.addProperty("createdAt", row.createdAt); tool.addProperty("successes", row.successes); tool.addProperty("failures", row.failures); tool.addProperty("cancellations", row.cancellations); tool.addProperty("reuseSuccesses", row.reuseSuccesses); tool.addProperty("reuseFailures", row.reuseFailures); tool.addProperty("certified", row.certified); tool.addProperty("lastResult", row.lastResult); tool.add("trials", JSON.toJsonTree(row.receipts)); tools.add(tool); } content.add("tools", tools); JsonObject envelope = new JsonObject(); envelope.add("content", content); envelope.addProperty("hmac", signature(content)); write(directory.resolve("learning.json"), JSON.toJson(envelope)); } private void restore() throws IOException { Path file = directory.resolve("learning.json"); if (!Files.exists(file, LinkOption.NOFOLLOW_LINKS)) return; checkFile(file, MAX_LEDGER_BYTES); try { JsonObject envelope = JsonParser.parseString(Files.readString(file, StandardCharsets.UTF_8)).getAsJsonObject(); if (!envelope.keySet().equals(Set.of("content", "hmac"))) throw invalid("Registro corporal incompatible"); JsonObject content = envelope.getAsJsonObject("content"); if (!MessageDigest.isEqual(signature(content).getBytes(StandardCharsets.US_ASCII), envelope.get("hmac").getAsString().getBytes(StandardCharsets.US_ASCII))) throw invalid("El registro corporal fue adulterado"); if (!content.keySet().equals(Set.of("schema", "tools")) || content.get("schema").getAsInt() != 1 || content.getAsJsonArray("tools").size() > MAX_ENTRIES) throw invalid("Registro corporal incompatible"); Set receipts = new HashSet<>(); for (JsonElement item : content.getAsJsonArray("tools")) { JsonObject raw = item.getAsJsonObject(); if (!raw.keySet().equals(Set.of("author", "sha256", "manifest", "createdAt", "successes", "failures", "cancellations", "reuseSuccesses", "reuseFailures", "certified", "lastResult", "trials"))) throw invalid("Campos del registro corporal incompatibles"); InfectedToolManifest manifest = InfectedToolManifest.parse(raw.get("manifest").toString(), UUID.fromString(raw.get("author").getAsString())); if (!manifest.sha256().equals(raw.get("sha256").getAsString()) || entries.containsKey(key(manifest.id(), manifest.version()))) throw invalid("Versión o hash corporal alterados"); Learned row = new Learned(manifest); row.createdAt = raw.get("createdAt").getAsString(); row.successes = counter(raw, "successes"); row.failures = counter(raw, "failures"); row.cancellations = counter(raw, "cancellations"); row.reuseSuccesses = counter(raw, "reuseSuccesses"); row.reuseFailures = counter(raw, "reuseFailures"); row.certified = raw.get("certified").getAsBoolean(); row.lastResult = raw.get("lastResult").getAsString(); if (raw.getAsJsonArray("trials").size() > 32) throw invalid("Demasiadas evidencias corporales"); for (JsonElement record : raw.getAsJsonArray("trials")) { Receipt receipt = JSON.fromJson(record, Receipt.class); if (receipt.execution == null || !receipts.add(receipt.execution) || !manifest.author().equals(receipt.actor) || receipt.started < 0 || receipt.steps < 0 || receipt.steps > 16 || receipt.useful < 0 || receipt.useful > 16 || !Set.of("SUCCEEDED", "FAILED", "CANCELLED").contains(receipt.result)) throw invalid("Evidencia corporal inválida"); row.receipts.add(receipt); } if (row.certified && (row.successes < 3 || (double)row.successes / (row.successes + row.failures) < .8 || row.receipts.stream().filter(receipt -> receipt.result.equals("SUCCEEDED") && receipt.useful > 0).count() < 3)) throw invalid("Certificación corporal sin evidencia"); entries.put(key(manifest.id(), manifest.version()), row); } for (Learned row : entries.values()) validateComposition(row.manifest, new HashSet<>(), 0); } catch (RuntimeException | StackOverflowError malformed) { entries.clear(); throw new IOException("Registro corporal inválido; no se otorgaron herramientas", malformed); } } private static long counter(JsonObject object, String key) { long number = object.get(key).getAsBigDecimal().longValueExact(); if (number < 0 || number > 10_000_000) throw invalid("Contador corporal inválido"); return number; } private byte[] readOrCreateKey() throws IOException { Path file = directory.resolve("certification.key"); if (Files.exists(file, LinkOption.NOFOLLOW_LINKS)) { checkFile(file, 32); byte[] key = Files.readAllBytes(file); if (key.length != 32) throw new IOException("Clave corporal inválida"); return key; } if (Files.exists(directory.resolve("learning.json"), LinkOption.NOFOLLOW_LINKS)) throw new IOException("Falta la clave del registro corporal"); byte[] key = new byte[32]; new SecureRandom().nextBytes(key); Files.write(file, key, StandardOpenOption.CREATE_NEW); return key; } private String signature(JsonObject content) { try { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(signingKey, "HmacSHA256")); return HexFormat.of().formatHex(mac.doFinal(InfectedToolManifest.canonical(content).getBytes(StandardCharsets.UTF_8))); } catch (GeneralSecurityException failure) { throw new IllegalStateException(failure); } } private void checkDirectory() throws IOException { for (Path path = directory; path != null; path = path.getParent()) if (Files.exists(path, LinkOption.NOFOLLOW_LINKS)) { BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); if (attrs.isSymbolicLink() || attrs.isOther() || !path.toRealPath().equals(path)) throw new IOException("Tools/infected no atraviesa enlaces o redirecciones"); } } private static void checkFile(Path file, long max) throws IOException { if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(file) || Files.size(file) > max) throw new IOException("Archivo corporal inválido"); } private void write(Path file, String source) throws IOException { if (!file.getParent().equals(directory) || Files.isSymbolicLink(file) || source.getBytes(StandardCharsets.UTF_8).length > MAX_LEDGER_BYTES) throw new IOException("Escritura corporal fuera de límites"); Path temp = directory.resolve("writing_" + UUID.randomUUID() + ".tmp"); try { Files.writeString(temp, source, StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); try { Files.move(temp, file, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); } catch (AtomicMoveNotSupportedException unavailable) { Files.move(temp, file, StandardCopyOption.REPLACE_EXISTING); } } finally { Files.deleteIfExists(temp); } } private void log(String event, InfectedToolManifest manifest, UUID actor, String detail) { LogUtils.getLogger().info("Tools/infected {} {} v{} actor={} {}", event, manifest.id(), manifest.version(), actor, detail); } private static String key(String id, int version) { return id + ":" + version; } private static IllegalArgumentException invalid(String text) { return InfectedToolManifest.invalid(text); } public record Statistics(long successes, long failures, long cancellations, long reuseSuccesses, long reuseFailures, boolean certified, String lastResult) { } private record Counts(int steps, int ticks) { } private record Compiled(String primitive, JsonObject args, int timeoutTicks) { } private record Receipt(UUID execution, UUID actor, long started, String result, int steps, int useful) { } private static final class Learned { final InfectedToolManifest manifest; String createdAt = Instant.now().toString(), lastResult = ""; long successes, failures, cancellations, reuseSuccesses, reuseFailures; boolean certified; final List receipts = new ArrayList<>(); Learned(InfectedToolManifest manifest) { this.manifest = manifest; } } private static final class Execution { final UUID id = UUID.randomUUID(); final Learned row; final ParasiteEntity body; final UUID actor; final long started; final List plan; final boolean trial; final CompletableFuture result = new CompletableFuture<>(); int completed, useful, duration; long stepStarted, attacksBefore, scaresBefore; Vec3 before, targetBefore; Compiled step; CompletableFuture voice; Execution(Learned row, ParasiteEntity body, UUID actor, long started, List plan, boolean trial) { this.row = row; this.body = body; this.actor = actor; this.started = started; this.plan = plan; this.trial = trial; } } }