refactor: 存档系统挪至渲染端

This commit is contained in:
AncTe committed 2026-09-17 13:29:34 +08:00
1 parent 89434537c8
commit 94ceb50cfe
12 files changed
+212 -215

No files matched your search

+3
View File
@@ -6,3 +6,6 @@ export function create() {
export * from './load';
export * from './material';
export * from './save';
export * from './types';
@@ -0,0 +1,2 @@
export * from './system';
export * from './types';
@@ -0,0 +1,222 @@
import Dexie, { Table } from 'dexie';
import { logger } from '@motajs/common';
import {
IGlobalTrasaction,
ISaveRead,
ISaveSystem,
ISaveSystemConfig
} from './types';
import { isNil } from 'lodash-es';
import { SaveCompression, ISaveableContent } from '@user/data-common';
interface ISaveRecord {
/** 存档 id */
readonly id: number;
/** 存档压缩级别 */
readonly compression: SaveCompression;
/** 存档内容 */
readonly data: Map<string, unknown>;
}
interface IGlobalRecord {
/** 全局存储的键名 */
readonly key: string;
/** 全局存储的内容 */
readonly value: unknown;
}
export class GlobalTransaction implements IGlobalTrasaction {
constructor(readonly table: Table<IGlobalRecord, string>) {}
async get<T>(key: string): Promise<T> {
const record = await this.table.get(key);
return record!.value as T;
}
async set(key: string, value: unknown): Promise<void> {
await this.table.put({ key, value });
}
}
export class SaveSystem implements ISaveSystem {
db!: Dexie;
/** 当前的撤回栈 */
private readonly undoStack: ISaveRead[] = [];
/** 当前的重做栈 */
private readonly redoStack: ISaveRead[] = [];
/** 撤回栈与重做栈的最大长度 */
private stackSize: number = 20;
/** 自动存档压缩级别 */
private autosaveLevel: SaveCompression = SaveCompression.LowCompression;
/** 普通存档压缩级别 */
private commonSaveLevel: SaveCompression = SaveCompression.HighCompression;
/** 普通存档容忍时长 */
private saveTimeTolerance: number = 100;
/** 自动存档容忍时长 */
private autosaveTimeTolerance: number = 50;
init(name: string) {
this.db = new Dexie(name);
this.db.version(1).stores({
saves: 'id',
global: 'key'
});
}
config(config: Readonly<Partial<ISaveSystemConfig>>): void {
if (!isNil(config.autosaveLevel)) {
this.autosaveLevel = config.autosaveLevel;
}
if (!isNil(config.commonSaveLevel)) {
this.commonSaveLevel = config.commonSaveLevel;
}
if (!isNil(config.saveTimeTolerance)) {
this.saveTimeTolerance = config.saveTimeTolerance;
}
if (!isNil(config.autosaveTimeTolerance)) {
this.autosaveTimeTolerance = config.autosaveTimeTolerance;
}
if (!isNil(config.autosaveStackSize)) {
const size = config.autosaveStackSize;
this.stackSize = size;
if (this.undoStack.length > size) {
this.undoStack.splice(0, this.undoStack.length - size);
}
if (this.redoStack.length > size) {
this.redoStack.splice(0, this.redoStack.length - size);
}
}
}
undoAutosave(
current: Map<string, ISaveableContent<unknown>>
): ISaveRead | null {
if (this.undoStack.length === 0) return null;
const data = new Map<string, unknown>();
for (const [key, content] of current) {
data.set(key, content.saveState(this.autosaveLevel));
}
this.redoStack.push({ compression: this.autosaveLevel, data });
if (this.redoStack.length > this.stackSize) {
this.redoStack.splice(0, this.redoStack.length - this.stackSize);
}
return this.undoStack.pop()!;
}
redoAutosave(
current: Map<string, ISaveableContent<unknown>>
): ISaveRead | null {
if (this.redoStack.length === 0) return null;
const data = new Map<string, unknown>();
for (const [key, content] of current) {
data.set(key, content.saveState(this.autosaveLevel));
}
this.undoStack.push({ compression: this.autosaveLevel, data });
if (this.undoStack.length > this.stackSize) {
this.undoStack.splice(0, this.undoStack.length - this.stackSize);
}
return this.redoStack.pop()!;
}
getUndoStack(): ISaveRead[] {
return this.undoStack.slice();
}
getRedoStack(): ISaveRead[] {
return this.redoStack.slice();
}
autosave(state: Map<string, ISaveableContent<unknown>>): void {
const data = new Map<string, unknown>();
for (const [key, content] of state) {
data.set(key, content.saveState(this.autosaveLevel));
}
this.undoStack.push({ compression: this.autosaveLevel, data });
this.redoStack.length = 0;
if (this.undoStack.length > this.stackSize) {
this.undoStack.splice(0, this.undoStack.length - this.stackSize);
}
}
async saveAutosaveToDB(): Promise<void> {
if (this.undoStack.length === 0) return;
const t0 = performance.now();
const top = this.undoStack[this.undoStack.length - 1];
const table = this.db.table<ISaveRecord, number>('saves');
await table.put({
id: -1,
compression: top.compression,
data: top.data
});
const t1 = performance.now();
if (t1 - t0 > this.autosaveTimeTolerance) {
logger.warn(
115,
(t1 - t0).toFixed(0),
this.autosaveTimeTolerance.toString()
);
}
}
async save(
id: number,
state: Map<string, ISaveableContent<unknown>>
): Promise<void> {
const t0 = performance.now();
const data = new Map<string, unknown>();
for (const [key, content] of state) {
data.set(key, content.saveState(this.commonSaveLevel));
}
const table = this.db.table<ISaveRecord, number>('saves');
await table.put({ id, compression: this.commonSaveLevel, data });
await this.setGlobal('lastSlot', id);
const t1 = performance.now();
if (t1 - t0 > this.saveTimeTolerance) {
logger.warn(
114,
(t1 - t0).toFixed(0),
this.saveTimeTolerance.toString()
);
}
}
async load(id: number): Promise<ISaveRead | null> {
const table = this.db.table<ISaveRecord, number>('saves');
const record = await table.get(id);
if (record === undefined) return null;
return { compression: record.compression, data: record.data };
}
async deleteSave(id: number): Promise<void> {
const table = this.db.table<ISaveRecord, number>('saves');
await table.delete(id);
}
async getLastSlot(): Promise<number> {
const value = await this.getGlobal<number | undefined>('lastSlot');
return value ?? 0;
}
async getGlobal<T>(key: string): Promise<T | null> {
const table = this.db.table<IGlobalRecord, string>('global');
const record = await table.get(key);
if (!record) return null;
else return record.value as T;
}
async setGlobal(key: string, value: unknown): Promise<void> {
const table = this.db.table<IGlobalRecord, string>('global');
await table.put({ key, value });
}
async startGlobalTransaction<R>(
handle: (transaction: IGlobalTrasaction) => PromiseLike<R>
): Promise<R> {
const globalTable = this.db.table<IGlobalRecord, string>('global');
return this.db.transaction('rw', globalTable, () => {
return handle(new GlobalTransaction(globalTable));
});
}
}
+142
View File
@@ -0,0 +1,142 @@
import { Dexie, Table } from 'dexie';
import { SaveCompression, ISaveableContent } from '@user/data-common';
export interface IGlobalTrasaction {
/** 全局存储对应的表 */
readonly table: Table<unknown, string>;
/**
* 获取指定键值对应的数据
* @param key 全局键值
*/
get<T>(key: string): Promise<T>;
/**
* 设置指定键值存储的数据
* @param key 全局键值
* @param value 存储数据
*/
set(key: string, value: unknown): Promise<void>;
}
export interface ISaveSystemConfig {
/** 自动存档使用的压缩等级 */
autosaveLevel: SaveCompression;
/** 普通存档使用的压缩等级 */
commonSaveLevel: SaveCompression;
/** 可容忍的最大存档耗时,超过此值会抛出警告 */
saveTimeTolerance: number;
/** 可容忍的最大自动存档耗时,超过此值会抛出警告 */
autosaveTimeTolerance: number;
/** 自动存档栈最大大小 */
autosaveStackSize: number;
}
export interface ISaveRead {
/** 该存档的压缩等级 */
readonly compression: SaveCompression;
/** 该存档的数据 */
readonly data: Map<string, unknown>;
}
export interface ISaveSystem {
/** Dexie 数据库 */
readonly db: Dexie;
/**
* 初始化存档数据库
* @param name 数据库名称
*/
init(name: string): void;
/**
* 配置此存档系统
* @param config 配置对象
*/
config(config: Readonly<Partial<ISaveSystemConfig>>): void;
/**
* 从 `undo` 栈读取上一个自动存档,然后将当前状态加入 `redo` 栈
* @param current 当前游戏状态,需要加入 `redo` 栈
*/
undoAutosave(
current: Map<string, ISaveableContent<unknown>>
): ISaveRead | null;
/**
* 从 `redo` 栈读取自动存档,并将当前状态加入 `undo` 栈
* @param current 当前游戏状态,需要加入 `undo` 栈
*/
redoAutosave(
current: Map<string, ISaveableContent<unknown>>
): ISaveRead | null;
/**
* 获取当前的撤回栈
*/
getUndoStack(): ISaveRead[];
/**
* 获取当前的重做栈
*/
getRedoStack(): ISaveRead[];
/**
* 进行自动存档,加入撤回栈
* @param state 状态对象
*/
autosave(state: Map<string, ISaveableContent<unknown>>): void;
/**
* 将 `undo` 栈顶的自动存档真正存入 `IndexedDB`
*/
saveAutosaveToDB(): Promise<void>;
/**
* 将状态对象存入存档
* @param id 存档 id,用于建立存档索引及查询
* @param state 状态对象
*/
save(
id: number,
state: Map<string, ISaveableContent<unknown>>
): Promise<void>;
/**
* 根据 id 读取指定存档
* @param id 存档 id
*/
load(id: number): Promise<ISaveRead | null>;
/**
* 删除指定存档
* @param id 存档 id
*/
deleteSave(id: number): Promise<void>;
/**
* 获取最后一次存档的存档栏位
*/
getLastSlot(): Promise<number>;
/**
* 获取指定键值对应的全局存储
* @param key 全局键值
*/
getGlobal<T>(key: string): Promise<T | null>;
/**
* 设置指定键值对应的全局存储
* @param key 全局键值
* @param value 存储数据
*/
setGlobal(key: string, value: unknown): Promise<void>;
/**
* 进行全局存储的事务处理,适用于多内容查询与设置,当出现错误时其中的任何写入操作都不会真正存储
* @param handle 事务处理函数
*/
startGlobalTransaction<R>(
handle: (transaction: IGlobalTrasaction) => PromiseLike<R>
): Promise<R>;
}
+12
View File
@@ -0,0 +1,12 @@
import { ISaveSystem } from '@user/data-common';
import { ICoreState } from '@user/data-state';
export interface IClientBase extends ICoreState {
/** 存档系统 */
readonly save: ISaveSystem;
}
export interface IClientBaseExtended {
/** 当前对象的渲染端基本层对象(Layer 4 对象) */
readonly state: IClientBase;
}