import { GameState, SaveMeta, SnapshotMeta, Id } from '../types/domain' import { validateLoadedState, exportEnvelope, parseImportEnvelope } from './migrate' const GAME_VERSION = '0.1.41' const DB_PREFIX = 'cotyc-save-' let seqCounter = 0 export interface SaveDbDriver { open(name: string): Promise close(): Promise run(sql: string, params?: unknown[]): Promise all(sql: string, params?: unknown[]): Promise exec(sql: string): Promise } export interface TableDef { name: string cols: Record } export class SaveSlot { constructor( public slot: number, private driver: SaveDbDriver ) {} private get name(): string { return `${DB_PREFIX}${this.slot}` } async open(): Promise { await this.driver.open(this.name) await this.ensureSchema() } private async ensureSchema(): Promise { const driver = this.driver await driver.exec(`CREATE TABLE IF NOT EXISTS meta (key string PRIMARY KEY, value string)`) await driver.exec(`CREATE TABLE IF NOT EXISTS snapshot (id string PRIMARY KEY, year number, month number, savedAt string, label string, data string)`) } async saveState(state: GameState, label: string): Promise { const id = `${state.year}.${state.month}.${Date.now().toString(36)}-${(seqCounter++ % 1296).toString(36)}` const json = JSON.stringify(state) await this.driver.run( `INSERT INTO snapshot (id, year, month, savedAt, label, data) VALUES (?, ?, ?, ?, ?, ?)`, [id, state.year, state.month, new Date().toISOString(), label, json] ) // 0.1.38 P1-F:回读校验——确认数据真正落盘 const verify = await this.driver.all<{ data: string }>( `SELECT data FROM snapshot WHERE id = ?`, [id] ) if (!verify.length || !verify[0]!.data) { throw new Error(`存档写入校验失败:数据未落盘(slot=${this.slot}, id=${id})`) } // 校验 JSON 可解析且关键字段在 try { const check = JSON.parse(verify[0]!.data) as GameState if (!check.family || !check.members || typeof check.seed !== 'string') { throw new Error('存档回读校验失败:关键字段缺失') } } catch (e) { throw new Error(`存档回读校验失败:${String(e).slice(0, 80)}`) } const keep = 12 const rows = await this.driver.all<{ id: string }>( `SELECT id FROM snapshot ORDER BY year DESC, month DESC, savedAt DESC` ) if (rows.length > keep) { const drop = rows.slice(keep).map((r) => r.id) for (const d of drop) { await this.driver.run(`DELETE FROM snapshot WHERE id = ?`, [d]) } } return id } async listSnapshots(): Promise { const rows = await this.driver.all(`SELECT id, year, month, savedAt, label FROM snapshot ORDER BY year DESC, month DESC`) return rows.map((r) => ({ id: r.id, year: r.year, month: r.month, savedAt: r.savedAt, label: r.label })) } async loadState(id?: string): Promise { const rows = await this.driver.all<{ data: string }>( id ? `SELECT data FROM snapshot WHERE id = ?` : `SELECT data FROM snapshot ORDER BY year DESC, month DESC, savedAt DESC LIMIT 1`, id ? [id] : [] ) if (!rows || rows.length === 0) return null const state = JSON.parse(rows[0]!.data) as GameState const r = validateLoadedState(state) if (!r.ok) throw new Error(r.message) return r.state } async deleteSnapshot(id: string): Promise { await this.driver.run(`DELETE FROM snapshot WHERE id = ?`, [id]) } async getMeta(): Promise { const rows = await this.driver.all<{ value: string }>(`SELECT value FROM meta WHERE key = 'meta'`) if (!rows || rows.length === 0) return null return JSON.parse(rows[0].value) as SaveMeta } async setMeta(meta: SaveMeta): Promise { const exists = await this.driver.all<{ key: string }>(`SELECT key FROM meta WHERE key = 'meta'`) const json = JSON.stringify(meta) if (exists.length > 0) { await this.driver.run(`UPDATE meta SET value = ? WHERE key = 'meta'`, [json]) } else { await this.driver.run(`INSERT INTO meta (key, value) VALUES (?, ?)`, ['meta', json]) } } async exportAll(): Promise { const state = await this.loadState() if (!state) throw new Error('无可导出存档') const meta = await this.getMeta() const envelope = JSON.parse(exportEnvelope(state)) as { payload: GameState } return JSON.stringify({ app: 'cotyc', meta, payload: envelope.payload }) } async importAll(data: string): Promise { try { const parsed = parseImportEnvelope(data) if (!parsed.ok) return false const state = parsed.state await this.driver.exec(`DELETE FROM snapshot`) await this.driver.exec(`DELETE FROM meta`) await this.saveState(state, 'import') const aliveCount = Object.values(state.members).filter((c) => c.alive).length await this.setMeta(buildSimpleMeta(state, this.slot, aliveCount)) return true } catch { return false } } async wipe(): Promise { await this.driver.exec(`DELETE FROM snapshot`) await this.driver.exec(`DELETE FROM meta`) await this.driver.exec(`DELETE FROM chronicle`) } } export function buildSimpleMeta(state: GameState, slot: number, memberCount: number): SaveMeta { return { slot, surname: state.family.surname, name: state.family.name, estate: state.family.estate, year: state.year, month: state.month, generation: state.family.generation, members: memberCount, reputation: state.family.reputation, updatedAt: new Date().toISOString(), version: GAME_VERSION } } export function saveNameOf(meta: SaveMeta | null): string { if (!meta) return '' return `${meta.name} · ${meta.estate}` }