Files
MineGeo/scripts/assets.mjs
pavel444-byte ed4d264801
Some checks failed
Deploy MineGeo to Dokploy / deploy (push) Failing after 8m10s
Verify and restore the complete Minecraft asset cache
2026-10-27 21:07:56 +05:00

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