Add panorama previews and repair beta export rendering

This commit is contained in:
2026-10-04 23:12:34 +05:00
parent 763cf8713c
commit aec0641f87
21 changed files with 802 additions and 60 deletions

View File

@@ -18,7 +18,7 @@ async function assetsReady() {
try {
const marker = JSON.parse(await readFile(completionMarker, "utf8"));
if (
marker.format !== "minegeo-assets/v1" ||
marker.format !== "minegeo-assets/v2" ||
marker.minecraftVersion !== "1.21.11" ||
!marker.files
)
@@ -89,7 +89,7 @@ export async function prepareAssets() {
filter: (f) =>
/^assets\/minecraft\/(models|blockstates|textures\/block)\//.test(
f.name,
) && /\.(json|png)$/.test(f.name),
) && /\.(json|png|mcmeta)$/.test(f.name),
});
for (const [name, bytes] of Object.entries(entries)) {
const path = `${assetsRoot}/${name}`;
@@ -100,7 +100,7 @@ export async function prepareAssets() {
await writeFile(
completionMarker,
JSON.stringify({
format: "minegeo-assets/v1",
format: "minegeo-assets/v2",
minecraftVersion: "1.21.11",
clientSha1: version.downloads.client.sha1,
files: Object.keys(entries).length,

View File

@@ -0,0 +1,147 @@
import { createClient } from "@supabase/supabase-js";
import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
import { gunzipSync } from "node:zlib";
import { renderSnapshot } from "../renderer/render.mjs";
if (existsSync(".env.local")) process.loadEnvFile(".env.local");
const id = process.argv[2];
if (
!/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(
id || "",
)
)
throw new Error(
"Usage: npm run repair:panoramas -- <private plugin draft UUID>",
);
const client = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.SUPABASE_SECRET_KEY,
{ auth: { persistSession: false, autoRefreshToken: false } },
);
function check(r) {
if (r.error) throw new Error(r.error.message);
return r.data;
}
const dataset = check(
await client.from("datasets").select("*").eq("id", id).single(),
);
if (
dataset.source !== "plugin" ||
dataset.revision_id ||
!["draft", "rejected"].includes(dataset.state)
)
throw new Error(
"Only a private, unpublished plugin draft can be repaired. Withdraw review or recover failed processing first.",
);
const job = check(
await client
.from("upload_jobs")
.select("state")
.eq("dataset_id", id)
.single(),
);
if (job.state !== "complete")
throw new Error("Wait for processing to finish first.");
const input = check(
await client
.from("dataset_inputs")
.select("manifest,validated")
.eq("dataset_id", id)
.single(),
);
if (!input.validated) throw new Error("The export must be validated first.");
const asset = input.manifest.assets.find((a) => a.path === "terrain.json.gz");
if (!asset) throw new Error("Missing original terrain export");
const raw = check(
await client.storage
.from("minegeo-raw")
.download(`${dataset.owner_id}/${id}/${asset.path}`),
);
const bytes = Buffer.from(await raw.arrayBuffer());
if (
bytes.length !== asset.bytes ||
createHash("sha256").update(bytes).digest("hex") !== asset.sha256
)
throw new Error("Original terrain checksum mismatch");
const output = `.cache/panorama-repair/${id}`;
await mkdir(output, { recursive: true });
const terrain = JSON.parse(
gunzipSync(bytes, { maxOutputLength: 256 * 1024 * 1024 }).toString(),
);
await renderSnapshot(
terrain,
input.manifest.availableViewpoints ?? input.manifest.viewpoints,
output,
{
size: Number(process.env.MINEGEO_FACE_SIZE || 1024),
onProgress: async ({ percent, message }) =>
console.log(`${Math.floor(percent)}% ${message}`),
},
);
// Render first, then briefly lock the draft while replacing its private images.
// Its complete job stays complete, so another worker cannot pick it up.
const locked = check(
await client
.from("datasets")
.update({ state: "processing", updated_at: new Date().toISOString() })
.eq("id", id)
.eq("state", dataset.state)
.eq("updated_at", dataset.updated_at)
.is("revision_id", null)
.select("id"),
);
if (locked.length !== 1)
throw new Error(
"Draft changed during rendering; images were retained locally but nothing was replaced.",
);
try {
for (const entry of (await readdir(output, { recursive: true })).filter((p) =>
p.endsWith(".webp"),
)) {
check(
await client.storage
.from("minegeo-media")
.upload(
`${id}/${entry.replaceAll("\\", "/")}`,
await readFile(`${output}/${entry}`),
{ upsert: true, contentType: "image/webp", cacheControl: "60" },
),
);
}
check(
await client
.from("datasets")
.update({ state: dataset.state, updated_at: new Date().toISOString() })
.eq("id", id)
.eq("state", "processing")
.is("revision_id", null)
.select("id")
.single(),
);
await writeFile(
`${output}/repair.json`,
JSON.stringify({
datasetId: id,
repaired: true,
at: new Date().toISOString(),
}),
);
console.log(
`Repaired draft ${id}. Reload its dashboard preview; no Minecraft export or new upload is needed.`,
);
} catch (error) {
await client
.from("datasets")
.update({
state: "failed",
rejection_reason:
"Image repair interrupted. Reprocess the retained raw upload.",
updated_at: new Date().toISOString(),
})
.eq("id", id)
.eq("state", "processing")
.is("revision_id", null);
throw error;
}