Save the running world before importing a world file (t79)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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; });
|
||||||
Reference in New Issue
Block a user