"use strict"; (function (global) { const STORAGE_PREFIX = "tarinai_save_slot_v2_"; const EXPORT_PREFIX = "TN2!"; const SLOT_COUNT = 9; const Snapshot = global.TarinaiSnapshot; if (!Snapshot) throw new Error("TarinaiSnapshot is not available for save_system.js"); const createSnapshot = (...args) => Snapshot.createSnapshot(...args); const restoreSnapshot = (...args) => Snapshot.restoreSnapshot(...args); const SAVE_TEXT_ALPHABET = (() => { let out = ""; for (let i = 33; i <= 126; i++) { const ch = String.fromCharCode(i); if (ch === "*" || ch === "'" || ch === "_") continue; out += ch; } return out; })(); const SAVE_TEXT_REVERSE = (() => { const map = new Map(); for (let i = 0; i < SAVE_TEXT_ALPHABET.length; i++) map.set(SAVE_TEXT_ALPHABET[i], i); return map; })(); if (SAVE_TEXT_ALPHABET.length !== 91) throw new Error("save alphabet must contain 91 printable ASCII chars"); function base91Encode(bytes) { let b = 0; let n = 0; let out = ""; for (const byte of bytes || []) { b |= (byte & 255) << n; n += 8; if (n > 13) { let v = b & 8191; if (v > 88) { b >>= 13; n -= 13; } else { v = b & 16383; b >>= 14; n -= 14; } out += SAVE_TEXT_ALPHABET[v % 91] + SAVE_TEXT_ALPHABET[Math.floor(v / 91)]; } } if (n) { out += SAVE_TEXT_ALPHABET[b % 91]; if (n > 7 || b > 90) out += SAVE_TEXT_ALPHABET[Math.floor(b / 91)]; } return out; } function base91Decode(text) { const clean = String(text || "").trim(); let v = -1; let b = 0; let n = 0; const out = []; for (const ch of clean) { const c = SAVE_TEXT_REVERSE.get(ch); if (c == null) continue; if (v < 0) { v = c; } else { v += c * 91; b |= v << n; n += (v & 8191) > 88 ? 13 : 14; do { out.push(b & 255); b >>= 8; n -= 8; } while (n > 7); v = -1; } } if (v >= 0) out.push((b | (v << n)) & 255); return new Uint8Array(out); } async function compressBytes(bytes) { if (typeof CompressionStream === "function") { const stream = new Blob([bytes]).stream().pipeThrough(new CompressionStream("deflate-raw")); return new Uint8Array(await new Response(stream).arrayBuffer()); } return bytes; } async function decompressBytes(bytes, codec = "R") { if (codec === "D") { if (typeof DecompressionStream !== "function") throw new Error("deflate decoder is not available in this browser"); const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate-raw")); return new Uint8Array(await new Response(stream).arrayBuffer()); } return bytes; } async function encodeSnapshot(snapshot) { const compact = snapshot && snapshot.v === 2 ? snapshot : createSnapshot(global.world); const json = JSON.stringify(compact); const bytes = new TextEncoder().encode(json); let codec = "R"; let packed = bytes; try { const compressed = await compressBytes(bytes); if (compressed.length < bytes.length) { codec = "D"; packed = compressed; } } catch (error) { console.warn("save compression fallback", error); } return `${EXPORT_PREFIX}${codec}${base91Encode(packed)}`; } async function decodeSnapshot(text) { const raw = String(text || "").trim(); if (!raw) throw new Error("empty import text"); if (raw.startsWith("{") || raw.startsWith("[")) { const data = JSON.parse(raw); if (data?.v !== 2) throw new Error("unsupported save version"); return data; } if (!raw.startsWith(EXPORT_PREFIX)) throw new Error("unsupported save text"); const codec = raw.charAt(EXPORT_PREFIX.length) || "R"; const payload = raw.slice(EXPORT_PREFIX.length + 1); const packed = base91Decode(payload); const bytes = await decompressBytes(packed, codec); const json = new TextDecoder().decode(bytes); const data = JSON.parse(json); if (data?.v !== 2) throw new Error("unsupported save version"); return data; } function slotKey(slot) { return `${STORAGE_PREFIX}${slot}`; } async function saveSlot(slot) { const snapshot = createSnapshot(global.world); localStorage.setItem(slotKey(slot), JSON.stringify(snapshot)); return snapshot; } async function loadSlot(slot) { const raw = localStorage.getItem(slotKey(slot)); if (!raw) throw new Error("empty slot"); const snapshot = JSON.parse(raw); if (snapshot?.v !== 2) throw new Error("unsupported slot version"); return restoreSnapshot(snapshot, global.world); } function deleteSlot(slot) { localStorage.removeItem(slotKey(slot)); } function readSlot(slot) { const raw = localStorage.getItem(slotKey(slot)); if (!raw) return null; try { return JSON.parse(raw); } catch (_) { return null; } } const htmlEscape = global.TarinaiUIHelpers.htmlEscape; function formatSlotSummary(snapshot) { if (!snapshot) return "\u7a7a\u304d"; const m = snapshot.m || []; const date = snapshot.c ? new Date(snapshot.c).toLocaleString("ja-JP", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : "\u65e5\u6642\u4e0d\u660e"; const day = m[0] || 1; const population = m[1] || 0; const fieldType = m[2] || "\u5ead"; const timeText = Number.isFinite(m[3]) ? `${Math.floor((m[3] / 10) % 24).toString().padStart(2, "0")}:00` : ""; return `${date} / ${fieldType} / ${day}\u65e5\u76ee ${timeText} / ${population}\u5339`; } function ensureDialog() { let dialog = document.getElementById("saveDialog"); if (dialog) return dialog; dialog = document.createElement("div"); dialog.id = "saveDialog"; dialog.className = "field-dialog hidden"; dialog.setAttribute("role", "dialog"); dialog.setAttribute("aria-modal", "true"); dialog.innerHTML = `
\u30bb\u30fc\u30d6\u6a5f\u80fd\u306f\u5b9f\u9a13\u4e2d\u3067\u3042\u308a\u3001\u4fdd\u8a3c\u5bfe\u8c61\u5916\u3067\u3059\u3002
9\u3064\u306e\u30b9\u30ed\u30c3\u30c8\u306b\u73fe\u5728\u306e\u30b3\u30ed\u30cb\u30fc\u3092\u4fdd\u5b58\u3067\u304d\u307e\u3059\u3002\u30cf\u30c3\u30b7\u30e5\u30c6\u30ad\u30b9\u30c8\u306f\u5225\u7aef\u672b\u3084\u5225\u30d6\u30e9\u30a6\u30b6\u3078\u306e\u79fb\u52d5\u306b\u4f7f\u3048\u307e\u3059\u3002