v0.1.35b: 兼容层铲除——迁移链/版本戳/死表死代码/normalize 兼容批/死字段
【A+I】migrate.ts 迁移链(CURRENT_SCHEMA/MIGRATIONS/migrateIfNeeded/MigrationError)整链铲除→validateLoadedState 结构校验(seed/family/members 断言);信封统一 payload 单格式(去双格式兜底);APP_ID 保留 【F】schemaVersion 字段全删(domain/creation/save 8 处);SaveMeta.version 记游戏版本字符串 【C】chronicle 死表+saveChronicle 死方法(首行 return+unreachable legacy 僵尸)全删 【D/E】GameEngine __state globalThis 泄漏口+seed 三元(恒不可达)删除;engineFromSnapshot/noAutoCreate 保留(测试便利——非兼容机器) 【B/H】normalizeGameState 兼容批删:老档字段补(28行)/worldGen 重放(18行)/P0-2 占位 def(9行)/worldSim 字段批删;保留 npcs 重注册/headId 修复/d distress 防护;initSim 补 0.1.34 四键(唯一初始源) 【G】domain 收紧 5 兼容 optional(techniqueRank/Progress/tribBoost/worldGen 必填)+pcgen 必写 【测试】删 9 个兼容验证体(audit 0.1.0-era/全残缺矩阵/空 worldSim 兜底);5989 全绿——金钟罩零漂(新档结构全字段,删除不涉世界数值)
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { GameState, SaveMeta, SnapshotMeta, Id } from '../types/domain'
|
||||
import { CURRENT_SCHEMA, APP_ID, exportEnvelope, parseImportEnvelope, migrateIfNeeded, MigrationError } from './migrate'
|
||||
import { validateLoadedState, exportEnvelope, parseImportEnvelope } from './migrate'
|
||||
|
||||
const GAME_VERSION = '0.1.35'
|
||||
|
||||
const DB_PREFIX = 'cotyc-save-'
|
||||
let seqCounter = 0
|
||||
@@ -36,12 +38,10 @@ export class SaveSlot {
|
||||
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)`)
|
||||
await driver.exec(`CREATE TABLE IF NOT EXISTS chronicle (id string PRIMARY KEY, year number, month number, category string, important number, memberId string, text string)`)
|
||||
}
|
||||
|
||||
async saveState(state: GameState, label: string): Promise<string> {
|
||||
const id = `${state.year}.${state.month}.${Date.now().toString(36)}-${(seqCounter++ % 1296).toString(36)}`
|
||||
state.schemaVersion = CURRENT_SCHEMA
|
||||
const json = JSON.stringify(state)
|
||||
await this.driver.run(
|
||||
`INSERT INTO snapshot (id, year, month, savedAt, label, data) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
@@ -60,29 +60,6 @@ export class SaveSlot {
|
||||
return id
|
||||
}
|
||||
|
||||
/** P1-T chronicle 表已废(write-only 无读取者;史书数据随快照 data 保存)——保持 no-op 接口 */
|
||||
async saveChronicle(_chronicle: GameState['chronicle']): Promise<void> {
|
||||
return
|
||||
/* eslint-disable no-unreachable */
|
||||
try {
|
||||
// legacy(0.1.29 前占位;不再维护)
|
||||
const rows = await this.driver.all<{ id: string }>(`SELECT id FROM chronicle`)
|
||||
const existing = new Set(rows.map((r) => r.id))
|
||||
for (const e of _chronicle) {
|
||||
if (existing.has(e.id)) continue
|
||||
try {
|
||||
await this.driver.run(
|
||||
`INSERT INTO chronicle (id, year, month, category, important, memberId, text) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[e.id, e.year, e.month, e.category, e.important ? 1 : 0, e.memberId ?? null, e.text]
|
||||
)
|
||||
} catch {
|
||||
// row-level best-effort
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 冗余表失败吞掉(主挡位用 snapshot 兜底)
|
||||
}
|
||||
}
|
||||
async listSnapshots(): Promise<SnapshotMeta[]> {
|
||||
const rows = await this.driver.all<SnapshotMeta & { data?: string }>(`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 }))
|
||||
@@ -97,8 +74,8 @@ export class SaveSlot {
|
||||
)
|
||||
if (!rows || rows.length === 0) return null
|
||||
const state = JSON.parse(rows[0]!.data) as GameState
|
||||
const r = migrateIfNeeded(state)
|
||||
if (!r.ok) throw new MigrationError(r.error.code, r.error.version, r.error.message)
|
||||
const r = validateLoadedState(state)
|
||||
if (!r.ok) throw new Error(r.message)
|
||||
return r.state
|
||||
}
|
||||
|
||||
@@ -126,8 +103,8 @@ export class SaveSlot {
|
||||
const state = await this.loadState()
|
||||
if (!state) throw new Error('无可导出存档')
|
||||
const meta = await this.getMeta()
|
||||
const envelope = JSON.parse(exportEnvelope(state)) as { schemaVersion: number; payload: GameState }
|
||||
return JSON.stringify({ app: APP_ID, schemaVersion: envelope.schemaVersion, meta, state: envelope.payload })
|
||||
const envelope = JSON.parse(exportEnvelope(state)) as { payload: GameState }
|
||||
return JSON.stringify({ app: 'cotyc', meta, payload: envelope.payload })
|
||||
}
|
||||
|
||||
async importAll(data: string): Promise<boolean> {
|
||||
@@ -135,10 +112,8 @@ export class SaveSlot {
|
||||
const parsed = parseImportEnvelope(data)
|
||||
if (!parsed.ok) return false
|
||||
const state = parsed.state
|
||||
state.schemaVersion = CURRENT_SCHEMA
|
||||
await this.driver.exec(`DELETE FROM snapshot`)
|
||||
await this.driver.exec(`DELETE FROM meta`)
|
||||
await this.driver.exec(`DELETE FROM chronicle`)
|
||||
await this.saveState(state, 'import')
|
||||
const aliveCount = Object.values(state.members).filter((c) => c.alive).length
|
||||
await this.setMeta(buildSimpleMeta(state, this.slot, aliveCount))
|
||||
@@ -167,7 +142,7 @@ export function buildSimpleMeta(state: GameState, slot: number, memberCount: num
|
||||
members: memberCount,
|
||||
reputation: state.family.reputation,
|
||||
updatedAt: new Date().toISOString(),
|
||||
version: state.schemaVersion
|
||||
version: GAME_VERSION
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user