Files
MineGeo/scripts/test-minecraft-versions.mjs

206 lines
6.5 KiB
JavaScript

import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { mkdir, readFile, writeFile, copyFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { createHash } from "node:crypto";
import { gunzipSync } from "node:zlib";
import path from "node:path";
import { stripVTControlCharacters } from "node:util";
// Disposable servers listen only on loopback. No fixture uploads or tokens are
// sent to MineGeo. Keep exports/logs locally as compatibility evidence.
const root = path.resolve(".cache/minecraft-compat");
const java = process.env.JAVA_HOME
? path.join(
process.env.JAVA_HOME,
"bin",
process.platform === "win32" ? "java.exe" : "java",
)
: "java";
async function serverJar(version) {
await mkdir(root, { recursive: true });
const file = `${root}/paper-${version}.jar`;
if (version === "1.21.11" && !existsSync(file))
await copyFile(".cache/paper-proof/paper.jar", file);
if (existsSync(file)) return file;
const response = await fetch(
`https://fill.papermc.io/v3/projects/paper/versions/${version}/builds`,
{
headers: {
"User-Agent": "MineGeo/0.1.3 local compatibility verification",
},
},
);
assert.equal(response.status, 200);
const builds = await response.json();
const download = (builds.find((b) => b.channel === "STABLE") ?? builds[0])
.downloads["server:default"];
const bytes = Buffer.from(await (await fetch(download.url)).arrayBuffer());
assert.equal(
createHash("sha256").update(bytes).digest("hex"),
download.checksums.sha256,
);
await writeFile(file, bytes);
return file;
}
function monitor(child, label) {
let output = "";
const waiters = new Set();
let ended = false;
const record = (bytes) => {
output += stripVTControlCharacters(bytes.toString());
for (const w of waiters)
if (w.pattern.test(output.slice(w.start))) {
clearTimeout(w.timer);
waiters.delete(w);
w.resolve();
}
};
child.stdout.on("data", record);
child.stderr.on("data", record);
const exit = new Promise((resolve) =>
child.on("exit", (code) => {
ended = true;
for (const w of waiters) {
clearTimeout(w.timer);
w.reject(new Error(`${label} exited ${code}: ${output.slice(-5000)}`));
}
waiters.clear();
resolve(code);
}),
);
return {
get output() {
return output;
},
exit,
wait(pattern, start = output.length, timeout = 180000) {
return new Promise((resolve, reject) => {
if (pattern.test(output.slice(start))) return resolve();
if (ended)
return reject(
new Error(`${label} already exited: ${output.slice(-5000)}`),
);
const w = {
pattern,
start,
resolve,
reject,
timer: setTimeout(() => {
waiters.delete(w);
reject(
new Error(
`${label} timed out for ${pattern}: ${output.slice(-5000)}`,
),
);
}, timeout),
};
waiters.add(w);
});
},
async command(command, pattern) {
const ready = this.wait(pattern);
child.stdin.write(command + "\n");
await ready;
},
};
}
if (process.argv.includes("--fabric"))
throw new Error(
"This check runs PaperMC servers only; Minecraft clients are disabled.",
);
const selected = process.argv
.find((arg) => arg.startsWith("--version="))
?.split("=")[1];
const matrix = ["1.21.11", "26.1.2", "26.2"].filter(
(v) => !selected || v === selected,
);
for (const version of matrix) {
const directory = `${root}/plugin-${version}-${Date.now()}`;
await mkdir(`${directory}/plugins`, { recursive: true });
await copyFile(await serverJar(version), `${directory}/paper.jar`);
await copyFile(
"public/tools/plugin/minegeo-plugin-0.1.3.jar",
`${directory}/plugins/minegeo.jar`,
);
await writeFile(`${directory}/eula.txt`, "eula=true\n");
await writeFile(
`${directory}/server.properties`,
[
"server-ip=127.0.0.1",
`server-port=25582`,
"online-mode=false",
"level-type=minecraft:flat",
'generator-settings={"layers":[{"block":"minecraft:bedrock","height":1},{"block":"minecraft:dirt","height":2},{"block":"minecraft:grass_block","height":1}],"biome":"minecraft:plains"}',
"generate-structures=false",
"view-distance=3",
"simulation-distance=2",
"max-players=1",
"spawn-protection=0",
].join("\n") + "\n",
);
const server = spawn(java, ["-Xmx1536M", "-jar", "paper.jar", "--nogui"], {
cwd: directory,
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
});
const logs = monitor(server, `Paper ${version}`);
try {
await logs.wait(/Done \(/, 0);
console.log(`Paper ${version} started.`);
await logs.command("version MineGeo", /MineGeo version 0\.1\.3/);
await logs.command(
"minegeo website",
/MineGeo website: https:\/\/minegeo\.vercel\.app/,
);
await logs.command(
"minegeo website http://127.0.0.1:9999",
/website is fixed to https:\/\/minegeo\.vercel\.app/,
);
await logs.command(
"minegeo server play.compat.example",
/Capture server address saved/,
);
await logs.command("minegeo area world -16 -16 16 16", /area selected/);
await logs.command("forceload add -16 -16 15 15", /Marked|already marked/);
await new Promise((r) => setTimeout(r, 5000));
await logs.command(
"minegeo export Compatibility World",
/Export complete:/,
);
const relative = (
await readFile(`${directory}/plugins/MineGeo/latest-export.txt`, "utf8")
).trim();
const manifest = JSON.parse(
await readFile(
`${directory}/plugins/MineGeo/${relative}/manifest.json`,
"utf8",
),
);
assert.equal(manifest.minecraftVersion, version);
assert.equal(manifest.viewpoints.length, 24);
const terrain = JSON.parse(
gunzipSync(
await readFile(
`${directory}/plugins/MineGeo/${relative}/terrain.json.gz`,
),
).toString(),
);
assert.ok(terrain.blocks.length > 0);
await logs.command(
"minegeo upload",
/Create a token in the website dashboard/,
);
console.log(
`PLUGIN ${version} PASSED: fixed endpoint, commands, real export, correct version, ${terrain.blocks.length} blocks.`,
);
} finally {
server.stdin.write("stop\n");
const timeout = setTimeout(() => server.kill(), 20000);
await logs.exit;
clearTimeout(timeout);
await writeFile(`${directory}/server.log`, logs.output);
}
}