Several worlds: a Worlds list with load, rename and delete; old save migrates to World 1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+80
-9
@@ -15,16 +15,88 @@ const DB = {
|
||||
},
|
||||
async op(mode, fn) {
|
||||
const db = await this.open();
|
||||
return new Promise((res, rej) => { const tx = db.transaction('saves', mode); const st = tx.objectStore('saves'); const rq = fn(st); rq.onsuccess = () => res(rq.result); rq.onerror = () => rej(rq.error); });
|
||||
return new Promise((res, rej) => {
|
||||
const tx = db.transaction('saves', mode); let rq;
|
||||
tx.oncomplete = () => res(rq && rq.result);
|
||||
tx.onabort = tx.onerror = () => rej(tx.error || new Error('Storage transaction failed.'));
|
||||
try { rq = fn(tx.objectStore('saves')); } catch (e) { tx.abort(); rej(e); }
|
||||
});
|
||||
},
|
||||
put(k, v) { return this.op('readwrite', s => s.put(v, k)); },
|
||||
get(k) { return this.op('readonly', s => s.get(k)); },
|
||||
del(k) { return this.op('readwrite', s => s.delete(k)); },
|
||||
};
|
||||
let worldIdTime = 0, worldIdCounter = 0, worldWrites = Promise.resolve();
|
||||
function newWorldId() {
|
||||
const now = Date.now(); worldIdCounter = now === worldIdTime ? worldIdCounter + 1 : 0; worldIdTime = now;
|
||||
return 'w' + now + (worldIdCounter ? '-' + worldIdCounter : '');
|
||||
}
|
||||
function worldWrite(fn) { const p = worldWrites.then(fn); worldWrites = p.catch(() => {}); return p; }
|
||||
async function listWorlds() { return (await DB.get('worlds') || []).sort((a, b) => b.savedAt - a.savedAt); }
|
||||
function putWorldEntry(entry) {
|
||||
return worldWrite(async () => {
|
||||
const list = await listWorlds(), i = list.findIndex(w => w.id === entry.id);
|
||||
if (i < 0) list.push(entry); else list[i] = { ...list[i], ...entry };
|
||||
await DB.put('worlds', list);
|
||||
});
|
||||
}
|
||||
async function worldNewest(id) {
|
||||
const a = await DB.get(`${id}:main`), b = await DB.get(`${id}:auto`);
|
||||
const sv = a && b ? (a.savedAt >= b.savedAt ? a : b) : a || b;
|
||||
if (!sv) return null;
|
||||
const entry = (await listWorlds()).find(w => w.id === id);
|
||||
return { ...sv, worldId: id, worldName: entry ? entry.name : 'World' };
|
||||
}
|
||||
function deleteWorld(id) {
|
||||
return worldWrite(async () => {
|
||||
const list = (await listWorlds()).filter(w => w.id !== id);
|
||||
await DB.op('readwrite', st => { st.delete(`${id}:main`); st.delete(`${id}:auto`); st.put(list, 'worlds'); });
|
||||
if (GAME.worldId === id) { GAME.worldId = ''; GAME.worldName = ''; }
|
||||
});
|
||||
}
|
||||
function renameWorld(id, name) {
|
||||
return worldWrite(async () => {
|
||||
const list = await listWorlds(), entry = list.find(w => w.id === id);
|
||||
if (!entry) return;
|
||||
entry.name = String(name).trim().slice(0, 40) || entry.name;
|
||||
await DB.put('worlds', list);
|
||||
if (GAME.worldId === id) GAME.worldName = entry.name;
|
||||
});
|
||||
}
|
||||
async function migrateWorlds() {
|
||||
try {
|
||||
if (await DB.get('worlds') !== undefined) return;
|
||||
const a = await DB.get('main'), b = await DB.get('auto'), sv = a && b ? (a.savedAt >= b.savedAt ? a : b) : a || b;
|
||||
if (!sv) return;
|
||||
const id = newWorldId();
|
||||
await DB.op('readwrite', st => { if (a) st.put(a, `${id}:main`); if (b) st.put(b, `${id}:auto`); });
|
||||
// Verify every byte before retiring the only old copies.
|
||||
for (const [slot, old] of [['main', a], ['auto', b]]) if (old) {
|
||||
const copy = await DB.get(`${id}:${slot}`);
|
||||
if (!copy || JSON.stringify(copy.meta) !== JSON.stringify(old.meta) || copy.savedAt !== old.savedAt || copy.playTime !== old.playTime) throw new Error('Save copy failed.');
|
||||
for (const k in old.buffers) {
|
||||
const x = old.buffers[k], y = copy.buffers[k];
|
||||
if (!y || x.constructor !== y.constructor || x.length !== y.length || !x.every((v, i) => v === y[i])) throw new Error('Save copy failed.');
|
||||
}
|
||||
}
|
||||
await DB.op('readwrite', st => {
|
||||
st.put([{ id, name: 'World 1', seed: sv.meta.seed, w: sv.meta.W, h: sv.meta.H, createdAt: sv.savedAt, savedAt: sv.savedAt, playTime: sv.playTime || 0 }], 'worlds');
|
||||
st.delete('main'); st.delete('auto');
|
||||
});
|
||||
} catch (e) { toast('Could not move the old save. It has been kept. Reload to try again.'); throw e; }
|
||||
}
|
||||
async function saveWorld(slot, quiet) {
|
||||
try {
|
||||
const s = serializeWorld();
|
||||
await DB.put(slot, { meta: s.meta, buffers: s.buffers, savedAt: Date.now(), playTime: GAME.playTime });
|
||||
const s = serializeWorld(), savedAt = Date.now(), playTime = GAME.playTime;
|
||||
if (!GAME.worldId) { GAME.worldId = newWorldId(); GAME.worldName = `World ${(await listWorlds()).length + 1}`; }
|
||||
const id = GAME.worldId, name = GAME.worldName;
|
||||
await worldWrite(async () => {
|
||||
const list = await listWorlds();
|
||||
let entry = list.find(w => w.id === id);
|
||||
if (!entry) { entry = { id, name, createdAt: savedAt }; list.push(entry); }
|
||||
Object.assign(entry, { savedAt, playTime, seed: s.meta.seed, w: s.meta.W, h: s.meta.H });
|
||||
await DB.op('readwrite', st => { st.put({ meta: s.meta, buffers: s.buffers, savedAt, playTime }, `${id}:${slot}`); st.put(list, 'worlds'); });
|
||||
});
|
||||
if (!quiet) toast(slot === 'auto' ? 'Autosaved.' : 'World saved.');
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -33,11 +105,8 @@ async function saveWorld(slot, quiet) {
|
||||
}
|
||||
}
|
||||
async function newestSave() {
|
||||
try {
|
||||
const a = await DB.get('main'), b = await DB.get('auto');
|
||||
if (a && b) return a.savedAt >= b.savedAt ? a : b;
|
||||
return a || b || null;
|
||||
} catch (e) { return null; }
|
||||
try { for (const entry of await listWorlds()) { const sv = await worldNewest(entry.id); if (sv) return sv; } return null; }
|
||||
catch (e) { return null; }
|
||||
}
|
||||
function b64FromBytes(u8) { let s = ''; const CH = 0x8000; for (let i = 0; i < u8.length; i += CH) s += String.fromCharCode.apply(null, u8.subarray(i, i + CH)); return btoa(s); }
|
||||
function bytesFromB64(b64) { const s = atob(b64); const u8 = new Uint8Array(s.length); for (let i = 0; i < s.length; i++) u8[i] = s.charCodeAt(i); return u8; }
|
||||
@@ -84,7 +153,9 @@ async function importWorldFile(file) {
|
||||
const C = T[b.type] || Uint8Array;
|
||||
bufs[k] = new C(bytes.buffer, bytes.byteOffset, bytes.byteLength / C.BYTES_PER_ELEMENT);
|
||||
}
|
||||
startFromSave({ meta: obj.meta, buffers: bufs });
|
||||
const id = newWorldId(), name = `Imported ${obj.meta.seed}`.slice(0, 40);
|
||||
if (!startFromSave({ meta: obj.meta, buffers: bufs, worldId: id, worldName: name })) return;
|
||||
if (!await saveWorld('auto')) return;
|
||||
toast('World imported.');
|
||||
} catch (e) { toast('That file could not be loaded: ' + (e.message || 'unknown error')); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user