package ar.com.companeros.horror.tools; import ar.com.companeros.Companeros; import ar.com.companeros.horror.*; import com.google.gson.*; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.util.*; import java.util.concurrent.CompletableFuture; import net.minecraft.core.BlockPos; import net.minecraft.gametest.framework.*; import net.minecraft.nbt.*; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.phys.Vec3; import net.minecraftforge.gametest.*; /** La certificación se prueba con locomoción del cuerpo real, nunca un booleano dado por Luna. */ @GameTestHolder(Companeros.ID) @PrefixGameTestTemplate(false) public final class InfectedToolTests { @GameTest(template = "empty") public void bodyPoseCatalogIsBoundedAndAcceptsLegacyTools(GameTestHelper helper) { for (String mode : List.of("auto", "hunched", "crawl", "all_fours", "wall_brace")) { JsonObject args = new JsonObject(); args.addProperty("mode", mode); InfectedToolManifest.validatePrimitive("posture", args, 1, false); args.addProperty("durationTicks", 1200); InfectedToolManifest.validatePrimitive("posture", args, 1, false); } JsonObject args = new JsonObject(); args.addProperty("mode", "wall_brace"); args.addProperty("durationTicks", 1201); rejected(helper, () -> InfectedToolManifest.validatePrimitive("posture", args, 1, false), "Una herramienta no deja una postura activa indefinidamente"); args.addProperty("durationTicks", 20); args.addProperty("block", "inventado"); rejected(helper, () -> InfectedToolManifest.validatePrimitive("posture", args, 1, false), "No acepta soportes ni coordenadas elegidos por el modelo"); args.remove("block"); args.addProperty("mode", "teleport"); rejected(helper, () -> InfectedToolManifest.validatePrimitive("posture", args, 1, false), "La preferencia pertenece a un repertorio cerrado"); helper.succeed(); } private static final UUID AUTHOR = UUID.fromString("00000000-0000-0000-0000-000000000973"); @GameTest(template = "empty") public void bodySandboxRejectsExternalActionsSafetyChangesAndUnverifiedDependencies(GameTestHelper helper) throws Exception { Path folder = folder(); InfectedTools tools = new InfectedTools(folder); JsonObject source = manifest("pending_body", false); InfectedToolManifest pending = tools.proposeForAuthor(AUTHOR, source.toString()); JsonObject changed = source.deepCopy(); changed.addProperty("description", "Sustituido"); rejected(helper, () -> tools.proposeForAuthor(AUTHOR, changed.toString()), "No sustituye una versión existente"); for (String forbidden : List.of("shell", "write_file", "set_stage", "set_sensitivity", "set_attribute", "give_item")) { JsonObject invalid = source.deepCopy(); invalid.addProperty("id", "bad_" + forbidden); invalid.getAsJsonArray("steps").get(0).getAsJsonObject().addProperty("primitive", forbidden); rejected(helper, () -> tools.proposeForAuthor(AUTHOR, invalid.toString()), "Rechaza " + forbidden + " antes de ejecutar"); } JsonObject uuid = source.deepCopy(); uuid.addProperty("id", "target_spoof"); uuid.getAsJsonArray("steps").get(0).getAsJsonObject().getAsJsonObject("args").addProperty("targetUUID", UUID.randomUUID().toString()); rejected(helper, () -> tools.proposeForAuthor(AUTHOR, uuid.toString()), "No admite elegir UUID ni pasar argumentos ocultos"); JsonObject spoof = source.deepCopy(); spoof.addProperty("author", UUID.randomUUID().toString()); rejected(helper, () -> tools.proposeForAuthor(AUTHOR, spoof.toString()), "El JSON no suplanta al autor del servidor"); JsonObject dependency = new JsonObject(); dependency.addProperty("id", pending.id()); dependency.addProperty("version", pending.version()); dependency.addProperty("sha256", pending.sha256()); JsonArray dependencies = new JsonArray(); dependencies.add(dependency); JsonObject composed = source.deepCopy(); composed.addProperty("id", "unverified_child"); composed.add("dependencies", dependencies); rejected(helper, () -> tools.proposeForAuthor(AUTHOR, composed.toString()), "Una propuesta no es una dependencia certificada"); helper.assertTrue(tools.available(UUID.randomUUID()).isEmpty() && !tools.statistics(pending.id(), 1).certified(), "La propuesta pertenece sólo a su autor y no tiene éxitos inventados"); InfectedTools restored = new InfectedTools(folder); helper.assertTrue(restored.available(AUTHOR).size() == 1 && restored.statistics(pending.id(), 1).successes() == 0, "La propuesta recuperada conserva autor y cero pruebas"); helper.succeed(); } @GameTest(template = "empty") public void signedBodyLedgerRejectsForgedSuccessAndMissingKey(GameTestHelper helper) throws Exception { Path folder = folder(); InfectedTools tools = new InfectedTools(folder); tools.proposeForAuthor(AUTHOR, manifest("signed_body", false).toString()); Path ledger = folder.resolve("learning.json"); String original = Files.readString(ledger, StandardCharsets.UTF_8); JsonObject modified = JsonParser.parseString(original).getAsJsonObject(); JsonObject row = modified.getAsJsonObject("content").getAsJsonArray("tools").get(0).getAsJsonObject(); row.addProperty("successes", 3); row.addProperty("certified", true); Files.writeString(ledger, modified.toString(), StandardCharsets.UTF_8); boolean blocked = false; try { new InfectedTools(folder); } catch (IOException expected) { blocked = true; } helper.assertTrue(blocked, "Editar estadísticas no concede una certificación corporal"); Files.writeString(ledger, original, StandardCharsets.UTF_8); Files.move(folder.resolve("certification.key"), folder.resolve("original.key")); blocked = false; try { new InfectedTools(folder); } catch (IOException expected) { blocked = true; } helper.assertTrue(blocked && !Files.exists(folder.resolve("certification.key")), "No reemplaza la clave para aprobar un registro existente"); helper.succeed(); } @GameTest(batch = "horror_03_body_tools", template = "empty", timeoutTicks = 280) public void physicalBodyTrialsCertifyPersistReuseAndRunByEventWithoutLuna(GameTestHelper helper) throws Exception { Fixture fixture = new Fixture(helper); Path folder = folder(); InfectedTools tools = new InfectedTools(folder); InfectedToolManifest source = tools.propose(fixture.body, manifest("physical_retreat", true).toString()); InfectedToolManifest pending = tools.propose(fixture.body, manifest("private_wait", false).toString()); helper.assertTrue(!tools.available(UUID.randomUUID()).contains(source), "Un huésped distinto no ve la herramienta pendiente"); trial(helper, fixture, tools, source, 0, () -> { try { helper.assertTrue(tools.statistics(source.id(), 1).successes() == 3 && tools.statistics(source.id(), 1).certified(), "Tres retiradas con desplazamiento físico certifican el hash observado"); InfectedTools restored = new InfectedTools(folder); helper.assertTrue(restored.statistics(source.id(), 1).certified() && restored.statistics(source.id(), 1).successes() == 3, "La firma y los recibos recuperan la capacidad tras reiniciar"); fixture.resetBody(); fixture.body.setTarget(null); restored.advance(fixture.server()); fixture.body.setTarget(fixture.human); restored.advance(fixture.server()); helper.assertTrue(!restored.busy() && fixture.body.getTarget() == null, "La política aprendida respeta el descanso del humano anterior"); fixture.freshWitness(); fixture.body.setTarget(fixture.human); restored.advance(fixture.server()); helper.assertTrue(restored.busy(), "Ver una nueva presa disponible dispara la política certificada sin llamar a un modelo"); awaitIdle(helper, fixture, restored, () -> { helper.assertTrue(restored.statistics(source.id(), 1).reuseSuccesses() == 1, "La ejecución automática también obtiene un resultado físico real"); fixture.replaceHost(); fixture.resetBody(); rejected(helper, () -> restored.start(fixture.body, pending.id(), pending.version(), Map.of()), "El nuevo huésped no prueba herramientas privadas del anterior"); CompletableFuture reused = restored.start(fixture.body, source.id(), source.version(), Map.of()); await(helper, fixture, restored, reused, () -> { helper.assertTrue(reused.join() == InfectedTools.Outcome.SUCCEEDED && restored.statistics(source.id(), 1).reuseSuccesses() == 2, "Otro huésped canónico reutiliza la herramienta certificada con su propio cuerpo"); fixture.close(); helper.succeed(); }); }); } catch (Throwable failure) { tools.cancelCurrent(); fixture.close(); throw unchecked(failure); } }); helper.runAfterDelay(250, () -> { if (!fixture.closed) { tools.cancelCurrent(); fixture.close(); helper.fail("La prueba corporal excedió su plazo"); } }); } @GameTest(batch = "horror_04_body_noop", template = "empty", timeoutTicks = 50) public void waitingDoesNotCertifyAndConcurrentOrNonCanonicalBodiesAreRejected(GameTestHelper helper) throws Exception { Fixture fixture = new Fixture(helper); InfectedTools tools = new InfectedTools(folder()); InfectedToolManifest source = tools.propose(fixture.body, manifest("noop_body", false).toString()); CompletableFuture running = tools.start(fixture.body, source.id(), 1, Map.of()); rejected(helper, () -> tools.start(fixture.body, source.id(), 1, Map.of()), "No hay dos herramientas simultáneas en el mismo cuerpo"); var impostor = HorrorModule.STALKER.get().create(helper.getLevel()); impostor.setHost(fixture.body.hostId().orElseThrow(), "FakeHost"); rejected(helper, () -> tools.propose(impostor, manifest("impostor_body", false).toString()), "El UUID del huésped no basta para suplantar al cuerpo canónico"); await(helper, fixture, tools, running, () -> { helper.assertTrue(running.join() == InfectedTools.Outcome.FAILED && tools.statistics(source.id(), 1).successes() == 0 && tools.statistics(source.id(), 1).failures() == 1 && !tools.statistics(source.id(), 1).certified(), "Esperar realmente sin progreso no fabrica pruebas de utilidad"); fixture.close(); helper.succeed(); }); } // Las ejecuciones avanzan por ticks; el movimiento lo realiza la IA y navegación reales del mob. private static void trial(GameTestHelper helper, Fixture fixture, InfectedTools tools, InfectedToolManifest source, int index, Runnable done) { fixture.resetBody(); CompletableFuture running = tools.start(fixture.body, source.id(), 1, Map.of()); await(helper, fixture, tools, running, () -> { helper.assertTrue(running.join() == InfectedTools.Outcome.SUCCEEDED && tools.statistics(source.id(), 1).successes() == index + 1, "La prueba " + (index + 1) + " tiene un recibo único y desplazamiento físico"); if (index < 2) trial(helper, fixture, tools, source, index + 1, done); else done.run(); }); } private static void await(GameTestHelper helper, Fixture fixture, InfectedTools tools, CompletableFuture running, Runnable done) { helper.runAfterDelay(1, () -> { try { tools.advance(fixture.server()); if (running.isDone()) done.run(); else await(helper, fixture, tools, running, done); } catch (Throwable failure) { tools.cancelCurrent(); fixture.close(); throw unchecked(failure); } }); } private static void awaitIdle(GameTestHelper helper, Fixture fixture, InfectedTools tools, Runnable done) { helper.runAfterDelay(1, () -> { try { tools.advance(fixture.server()); if (!tools.busy()) done.run(); else awaitIdle(helper, fixture, tools, done); } catch (Throwable failure) { tools.cancelCurrent(); fixture.close(); throw unchecked(failure); } }); } private static final class Fixture implements AutoCloseable { final GameTestHelper helper; final HorrorState previous; final BlockPos position; ServerPlayer human; ParasiteEntity body; boolean closed; Fixture(GameTestHelper helper) { this.helper = helper; previous = HorrorState.get(server()); for (int x = 0; x <= 13; x++) for (int z = 0; z <= 13; z++) helper.setBlock(new BlockPos(x, 1, z), Blocks.DIRT); position = helper.absolutePos(new BlockPos(6, 2, 6)); freshWitness(); replaceHost(); } void freshWitness() { human = new ServerPlayer(server(), helper.getLevel(), new com.mojang.authlib.GameProfile(UUID.randomUUID(), "BodyWitness")); human.connection = new net.minecraft.server.network.ServerGamePacketListenerImpl(server(), new FixtureConnection(), human); for (int tick = 0; tick < 61; tick++) human.tick(); human.moveTo(position.getX() + .5, position.getY(), position.getZ() + 6.5, 0, 0); } net.minecraft.server.MinecraftServer server() { return helper.getLevel().getServer(); } void replaceHost() { if (body != null) body.discard(); HorrorState state = new HorrorState(helper.getLevel().getGameTime()); server().overworld().getDataStorage().set("raps_parasite", state); UUID host = UUID.randomUUID(); String name = "It_" + host.toString().substring(0, 8); state.selectHost(host, name, helper.getLevel().getGameTime()); CompoundTag inventory = new CompoundTag(); inventory.put("Inventory", new ListTag()); inventory.put("EnderItems", new ListTag()); state.prepareQuarantine(inventory, helper.getLevel().dimension().location().toString(), position); state.commitTransfer(); state.created(); body = HorrorModule.STALKER.get().create(helper.getLevel()); body.setUUID(state.entityId().orElseThrow()); body.setHost(host, name); body.setMutationStage(2); resetBody(); if (!helper.getLevel().addFreshEntity(body)) throw new IllegalStateException("No se añadió el cuerpo de prueba"); } void resetBody() { if (!ar.com.companeros.horror.HorrorPacing.get(server()).available(human.getUUID(), helper.getLevel().getGameTime())) freshWitness(); body.getNavigation().stop(); body.setDeltaMovement(Vec3.ZERO); body.moveTo(position.getX() + .5, position.getY(), position.getZ() + .5, 0, 0); body.setIntent(ParasiteEntity.Intent.STALK, 20); body.setTarget(human); } @Override public void close() { if (closed) return; closed = true; body.discard(); server().overworld().getDataStorage().set("raps_parasite", previous); } } private static final class FixtureConnection extends net.minecraft.network.Connection { FixtureConnection() { super(net.minecraft.network.protocol.PacketFlow.SERVERBOUND); } @Override public void send(net.minecraft.network.protocol.Packet packet) { } @Override public void send(net.minecraft.network.protocol.Packet packet, net.minecraft.network.PacketSendListener listener) { } } private static JsonObject manifest(String id, boolean movement) { JsonObject source = new JsonObject(); source.addProperty("schema", 1); source.addProperty("id", id); source.addProperty("version", 1); source.addProperty("description", "Composición corporal de prueba con resultados observables"); JsonArray permissions = new JsonArray(); permissions.add("BODY_MOVE"); source.add("permissions", permissions); source.add("dependencies", new JsonArray()); JsonObject limits = new JsonObject(); limits.addProperty("maxTicks", 50); limits.addProperty("maxSteps", 2); source.add("limits", limits); JsonArray steps = new JsonArray(); steps.add(step("wait", 3, 5)); if (movement) { steps.add(step("retreat", 20, 30)); JsonArray triggers = new JsonArray(); triggers.add("ON_TARGET_VISIBLE"); source.add("triggers", triggers); } source.add("steps", steps); return source; } private static JsonObject step(String primitive, int duration, int timeout) { JsonObject step = new JsonObject(); step.addProperty("primitive", primitive); step.addProperty("timeoutTicks", timeout); JsonObject args = new JsonObject(); args.addProperty("durationTicks", duration); step.add("args", args); return step; } private static Path folder() { return Path.of("infected-tool-test-output", UUID.randomUUID().toString()).toAbsolutePath(); } private static void rejected(GameTestHelper helper, Throwing action, String why) { boolean rejected = false; try { action.run(); } catch (IllegalArgumentException | IOException expected) { rejected = true; } catch (Exception failure) { throw unchecked(failure); } helper.assertTrue(rejected, why); } private static RuntimeException unchecked(Throwable failure) { return failure instanceof RuntimeException runtime ? runtime : new IllegalStateException(failure); } @FunctionalInterface private interface Throwing { void run() throws Exception; } }