Files
deepgrain-foundry/test/t_load.js
T

143 lines
8.7 KiB
JavaScript

// Reload wakes nearby areas first, without losing distant water, fire or saved state.
const fs = require('fs');
const path = require('path');
const { load, suite, flatWorld, rect, count, ticks } = require('./harness');
const { run } = load();
const { check, done } = suite();
// Exercise the real hover function headlessly, without starting a browser or GPU.
run("const localStorage = { getItem: () => '{}', setItem() {} };");
for (const f of ['ui/state.js', 'ui/hud.js', 'ui/cursor.js', 'ui/info.js'])
run(fs.readFileSync(path.join(__dirname, '../src', f), 'utf8'));
flatWorld(run, 256, 256, 200);
run('globalThis.validationSave = serializeWorld();');
function refusedBeforeAlloc(label, doctor, message) {
check(label, run(`(() => {
const s = { meta: { ...validationSave.meta, cols: { ...validationSave.meta.cols } }, buffers: { ...validationSave.buffers } };
${doctor}
const originalAlloc = alloc, originalMat = mat, originalBlocks = blockP;
const oldW = W, oldH = H, oldMoney = money, oldX = bot.x;
let allocations = 0, error;
alloc = () => { allocations++; throw new Error('Allocation reached'); };
try { deserializeWorld(s.meta, s.buffers); } catch (e) { error = e; }
finally { alloc = originalAlloc; }
return error instanceof Error && ${message}.test(error.message) && allocations === 0
&& W === oldW && H === oldH && mat === originalMat && blockP === originalBlocks
&& money === oldMoney && bot.x === oldX;
})()`));
}
for (const [w, h] of [[1000000, 1000000], [6145, 1024], [2048, 2049], [0, 1024], [2048, -1], [2048.5, 1024], [2048, NaN]])
refusedBeforeAlloc(`invalid size ${w} x ${h} is refused before touching the world`, `s.meta.W = ${w}; s.meta.H = ${h};`, '/world size/i');
for (const key of ['mat', 'temp', 'life', 'flags', 'part', 'blockP', 'blockV', 'bgm', 'fog']) {
refusedBeforeAlloc(`short ${key} buffer is refused before touching the world`, `s.buffers.${key} = s.buffers.${key}.subarray(1);`, `/${key}/i`);
refusedBeforeAlloc(`long ${key} buffer is refused before touching the world`, `s.buffers.${key} = new s.buffers.${key}.constructor(s.buffers.${key}.length + 1);`, `/${key}/i`);
}
for (const key of ['mat', 'temp', 'life', 'flags', 'part', 'blockP', 'blockV'])
refusedBeforeAlloc(`missing ${key} buffer is refused before touching the world`, `delete s.buffers.${key};`, `/${key}/i`);
for (const key of ['surfY', 'dirtY', 'graniteY', 'basaltY', 'magmaY'])
refusedBeforeAlloc(`short ${key} column is refused before touching the world`, `s.meta.cols.${key} = [];`, `/${key}/i`);
refusedBeforeAlloc('short slope phases are refused before touching the world', 's.meta.slopePhases = [0];', '/slopePhases/i');
for (const [label, doctor, clean] of [
['unknown cell material', 's.buffers.mat[20 * W + 20] = 250;', 'mat[20 * W + 20] === EMPTY'],
['unknown block code', 's.buffers.blockP[5 * BW + 5] = 250;', 'blockP[5 * BW + 5] === 0'],
]) {
check(label + ' loads cleaned and permits hover and removal', run(`(() => {
const s = serializeWorld(); ${doctor}
try {
deserializeWorld(s.meta, s.buffers);
SET.fog = false; player.lens = false;
return ${clean} && hoverInfo(20, 20).air && removePieceAt(20, 20) === '';
} catch (e) { return false; }
})()`));
}
// Retired save formats are no longer translated into current construction or trees.
flatWorld(run, 256, 256, 200);
run(`
placeBlock(BP_FILTER, 128, 5, 10, true, { m: [SAND, COAL], inv: 1 });
placeBlock(BP_PIPE, 0, 8, 10, true);
bgm[100 * W + 40] = LEAVES;
globalThis.currentSave = serializeWorld();
deserializeWorld(currentSave.meta, currentSave.buffers);
`);
check('current filter settings and direction survive loading', run('blockV[10 * BW + 5] === 128 && JSON.stringify(filterCfg.get(10 * BW + 5)) === JSON.stringify(filterRec({ m: [SAND, COAL], inv: 1 }))'));
check('current thin pipes and background trees survive loading', run('part[40 * W + 32] === P_PIPE && mat[40 * W + 32] === EMPTY && bgm[100 * W + 40] === LEAVES'));
run(`
currentSave.meta.filters = [];
currentSave.buffers.blockV[10 * BW + 5] = 128 | SAND;
delete currentSave.buffers.bgm;
currentSave.buffers.mat[100 * W + 40] = LEAVES;
currentSave.buffers.mat[110 * W + 40] = WOOD;
currentSave.buffers.flags[110 * W + 40] = F_NAT;
deserializeWorld(currentSave.meta, currentSave.buffers);
`);
check('loading does not invent a filter setting from its old block variant', run('!filterCfg.has(10 * BW + 5) && blockV[10 * BW + 5] === (128 | SAND)'));
check('loading does not move foreground trees into a missing background layer', run('mat[100 * W + 40] === LEAVES && mat[110 * W + 40] === WOOD && flags[110 * W + 40] === F_NAT && bgm[100 * W + 40] === EMPTY && bgm[110 * W + 40] === EMPTY'));
flatWorld(run, 1024, 256, 200);
run(`
bot.x = 510; bot.y = 170;
setCell(192 * W + 16, WOOD, AMB[192], F_BURN); life[192 * W + 16] = FUEL[WOOD];
temp[210 * W + 16] = AMB[210] + 200;
temp[210 * W + 512] = AMB[210] + 200;
`);
rect(run, 48, 160, 49, 199, run('STONE'));
rect(run, 78, 160, 79, 199, run('STONE'));
rect(run, 50, 170, 60, 179, run('WATER'));
run('globalThis.loadSave = serializeWorld(); deserializeWorld(loadSave.meta, loadSave.buffers);');
check('nearby areas wake immediately, with heat', run('awake[6 * CW + 16] > 0 && hot[6 * CW + 16] > 0'));
check('distant areas wait, including their heat', run('awake[6 * CW] === 0 && hot[6 * CW] === 0'));
ticks(run, 1);
check('nearby areas are awake on the first tick while far areas wait', run('awake[6 * CW + 16] > 0 && awake[6 * CW] === 0'));
check('the order contains every area once and goes outward from the saved bot', run(`(() => {
const visited = new Uint8Array(NCH);
let previous = -1;
for (const c of loadWakeOrder) {
const d = (c % CW * 32 + 16 - (bot.x + bot.w / 2)) ** 2 + (((c / CW) | 0) * 32 + 16 - (bot.y + bot.h / 2)) ** 2;
if (visited[c] || d < previous) return false;
visited[c] = 1; previous = d;
}
return visited.every(v => v === 1);
})()`));
check('waiting leaves distant fire and water unchanged', run('life[192 * W + 16] === FUEL[WOOD] && mat[170 * W + 50] === WATER'));
ticks(run, 120);
check('all areas of a small world have had their turn', run('loadWakeOrder === null && loadWakePending === null'));
check('distant burning wood resumes burning after its turn', run('life[192 * W + 16] < FUEL[WOOD] || mat[192 * W + 16] !== WOOD'));
check('distant stored heat resumes flowing', run('temp[210 * W + 16] < AMB[210] + 190'));
ticks(run, 600);
check('distant water settles without losing grains', count(run, run('WATER')) === 110 && count(run, run('WATER'), 50, 196, 77, 199) === 110);
check('the settled water area goes back to sleep', run('awake[6 * CW + 1] === 0 && awake[6 * CW + 2] === 0'));
// Moving to a waiting area must apply its saved heat hint, even at ambient with F_BURN.
run('deserializeWorld(loadSave.meta, loadSave.buffers); bot.x = 14; bot.y = 190;');
check('the destination is still waiting before moving wakes it', run('loadWakePending[6 * CW] === 2 && hot[6 * CW] === 0'));
ticks(run, 8);
check('moving near a waiting area wakes it within eight ticks, including heat', run('loadWakePending[6 * CW] === 0 && hot[6 * CW] > 0'));
// Burning is probabilistic: waking does not promise a fuel draw on that exact tick.
ticks(run, 32);
check('newly woken fuel resumes burning', run('life[192 * W + 16] < FUEL[WOOD]'));
// Saving midway keeps the actual cells, and loading starts a fresh queue at the new bot.
run('globalThis.midLoadSave = serializeWorld(); deserializeWorld(midLoadSave.meta, midLoadSave.buffers);');
check('saving midway preserves all cell buffers', run(`(() => {
const saved = serializeWorld();
return ['mat', 'temp', 'life', 'flags'].every(key => saved.buffers[key].every((v, i) => v === midLoadSave.buffers[key][i]));
})()`));
check('loading midway starts a new queue around the current bot', run('loadWakeIndex === 0 && loadWakePending[6 * CW] === 0 && loadWakePending[6 * CW + 16] > 0'));
run('alloc(64, 64);');
check('alloc clears a previous load queue', run('loadWakeOrder === null && loadWakePending === null && loadWakeIndex === 0'));
run('deserializeWorld(loadSave.meta, loadSave.buffers); generateWorld(77, 256, 256, null);');
check('generating a new world clears the load queue', run('loadWakeOrder === null && loadWakePending === null && loadWakeIndex === 0'));
// The Huge-world budget is checked with real ticks on quiet terrain.
flatWorld(run, 6144, 2048, 1900);
run('startLoadWake();');
check('a Huge world has 12288 scheduled areas', run('loadWakeOrder.length === 12288'));
ticks(run, 1023);
check('the final areas are still queued at tick 1023', run('loadWakeOrder !== null && loadWakePending.some(v => v > 0)'));
ticks(run, 1);
check('every Huge-world area has had its turn by tick 1024 (17.07 seconds)', run('loadWakeOrder === null && loadWakePending === null && loadWakeIndex === 12288'));
done();