Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 8m10s
170 lines
5.8 KiB
JavaScript
170 lines
5.8 KiB
JavaScript
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
import { createHash } from "node:crypto";
|
|
import { unzipSync } from "fflate";
|
|
export const assetsRoot = ".cache/minecraft/1.21.11";
|
|
export function assetsDirectory(version = "1.21.11") {
|
|
if (!/^(?:1\.21\.11|26\.[12](?:\.\d+)*)$/.test(version))
|
|
throw new Error("Unsupported Minecraft asset version");
|
|
return `.cache/minecraft/${version}`;
|
|
}
|
|
const requiredAssets = [
|
|
"blockstates/grass_block.json",
|
|
"blockstates/stone.json",
|
|
"models/block/block.json",
|
|
"models/block/cube.json",
|
|
"models/block/cube_all.json",
|
|
"textures/block/grass_block_top.png",
|
|
"textures/block/dirt.png",
|
|
"textures/block/stone.png",
|
|
"textures/block/water_still.png",
|
|
];
|
|
async function assetsReady(minecraftVersion) {
|
|
const assetsRoot = assetsDirectory(minecraftVersion);
|
|
const completionMarker = `${assetsRoot}/.minegeo-assets.json`;
|
|
try {
|
|
const marker = JSON.parse(await readFile(completionMarker, "utf8"));
|
|
if (
|
|
marker.format !== "minegeo-assets/v3" ||
|
|
marker.minecraftVersion !== minecraftVersion ||
|
|
!marker.files ||
|
|
!Array.isArray(marker.entries) ||
|
|
marker.entries.length !== marker.files
|
|
)
|
|
return false;
|
|
// Verify the complete extracted inventory, not just common blocks. A
|
|
// partial cache otherwise silently drops newer or less common textures.
|
|
let next = 0;
|
|
await Promise.all(
|
|
Array.from({ length: 16 }, async () => {
|
|
while (next < marker.entries.length) {
|
|
const entry = marker.entries[next++];
|
|
if (
|
|
!Array.isArray(entry) ||
|
|
entry.length !== 2 ||
|
|
typeof entry[0] !== "string" ||
|
|
!/^assets\/minecraft\/(models|blockstates|textures)\/[a-z0-9_./-]+\.(json|png|mcmeta)$/.test(
|
|
entry[0],
|
|
) ||
|
|
entry[0].includes("..") ||
|
|
!Number.isSafeInteger(entry[1]) ||
|
|
entry[1] < 1
|
|
)
|
|
throw new Error("Invalid asset inventory");
|
|
const info = await stat(`${assetsRoot}/${entry[0]}`);
|
|
if (!info.isFile() || info.size !== entry[1])
|
|
throw new Error("Missing asset");
|
|
}
|
|
}),
|
|
);
|
|
for (const asset of requiredAssets) {
|
|
const bytes = await readFile(`${assetsRoot}/assets/minecraft/${asset}`);
|
|
if (asset.endsWith(".json")) JSON.parse(bytes.toString());
|
|
else if (
|
|
bytes.length < 8 ||
|
|
bytes.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a"
|
|
)
|
|
return false;
|
|
}
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
export async function prepareAssets(minecraftVersion = "1.21.11") {
|
|
const assetsRoot = assetsDirectory(minecraftVersion);
|
|
const completionMarker = `${assetsRoot}/.minegeo-assets.json`;
|
|
if (await assetsReady(minecraftVersion)) return assetsRoot;
|
|
console.log(
|
|
`Preparing Minecraft ${minecraftVersion} models and textures for terrain rendering…`,
|
|
);
|
|
const manifest = await (
|
|
await fetch(
|
|
"https://piston-meta.mojang.com/mc/game/version_manifest_v2.json",
|
|
{ signal: AbortSignal.timeout(60000) },
|
|
)
|
|
).json();
|
|
const entry = manifest.versions.find((v) => v.id === minecraftVersion);
|
|
if (!entry)
|
|
throw new Error(`Official Minecraft ${minecraftVersion} metadata missing`);
|
|
const version = await (
|
|
await fetch(entry.url, { signal: AbortSignal.timeout(60000) })
|
|
).json();
|
|
await mkdir(".cache/minecraft", { recursive: true });
|
|
const jarPath = `.cache/minecraft/client-${minecraftVersion}.jar`;
|
|
let jar;
|
|
try {
|
|
jar = await readFile(jarPath);
|
|
} catch {
|
|
/* No cached client yet. */
|
|
}
|
|
if (
|
|
!jar ||
|
|
createHash("sha1").update(jar).digest("hex") !==
|
|
version.downloads.client.sha1
|
|
) {
|
|
const response = await fetch(version.downloads.client.url, {
|
|
signal: AbortSignal.timeout(120000),
|
|
});
|
|
if (!response.ok)
|
|
throw new Error(
|
|
`Minecraft asset download failed: HTTP ${response.status}`,
|
|
);
|
|
jar = Buffer.from(await response.arrayBuffer());
|
|
if (
|
|
createHash("sha1").update(jar).digest("hex") !==
|
|
version.downloads.client.sha1
|
|
)
|
|
throw new Error(
|
|
"Minecraft client checksum mismatch; downloaded assets were not saved",
|
|
);
|
|
await writeFile(jarPath, jar);
|
|
}
|
|
const entries = unzipSync(jar, {
|
|
filter: (f) =>
|
|
/^assets\/minecraft\/(models|blockstates|textures)\//.test(f.name) &&
|
|
/\.(json|png|mcmeta)$/.test(f.name),
|
|
});
|
|
const files = Object.entries(entries);
|
|
const directories = new Set(
|
|
files.map(
|
|
([name]) => `${assetsRoot}/${name.slice(0, name.lastIndexOf("/"))}`,
|
|
),
|
|
);
|
|
await Promise.all(
|
|
[...directories].map((path) => mkdir(path, { recursive: true })),
|
|
);
|
|
let nextFile = 0;
|
|
const writes = await Promise.allSettled(
|
|
Array.from({ length: Math.min(16, files.length) }, async () => {
|
|
while (nextFile < files.length) {
|
|
const [name, bytes] = files[nextFile++];
|
|
await writeFile(`${assetsRoot}/${name}`, bytes);
|
|
}
|
|
}),
|
|
);
|
|
const failedWrite = writes.find((result) => result.status === "rejected");
|
|
if (failedWrite?.status === "rejected") throw failedWrite.reason;
|
|
// Completion is recorded last, so a killed extraction is repaired next time.
|
|
await writeFile(
|
|
completionMarker,
|
|
JSON.stringify({
|
|
format: "minegeo-assets/v3",
|
|
minecraftVersion,
|
|
clientSha1: version.downloads.client.sha1,
|
|
files: Object.keys(entries).length,
|
|
entries: Object.entries(entries).map(([name, bytes]) => [
|
|
name,
|
|
bytes.length,
|
|
]),
|
|
}),
|
|
);
|
|
if (!(await assetsReady(minecraftVersion)))
|
|
throw new Error(
|
|
"Minecraft rendering assets are incomplete or corrupt. Run npm run setup to repair them.",
|
|
);
|
|
console.log(
|
|
`Prepared ${Object.keys(entries).length} verified vanilla model/texture assets.`,
|
|
);
|
|
return assetsRoot;
|
|
}
|