Save the running world before importing a world file (t79)

This commit is contained in:
2026-10-29 17:52:24 -04:00
parent 5e26e4634d
commit cf762268ce
3 changed files with 97 additions and 0 deletions
+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: t79 preserve progress before world import (Codex)
- importWorldFile uses GAME.running to await saveWorld('auto', true) before reading
the file or starting loading. Save failure cancels import with a toast.
- Files: src/ui/saves.js, test/t_import_save.js, tasks/t79_importing_world_mid.md
and this log. Regression failed first; seven focused checks, build.py and all
headless suites passed (python3 test/run_all.py; /tmp/t79-headless.log).
- Browser suite and screenshots not run as requested; owner playtesting remains
open. No simulation/worldgen changes; no commit.
## 2026-10-29: t78 damaged blueprint code validation (Codex) ## 2026-10-29: t78 damaged blueprint code validation (Codex)
- bpValid checks block codes, non-fixed machine types and mold kinds against their - bpValid checks block codes, non-fixed machine types and mold kinds against their
+4
View File
@@ -184,6 +184,10 @@ async function exportWorld() {
catch (e) { if (e && e.code !== 'declined') toast('The world file was not saved.'); } catch (e) { if (e && e.code !== 'declined') toast('The world file was not saved.'); }
} }
async function importWorldFile(file) { async function importWorldFile(file) {
if (GAME.running && !await saveWorld('auto', true)) {
toast('The current world could not be saved. Import cancelled.');
return;
}
const progress = (f, s) => showLoading(true, f, s, 'Loading world'); const progress = (f, s) => showLoading(true, f, s, 'Loading world');
progress(0.05, 'Reading the save'); progress(0.05, 'Reading the save');
try { try {
+83
View File
@@ -0,0 +1,83 @@
// Import must preserve the running world before reading or replacing it.
const fs = require('fs');
const path = require('path');
const { load, suite, flatWorld } = require('./harness');
const { run } = load(), { check, done } = suite();
run(fs.readFileSync(path.join(__dirname, '../src/ui/saves.js'), 'utf8'));
run(`
globalThis.GAME = { running: true, paused: true, worldId: 'current', worldName: 'Current', playTime: 42 };
globalThis.messages = [];
globalThis.toast = text => messages.push(text);
globalThis.$ = () => ({});
globalThis.showLoading = () => {};
globalThis.stored = new Map();
globalThis.failStorage = false;
globalThis.releaseWrite = null;
DB.get = async key => stored.get(key);
DB.op = async (mode, fn) => {
if (failStorage) throw new Error('Storage full');
await new Promise(resolve => { releaseWrite = resolve; });
fn({ put: (value, key) => stored.set(key, value) });
};
globalThis.loads = 0;
globalThis.startFromSave = async sv => {
loads++;
deserializeWorld(sv.meta, sv.buffers);
GAME.worldId = sv.worldId; GAME.worldName = sv.worldName;
return true;
};
`);
async function main() {
flatWorld(run, 64, 64, 48);
const incoming = run('serializeWorld()');
const buffers = {};
for (const [key, arr] of Object.entries(incoming.buffers)) {
buffers[key] = { type: arr.constructor.name, gz: false,
b64: Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength).toString('base64') };
}
const text = JSON.stringify({ format: 'deepgrain-world', meta: incoming.meta, buffers });
run('money = 12345; setCell(10 * W + 10, CUORE, AMB[10], 0);');
let reads = 0, savedBeforeRead = false;
// Release each storage transaction on the next turn, after observing whether
// the importer tried to read the file while the first save was still pending.
const pending = run('importWorldFile')({ text: async () => {
reads++;
savedBeforeRead = run("stored.has('current:auto')");
return text;
} });
await new Promise(resolve => setImmediate(resolve));
check('file waits for the running world save to commit', reads === 0);
let settled = false;
pending.finally(() => { settled = true; });
const deadline = Date.now() + 10000;
while (!settled && Date.now() < deadline) {
if (run('releaseWrite !== null')) run('releaseWrite(); releaseWrite = null;');
await new Promise(resolve => setTimeout(resolve, 1));
}
if (!settled) throw new Error('Import timed out');
await pending;
check('current world is saved before reading the import', savedBeforeRead);
check('autosave contains latest money and grains', run(`(() => {
const sv = stored.get('current:auto');
return !!sv && sv.meta.economy.money === 12345;
})()`) && (await run("worldNewest('current')"))?.buffers.mat[650] === run('CUORE'));
check('good import loads and saves under its own id', run(`loads === 1 && GAME.worldId !== 'current'
&& stored.has(GAME.worldId + ':auto') && messages.includes('World imported.')`));
run('failStorage = true; messages.length = 0; loads = 0;');
const before = run('mat');
reads = 0;
await run('importWorldFile')({ text: async () => { reads++; return text; } });
check('failed save stops before reading or replacing the world', reads === 0 && run('loads') === 0 && before === run('mat'));
check('failed save explains that the current world could not be saved',
run("messages.some(s => /current world/i.test(s) && /could not be saved/i.test(s))"));
// Title-screen import must not attempt to save a nonexistent running world.
run('GAME.running = false; messages.length = 0; loads = 0;');
reads = 0;
await run('importWorldFile')({ text: async () => { reads++; return text; } });
check('title-screen import still reads and loads without a pre-save', reads === 1 && run('loads') === 1);
done();
}
main().catch(e => { console.error(e); process.exitCode = 1; });