// Persistence in browser cookies. // // A cookie holds ~4 KB, so a save is base64url-encoded and split in chunks: // ic_game_n -> number of chunks // ic_game_0.. -> the chunks // There is ONE game, shared by every theme (a theme is only a skin). // Preferences (theme, language) are stored in a single small cookie. import { readCookie, removeCookie, writeCookie } from './cookies.js'; const CHUNK_SIZE = 3000; const MAX_CHUNKS = 6; const PREFS_KEY = 'ic_prefs'; const GAME_KEY = 'ic_game'; const legacyKey = (themeId) => `ic_save_${themeId}`; // v1: one save per theme // ------------------------------------------------------------------ encoding export function encode(text) { const bytes = new TextEncoder().encode(text); let binary = ''; for (const b of bytes) binary += String.fromCharCode(b); return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, ''); } export function decode(encoded) { const base64 = encoded.replaceAll('-', '+').replaceAll('_', '/'); const binary = atob(base64 + '='.repeat((4 - (base64.length % 4)) % 4)); return new TextDecoder().decode(Uint8Array.from(binary, (c) => c.charCodeAt(0))); } // ------------------------------------------------------------------- chunked function writeChunked(key, text) { const encoded = encode(text); const chunks = []; for (let i = 0; i < encoded.length; i += CHUNK_SIZE) chunks.push(encoded.slice(i, i + CHUNK_SIZE)); if (chunks.length > MAX_CHUNKS) throw new Error('save_too_large'); const previous = Number(readCookie(`${key}_n`)) || 0; chunks.forEach((chunk, i) => writeCookie(`${key}_${i}`, chunk)); writeCookie(`${key}_n`, String(chunks.length)); for (let i = chunks.length; i < previous; i++) removeCookie(`${key}_${i}`); } function readChunked(key) { const count = Number(readCookie(`${key}_n`)); if (!Number.isInteger(count) || count < 1 || count > MAX_CHUNKS) return null; let encoded = ''; for (let i = 0; i < count; i++) { const chunk = readCookie(`${key}_${i}`); if (chunk == null) return null; encoded += chunk; } try { return decode(encoded); } catch { return null; } } function removeChunked(key) { const count = Number(readCookie(`${key}_n`)) || 0; for (let i = 0; i < count; i++) removeCookie(`${key}_${i}`); removeCookie(`${key}_n`); } // -------------------------------------------------------------------- public /** False when the browser refuses cookies (private mode, blocked, ...). */ export function cookiesAvailable() { try { writeCookie('ic_probe', '1'); const ok = readCookie('ic_probe') === '1'; removeCookie('ic_probe'); return ok; } catch { return false; } } /** Throws if the save does not fit in cookies. */ export function saveGameText(text) { writeChunked(GAME_KEY, text); } export function loadGameText() { return readChunked(GAME_KEY); } export function deleteGame() { removeChunked(GAME_KEY); } /** * Saves from the first version (one per theme). Returns their texts and * removes those cookies, so the caller can merge them into the shared game. */ export function takeLegacySaves(themeIds) { const texts = []; for (const id of themeIds) { const text = readChunked(legacyKey(id)); if (text) texts.push(text); removeChunked(legacyKey(id)); } return texts; } export function savePrefs(prefs) { writeCookie(PREFS_KEY, encode(JSON.stringify(prefs))); } export function loadPrefs() { try { const raw = readCookie(PREFS_KEY); const prefs = raw && JSON.parse(decode(raw)); return prefs && typeof prefs === 'object' ? prefs : {}; } catch { return {}; } }