mirror of
https://github.com/motajs/template.git
synced 2026-11-02 06:34:31 +08:00
refactor: 存档系统挪至渲染端
This commit is contained in:
1 parent
89434537c8
commit
94ceb50cfe
12 files changed
+212
-215
No files matched your search
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user