The first commit, the core and all files is adding to repo
This commit is contained in:
48
scripts/assets.mjs
Normal file
48
scripts/assets.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
import { mkdir, readFile, writeFile, access } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { unzipSync } from "fflate";
|
||||
export const assetsRoot = ".cache/minecraft/1.21.11";
|
||||
export async function prepareAssets() {
|
||||
try {
|
||||
await access(`${assetsRoot}/assets/minecraft/blockstates/grass_block.json`);
|
||||
return;
|
||||
} catch {}
|
||||
const manifest = await (
|
||||
await fetch(
|
||||
"https://piston-meta.mojang.com/mc/game/version_manifest_v2.json",
|
||||
)
|
||||
).json();
|
||||
const entry = manifest.versions.find((v) => v.id === "1.21.11");
|
||||
if (!entry) throw new Error("Official Minecraft 1.21.11 metadata missing");
|
||||
const version = await (await fetch(entry.url)).json();
|
||||
await mkdir(".cache/minecraft", { recursive: true });
|
||||
const jarPath = ".cache/minecraft/client-1.21.11.jar";
|
||||
let jar;
|
||||
try {
|
||||
jar = await readFile(jarPath);
|
||||
} catch {
|
||||
const response = await fetch(version.downloads.client.url);
|
||||
if (!response.ok) throw new Error("Minecraft asset download failed");
|
||||
jar = Buffer.from(await response.arrayBuffer());
|
||||
await writeFile(jarPath, jar);
|
||||
}
|
||||
if (
|
||||
createHash("sha1").update(jar).digest("hex") !==
|
||||
version.downloads.client.sha1
|
||||
)
|
||||
throw new Error("Minecraft client checksum mismatch");
|
||||
const entries = unzipSync(jar, {
|
||||
filter: (f) =>
|
||||
/^assets\/minecraft\/(models|blockstates|textures\/block)\//.test(
|
||||
f.name,
|
||||
) && /\.(json|png)$/.test(f.name),
|
||||
});
|
||||
for (const [name, bytes] of Object.entries(entries)) {
|
||||
const path = `${assetsRoot}/${name}`;
|
||||
await mkdir(path.slice(0, path.lastIndexOf("/")), { recursive: true });
|
||||
await writeFile(path, bytes);
|
||||
}
|
||||
console.log(
|
||||
`Prepared ${Object.keys(entries).length} verified vanilla model/texture assets.`,
|
||||
);
|
||||
}
|
||||
37
scripts/clean-build.mjs
Normal file
37
scripts/clean-build.mjs
Normal file
@@ -0,0 +1,37 @@
|
||||
import { resolve } from "node:path";
|
||||
import { existsSync } from "node:fs";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { spawn } from "node:child_process";
|
||||
// OneDrive marks directories as Windows reparse points, which Next's Node
|
||||
// cleanup may mistake for links. Use native PowerShell for this build cache.
|
||||
const root = resolve("."),
|
||||
target = resolve(root, ".next");
|
||||
if (target !== resolve(root, ".next") || !target.startsWith(root))
|
||||
throw new Error("Unsafe build cache target");
|
||||
if (existsSync(target)) {
|
||||
if (process.platform === "win32")
|
||||
await new Promise((accept, reject) => {
|
||||
const escaped = target.replaceAll("'", "''");
|
||||
const child = spawn(
|
||||
"powershell.exe",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
`Remove-Item -LiteralPath '${escaped}' -Recurse -Force -ErrorAction Stop`,
|
||||
],
|
||||
{ stdio: "inherit", windowsHide: true },
|
||||
);
|
||||
child.on("exit", (code) =>
|
||||
code === 0
|
||||
? accept()
|
||||
: reject(
|
||||
new Error(
|
||||
"Could not clear the Next build cache. Stop the dev server first.",
|
||||
),
|
||||
),
|
||||
);
|
||||
child.on("error", reject);
|
||||
});
|
||||
else await rm(target, { recursive: true, force: true });
|
||||
}
|
||||
13
scripts/fabric-proof.mjs
Normal file
13
scripts/fabric-proof.mjs
Normal file
@@ -0,0 +1,13 @@
|
||||
import {spawn} from 'node:child_process';
|
||||
import {readFile,writeFile,rename} from 'node:fs/promises';
|
||||
import {existsSync} from 'node:fs';
|
||||
const directory='.cache/paper-proof',plugin=`${directory}/plugins/minegeo.jar`,disabled=plugin+'.disabled';
|
||||
if(existsSync(plugin))await rename(plugin,disabled);
|
||||
const previous=await readFile(`${directory}/server.properties`,'utf8');
|
||||
await writeFile(`${directory}/server.properties`,previous.replace('online-mode=true','online-mode=false'));
|
||||
let client,server,success=false;const points=[[0,100,20],[-20,100,20],[20,100,-20],[-20,100,-20],[30,100,0]];
|
||||
try{
|
||||
await new Promise((accept,reject)=>{server=spawn('java',['-Xmx2G','-jar','paper.jar','--nogui'],{cwd:directory,stdio:['pipe','pipe','pipe'],windowsHide:true});let started=false;let tail='';const output=data=>{process.stdout.write(data);tail=(tail+data.toString()).slice(-6000);if(!started&&tail.includes('Done (')){started=true;server.stdin.write('setworldspawn 0 100 20\n');accept();}if(data.toString().includes('MineGeoTest joined'))server.stdin.write('gamemode creative MineGeoTest\ntp MineGeoTest 0 100 20\n');};server.stdout.on('data',output);server.stderr.on('data',output);server.on('exit',code=>{if(!started)reject(new Error('Fixture server exited '+code));});setTimeout(()=>{if(!started)reject(new Error('Fixture server startup timed out'));},120000).unref();});
|
||||
await new Promise((accept,reject)=>{client=spawn(process.platform==='win32'?'gradlew.bat':'./gradlew',[':fabric:runCaptureTest'],{cwd:'minecraft',stdio:['pipe','pipe','pipe'],shell:process.platform==='win32',windowsHide:true});let seen=new Set();const output=data=>{process.stdout.write(data);const text=data.toString();for(let i=1;i<=4;i++)if(text.includes('MINEGEO_TEST_CAPTURE_'+i)&&!seen.has(i)){seen.add(i);const p=points[i];server.stdin.write(`tp MineGeoTest ${p.join(' ')}\n`);}if(text.includes('MINEGEO_TEST_PASS'))success=true;};client.stdout.on('data',output);client.stderr.on('data',output);client.on('exit',code=>success&&code===0?accept():reject(new Error('Fabric capture test did not pass: '+code)));const timer=setTimeout(()=>{client.kill();reject(new Error('Fabric fixture timed out'));},360000);timer.unref();});
|
||||
console.log('Fabric captured five real panoramas on a server without MineGeo installed, and restored settings on success/cancellation.');
|
||||
}finally{server?.stdin.write('stop\n');await writeFile(`${directory}/server.properties`,previous);if(existsSync(disabled))await rename(disabled,plugin);}
|
||||
24
scripts/java-build.mjs
Normal file
24
scripts/java-build.mjs
Normal file
@@ -0,0 +1,24 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, copyFile } from "node:fs/promises";
|
||||
const windows = process.platform === "win32";
|
||||
const command = windows ? "gradlew.bat" : "./gradlew";
|
||||
const targets = process.argv.slice(2);
|
||||
if (!targets.length) targets.push(":plugin:build", ":fabric:build");
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn(command, targets, {
|
||||
cwd: "minecraft",
|
||||
stdio: "inherit",
|
||||
shell: windows,
|
||||
});
|
||||
child.on("error", reject);
|
||||
child.on("exit", (code) =>
|
||||
code === 0 ? resolve() : reject(new Error(`Gradle exited ${code}`)),
|
||||
);
|
||||
});
|
||||
await mkdir("public/downloads", { recursive: true });
|
||||
for (const name of ["plugin", "fabric"]) {
|
||||
const file = `minegeo-${name}-1.21.11.jar`,
|
||||
path = `minecraft/${name}/build/libs/${file}`;
|
||||
if (existsSync(path)) await copyFile(path, `public/downloads/${file}`);
|
||||
}
|
||||
113
scripts/proof.mjs
Normal file
113
scripts/proof.mjs
Normal file
@@ -0,0 +1,113 @@
|
||||
import { readFile, writeFile, mkdir, copyFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { spawn } from "node:child_process";
|
||||
import { gunzipSync } from "node:zlib";
|
||||
import { renderSnapshot } from "../renderer/render.mjs";
|
||||
import { prepareAssets } from "./assets.mjs";
|
||||
const directory = ".cache/paper-proof";
|
||||
await mkdir(`${directory}/plugins`, { recursive: true });
|
||||
const jar = `${directory}/paper.jar`;
|
||||
if (!existsSync(jar)) {
|
||||
const builds = await (
|
||||
await fetch(
|
||||
"https://fill.papermc.io/v3/projects/paper/versions/1.21.11/builds",
|
||||
{ headers: { "User-Agent": "MineGeo/0.1 local development" } },
|
||||
)
|
||||
).json();
|
||||
const build = builds.find((b) => b.channel === "STABLE");
|
||||
const download = build.downloads["server:default"];
|
||||
const bytes = Buffer.from(await (await fetch(download.url)).arrayBuffer());
|
||||
if (
|
||||
createHash("sha256").update(bytes).digest("hex") !==
|
||||
download.checksums.sha256
|
||||
)
|
||||
throw new Error("Paper checksum mismatch");
|
||||
await writeFile(jar, bytes);
|
||||
}
|
||||
await copyFile(
|
||||
"minecraft/plugin/build/libs/minegeo-plugin-1.21.11.jar",
|
||||
`${directory}/plugins/minegeo.jar`,
|
||||
);
|
||||
// This disposable loopback-only server exists solely for the requested integration test.
|
||||
await writeFile(`${directory}/eula.txt`, "eula=true\n");
|
||||
await writeFile(
|
||||
`${directory}/server.properties`,
|
||||
"server-ip=127.0.0.1\nserver-port=25579\nonline-mode=true\nlevel-seed=928341\nview-distance=5\nsimulation-distance=2\nspawn-protection=0\nenable-rcon=false\nmax-players=1\n",
|
||||
);
|
||||
if (!existsSync(`${directory}/plugins/MineGeo/export/terrain.json.gz`)) {
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = spawn("java", ["-Xmx2G", "-jar", "paper.jar", "--nogui"], {
|
||||
cwd: directory,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
let ready = false,
|
||||
exported = false,
|
||||
buffer = "";
|
||||
const timer = setTimeout(() => {
|
||||
child.stdin.write("stop\n");
|
||||
reject(new Error("Paper proof timed out"));
|
||||
}, 240000);
|
||||
process.once("SIGINT", () => child.stdin.write("stop\n"));
|
||||
const output = (data) => {
|
||||
const text = data.toString();
|
||||
process.stdout.write(text);
|
||||
buffer = (buffer + text).slice(-20000);
|
||||
if (!ready && buffer.includes("Done (")) {
|
||||
ready = true;
|
||||
// The fixture explicitly generates its test area before export. The production
|
||||
// plugin never generates chunks. No player connects during this test.
|
||||
child.stdin.write("forceload add -48 -48 47 47\n");
|
||||
setTimeout(
|
||||
() =>
|
||||
child.stdin.write(
|
||||
"fill -47 99 -47 47 99 47 minecraft:grass_block\nfill -6 100 -6 6 103 6 minecraft:stone_bricks\nfill -5 104 -5 5 105 5 minecraft:oak_planks\nfill 16 100 16 24 100 24 minecraft:water\nminegeo area world -48 -48 48 48\nminegeo export Paper Proof\n",
|
||||
),
|
||||
12000,
|
||||
);
|
||||
}
|
||||
if (!exported && buffer.includes("Export complete:")) {
|
||||
exported = true;
|
||||
child.stdin.write("stop\n");
|
||||
}
|
||||
if (!exported && text.includes("Export failed:"))
|
||||
child.stdin.write("stop\n");
|
||||
};
|
||||
child.stdout.on("data", output);
|
||||
child.stderr.on("data", output);
|
||||
child.on("exit", (code) => {
|
||||
clearTimeout(timer);
|
||||
exported
|
||||
? resolve()
|
||||
: reject(new Error(`Paper exited ${code} without an export`));
|
||||
});
|
||||
});
|
||||
}
|
||||
const source = `${directory}/plugins/MineGeo/export`;
|
||||
const manifest = JSON.parse(await readFile(`${source}/manifest.json`, "utf8"));
|
||||
const terrain = JSON.parse(
|
||||
gunzipSync(await readFile(`${source}/terrain.json.gz`)).toString(),
|
||||
);
|
||||
await prepareAssets();
|
||||
const report = await renderSnapshot(
|
||||
terrain,
|
||||
manifest.viewpoints,
|
||||
"public/generated/paper-proof",
|
||||
{ size: 1024 },
|
||||
);
|
||||
await mkdir(".data/fixtures", { recursive: true });
|
||||
await copyFile(`${source}/manifest.json`, ".data/fixtures/paper-proof.json");
|
||||
await writeFile(
|
||||
"artifacts-renderer-proof.json",
|
||||
JSON.stringify(
|
||||
{
|
||||
...report,
|
||||
source: "real Paper 1.21.11 terrain snapshot",
|
||||
bounds: manifest.bounds,
|
||||
viewpoints: manifest.viewpoints.length,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
console.log("RENDERER PROOF:", JSON.stringify(report));
|
||||
21
scripts/seed-proof.ts
Normal file
21
scripts/seed-proof.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import {createClient} from '@supabase/supabase-js';
|
||||
import {createHash,randomBytes} from 'node:crypto';
|
||||
import {mkdir,readFile,writeFile} from 'node:fs/promises';
|
||||
import {existsSync} from 'node:fs';
|
||||
process.loadEnvFile('.env.local');
|
||||
const client=createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!,process.env.SUPABASE_SECRET_KEY!,{auth:{persistSession:false}});
|
||||
function check<T>(r:{data:T,error:{message:string}|null}):NonNullable<T>{if(r.error)throw new Error(r.error.message);return r.data!;}
|
||||
await mkdir('.data',{recursive:true});
|
||||
if(!existsSync('.data/test-account.json')){
|
||||
const password=randomBytes(32).toString('hex'),token='mg_'+randomBytes(32).toString('hex'),email='minegeo-fixture-'+randomBytes(4).toString('hex')+'@example.com';
|
||||
const {user}=check(await client.auth.admin.createUser({email,password,email_confirm:true}));if(!user)throw new Error('No fixture account returned');
|
||||
check(await client.from('profiles').insert({id:user.id,display_name:'MineGeo test world',role:'moderator'}));
|
||||
check(await client.from('contributor_tokens').insert({owner_id:user.id,token_hash:createHash('sha256').update(token).digest('hex'),label:'Local integration fixture'}));
|
||||
await writeFile('.data/test-account.json',JSON.stringify({id:user.id,email,password,token}));
|
||||
}
|
||||
const manifest=JSON.parse(await readFile('.cache/paper-proof/plugins/MineGeo/export/manifest.json','utf8'));
|
||||
manifest.name='The First Expedition';manifest.serverName='MineGeo Test World';manifest.description='A real Paper 1.21.11 renderer test world: terrain, water and a builder’s outpost. Exported with no connected capture player. This small map is for trying MineGeo.';
|
||||
await mkdir('.data/proof-upload',{recursive:true});
|
||||
await writeFile('.data/proof-upload/manifest.json',JSON.stringify(manifest));
|
||||
await writeFile('.data/proof-upload/terrain.json.gz',await readFile('.cache/paper-proof/plugins/MineGeo/export/terrain.json.gz'));
|
||||
console.log('Fixture contributor and real Paper upload are ready. Credentials remain in ignored local files.');
|
||||
32
scripts/setup.mjs
Normal file
32
scripts/setup.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { copyFile, mkdir } from "node:fs/promises";
|
||||
import { prepareAssets } from "./assets.mjs";
|
||||
if (!existsSync(".env.local")) await copyFile(".env.example", ".env.local");
|
||||
await mkdir(".data", { recursive: true });
|
||||
await mkdir("public/generated", { recursive: true });
|
||||
await mkdir("public/downloads", { recursive: true });
|
||||
await new Promise((resolve, reject) => {
|
||||
const p = spawn(
|
||||
process.execPath,
|
||||
["node_modules/playwright/cli.js", "install", "chromium"],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
p.on("exit", (code) =>
|
||||
code === 0
|
||||
? resolve()
|
||||
: reject(new Error(`Chromium installation exited ${code}`)),
|
||||
);
|
||||
});
|
||||
await prepareAssets();
|
||||
process.loadEnvFile(".env.local");
|
||||
const configured = Boolean(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL &&
|
||||
process.env.SUPABASE_SECRET_KEY &&
|
||||
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
|
||||
);
|
||||
console.log(
|
||||
configured
|
||||
? "Supabase configuration present. Apply the checked-in migration before starting."
|
||||
: "Supabase is not configured. Local guest sandbox is available; hosted uploads/moderation remain disabled.",
|
||||
);
|
||||
37
scripts/supervise.mjs
Normal file
37
scripts/supervise.mjs
Normal file
@@ -0,0 +1,37 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
if (existsSync(".env.local")) process.loadEnvFile(".env.local");
|
||||
const dev = process.argv[2] === "dev";
|
||||
const children = [];
|
||||
let stopping = false;
|
||||
function run(args) {
|
||||
const c = spawn(process.execPath, args, {
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
children.push(c);
|
||||
c.on("exit", (code) => {
|
||||
if (!stopping) {
|
||||
console.error(
|
||||
`MineGeo process exited (${code}). Shutting down companion process.`,
|
||||
);
|
||||
stop(code ?? 1);
|
||||
}
|
||||
});
|
||||
return c;
|
||||
}
|
||||
function stop(code = 0) {
|
||||
if (stopping) return;
|
||||
stopping = true;
|
||||
for (const c of children) c.kill("SIGTERM");
|
||||
setTimeout(() => process.exit(code), 500).unref();
|
||||
}
|
||||
process.on("SIGINT", () => stop());
|
||||
process.on("SIGTERM", () => stop());
|
||||
run([
|
||||
"node_modules/next/dist/bin/next",
|
||||
dev ? "dev" : "start",
|
||||
"--hostname",
|
||||
process.env.MINEGEO_HOST || "127.0.0.1",
|
||||
]);
|
||||
run(["--import", "tsx", "worker/main.ts"]);
|
||||
Reference in New Issue
Block a user