From a707be4856c55d557ebf5ebf3c3b5490640522a2 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Thu, 10 Sep 2026 15:35:01 +0800 Subject: [PATCH] feat(03-01): add Node-safe core state construction - isolate legacy loading behind internal dependencies - provide memory saves and remove singleton back-edges --- packages-user/data-base/src/game.ts | 13 +- packages-user/data-base/src/hero/mover.ts | 2 +- packages-user/data-common/src/save/index.ts | 1 + packages-user/data-common/src/save/memory.ts | 152 ++++++++++++++++++ packages-user/data-state/src/core.ts | 71 ++++---- .../data-state/src/enemy/calculator.ts | 8 +- .../data-state/src/legacy/dependencies.ts | 76 +++++++++ packages-user/data-state/src/legacy/move.ts | 28 ++-- packages/common/src/logger.ts | 23 ++- 9 files changed, 310 insertions(+), 64 deletions(-) create mode 100644 packages-user/data-common/src/save/memory.ts create mode 100644 packages-user/data-state/src/legacy/dependencies.ts diff --git a/packages-user/data-base/src/game.ts b/packages-user/data-base/src/game.ts index 66745cc..40f0b41 100644 --- a/packages-user/data-base/src/game.ts +++ b/packages-user/data-base/src/game.ts @@ -70,13 +70,18 @@ class GameLoading extends EventEmitter { } export const loading = new GameLoading(); -main.loading = loading; +if (typeof main !== 'undefined') { + main.loading = loading; +} let clientRegistered = false; let dataRegistered = false; function checkRegistered() { - if (main.replayChecking || main.mode === 'editor') { + if ( + typeof main !== 'undefined' && + (main.replayChecking || main.mode === 'editor') + ) { clientRegistered = true; } if (clientRegistered && dataRegistered) { @@ -168,8 +173,8 @@ class GameListener extends EventEmitter { constructor() { super(); - if (main.replayChecking) return; - if (window.core) { + if (typeof main === 'undefined' || main.replayChecking) return; + if (typeof window !== 'undefined' && window.core) { this.init(); } else { loading.once('coreInit', () => { diff --git a/packages-user/data-base/src/hero/mover.ts b/packages-user/data-base/src/hero/mover.ts index 45c3612..ef4910b 100644 --- a/packages-user/data-base/src/hero/mover.ts +++ b/packages-user/data-base/src/hero/mover.ts @@ -40,7 +40,7 @@ export class HeroMover readonly tile: T, faceHandler: IFaceHandler ) { - super(faceHandler, tile.getCurrentFaceDirection()); + super(faceHandler, FaceDirection.Unknown); this.state = tile.state; } diff --git a/packages-user/data-common/src/save/index.ts b/packages-user/data-common/src/save/index.ts index 213e667..1603d0d 100644 --- a/packages-user/data-common/src/save/index.ts +++ b/packages-user/data-common/src/save/index.ts @@ -1,2 +1,3 @@ export * from './system'; +export * from './memory'; export * from './types'; diff --git a/packages-user/data-common/src/save/memory.ts b/packages-user/data-common/src/save/memory.ts new file mode 100644 index 0000000..2802e3b --- /dev/null +++ b/packages-user/data-common/src/save/memory.ts @@ -0,0 +1,152 @@ +import { Dexie, Table } from 'dexie'; +import { isNil } from 'lodash-es'; +import { + IGlobalTrasaction, + ISaveRead, + ISaveSystem, + ISaveSystemConfig, + ISaveableContent, + SaveCompression +} from './types'; + +interface MemorySaveRecord { + readonly compression: SaveCompression; + readonly data: Map; +} + +class MemoryGlobalTransaction implements IGlobalTrasaction { + readonly table!: Table; + + constructor(private readonly values: Map) {} + + async get(key: string): Promise { + return this.values.get(key) as T; + } + + async set(key: string, value: unknown): Promise { + this.values.set(key, value); + } +} + +export class MemorySaveSystem implements ISaveSystem { + db!: Dexie; + + private readonly saves: Map = new Map(); + private readonly values: Map = new Map(); + private readonly undoStack: ISaveRead[] = []; + private readonly redoStack: ISaveRead[] = []; + private stackSize: number = 20; + private autosaveLevel: SaveCompression = SaveCompression.LowCompression; + private commonSaveLevel: SaveCompression = SaveCompression.HighCompression; + + init(): void {} + + config(config: Readonly>): void { + if (!isNil(config.autosaveLevel)) { + this.autosaveLevel = config.autosaveLevel; + } + if (!isNil(config.commonSaveLevel)) { + this.commonSaveLevel = config.commonSaveLevel; + } + if (!isNil(config.autosaveStackSize)) { + this.stackSize = config.autosaveStackSize; + this.trim(this.undoStack); + this.trim(this.redoStack); + } + } + + undoAutosave( + current: Map> + ): ISaveRead | null { + if (this.undoStack.length === 0) return null; + this.redoStack.push(this.capture(current, this.autosaveLevel)); + this.trim(this.redoStack); + return this.undoStack.pop()!; + } + + redoAutosave( + current: Map> + ): ISaveRead | null { + if (this.redoStack.length === 0) return null; + this.undoStack.push(this.capture(current, this.autosaveLevel)); + this.trim(this.undoStack); + return this.redoStack.pop()!; + } + + getUndoStack(): ISaveRead[] { + return this.undoStack.slice(); + } + + getRedoStack(): ISaveRead[] { + return this.redoStack.slice(); + } + + autosave(state: Map>): void { + this.undoStack.push(this.capture(state, this.autosaveLevel)); + this.redoStack.length = 0; + this.trim(this.undoStack); + } + + async saveAutosaveToDB(): Promise {} + + async save( + id: number, + state: Map> + ): Promise { + this.saves.set(id, this.captureRecord(state, this.commonSaveLevel)); + await this.setGlobal('lastSlot', id); + } + + async load(id: number): Promise { + const record = this.saves.get(id); + if (!record) return null; + return { compression: record.compression, data: record.data }; + } + + async deleteSave(id: number): Promise { + this.saves.delete(id); + } + + async getLastSlot(): Promise { + return (await this.getGlobal('lastSlot')) ?? 0; + } + + async getGlobal(key: string): Promise { + return this.values.has(key) ? (this.values.get(key) as T) : null; + } + + async setGlobal(key: string, value: unknown): Promise { + this.values.set(key, value); + } + + async startGlobalTransaction( + handle: (transaction: IGlobalTrasaction) => PromiseLike + ): Promise { + return handle(new MemoryGlobalTransaction(this.values)); + } + + private capture( + state: Map>, + compression: SaveCompression + ): ISaveRead { + const record = this.captureRecord(state, compression); + return { compression: record.compression, data: record.data }; + } + + private captureRecord( + state: Map>, + compression: SaveCompression + ): MemorySaveRecord { + const data = new Map(); + for (const [key, content] of state) { + data.set(key, content.saveState(compression)); + } + return { compression, data }; + } + + private trim(stack: ISaveRead[]): void { + if (stack.length > this.stackSize) { + stack.splice(0, stack.length - this.stackSize); + } + } +} diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index 675cec0..05b4cca 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -15,7 +15,6 @@ import { IHeroAttr, IEnemyAttr, ISaveSystem, - SaveSystem, GameEventStore, IGameEventStore, IItemStore, @@ -33,7 +32,6 @@ import { FlagSystem, IMotaDataLoader, MotaDataLoader, - loading, IReadonlyEnemy, IMapState, MapState @@ -68,11 +66,9 @@ import { TILE_WIDTH } from './shared'; import { - ItemLegacyBridge, - LegacyItemData, - LegacyTileData, - TileLegacyBridge -} from './legacy'; + createLegacyDependencies, + ILegacyLoadData +} from './legacy/dependencies'; import { ILoadProgressTotal, LoadProgressTotal } from '@motajs/loader'; import { isNil } from 'lodash-es'; import { DirectionMapper, IDirectionMapper, logger } from '@motajs/common'; @@ -83,8 +79,8 @@ export class CoreState implements ICoreState { readonly saveSystem: ISaveSystem; readonly roleFace: IRoleFaceBinder; readonly faceManager: IFaceManager; - readonly tileStore: ITileStore; - readonly itemStore: IItemStore; + readonly tileStore: ITileStore; + readonly itemStore: IItemStore; readonly mapStore: IMapStore; readonly eventStore: IGameEventStore; readonly directionMapper: IDirectionMapper; @@ -113,10 +109,12 @@ export class CoreState implements ICoreState { > = new Map(); constructor() { + const legacy = createLegacyDependencies(); + //#region L0 初始化 // 存档系统 - this.saveSystem = new SaveSystem(); + this.saveSystem = legacy.saveSystem; // 配置存档系统,一般情况下不建议动,除非你知道你在干什么 this.saveSystem.config({ autosaveLevel: SaveCompression.LowCompression, @@ -137,12 +135,17 @@ export class CoreState implements ICoreState { this.faceManager.registerById('dir8', dir8); // 图块 - const tileStore = new TileStore(); - tileStore.attachLegacyConverter(new TileLegacyBridge()); + const tileStore = new TileStore(); + if (legacy.tileConverter) { + tileStore.attachLegacyConverter(legacy.tileConverter); + } this.tileStore = tileStore; // 道具 - const itemStore = new ItemStore(); - itemStore.attachLegacyConverter(new ItemLegacyBridge(this)); + const itemStore = new ItemStore(); + const itemConverter = legacy.createItemConverter(this); + if (itemConverter) { + itemStore.attachLegacyConverter(itemConverter); + } this.itemStore = itemStore; // 地图 const mapStore = new MapStore(); @@ -173,7 +176,7 @@ export class CoreState implements ICoreState { // 怪物管理器 const comparer = new MainEnemyComparer(); - const enemyManager = new EnemyManager(new EnemyLegacyBridge()); + const enemyManager = new EnemyManager(legacy.enemyBridge); enemyManager.attachEnemyComparer(comparer); enemyManager.setAttributeDefaults('hp', 0); enemyManager.setAttributeDefaults('atk', 0); @@ -217,20 +220,9 @@ export class CoreState implements ICoreState { this.addSaveableContent('@system/flags', this.flags); this.addSaveableContent('@system/maps', this.maps); this.addSaveableContent('@system/enemy', this.enemyManager); - // 初始化存档数据库,不要动 - loading.once('coreInit', () => { - this.saveSystem.init(`@game/${core.firstData.name}`); - }); - - // 加载初始化,先使用兼容层实现 - loading.once('loaded', () => { - this.initTileStore(core.maps.blocksInfo); - this.initItemStore(core.items.items); - this.initEnemyManager(enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80); - this.initMapState( - core.floorIds, - core.floors as Record - ); + // 旧样板初始化只能经由内部边界进入,Node 路径不会注册加载回调 + legacy.registerLoading(data => { + this.initLegacyData(data); }); // 勇士顶层初始化 @@ -242,11 +234,18 @@ export class CoreState implements ICoreState { //#region 私有方法 + private initLegacyData(data: ILegacyLoadData): void { + this.initTileStore(data.tiles); + this.initItemStore(data.items); + this.initEnemyManager(data.enemies); + this.initMapState(data.floors, data.maps); + } + /** * 初始化图块存储对象 * @param data 旧样板图块定义对象 */ - private initTileStore(data: typeof core.maps.blocksInfo) { + private initTileStore(data: ILegacyLoadData['tiles']) { const entries = Object.entries(data); for (const [key, block] of entries) { this.tileStore.fromLegacy(Number(key), block); @@ -277,7 +276,7 @@ export class CoreState implements ICoreState { * 初始化道具存储对象 * @param data 旧样板道具定义对象 */ - private initItemStore(data: typeof core.items.items) { + private initItemStore(data: ILegacyLoadData['items']) { const entries = Object.entries(data); for (const [id, legacy] of entries) { const num = this.tileStore.idToNumber(id); @@ -293,7 +292,7 @@ export class CoreState implements ICoreState { * 初始化怪物管理器对象 * @param data 旧样板怪物存储对象 */ - private initEnemyManager(data: Record) { + private initEnemyManager(data: ILegacyLoadData['enemies']) { const manager = this.enemyManager; const reference = new Map>(); for (const [id, enemy] of Object.entries(structuredClone(data))) { @@ -326,8 +325,8 @@ export class CoreState implements ICoreState { } private initMapState( - floors: FloorIds[], - data: Record + floors: ILegacyLoadData['floors'], + data: ILegacyLoadData['maps'] ) { const reference = new Map>(); for (const id of floors) { @@ -436,3 +435,7 @@ export class CoreState implements ICoreState { //#endregion } + +export function createCoreState(): CoreState { + return new CoreState(); +} diff --git a/packages-user/data-state/src/enemy/calculator.ts b/packages-user/data-state/src/enemy/calculator.ts index 1c26822..7563a18 100644 --- a/packages-user/data-state/src/enemy/calculator.ts +++ b/packages-user/data-state/src/enemy/calculator.ts @@ -6,7 +6,6 @@ import { } from '@user/data-system'; import { IEnemyAttr, IHeroAttr } from '@user/data-common'; import { IVampireValue } from './special'; -import { state } from '../ins'; import { logger } from '@motajs/common'; export class MainDamageCalculator implements IDamageCalculator< @@ -30,7 +29,10 @@ export class MainDamageCalculator implements IDamageCalculator< let monHp = enemy.getAttribute('hp'); // 无敌 - if (enemy.hasSpecial(20) && core.itemCount('cross') < 1) { + if ( + enemy.hasSpecial(20) && + (handler.state.hero.items.getItemState('cross')?.count ?? 0) < 1 + ) { return { damage: Infinity, turn: 0 }; } @@ -147,7 +149,7 @@ export class MainDamageCalculator implements IDamageCalculator< // 仇恨,无法被魔防减伤 if (enemy.hasSpecial(17)) { - damage += state.flags.getFieldValueDefaults('hatred', 0); + damage += handler.state.flags.getFieldValueDefaults('hatred', 0); } return { diff --git a/packages-user/data-state/src/legacy/dependencies.ts b/packages-user/data-state/src/legacy/dependencies.ts new file mode 100644 index 0000000..fc6064b --- /dev/null +++ b/packages-user/data-state/src/legacy/dependencies.ts @@ -0,0 +1,76 @@ +import { + IItemLegacyConverter, + IHeroAttr, + IEnemyAttr, + ISaveSystem, + ITileLegacyConverter, + MemorySaveSystem, + SaveSystem +} from '@user/data-common'; +import { IEnemyLegacyBridge, loading } from '@user/data-base'; +import { IStateSystem } from '@user/data-system'; +import { EnemyLegacyBridge } from '../enemy/legacy'; +import { ItemLegacyBridge, LegacyItemData } from './item'; +import { LegacyTileData, TileLegacyBridge } from './tile'; + +export interface ILegacyLoadData { + readonly tiles: typeof core.maps.blocksInfo; + readonly items: typeof core.items.items; + readonly enemies: Record; + readonly floors: FloorIds[]; + readonly maps: Record; +} + +export interface ILegacyDependencies { + readonly saveSystem: ISaveSystem; + readonly tileConverter: ITileLegacyConverter | null; + readonly enemyBridge: IEnemyLegacyBridge; + createItemConverter( + state: IStateSystem + ): IItemLegacyConverter | null; + registerLoading(onLoaded: (data: ILegacyLoadData) => void): void; +} + +function hasLegacyHost(): boolean { + return ( + typeof main !== 'undefined' && + typeof core !== 'undefined' && + typeof window !== 'undefined' && + typeof document !== 'undefined' && + typeof indexedDB !== 'undefined' + ); +} + +export function createLegacyDependencies(): ILegacyDependencies { + if (!hasLegacyHost()) { + return { + saveSystem: new MemorySaveSystem(), + tileConverter: null, + enemyBridge: new EnemyLegacyBridge(), + createItemConverter: () => null, + registerLoading: () => {} + }; + } + + const saveSystem = new SaveSystem(); + return { + saveSystem, + tileConverter: new TileLegacyBridge(), + enemyBridge: new EnemyLegacyBridge(), + createItemConverter: state => new ItemLegacyBridge(state), + registerLoading: onLoaded => { + loading.once('coreInit', () => { + saveSystem.init(`@game/${core.firstData.name}`); + }); + loading.once('loaded', () => { + onLoaded({ + tiles: core.maps.blocksInfo, + items: core.items.items, + enemies: enemys_fcae963b_31c9_42b4_b48c_bb48d09f3f80, + floors: core.floorIds, + maps: core.floors as Record + }); + }); + } + }; +} diff --git a/packages-user/data-state/src/legacy/move.ts b/packages-user/data-state/src/legacy/move.ts index 0a0db29..d1beadc 100644 --- a/packages-user/data-state/src/legacy/move.ts +++ b/packages-user/data-state/src/legacy/move.ts @@ -3,9 +3,13 @@ import { backDir, toDir } from './utils'; import { loading } from '@user/data-base'; import type { HeroKeyMover } from '@user/client-modules'; import { sleep } from '@motajs/common'; -import { state } from '..'; import { fromDirectionString } from '@user/data-common'; +function getDataStateMover() { + if (typeof Mota === 'undefined') return null; + return Mota.require('@user/data-state').state.hero.location.mover; +} + // todo: 转身功能 interface MoveStepDir { @@ -288,7 +292,7 @@ export class HeroMover extends ObjectMoverBase { protected async onMoveStart(controller: IMoveController): Promise { this.beforeMoveSpeed = this.moveSpeed; if (!core.isReplaying() || core.status.replay.speed <= 12) { - state.hero.mover.startMove(); + getDataStateMover()?.start(); } // 这里要检查前面那一格能不能走,不能走则不触发平滑视角,以避免撞墙上视角卡住 if (!this.ignoreTerrain) { @@ -310,7 +314,6 @@ export class HeroMover extends ObjectMoverBase { protected async onMoveEnd(controller: IMoveController): Promise { this.moveSpeed = this.beforeMoveSpeed; this.onSetMoveSpeed(this.moveSpeed, controller); - await state.hero.mover.endMove(); // viewport.sync('endMove'); core.clearContinueAutomaticRoute(); core.stopAutomaticRoute(); @@ -442,22 +445,17 @@ export class HeroMover extends ObjectMoverBase { const replaying = core.isReplaying(); if (replaying) { if (core.status.replay.speed > 12) { - await state.hero.mover.endMove(); await sleep(speed); - state.hero.mover.setPosition(x, y); + getDataStateMover()?.setPos(x, y); } else { - state.hero.mover.startMove(); - await state.hero.mover.move( - fromDirectionString(moveDir), - this.moveSpeed / core.status.replay.speed - ); + const mover = getDataStateMover(); + mover?.step(fromDirectionString(moveDir)); + await mover?.start()?.onEnd; } } else { - state.hero.mover.startMove(); - await state.hero.mover.move( - fromDirectionString(moveDir), - this.moveSpeed - ); + const mover = getDataStateMover(); + mover?.step(fromDirectionString(moveDir)); + await mover?.start()?.onEnd; } } diff --git a/packages/common/src/logger.ts b/packages/common/src/logger.ts index 98dba26..c7810ff 100644 --- a/packages/common/src/logger.ts +++ b/packages/common/src/logger.ts @@ -21,8 +21,13 @@ interface LoggerCatchReturns { info: LoggerCatchInfo[]; } -let logTip: HTMLSpanElement; -if (!main.replayChecking) { +let logTip: HTMLSpanElement | undefined; +const hasBrowserHost = + typeof document !== 'undefined' && typeof main !== 'undefined'; +const isReplayChecking = () => + typeof main !== 'undefined' && main.replayChecking; + +if (hasBrowserHost && !isReplayChecking()) { const tip = document.createElement('span'); logTip = tip; tip.style.position = 'fixed'; @@ -40,7 +45,7 @@ if (!main.replayChecking) { } const hideTipText = debounce(() => { - if (main.replayChecking) return; + if (isReplayChecking() || !logTip) return; logTip.style.display = 'none'; }, 5000); @@ -125,14 +130,16 @@ export class Logger { }); } if (this.level <= LogLevel.ERROR && this.enabled) { - if (!main.replayChecking) { + if (!isReplayChecking() && logTip) { logTip.style.color = 'lightcoral'; logTip.style.display = 'block'; logTip.textContent = `Error thrown, please check in console.`; hideTipText(); } const n = Math.floor(code / 50) + 1; - const url = `${location.origin}/_docs/logger/error/error${n}.html#error-code-${code}`; + const origin = + typeof location === 'undefined' ? '' : location.origin; + const url = `${origin}/_docs/logger/error/error${n}.html#error-code-${code}`; console.error(`[ERROR Code ${code}] ${text} See ${url}`); } } @@ -158,14 +165,16 @@ export class Logger { }); } if (this.level <= LogLevel.WARNING && this.enabled) { - if (!main.replayChecking) { + if (!isReplayChecking() && logTip) { logTip.style.color = 'gold'; logTip.style.display = 'block'; logTip.textContent = `Warning thrown, please check in console.`; hideTipText(); } const n = Math.floor(code / 50) + 1; - const url = `${location.origin}/_docs/logger/warn/warn${n}.html#warn-code-${code}`; + const origin = + typeof location === 'undefined' ? '' : location.origin; + const url = `${origin}/_docs/logger/warn/warn${n}.html#warn-code-${code}`; console.warn(`[WARNING Code ${code}] ${text} See ${url}`); } }