Files
deepgrain-foundry/src/sim/save.js
T

186 lines
12 KiB
JavaScript

// Saving and loading a world (serializeWorld / deserializeWorld) and old-save migration.
// ---------- Save / load ----------
const SAVE_VERSION = 2;
const TOFF = 64; // temperatures are saved with an offset so sub-zero snow and ice survive
function serializeWorld() {
// grains in flight from a screw or belt land where they are (rain in the air simply carries on)
for (const f of flyers) if (!f.bag && !f.rain) landFlyer(f, Math.max(0, Math.min(W - 1, Math.floor(f.x))), Math.max(0, Math.min(H - 1, Math.floor(f.y))));
flyers = flyers.filter(f => f.bag || f.rain);
const t16 = new Uint16Array(N);
for (let i = 0; i < N; i++) { const v = (temp[i] + TOFF) * 16; t16[i] = v < 0 ? 0 : v > 65535 ? 65535 : v; }
const meta = {
dev: devWorld, devLog: devLog.map(e => e.slice()),
slopePhases: Array.from(slopePhase),
v: SAVE_VERSION, seed: worldSeed, W, H, tick, surfBase, underD, shallowD, lapse, tOff: TOFF,
saplings: saplings.map(s => [s.x, s.y, s.age, s.th, s.seed]),
economy: { money, debt, earned, recent: Array.from(mkRecent, v => Math.round(v)), log: shipLog, orders, ordersDone, nextOrderT, orderSeq },
racks: [...rackState].map(([b, s]) => [b, s.q.map(r => r.slice()), s.open]),
filters: [...filterCfg].map(([b, c]) => [b, c.m, c.inv]),
weather: { moist: (() => { const m = Float32Array.from(weather.moist); for (const f of flyers) if (f.rain) m[Math.min(m.length - 1, Math.max(0, (f.x / WBAND) | 0))] += 1; return Array.from(m, v => Math.round(v)); })(), raining: Array.from(weather.raining), shower: weather.shower, next: weather.next, wind: weather.wind, seen: weather.seen, base: weather.base },
bot: { x: bot.x, y: bot.y, lamp: bot.lamp },
bag: Array.from(bag), player: { ...player }, seen: Array.from(seen), ledger, stats: { ...stats },
machines: machines.filter(Boolean).map(m => m.type === 'silo' ? { type: m.type, bx: m.x / BS, by: m.y / BS, dir: m.dir, on: m.on, sm: m.sm || 0, sn: m.sn || 0, open: !!m.open } : { type: m.type, bx: m.x / BS, by: m.y / BS, dir: m.dir, on: m.on }),
molds: molds.map(m => ({ kind: m.kind, x: m.x, y: m.y, wall: m.wall })),
ents: ents.map(e => [e.m, e.ix, e.iy, Math.round(e.T)]),
flyers: flyers.filter(f => f.bag).map(f => f.m), // what you blew out goes back in the bag on load
ghostBlocks: [...ghostBlocks.entries()], ghostMachines, ghostMolds,
specks: 1,
pipes: (() => { if (pipesDirty) rebuildPipes(); return pipeNets.filter(pn => pn.n > 0).map(pn => [pn.blocks[0], pn.m, pn.n, Math.round(pn.T)]); })(),
cols: { surfY: Array.from(surfY), dirtY: Array.from(dirtY), graniteY: Array.from(graniteY), basaltY: Array.from(basaltY), magmaY: Array.from(magmaY) },
};
return { meta, buffers: { fog: fog.slice(), mat: mat.slice(), temp: t16, life: life.slice(), flags: flags.slice(), part: part.slice(), blockP: blockP.slice(), blockV: blockV.slice(), bgm: bgm.slice() } };
}
function deserializeWorld(meta, buffers) {
if (meta.v !== SAVE_VERSION) throw new Error('This world was saved by an older version of the game and cannot be loaded.');
alloc(meta.W, meta.H);
devWorld = meta.dev === true;
devLog = devWorld && Array.isArray(meta.devLog) ? meta.devLog.filter(e => Array.isArray(e) && Number.isSafeInteger(e[0]) && e[0] >= 0 && typeof e[1] === 'string').slice(-200).map(e => [e[0], e[1]]) : [];
worldSeed = meta.seed;
if (meta.surfBase) { surfBase = meta.surfBase; underD = meta.underD; }
shallowD = meta.shallowD || 0; lapse = meta.lapse || 0.36;
surfY.set(meta.cols.surfY); dirtY.set(meta.cols.dirtY); graniteY.set(meta.cols.graniteY); basaltY.set(meta.cols.basaltY); magmaY.set(meta.cols.magmaY);
computeAmbient(); fillNoise();
mat.set(buffers.mat); life.set(buffers.life); flags.set(buffers.flags);
for (let i = 0; i < N; i++) 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; rebuildProcessors();
for (let b = 0; b < NB; b++) if (blockP[b] === BP_RACK) rackState.set(b, newRack());
for (const entry of Array.isArray(meta.racks) ? meta.racks : []) {
if (!Array.isArray(entry)) continue;
const [b, q, open] = entry;
if (!Number.isInteger(b) || !rackState.has(b) || !Array.isArray(q)) continue;
const s = newRack();
for (const r of q) {
if (!Array.isArray(r) || r.length !== 2) continue;
const [m, n] = r;
if (!Number.isInteger(m) || m <= 0 || m >= NMAT || PH[m] !== PH_ITEM || !Number.isInteger(n) || n <= 0 || n > RACK_CAP - s.n) continue;
rackPush(s, m, n);
}
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 (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); }
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;
saplings = (meta.saplings || []).map(([x, y, age, th, seed]) => ({ x, y, age, th, seed }));
findTreeBases();
resetEconomy();
const ec = meta.economy;
if (ec) {
money = ec.money; debt = ec.debt; earned = ec.earned || 0; shipLog = ec.log || [];
(ec.recent || []).forEach((v, k) => { if (k < NMAT) mkRecent[k] = v; });
orders = ec.orders || []; ordersDone = ec.ordersDone || 0; nextOrderT = ec.nextOrderT || tick + 3600; orderSeq = ec.orderSeq || 1;
}
// machines rebuild their hull cells; clear stale hull first
for (let i = 0; i < N; i++) if (mat[i] === HULL || mat[i] === DOCKW) mat[i] = EMPTY;
const savedPart = buffers.part;
for (let i = 0; i < N; i++) {
const p = savedPart[i];
if (p === P_WHEEL || p === P_INTAKE) continue;
part[i] = p;
if (IS_SCREW[p]) markPow(i, true);
}
for (let i = 0; i < N; i++) { const m = mat[i]; if (m === BELTL || m === BELTR || m === SHAFT) markPow(i, true); }
// Old machines removed from the game are refunded.
const removedMachines = { refiner: 120, cutter: 90 };
for (const m of meta.machines) {
if (!Object.hasOwn(PIECE_BY_ID, m.type)) { money += Object.hasOwn(removedMachines, m.type) ? removedMachines[m.type] : 0; continue; }
const mm = addMachine(m.type, m.bx, m.by, m.dir); mm.on = m.on !== false;
if (m.type === 'silo') { mm.sm = m.sm || 0; mm.sn = m.sn || 0; mm.open = !!m.open; }
}
molds = [];
for (const m of meta.molds) molds.push(newMoldRecord(m.kind, m.x, m.y, m.wall));
for (const [m, x, y, T] of meta.ents || []) { const e = { m, x, y, ix: x, iy: y, vx: 0, vy: 0, T, rest: 0 }; ents.push(e); entMark(e, 1); }
if (!dock()) placeDock((W >> 1) + 34); // worlds from before the economy get their trade dock too
ghostBlocks = new Map(meta.ghostBlocks || []); ghostMachines = meta.ghostMachines || []; ghostMolds = meta.ghostMolds || [];
tick = meta.tick || 0;
if (meta.slopePhases) slopePhase.set(meta.slopePhases);
updateRoof(); resetWeather();
if (meta.weather) {
const mw = meta.weather;
(mw.moist || []).forEach((v, k) => { if (k < weather.moist.length) weather.moist[k] = v; });
(mw.raining || []).forEach((v, k) => { if (k < weather.raining.length) weather.raining[k] = v ? 1 : 0; });
weather.shower = mw.shower || null; weather.next = mw.next || weather.next; weather.wind = mw.wind || weather.wind; weather.seen = !!mw.seen; weather.base = mw.base || 0;
}
bot.x = meta.bot.x; bot.y = meta.bot.y; bot.lamp = meta.bot.lamp !== false; bot.vx = bot.vy = 0;
bag = new Int32Array(NMAT); meta.bag.forEach((v, k) => { if (k < NMAT) bag[k] = v; });
for (const m of meta.flyers || []) if (m < NMAT) bag[m]++; // grains that were mid-air when saved
// solids saved in the bag (older saves) come back as what drilling them gives
for (let k = 0; k < NMAT; k++) if (bag[k] && PH[k] === PH_SOLID && YLD[k]) { bag[YLD[k]] += bag[k]; bag[k] = 0; }
bagTotal = 0; for (let k = 0; k < NMAT; k++) bagTotal += bag[k];
Object.assign(player, meta.player);
seen = new Uint8Array(NMAT); meta.seen.forEach((v, k) => { if (k < NMAT) seen[k] = v; });
ledger = meta.ledger || {};
Object.assign(stats, meta.stats || {});
clearStraySeals();
powerDirty = true; rebuildPower(); slopeCleatsTick(false); updateRoof();
if (!weather.base) weather.base = worldWater(); // older saves: take the water it has now as its starting amount
if (!meta.specks) clearHardSpecks(true); // older saves: tidy away tiny specks of too-hard rock once
rebuildPipes();
for (const [b, m, n, T] of meta.pipes || []) { const id = pipeNetAt[b]; if (id >= 0) { const pn = pipeNets[id]; pn.m = m; pn.n = Math.min(n, pn.cap); pn.T = T; } }
if (buffers.fog && buffers.fog.length === fog.length) fog.set(buffers.fog);
else {
fogInit();
for (let b = 0; b < NB; b++) if (blockP[b]) fogRevealAround((b % BW) * BS + BS / 2, ((b / BW) | 0) * BS + BS / 2, 12);
for (const m of machines) if (m) for (let y = m.y; y < m.y + m.h; y += BS) for (let x = m.x; x < m.x + m.w; x += BS) fogRevealAround(x, y, 12);
for (const mo of molds) for (const i of moldCellsAt(mo.kind, mo.x, mo.y).walls) fogRevealAround(i % W, (i / W) | 0, 12);
}
fogLook(bot.x + bot.w / 2, bot.y + bot.h / 2);
startLoadWake();
EV.length = 0;
}
// Keep the load-time heat scan, but start distant areas in order instead of all at once.
function startLoadWake() {
awake.fill(0); hot.fill(0); hotLvl.fill(0);
loadWakePending = new Uint8Array(NCH).fill(1);
for (let c = 0; c < NCH; c++) {
const cx = c % CW, cy = (c / CW) | 0, x0 = cx << 5, y0 = cy << 5, x1 = Math.min(W, x0 + 32), y1 = Math.min(H, y0 + 32);
let dev = 0;
for (let y = y0; y < y1 && dev < 2; y++) for (let x = x0; x < x1; x++) { const d = Math.abs(temp[y * W + x] - AMB[y]); if (d > dev) dev = d; if (flags[y * W + x] & F_BURN) dev = 99; }
if (dev >= 2) loadWakePending[c] = 2;
}
const x = bot.x + bot.w / 2, y = bot.y + bot.h / 2;
const distance = c => (c % CW * 32 + 16 - x) ** 2 + (((c / CW) | 0) * 32 + 16 - y) ** 2;
loadWakeOrder = Uint32Array.from({ length: NCH }, (_, c) => c);
loadWakeOrder.sort((a, b) => distance(a) - distance(b));
loadWakeIndex = 0;
wakeLoadNearBot();
}
function wakeLoadArea(c) {
const pending = loadWakePending[c];
if (!pending) return;
loadWakePending[c] = 0;
awake[c] = 3;
if (pending === 2) { hot[c] = 6; hotLvl[c] = 2; }
}
function wakeLoadNearBot() {
const x = bot.x + bot.w / 2, y = bot.y + bot.h / 2;
const x0 = Math.max(0, Math.floor((x - 150) / 32)), x1 = Math.min(CW - 1, Math.floor((x + 150) / 32));
const y0 = Math.max(0, Math.floor((y - 150) / 32)), y1 = Math.min(CH - 1, Math.floor((y + 150) / 32));
for (let cy = y0; cy <= y1; cy++) for (let cx = x0; cx <= x1; cx++) {
const dx = Math.max(cx * 32 - x, 0, x - (cx * 32 + 31));
const dy = Math.max(cy * 32 - y, 0, y - (cy * 32 + 31));
if (dx * dx + dy * dy <= 150 * 150) wakeLoadArea(cy * CW + cx);
}
}
function loadWakeTick() {
if (tick % 8 === 0) wakeLoadNearBot();
// Twelve per tick finishes a Huge world in 1024 ticks, and a Small one in 171.
const end = Math.min(loadWakeIndex + 12, loadWakeOrder.length);
while (loadWakeIndex < end) wakeLoadArea(loadWakeOrder[loadWakeIndex++]);
if (loadWakeIndex === loadWakeOrder.length) loadWakeOrder = loadWakePending = null;
}