import assert from "node:assert/strict"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { gunzipSync } from "node:zlib"; import sharp from "sharp"; import { renderSnapshot } from "../renderer/render.mjs"; import { assertTerrainImage } from "../renderer/image-validation.mjs"; // Exercise the product's rendering worker without launching a Minecraft server // or driving the website. A cached real export can also be supplied as argv[2]. const output = ".cache/renderer-regression"; const snapshot = { bounds: { minX: -24, minZ: -24, maxX: 24, maxZ: 24 }, coverage: [], blocks: [], }; for (let x = -24; x < 24; x++) { for (let z = -24; z < 24; z++) { snapshot.blocks.push([x, 63, z, "minecraft:dirt"]); snapshot.blocks.push([ x, 64, z, x > 12 && z < -12 ? "minecraft:water[level=0]" : x < -12 ? "minecraft:grass_block" : "minecraft:grass_block[snowy=false]", ]); } } for (let x = -4; x <= 4; x++) { for (let z = -4; z <= 4; z++) { for (let y = 65; y <= 71; y++) { if (Math.abs(x) === 4 || Math.abs(z) === 4 || y === 71) { snapshot.blocks.push([ x, y, z, y === 71 ? "minecraft:oak_planks" : "minecraft:stone_bricks", ]); } } } } for (let y = 65; y <= 70; y++) snapshot.blocks.push([-16, y, 0, "minecraft:oak_log[axis=y]"]); for (let x = -18; x <= -14; x++) { for (let z = -2; z <= 2; z++) { snapshot.blocks.push([ x, 71, z, "minecraft:oak_leaves[distance=1,persistent=true,waterlogged=false]", ]); } } snapshot.blocks.push([ 5, 65, 0, "minecraft:oak_stairs[facing=east,half=bottom,shape=straight,waterlogged=false]", ]); let terrain = snapshot; let points = [ [-12.5, 12.5], [12.5, 12.5], [12.5, -12.5], [-12.5, -12.5], [0.5, 18.5], ].map(([x, z], i) => ({ id: `p${i}`, x, y: 66.62, z })); let source = "grass, dirt, water, tree and building regression fixture"; if (process.argv[2]) { const directory = process.argv[2]; const manifest = JSON.parse( await readFile(`${directory}/manifest.json`, "utf8"), ); terrain = JSON.parse( gunzipSync(await readFile(`${directory}/terrain.json.gz`)).toString(), ); points = manifest.viewpoints; source = "cached Paper export (no server started)"; } await mkdir(output, { recursive: true }); const report = await renderSnapshot(terrain, points, output, { size: 256 }); assert.ok(report.meshes > 0, "terrain must produce meshes"); assert.ok(report.emittedFaces > 0, "terrain must produce faces"); assert.ok(report.hiddenFaces > 0, "interior terrain faces must be culled"); assert.ok(report.geometryBytes > 0, "geometry must reach the renderer"); assert.equal( report.unsupported.some((s) => /minecraft:(stone|dirt|grass_block|water)(\[|$)/.test(s), ), false, ); await assertTerrainImage(await readFile(`${output}/map.webp`), "Surface map"); const mapMeta = await sharp(`${output}/map.webp`).metadata(); assert.equal(mapMeta.width, report.map.width); assert.equal(mapMeta.height, report.map.height); assert.equal(report.map.cameraY, 320); const { data: mapPixels, info: mapInfo } = await sharp(`${output}/map.webp`) .raw() .toBuffer({ resolveWithObject: true }); const westGrass = (24 * mapInfo.width + 24) * mapInfo.channels; assert.ok( mapPixels[westGrass + 1] > mapPixels[westGrass], "blocks without explicit default properties must appear on the aerial map", ); const mapPixel = (x, z) => { const pixelX = Math.floor(((x - snapshot.bounds.minX + 0.5) / 48) * mapInfo.width); const pixelY = Math.floor(((z - snapshot.bounds.minZ + 0.5) / 48) * mapInfo.height); const offset = (pixelY * mapInfo.width + pixelX) * mapInfo.channels; return mapPixels.subarray(offset, offset + 3); }; if (!process.argv[2]) { const shadedGrass = mapPixel(10, 9); const sunlitGrass = mapPixel(10, -9); assert.ok( sunlitGrass[1] - shadedGrass[1] > 20, "the raised building must cast a visible shadow on the aerial map", ); } for (const point of points) { for (const face of ["px", "nx", "py", "ny", "pz", "nz"]) { const metadata = await sharp( await readFile(`${output}/${point.id}/${face}.webp`), ).metadata(); assert.equal(metadata.format, "webp"); assert.equal(metadata.width, 256); assert.equal(metadata.height, 256); } await assertTerrainImage( await readFile(`${output}/${point.id}/ny.webp`), `Panorama ${point.id}, downward face`, ); } // The earlier renderer silently accepted this case with zero meshes and faces. const missingModel = { ...snapshot, blocks: [[0, 64, 0, "minecraft:minegeo_missing_model"]], }; await assert.rejects( renderSnapshot(missingModel, [points[0]], `${output}/missing-model`, { size: 256, }), /No terrain models could be rendered/, ); await writeFile( `${output}/verification.json`, JSON.stringify( { source, ...report, checks: [ "nonempty terrain", "valid WebP cubemap faces", "surface visible below every viewpoint", "missing model rejected", ], }, null, 2, ), ); console.log( "Renderer regression passed:", JSON.stringify({ source, ...report }), );