// Save strings for export and import: "CG1:" + CRC32 of the JSON (8 hex digits) + ":" + base64(UTF-8 JSON). // The checksum only catches corrupted or truncated strings. It is not protection against editing: // anyone can decode, change and re-encode a save, and a single-player game does not need more. import { STATE_VERSION } from "../engine/state.js"; const PREFIX = "CG1:"; const FORMAT = /^CG1:([0-9a-f]{8}):([A-Za-z0-9+/]+={0,2})$/; const CRC_TABLE = (() => { const table = new Uint32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; table[n] = c >>> 0; } return table; })(); export function crc32(bytes) { let c = 0xffffffff; for (const b of bytes) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } function toBase64(bytes) { let binary = ""; for (let i = 0; i < bytes.length; i += 0x8000) binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000)); return btoa(binary); } function fromBase64(text) { const binary = atob(text); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); return bytes; } export function encodeSave(state) { const bytes = new TextEncoder().encode(JSON.stringify(state)); return `${PREFIX}${crc32(bytes).toString(16).padStart(8, "0")}:${toBase64(bytes)}`; } // Upgrades older saves one version at a time. Add an entry here whenever STATE_VERSION // changes: MIGRATIONS[n] turns a version n state into a version n+1 state. export const MIGRATIONS = { // v2 added the world cycle: older saves start it at neutral. 1: (s) => ({ ...s, version: 2, world: { cycle: 0 } }), }; export function migrate(state) { if (!Number.isInteger(state?.version)) return { ok: false, error: "format" }; if (state.version > STATE_VERSION) return { ok: false, error: "tooNew" }; let s = state; while (s.version < STATE_VERSION) { const step = MIGRATIONS[s.version]; if (!step) return { ok: false, error: "version" }; s = step(s); } return { ok: true, state: s }; } // Returns { ok: true, state } or { ok: false, error: "format" | "checksum" | "tooNew" | "version" }. // Whitespace is ignored, so strings that were wrapped when pasted still work. export function decodeSave(text) { const match = FORMAT.exec(String(text).replace(/\s+/g, "")); if (!match) return { ok: false, error: "format" }; let bytes; try { bytes = fromBase64(match[2]); } catch { return { ok: false, error: "format" }; } if (crc32(bytes).toString(16).padStart(8, "0") !== match[1]) return { ok: false, error: "checksum" }; let state; try { state = JSON.parse(new TextDecoder().decode(bytes)); } catch { return { ok: false, error: "format" }; } return migrate(state); }