Reject blueprint codes with unknown pieces, machines or molds (t78)

This commit is contained in:
2026-10-29 17:50:07 -04:00
parent f7c989db6c
commit 5e26e4634d
3 changed files with 82 additions and 1 deletions
+11
View File
@@ -4,6 +4,17 @@ What agents have done, newest first. Add an entry for every piece of work: the d
note or request it was for, what changed, the files touched, the tests run, and anything left note or request it was for, what changed, the files touched, the tests run, and anything left
open or not verified. Keep each entry short; the git history has the details. open or not verified. Keep each entry short; the git history has the details.
## 2026-10-29: t78 damaged blueprint code validation (Codex)
- bpValid checks block codes, non-fixed machine types and mold kinds against their
tables before import; damaged codes show the existing message and paste nothing.
Blueprint format and simulation/worldgen behaviour are unchanged.
- Files: src/ui/saves.js, new test/t_blueprint_valid.js, tasks/t78_damaged_blueprint_code.md
and this log. Regression failed first; all 32 focused checks, build.py and all 36
headless suites passed (python3 test/run_all.py; /tmp/t78-headless.log).
- Browser/GPU tests and screenshots skipped at the owner's request; owner game
playtesting remains open. No commit.
## 2026-10-29: t82 dev console material table lookup (Codex) ## 2026-10-29: t82 dev console material table lookup (Codex)
+10 -1
View File
@@ -211,7 +211,16 @@ async function importWorldFile(file) {
function libLoad() { try { return JSON.parse(localStorage.getItem('deepgrain.blueprints') || '[]'); } catch (e) { return []; } } function libLoad() { try { return JSON.parse(localStorage.getItem('deepgrain.blueprints') || '[]'); } catch (e) { return []; } }
function libSave(list) { try { localStorage.setItem('deepgrain.blueprints', JSON.stringify(list)); return true; } catch (e) { toast('Blueprints cannot be stored in this view. Copy the blueprint code instead.'); return false; } } function libSave(list) { try { localStorage.setItem('deepgrain.blueprints', JSON.stringify(list)); return true; } catch (e) { toast('Blueprints cannot be stored in this view. Copy the blueprint code instead.'); return false; } }
function bpEncode(bp) { return 'DGBP2:' + btoa(unescape(encodeURIComponent(JSON.stringify(bp)))); } function bpEncode(bp) { return 'DGBP2:' + btoa(unescape(encodeURIComponent(JSON.stringify(bp)))); }
function bpValid(bp) { return !!(bp && bp.v === 2 && Array.isArray(bp.blocks) && bp.bw > 0 && bp.bh > 0); } function bpValid(bp) {
return !!(bp && bp.v === 2 && Array.isArray(bp.blocks) && bp.bw > 0 && bp.bh > 0
&& bp.blocks.every(b => Array.isArray(b) && Number.isInteger(b[2]) && Object.hasOwn(PIECE_BY_CODE, b[2]))
&& Array.isArray(bp.machines || []) && (bp.machines || []).every(m => {
if (!Array.isArray(m) || !Object.hasOwn(PIECE_BY_ID, m[0])) return false;
const p = PIECE_BY_ID[m[0]];
return p.kind === 'machine' && !p.fixed;
})
&& Array.isArray(bp.molds || []) && (bp.molds || []).every(m => Array.isArray(m) && Object.hasOwn(MOLDS, m[0])));
}
function bpDecode(code) { function bpDecode(code) {
const s = code.trim(); const s = code.trim();
if (s.startsWith('DGBP1:')) throw new Error('That blueprint is from an older version of the game, before the block grid. It cannot be used.'); if (s.startsWith('DGBP1:')) throw new Error('That blueprint is from an older version of the game, before the block grid. It cannot be used.');
+61
View File
@@ -0,0 +1,61 @@
// Shared blueprint codes must be safe before entering the clipboard or ghost queues.
const fs = require('fs');
const path = require('path');
const { load, suite, flatWorld } = require('./harness');
const { run } = load(), { check, done } = suite();
for (const file of ['saves.js', 'notebook-blueprints.js']) {
run(fs.readFileSync(path.join(__dirname, '../src/ui', file), 'utf8'));
}
flatWorld(run, 128, 128, 112);
run(`
globalThis.goodBlueprint = {
v: 2, bw: 8, bh: 8,
blocks: [[0, 0, BP_WALL, RSTONE]],
machines: [['fan', 1, 0, 0]],
molds: [['ingot', 0, 2, RSTONE]]
};
globalThis.GAME = { clip: null, paste: false };
globalThis.hudDirty = false;
globalThis.elements = { 'bp-import': {}, 'bp-paste': { value: '' } };
globalThis.$ = id => elements[id] || null;
globalThis.localStorage = { getItem: () => null };
globalThis.esc = s => s;
globalThis.toast = message => { globalThis.lastToast = message; };
globalThis.closeAllModals = () => {};
globalThis.dialog = (title, html, buttons) => { globalThis.importBlueprint = buttons[1].fn; };
renderBlueprintTab({ innerHTML: '', querySelectorAll: () => [] });
elements['bp-import'].onclick();
`);
check('a good blueprint code round-trips unchanged', run('JSON.stringify(bpDecode(bpEncode(goodBlueprint))) === JSON.stringify(goodBlueprint)'));
check('a good decoded blueprint has a build cost', run('bpCost(bpDecode(bpEncode(goodBlueprint))).length > 0'));
check('block-only codes still allow omitted machine and mold lists', run(`
(() => { const bp = bpDecode(bpEncode({ v: 2, bw: 1, bh: 1, blocks: [[0, 0, BP_WALL, RSTONE]] }));
return bp.machines.length === 0 && bp.molds.length === 0; })()
`));
for (const [name, damage] of [
['unknown piece 250', 'bp.blocks[0][2] = 250'],
['array property used as a piece code', "bp.blocks[0][2] = 'length'"],
['unknown machine', "bp.machines[0][0] = 'unknown-machine'"],
['block used as a machine', "bp.machines[0][0] = 'wall'"],
['fixed trade dock', "bp.machines[0][0] = 'dock'"],
['unknown mold', "bp.molds[0][0] = 'unknown-mold'"],
['inherited mold name', "bp.molds[0][0] = 'toString'"],
]) {
run(`{
clearAllGhosts(); GAME.clip = null; GAME.paste = false;
const bp = JSON.parse(JSON.stringify(goodBlueprint)); ${damage};
globalThis.damagedBlueprint = bp;
elements['bp-paste'].value = bpEncode(bp);
importBlueprint();
if (GAME.paste && GAME.clip) pasteGhost(GAME.clip, 2, 2);
}`);
check(`${name} fails validation`, run('!bpValid(damagedBlueprint)'));
check(`${name} shows the damaged message`, run("lastToast === 'That code is damaged.'"));
check(`${name} leaves the clipboard and paste mode empty`, run('GAME.clip === null && !GAME.paste'));
check(`${name} pastes nothing`, run('ghostBlocks.size === 0 && ghostMachines.length === 0 && ghostMolds.length === 0'));
}
run(`clearAllGhosts(); elements['bp-paste'].value = bpEncode(goodBlueprint); importBlueprint();
if (GAME.paste && GAME.clip) pasteGhost(GAME.clip, 2, 2);`);
check('a good code still imports and pastes all three piece kinds', run('GAME.paste && ghostBlocks.size === 1 && ghostMachines.length === 1 && ghostMolds.length === 1'));
done();