Volver al índice
src/main/java/ar/com/companeros/horror/tools/InfectedToolTests.java
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<InfectedTools.Outcome> 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<InfectedTools.Outcome> 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<InfectedTools.Outcome> 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; }
}