From cee843981f3ab9a7ad6aa7fc13601e0743846a8d Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Mon, 14 Sep 2026 18:09:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BD=95=E5=83=8F=E8=AE=B0=E5=BD=95=20?= =?UTF-8?q?&=20CoreState=20=E5=AD=98=E5=82=A8=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gsd/dispatch-isolation-sentinel.json | 2 +- packages-user/data-base/src/hero/equipment.ts | 11 +++++- packages-user/data-base/src/hero/items.ts | 14 ++++++- packages-user/data-base/src/hero/mover.ts | 26 ++++++++++++- packages-user/data-base/src/hero/types.ts | 3 +- packages-user/data-common/src/replay/types.ts | 20 ++++++++++ packages-user/data-common/src/types.ts | 3 ++ packages-user/data-state/src/core.ts | 37 +++++++++++++++++-- .../data-state/src/replay/commands.test.ts | 16 +++++++- packages-user/data-state/src/replay/index.ts | 1 - packages-user/data-state/src/replay/types.ts | 31 ---------------- packages-user/data-state/src/types.ts | 3 +- .../data-state/test/dataClosure.test.ts | 16 +++++++- packages-user/data-system/src/path/system.ts | 17 ++++++++- packages-user/data-system/src/types.ts | 4 +- packages/common/src/logger.json | 3 ++ 16 files changed, 156 insertions(+), 51 deletions(-) delete mode 100644 packages-user/data-state/src/replay/types.ts diff --git a/.gsd/dispatch-isolation-sentinel.json b/.gsd/dispatch-isolation-sentinel.json index 562138d..2989487 100644 --- a/.gsd/dispatch-isolation-sentinel.json +++ b/.gsd/dispatch-isolation-sentinel.json @@ -1 +1 @@ -{"isolation":"harness-worktree","harness_flag":"isolation=\"worktree\"","phase":"03","plan":null,"written_at":1789138005150} \ No newline at end of file +{"isolation":"none","harness_flag":null,"phase":"06","plan":null,"written_at":1789346124877} \ No newline at end of file diff --git a/packages-user/data-base/src/hero/equipment.ts b/packages-user/data-base/src/hero/equipment.ts index e9bda0f..81a6c2c 100644 --- a/packages-user/data-base/src/hero/equipment.ts +++ b/packages-user/data-base/src/hero/equipment.ts @@ -1,5 +1,5 @@ import { isNil } from 'lodash-es'; -import { IDataCommon } from '@user/data-common'; +import { IDataCommon, ReplayCommandCode } from '@user/data-common'; import { EquipStatus, IEquipmentState, @@ -127,6 +127,11 @@ export class HeroEquipment implements IHeroEquipment { return void 0; } + // 记录录像 + // TODO: 可以考虑把禁用录像记录放到录像系统里面,避免每个可能由代码触发的行动都写一个 noRoute 参数 + const replay = this.state.replaySystem; + replay.route.add(ReplayCommandCode.Equip, [uid]); + if (typeof slot === 'number') { // 数字槽位,直接进行指定替换 const curr = this.equips.get(slot); @@ -176,6 +181,10 @@ export class HeroEquipment implements IHeroEquipment { return void 0; } + // 记录录像 + const replay = this.state.replaySystem; + replay.route.add(ReplayCommandCode.Unequip, [slot]); + this.unloadEquipEffect(state); this.equips.delete(slot); diff --git a/packages-user/data-base/src/hero/items.ts b/packages-user/data-base/src/hero/items.ts index e174508..7c47cb3 100644 --- a/packages-user/data-base/src/hero/items.ts +++ b/packages-user/data-base/src/hero/items.ts @@ -1,5 +1,10 @@ import { isNil } from 'lodash-es'; -import { IDataCommon, ItemCategory, SaveCompression } from '@user/data-common'; +import { + IDataCommon, + ItemCategory, + ReplayCommandCode, + SaveCompression +} from '@user/data-common'; import { HeroEquipsStore } from './equipStore'; import { IHeroItems, @@ -107,7 +112,7 @@ export class HeroItems implements IHeroItems { this.addItem(item, 1); } - useItem(item: number | string): boolean { + useItem(item: number | string, noRoute: boolean = false): boolean { const state = this.internalGetItemState(item); if (!state) return false; @@ -121,6 +126,11 @@ export class HeroItems implements IHeroItems { if (!raw.effect.canUse(raw)) return false; + if (!noRoute) { + const replay = this.state.replaySystem; + replay.route.add(ReplayCommandCode.UseItem, [raw.num]); + } + raw.effect.useEffect(raw); if (raw.category === ItemCategory.Consumable) { diff --git a/packages-user/data-base/src/hero/mover.ts b/packages-user/data-base/src/hero/mover.ts index ef4910b..b6227bd 100644 --- a/packages-user/data-base/src/hero/mover.ts +++ b/packages-user/data-base/src/hero/mover.ts @@ -6,7 +6,8 @@ import { ObjectMoveStep, ObjectMover, ObjectMoveType, - IDataCommon + IDataCommon, + ReplayCommandCode } from '@user/data-common'; import { HeroMoveCode, @@ -177,6 +178,29 @@ export class HeroMover type === ObjectMoveType.DirFace || type === ObjectMoveType.Special ) { + // 录像记录 + if (!this.noRoute) { + const replay = this.state.replaySystem; + switch (handler.direction) { + case FaceDirection.Up: + replay.route.add(ReplayCommandCode.Up, []); + break; + case FaceDirection.Right: + replay.route.add(ReplayCommandCode.Right, []); + break; + case FaceDirection.Left: + replay.route.add(ReplayCommandCode.Left, []); + break; + case FaceDirection.Down: + replay.route.add(ReplayCommandCode.Down, []); + break; + default: + logger.warn(176); + break; + } + } + + // 移动判断 if (this.ignoreTerrain) { return HeroMoveCode.Step; } else { diff --git a/packages-user/data-base/src/hero/types.ts b/packages-user/data-base/src/hero/types.ts index b19c2e2..ff14772 100644 --- a/packages-user/data-base/src/hero/types.ts +++ b/packages-user/data-base/src/hero/types.ts @@ -562,9 +562,10 @@ export interface IHeroItems /** * 使用道具,仅对 Constant 与 Consumable 类型生效。Consumable 类型使用后数量减一。 * @param item 道具图块数字或字符串 id + * @param noRoute 是否不计入录像 * @returns 道具是否使用成功 */ - useItem(item: number | string): boolean; + useItem(item: number | string, noRoute?: boolean): boolean; /** * 获取指定道具的持有数量 diff --git a/packages-user/data-common/src/replay/types.ts b/packages-user/data-common/src/replay/types.ts index 38247d3..eed70e6 100644 --- a/packages-user/data-common/src/replay/types.ts +++ b/packages-user/data-common/src/replay/types.ts @@ -1,5 +1,25 @@ import { IHookable, IHookBase } from '@motajs/common'; +/** 录像指令码,数值属于录像格式的一部分 */ +export const enum ReplayCommandCode { + /** 向上移动一步 */ + Up, + /** 向右移动一步 */ + Right, + /** 向下移动一步 */ + Down, + /** 向左移动一步 */ + Left, + /** 自动寻路瞬移至目标点 */ + Teleport, + /** 使用道具 */ + UseItem, + /** 装备物品 */ + Equip, + /** 卸下装备 */ + Unequip +} + export type ReplayParamValue = number | string | boolean | bigint; export interface IReplayStepHandler { diff --git a/packages-user/data-common/src/types.ts b/packages-user/data-common/src/types.ts index 6d50297..0fb4afa 100644 --- a/packages-user/data-common/src/types.ts +++ b/packages-user/data-common/src/types.ts @@ -2,6 +2,7 @@ import { IDirectionMapper, ITileLocator } from '@motajs/common'; import { IFaceManager, IRoleFaceBinder } from './common'; import { IGameEventStore, IItemStore, IMapStore, ITileStore } from './store'; import { ISaveSystem } from './save'; +import { IReplaySystem } from './replay'; export interface IEnemyAttr { /** 怪物生命值 */ @@ -60,6 +61,8 @@ export interface IDataCommon { readonly directionMapper: IDirectionMapper; /** 存档系统 */ readonly saveSystem: ISaveSystem; + /** 录像系统 */ + readonly replaySystem: IReplaySystem; } export interface IDataCommonExtended { diff --git a/packages-user/data-state/src/core.ts b/packages-user/data-state/src/core.ts index 6ce3141..0f4340b 100644 --- a/packages-user/data-state/src/core.ts +++ b/packages-user/data-state/src/core.ts @@ -24,7 +24,8 @@ import { IMapStore, MapStore, IReplaySystem, - ReplaySystem + ReplaySystem, + ReplayCommandCode } from '@user/data-common'; import { EnemyManager, @@ -83,7 +84,6 @@ import { isNil } from 'lodash-es'; import { DefaultHeroMoveTopImpl } from './hero'; import { createEventRegistrations } from './event/registrations'; import { - ReplayCommandCode, ReplayEquipCommand, ReplayMoveCommand, ReplayTeleportCommand, @@ -101,6 +101,7 @@ export class CoreState implements ICoreState { readonly mapStore: IMapStore; readonly eventStore: IGameEventStore; readonly directionMapper: IDirectionMapper; + readonly replaySystem: IReplaySystem; // Layer 1 数据层,所有可存档内容都在这,一般用于数据存储 readonly maps: IMapState; @@ -112,7 +113,6 @@ export class CoreState implements ICoreState { readonly enemyContext: IEnemyContext; readonly eventSystem: IGameEventSystem; readonly pathfinding: IPathfindingSystem; - readonly replaySystem: IReplaySystem; // Layer 3 用户层,也就是最顶层的内容,一般仅用于初始化以及仅供渲染端调用的顶层模块 readonly loadProgress: ILoadProgressTotal; @@ -505,6 +505,37 @@ export class CoreState implements ICoreState { } } + saveState(compression: SaveCompression): ReadonlyMap { + const result = new Map(); + for (const [key, value] of this.saveables) { + const content = value.saveState(compression); + result.set(key, content); + } + return result; + } + + loadState( + state: ReadonlyMap, + compression: SaveCompression + ): void { + for (const [key, value] of this.saveables) { + // 使用 has 判断是否在映射中,而非值的非空判断,因为空值也有可能是存档的一部分 + if (!state.has(key)) { + logger.warn(177, key); + continue; + } + const data = state.get(key); + value.loadState(data, compression); + } + const loaded = new Set(state.keys()); + const total = new Set(this.saveables.keys()); + const remain = total.difference(loaded); + if (remain.size > 0) { + const ids = [...remain].join(' | '); + logger.warn(178, ids); + } + } + //#endregion } diff --git a/packages-user/data-state/src/replay/commands.test.ts b/packages-user/data-state/src/replay/commands.test.ts index 9d4e707..2af3970 100644 --- a/packages-user/data-state/src/replay/commands.test.ts +++ b/packages-user/data-state/src/replay/commands.test.ts @@ -7,7 +7,8 @@ import { beginReplaySafetyCollection, endReplaySafetyCollection, logReplaySafetyDetail, - shouldReplay + shouldReplay, + ReplayCommandCode } from '@user/data-common'; import { readFileSync } from 'node:fs'; import { describe, expect, it, vi } from 'vitest'; @@ -20,7 +21,18 @@ import { ReplayUnequipCommand, ReplayUseItemCommand } from './commands'; -import { ReplayCommandCode, REPLAY_COMMAND_ORDER } from './types'; + +/** 供测试读取的稳定指令码顺序 */ +export const REPLAY_COMMAND_ORDER: readonly ReplayCommandCode[] = [ + ReplayCommandCode.Up, + ReplayCommandCode.Right, + ReplayCommandCode.Down, + ReplayCommandCode.Left, + ReplayCommandCode.Teleport, + ReplayCommandCode.UseItem, + ReplayCommandCode.Equip, + ReplayCommandCode.Unequip +]; function step( command: number, diff --git a/packages-user/data-state/src/replay/index.ts b/packages-user/data-state/src/replay/index.ts index b9d7505..30272ff 100644 --- a/packages-user/data-state/src/replay/index.ts +++ b/packages-user/data-state/src/replay/index.ts @@ -1,2 +1 @@ export * from './commands'; -export * from './types'; diff --git a/packages-user/data-state/src/replay/types.ts b/packages-user/data-state/src/replay/types.ts deleted file mode 100644 index 2288643..0000000 --- a/packages-user/data-state/src/replay/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** 顶层拥有的稳定录像指令码,数值属于录像格式的一部分 */ -export const enum ReplayCommandCode { - /** 向上移动一步 */ - Up = 0, - /** 向右移动一步 */ - Right = 1, - /** 向下移动一步 */ - Down = 2, - /** 向左移动一步 */ - Left = 3, - /** 自动寻路至目标点 */ - Teleport = 4, - /** 使用道具 */ - UseItem = 5, - /** 装备物品 */ - Equip = 6, - /** 卸下装备 */ - Unequip = 7 -} - -/** 供测试读取的稳定指令码顺序 */ -export const REPLAY_COMMAND_ORDER: readonly ReplayCommandCode[] = [ - ReplayCommandCode.Up, - ReplayCommandCode.Right, - ReplayCommandCode.Down, - ReplayCommandCode.Left, - ReplayCommandCode.Teleport, - ReplayCommandCode.UseItem, - ReplayCommandCode.Equip, - ReplayCommandCode.Unequip -]; diff --git a/packages-user/data-state/src/types.ts b/packages-user/data-state/src/types.ts index 515f89a..84329a1 100644 --- a/packages-user/data-state/src/types.ts +++ b/packages-user/data-state/src/types.ts @@ -12,7 +12,8 @@ export interface ISaveableExecutor { afterLoad(data: T, state: IStateBase): void; } -export interface ICoreState extends IStateSystem { +export interface ICoreState + extends IStateSystem, ISaveableContent> { /** 加载进度对象 */ readonly loadProgress: ILoadProgressTotal; /** 数据端加载对象 */ diff --git a/packages-user/data-state/test/dataClosure.test.ts b/packages-user/data-state/test/dataClosure.test.ts index fdc1c5e..adcf13f 100644 --- a/packages-user/data-state/test/dataClosure.test.ts +++ b/packages-user/data-state/test/dataClosure.test.ts @@ -4,15 +4,27 @@ import { IReplaySandbox, IHeroAttr, ReplaySystem, - SaveCompression + SaveCompression, + ReplayCommandCode } from '@user/data-common'; import { IEnemy, IReadonlyHeroAttribute } from '@user/data-base'; import { IReadonlyEnemyHandler } from '@user/data-system'; import { createCoreState, CoreState } from '../src/core'; import { MainDamageCalculator } from '../src/enemy/calculator'; -import { REPLAY_COMMAND_ORDER, ReplayCommandCode } from '../src/replay/types'; import { createClosedLoopFixture } from './fixtures/closed-loop'; +/** 供测试读取的稳定指令码顺序 */ +export const REPLAY_COMMAND_ORDER: readonly ReplayCommandCode[] = [ + ReplayCommandCode.Up, + ReplayCommandCode.Right, + ReplayCommandCode.Down, + ReplayCommandCode.Left, + ReplayCommandCode.Teleport, + ReplayCommandCode.UseItem, + ReplayCommandCode.Equip, + ReplayCommandCode.Unequip +]; + vi.hoisted(() => { Map.prototype.getOrInsert ??= function ( this: Map, diff --git a/packages-user/data-system/src/path/system.ts b/packages-user/data-system/src/path/system.ts index 4bc5c5e..6ae5556 100644 --- a/packages-user/data-system/src/path/system.ts +++ b/packages-user/data-system/src/path/system.ts @@ -1,6 +1,10 @@ import { ITileLocator, logger } from '@motajs/common'; -import { IObjectMovable, IObjectMover } from '@user/data-common'; -import { IStateBase } from '@user/data-base'; +import { + IObjectMovable, + IObjectMover, + ReplayCommandCode +} from '@user/data-common'; +import { HeroMover, IStateBase } from '@user/data-base'; import { isNil } from 'lodash-es'; import { PathfindingFinder } from './finder'; import { @@ -95,6 +99,15 @@ export class PathfindingSystem implements IPathfindingSystem { if (!this.policy || this.policy(path)) { return this.startMove(path, false); } else { + // 录像记录 + // TODO:后续需要调整设计方式,最好不用 instanceof + if (this.mover instanceof HeroMover) { + const replay = this.state.replaySystem; + replay.route.add(ReplayCommandCode.Teleport, [ + target.x, + target.y + ]); + } return this.startMove(path, true); } } diff --git a/packages-user/data-system/src/types.ts b/packages-user/data-system/src/types.ts index fc2d678..4e1ab12 100644 --- a/packages-user/data-system/src/types.ts +++ b/packages-user/data-system/src/types.ts @@ -1,6 +1,6 @@ import { IStateBase } from '@user/data-base'; import { IEnemyContext } from './combat'; -import { IEnemyAttr, IHeroAttr, IReplaySystem } from '@user/data-common'; +import { IEnemyAttr, IHeroAttr } from '@user/data-common'; import { IGameEventSystem } from './event'; import { IPathfindingSystem } from './path'; @@ -11,8 +11,6 @@ export interface IStateSystem extends IStateBase { readonly eventSystem: IGameEventSystem; /** 已绑定勇士移动器的寻路系统 */ readonly pathfinding: IPathfindingSystem; - /** 当前 CoreState 独立拥有的录像系统 */ - readonly replaySystem: IReplaySystem; } export interface IStateSystemExtended { diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index f2c828b..73cb4dc 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -250,6 +250,9 @@ "173": "Pathfinding input is invalid or a required binding (map state, map layer) is missing. An empty result will be returned.", "174": "Pathfinding cost function returned a negative or NaN value. Default cost 1 will be used instead.", "175": "Replay error: Replay not executed execution failed. Command code: $1.", + "176": "Cannot add move step to replay system since only 'up,right,down,left' directions are acceptable.", + "177": "Save data for saveable content $1 is needed, but there's no data in save data. Maybe a version issue for it? System will try to load other contents to make game run normally.", + "178": "Save data with keys of '$1' are saved but not be loaded, is there some issue for it?", "1001": "Event(setBlock): Unknown tile '$1' when setting block." } }