import { mkdir, readFile, writeFile } from "node:fs/promises"; import { createHash } from "node:crypto"; import { unzipSync } from "fflate"; export const assetsRoot = ".cache/minecraft/1.21.11"; const completionMarker = `${assetsRoot}/.minegeo-assets.json`; 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() { try { const marker = JSON.parse(await readFile(completionMarker, "utf8")); if ( marker.format !== "minegeo-assets/v2" || marker.minecraftVersion !== "1.21.11" || !marker.files ) return false; // A blockstate alone is insufficient: interrupted setup can leave models // and textures absent. The marker is only written after all extraction. 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() { if (await assetsReady()) return; console.log( "Preparing Minecraft 1.21.11 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 === "1.21.11"); if (!entry) throw new Error("Official Minecraft 1.21.11 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-1.21.11.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\/block)\//.test( f.name, ) && /\.(json|png|mcmeta)$/.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); } // Completion is recorded last, so a killed extraction is repaired next time. await writeFile( completionMarker, JSON.stringify({ format: "minegeo-assets/v2", minecraftVersion: "1.21.11", clientSha1: version.downloads.client.sha1, files: Object.keys(entries).length, }), ); if (!(await assetsReady())) 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.`, ); }