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); } }