v0.1.0: 仙途家族志 · Chronicle of the Immortal Clan
家族模拟器首版:修仙/经营/战斗/外交/叙事全套系统 - Electron + React + TS + MetonaSqlark(aria+OPFS) - 水墨中国风 UI - 引擎种子随机、确定性可回放 - 19 项单元测试、端到端冒烟验证
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
import { SaveSlot } from './slots'
|
||||
import { GameState, SaveMeta, SnapshotMeta } from '../types/domain'
|
||||
import type { SaveDbDriver } from './slots'
|
||||
|
||||
// Adapter for @metona-team/metona-sqlark
|
||||
// The driver interface keeps the engine swappable (testable in node too).
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type AnyFactory = { create: (config: any) => Promise<any> }
|
||||
let MetonaSqlark: AnyFactory | null = null
|
||||
|
||||
interface SqlarkLike {
|
||||
query: (sql: string, params?: unknown[]) => Promise<unknown[]>
|
||||
}
|
||||
|
||||
export function setSqlarkBackend(impl: AnyFactory): void {
|
||||
MetonaSqlark = impl
|
||||
}
|
||||
|
||||
export async function makeDriver(): Promise<SaveDbDriver> {
|
||||
const factory = await getSqlarkFactory()
|
||||
return new SqlarkDriver(factory)
|
||||
}
|
||||
|
||||
class SqlarkDriver implements SaveDbDriver {
|
||||
private db: SqlarkLike | null = null
|
||||
|
||||
constructor(private factory: AnyFactory) {}
|
||||
|
||||
async open(name: string): Promise<void> {
|
||||
if (this.db) return
|
||||
this.db = (await this.factory.create({
|
||||
name,
|
||||
mode: 'aria',
|
||||
diskEngine: 'opfs',
|
||||
aria: { walSyncMode: 'full', compression: true }
|
||||
})) as SqlarkLike
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
this.db = null
|
||||
}
|
||||
|
||||
async run(sql: string, params: unknown[] = []): Promise<unknown> {
|
||||
if (!this.db) throw new Error('db not open')
|
||||
await this.db.query(sql, params)
|
||||
return null
|
||||
}
|
||||
|
||||
async all<T>(sql: string, params: unknown[] = []): Promise<T[]> {
|
||||
if (!this.db) throw new Error('db not open')
|
||||
const rows = (await this.db.query(sql, params)) as T[]
|
||||
return rows ?? []
|
||||
}
|
||||
|
||||
async exec(sql: string): Promise<unknown> {
|
||||
if (!this.db) throw new Error('db not open')
|
||||
await this.db.query(sql)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const META_DB = 'cotyc-appmeta'
|
||||
|
||||
let singletonManager: SlotManager | null = null
|
||||
export function getSlotManager(): SlotManager {
|
||||
if (!singletonManager) singletonManager = new SlotManager()
|
||||
return singletonManager
|
||||
}
|
||||
|
||||
export async function getSqlarkFactory(): Promise<AnyFactory> {
|
||||
if (!MetonaSqlark) {
|
||||
const mod = await import('@metona-team/metona-sqlark')
|
||||
MetonaSqlark = mod.MetonaSqlark ?? mod.MeSqlark
|
||||
}
|
||||
if (!MetonaSqlark) throw new Error('metona-sqlark not available')
|
||||
return MetonaSqlark
|
||||
}
|
||||
|
||||
const slotCache = new Map<number, SaveSlot>()
|
||||
export async function getSaveSlot(slot: number): Promise<SaveSlot> {
|
||||
let s = slotCache.get(slot)
|
||||
if (!s) {
|
||||
const factory = await getSqlarkFactory()
|
||||
s = new SaveSlot(slot, new SqlarkDriver(factory))
|
||||
slotCache.set(slot, s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
export class SlotManager {
|
||||
private metaDb: SqlarkLike | null = null
|
||||
|
||||
async ensureMeta(): Promise<void> {
|
||||
if (this.metaDb) return
|
||||
this.metaDb = (await (await getSqlarkFactory()).create({
|
||||
name: META_DB,
|
||||
mode: 'aria',
|
||||
diskEngine: 'opfs',
|
||||
aria: { walSyncMode: 'full' }
|
||||
})) as SqlarkLike
|
||||
await this.metaDb.query(`CREATE TABLE IF NOT EXISTS slots (slot number PRIMARY KEY, meta string)`)
|
||||
}
|
||||
|
||||
async listSlotMetas(): Promise<(SaveMeta | null)[]> {
|
||||
await this.ensureMeta()
|
||||
const rows = await this.metaDb!.query(`SELECT slot, meta FROM slots ORDER BY slot`)
|
||||
const map = new Map<number, SaveMeta>()
|
||||
for (const r of rows as { slot: number; meta: string }[]) {
|
||||
try {
|
||||
map.set(r.slot, JSON.parse(r.meta) as SaveMeta)
|
||||
} catch {
|
||||
map.set(r.slot, null as unknown as SaveMeta)
|
||||
}
|
||||
}
|
||||
const out: (SaveMeta | null)[] = []
|
||||
for (let i = 1; i <= 4; i++) {
|
||||
out.push(map.get(i) ?? null)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
async updateSlotMeta(slot: number, meta: SaveMeta): Promise<void> {
|
||||
await this.ensureMeta()
|
||||
const existing = await this.metaDb!.query(`SELECT slot FROM slots WHERE slot = ?`, [slot])
|
||||
const json = JSON.stringify(meta)
|
||||
if (existing.length > 0) {
|
||||
await this.metaDb!.query(`UPDATE slots SET meta = ? WHERE slot = ?`, [json, slot])
|
||||
} else {
|
||||
await this.metaDb!.query(`INSERT INTO slots (slot, meta) VALUES (?, ?)`, [slot, json])
|
||||
}
|
||||
}
|
||||
|
||||
async removeSlotMeta(slot: number): Promise<void> {
|
||||
await this.ensureMeta()
|
||||
await this.metaDb!.query(`DELETE FROM slots WHERE slot = ?`, [slot])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export type { GameState, SaveMeta, SnapshotMeta }
|
||||
Reference in New Issue
Block a user