Remove conversions for old saves (t83)

This commit is contained in:
2026-10-29 21:59:40 -04:00
parent 78eeb86171
commit f88035a58f
6 changed files with 40 additions and 22 deletions
+2 -2
View File
@@ -392,8 +392,8 @@ Dev commands are pure simulation operations; the browser owns the panel and conf
- functions: resetDevConsole, enableDevConsole, devError, devName, devGiveable, devMaterialList, devDistance, devSuggestions, devHistory, devRemember, devRecall, devComplete, devMaterial, devCommand - functions: resetDevConsole, enableDevConsole, devError, devName, devGiveable, devMaterialList, devDistance, devSuggestions, devHistory, devRemember, devRecall, devComplete, devMaterial, devCommand
- variables: devWorld, devLog, devFree, devSpeed, devPaused, devSteps, DEV_COMMANDS, DEV_MATERIALS - variables: devWorld, devLog, devFree, devSpeed, devPaused, devSteps, DEV_COMMANDS, DEV_MATERIALS
### src/sim/save.js (216 lines) ### src/sim/save.js (201 lines)
Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration. Saving and loading a world (serializeWorld / deserializeWorld).
- functions: serializeWorld, deserializeWorld, startLoadWake, wakeLoadArea, wakeLoadNearBot, loadWakeTick - functions: serializeWorld, deserializeWorld, startLoadWake, wakeLoadArea, wakeLoadNearBot, loadWakeTick
- variables: SAVE_VERSION, TOFF - variables: SAVE_VERSION, TOFF
+10
View File
@@ -4,6 +4,16 @@ 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: t83 remove old-save conversions (Codex)
- Removed only spring, solid-pipe, single-material-filter and foreground-tree conversions
from `src/sim/save.js`; world generation and its spring random draws are untouched.
- Files: save.js, test/t_{load,weather,conveyors}.js, docs/PROJECT_MAP.md, tasks/t83*.
Three expectations failed first; updated obsolete spring/filter migration tests deliberately.
- All 41 headless suites pass; pixel `test/play.py shots/t83-play` passes with no console
errors. Sandbox lacked WebGPU; host GPU rerun passed. Inspected saving-progress.png and
10-after.png (saved/reloaded world). Map regenerated; nothing left open.
## 2026-10-29: t81 shared piece direction variants (Codex) ## 2026-10-29: t81 shared piece direction variants (Codex)
- `pieceV` accepts a direction and resolved code; `dragPlan` calls it for previews and - `pieceV` accepts a direction and resolved code; `dragPlan` calls it for previews and
+1 -16
View File
@@ -1,4 +1,4 @@
// Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration. // Saving and loading a world (serializeWorld / deserializeWorld).
// ---------- Save / load ---------- // ---------- Save / load ----------
const SAVE_VERSION = 2; const SAVE_VERSION = 2;
const TOFF = 64; // temperatures are saved with an offset so sub-zero snow and ice survive const TOFF = 64; // temperatures are saved with an offset so sub-zero snow and ice survive
@@ -60,7 +60,6 @@ function deserializeWorld(meta, buffers) {
mat.set(buffers.mat); life.set(buffers.life); flags.set(buffers.flags); mat.set(buffers.mat); life.set(buffers.life); flags.set(buffers.flags);
for (let i = 0; i < N; i++) { for (let i = 0; i < N; i++) {
if (mat[i] >= NMAT) mat[i] = EMPTY; if (mat[i] >= NMAT) mat[i] = EMPTY;
else if (mat[i] === SPRING) mat[i] = STONE; // springs are gone: older worlds keep plain rock there
} }
blockP.set(buffers.blockP); blockV.set(buffers.blockV); filtersDirty = true; blockP.set(buffers.blockP); blockV.set(buffers.blockV); filtersDirty = true;
for (let b = 0; b < NB; b++) { for (let b = 0; b < NB; b++) {
@@ -81,22 +80,8 @@ function deserializeWorld(meta, buffers) {
} }
s.open = open === true; rackState.set(b, s); s.open = open === true; rackState.set(b, s);
} }
// pipes used to be solid copper blocks; now they are thin and let things past
for (let b = 0; b < NB; b++) if (blockP[b] === BP_PIPE) for (const i of blockCellList(b % BW, (b / BW) | 0)) if (mat[i] === PIPE) { mat[i] = EMPTY; part[i] = P_PIPE; }
for (const [b, m, inv] of meta.filters || []) if (isFilter(blockP[b])) filterCfg.set(b, filterRec({ m, inv })); for (const [b, m, inv] of meta.filters || []) if (isFilter(blockP[b])) filterCfg.set(b, filterRec({ m, inv }));
for (let b = 0; b < NB; b++) if (isFilter(blockP[b])) {
// the first filters kept one material in their block variant
if (!filterCfg.has(b)) filterCfg.set(b, filterRec({ m: [blockV[b] & 127], inv: 0 }));
blockV[b] &= 128;
}
if (buffers.bgm) { bgm.set(buffers.bgm); } if (buffers.bgm) { bgm.set(buffers.bgm); }
else {
// older saves kept trees in the foreground: move them behind
for (let i = 0; i < N; i++) {
const m = mat[i], y = (i / W) | 0;
if (m === LEAVES || (m === WOOD && (flags[i] & F_NAT) && y < surfY[i % W])) { bgm[i] = m; mat[i] = EMPTY; flags[i] = 0; }
}
}
const t16 = buffers.temp, off = meta.tOff || 0; for (let i = 0; i < N; i++) temp[i] = t16[i] / 16 - off; const t16 = buffers.temp, off = meta.tOff || 0; for (let i = 0; i < N; i++) temp[i] = t16[i] / 16 - off;
saplings = (meta.saplings || []).map(([x, y, age, th, seed]) => ({ x, y, age, th, seed })); saplings = (meta.saplings || []).map(([x, y, age, th, seed]) => ({ x, y, age, th, seed }));
findTreeBases(); findTreeBases();
+2 -2
View File
@@ -199,9 +199,9 @@ run('clearAllGhosts(); pasteGhost(flipBlueprint(copyRegion(120, 100, 123, 103)),
check('pasting it builds a filter with the same materials and mode', run('blockP[25*BW+34]') === run('BP_FILTER') && run('JSON.stringify(filterInfo(25*BW+34))') === JSON.stringify({ m: [run('CUORE'), run('FEORE')].sort((a, b) => a - b), inv: 1 }), run('JSON.stringify(filterInfo(25*BW+34))')); check('pasting it builds a filter with the same materials and mode', run('blockP[25*BW+34]') === run('BP_FILTER') && run('JSON.stringify(filterInfo(25*BW+34))') === JSON.stringify({ m: [run('CUORE'), run('FEORE')].sort((a, b) => a - b), inv: 1 }), run('JSON.stringify(filterInfo(25*BW+34))'));
run('globalThis.__s = serializeWorld(); deserializeWorld(__s.meta, __s.buffers);'); run('globalThis.__s = serializeWorld(); deserializeWorld(__s.meta, __s.buffers);');
check('save and load keep filters, their materials, mode and direction', run('blockP[25*BW+30]') === run('BP_FILTER') && run('blockV[25*BW+30]') === 128 && run('filterInfo(25*BW+30).inv') === 1 && run('filterInfo(25*BW+30).m.length') === 2 && run('part[100*W+120]') === run('P_FILTER')); check('save and load keep filters, their materials, mode and direction', run('blockP[25*BW+30]') === run('BP_FILTER') && run('blockV[25*BW+30]') === 128 && run('filterInfo(25*BW+30).inv') === 1 && run('filterInfo(25*BW+30).m.length') === 2 && run('part[100*W+120]') === run('P_FILTER'));
// a save from before: one material kept in the block variant // A retired single-material variant is no longer reconstructed on load.
run('__s.meta.filters = []; __s.buffers.blockV[25*BW+30] = 128 | SAND; deserializeWorld(__s.meta, __s.buffers);'); run('__s.meta.filters = []; __s.buffers.blockV[25*BW+30] = 128 | SAND; deserializeWorld(__s.meta, __s.buffers);');
check('older filters keep the one material they had', run('JSON.stringify(filterInfo(25*BW+30))') === JSON.stringify({ m: [run('SAND')], inv: 0 }) && run('blockV[25*BW+30]') === 128); check('loading does not reconstruct retired single-material filters', run('!filterCfg.has(25*BW+30) && blockV[25*BW+30] === (128 | SAND)'));
// the run of matching filters in a row // the run of matching filters in a row
fresh(); fresh();
run('for (let bx = 20; bx < 26; bx++) placeBlock(BP_FILTER, 128, bx, 25, true, { m: [SAND] }); placeBlock(BP_FILTER, 128, 26, 25, true, { m: [COAL] }); placeBlock(BP_FILTER, 0, 27, 25, true, { m: [COAL] });'); run('for (let bx = 20; bx < 26; bx++) placeBlock(BP_FILTER, 128, bx, 25, true, { m: [SAND] }); placeBlock(BP_FILTER, 128, 26, 25, true, { m: [COAL] }); placeBlock(BP_FILTER, 0, 27, 25, true, { m: [COAL] });');
+23
View File
@@ -51,6 +51,29 @@ for (const [label, doctor, clean] of [
})()`)); })()`));
} }
// 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); flatWorld(run, 1024, 256, 200);
run(` run(`
bot.x = 510; bot.y = 170; bot.x = 510; bot.y = 170;
+2 -2
View File
@@ -8,9 +8,9 @@ const rainInAir = (m) => run(`flyers.filter(f => f.rain && f.m === ${m}).length`
run('generateWorld(777, 1024, 640, null)'); run('generateWorld(777, 1024, 640, null)');
check('new worlds have no springs', count(run, run('SPRING')) === 0); check('new worlds have no springs', count(run, run('SPRING')) === 0);
check('and the same seed still makes the same land', run('surfY[512]') > 0 && run('!!dock()')); check('and the same seed still makes the same land', run('surfY[512]') > 0 && run('!!dock()'));
// an older save with a spring in it: the spring becomes plain rock // Loading no longer converts retired spring cells to rock.
run('globalThis.__s = serializeWorld(); __s.buffers.mat[300 * W + 500] = SPRING; deserializeWorld(__s.meta, __s.buffers);'); run('globalThis.__s = serializeWorld(); __s.buffers.mat[300 * W + 500] = SPRING; deserializeWorld(__s.meta, __s.buffers);');
check('springs in older worlds turn into plain rock', run('mat[300 * W + 500]') === run('STONE') && count(run, run('SPRING')) === 0); check('loading leaves retired spring cells unchanged', run('mat[300 * W + 500]') === run('SPRING') && count(run, run('SPRING')) === 1);
// steam in the open sky rises into the clouds instead of condensing // steam in the open sky rises into the clouds instead of condensing
flatWorld(run, 320, 200, 120); flatWorld(run, 320, 200, 120);