169 lines
5.1 KiB
JavaScript
169 lines
5.1 KiB
JavaScript
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 }),
|
|
);
|