diff --git a/packages-user/data-common/src/replay/sandbox.test.ts b/packages-user/data-common/src/replay/sandbox.test.ts index ab1a6a8..451d227 100644 --- a/packages-user/data-common/src/replay/sandbox.test.ts +++ b/packages-user/data-common/src/replay/sandbox.test.ts @@ -27,7 +27,7 @@ function createCommand( execute: (step: IReplayStepHandler) => Promise, notExecuted?: () => Promise ): IReplayCommand { - return notExecuted ? { execute, notExecuted } : { execute }; + return notExecuted ? { execute, finalize: notExecuted } : { execute }; } // 用录像系统构造一个可手动驱动的录像沙箱 diff --git a/packages-user/data-common/src/replay/sandbox.ts b/packages-user/data-common/src/replay/sandbox.ts index 243a2f8..0f97bbc 100644 --- a/packages-user/data-common/src/replay/sandbox.ts +++ b/packages-user/data-common/src/replay/sandbox.ts @@ -6,11 +6,17 @@ import { } from '@motajs/common'; import { IReplayArray, + IReplayCommand, + IReplayPassiveHandler, IReplayReadStream, IReplaySandbox, IReplaySandboxHooks, - IReplaySystem + IReplayStepHandler, + IReplaySystem, + ReplayCommandResult, + ReplayCommandType } from './types'; +import { isNil } from 'lodash-es'; export class ReplaySandbox extends Hookable @@ -25,9 +31,15 @@ export class ReplaySandbox private needPause: boolean = false; /** 当前录像是否播放完毕 */ private ending: boolean = false; + /** 暂停 `Promise` 的 `resolve` 函数 */ private pauseResolve: () => void = () => {}; + /** 被动录像步兑现函数 */ + private passiveResolve: (status: ReplayCommandResult) => void = () => {}; + /** 下一步被动录像步的 `StepHandler`,注意不是 `PassiveHandler` */ + private passiveHandler: IReplayStepHandler | null = null; + /** 录像的流式读取器 */ private reader: Readonly; @@ -49,6 +61,20 @@ export class ReplaySandbox return new HookController(this, hook); } + getPassive(): IReplayPassiveHandler | null { + if (!this.passiveHandler) { + logger.error(73); + return null; + } + const handler: IReplayPassiveHandler = { + step: this.passiveHandler, + next: status => { + this.passiveResolve(status); + } + }; + return handler; + } + setSpeed(speed: number): void { this.speed = speed; this.forEachHook(hook => hook.onSpeedSet?.(speed)); @@ -121,19 +147,63 @@ export class ReplaySandbox /** * 触发上一步指令的连续步骤后处理 */ - private async finalizeLast(): Promise { - if (this.last === -1) return true; + private async finalizeLast(): Promise { + if (this.last === -1) return ReplayCommandResult.Success; const last = this.system.getCommand(this.last); if (!last) { logger.warn(157, this.last.toString()); - return false; + return ReplayCommandResult.Failed; } - const success = (await last.notExecuted?.()) ?? true; - if (!success) { - logger.warn(175, this.last.toString()); - return false; + return last.finalize?.() ?? ReplayCommandResult.Success; + } + + /** + * 执行下一个录像步 + * @param command 录像步对象 + * @param next 录像步参数 + */ + private async executeCommand( + command: IReplayCommand, + next: IReplayStepHandler + ): Promise { + if (command.type === ReplayCommandType.Passive) { + // 被动录像步 + const { promise, resolve } = + Promise.withResolvers(); + this.passiveResolve = resolve; + const status = await promise; + if (isNil(status)) { + logger.error(71); + return ReplayCommandResult.Failed; + } else { + return status; + } + } else { + // 主动录像步 + const status = command.execute(next); + return status; + } + } + + /** + * 检查录像执行状态,进行适当的控制台输出 + * @param status 录像执行状态 + * @param method 录像步执行方法,可填 `active` `passive` `finalize` + */ + private checkReplayStatus( + status: ReplayCommandResult, + method: string + ): boolean { + if (status === ReplayCommandResult.Success) return true; + else if (status === ReplayCommandResult.Failed) { + logger.error(72, method); + return false; + } else { + logger.log( + `Ignored replay error with a Ignored status returned by ${method} command.` + ); + return true; } - return true; } async step(): Promise { @@ -143,44 +213,40 @@ export class ReplaySandbox return false; } const next = this.reader.read(); - if (!next) { - // notExecuted - const ne = await this.finalizeLast(); - if (ne) { - this.last = -1; - this.ending = true; - } - return false; - } - // notExecuted - if (next.command !== this.last) { + // finalize 执行 + if (!next || next.command !== this.last) { const ne = await this.finalizeLast(); - if (!ne) { + const success = this.checkReplayStatus(ne, 'finalize'); + if (success) { + if (!next) { + this.last = -1; + this.ending = true; + return false; + } + } else { return false; } } this.last = next.command; - // execute + // 获取指令本身 const command = this.system.getCommand(next.command); if (!command) { logger.warn(157, next.command.toString()); return false; } - const success = await command.execute(next); - if (!success) { - logger.warn( - 158, - next.command.toString(), - JSON.stringify(next.params) - ); + + const status = await this.executeCommand(command, next); + const success = this.checkReplayStatus( + status, + command.type === ReplayCommandType.Active ? 'execute' : 'passive' + ); + if (success) { + await Promise.all(this.forEachHook(hook => hook.onStep?.(next))); + return true; + } else { return false; } - - // hook - await Promise.all(this.forEachHook(hook => hook.onStep?.(next))); - - return true; } } diff --git a/packages-user/data-common/src/replay/types.ts b/packages-user/data-common/src/replay/types.ts index 057f94f..de42a18 100644 --- a/packages-user/data-common/src/replay/types.ts +++ b/packages-user/data-common/src/replay/types.ts @@ -21,6 +21,22 @@ export const enum ReplayCode { Unequip } +export const enum ReplayCommandType { + /** 主动录像,即录像指令本身会执行某些操作 */ + Active, + /** 被动录像,即外部通过 `IReplaySandbox.getPassive` 获取本步录像的参数,由外部进行后续操作 */ + Passive +} + +export const enum ReplayCommandResult { + /** 录像步执行成功 */ + Success, + /** 录像步执行失败 */ + Failed, + /** 录像步执行失败,但可以被忽略,继续执行后续录像。会在控制台输出信息。 */ + Ignored +} + export type ReplayParamValue = number | string | boolean | bigint; export interface IReplayStepHandler { @@ -32,19 +48,33 @@ export interface IReplayStepHandler { readonly index: number; } +export interface IReplayPassiveHandler { + /** 录像步信息 */ + readonly step: IReplayStepHandler; + + /** + * 本被动录像步执行完毕,进入下一个录像步 + * @param status 被动录像步执行情况 + */ + next(status: ReplayCommandResult): void; +} + export interface IReplayCommand { + /** 录像类型,是主动录像还是被动录像 */ + readonly type: ReplayCommandType; + /** * 执行当前录像步对应的操作逻辑 * @param step 当前录像步信息 - * @returns 当此录像步执行完毕时兑现的 `Promise`,兑现值表示此录像步是否播放成功 + * @returns 当此录像步执行完毕时兑现的 `Promise`,兑现值表示此录像步的执行情况 */ - execute(step: IReplayStepHandler): Promise; + execute(step: IReplayStepHandler): Promise; /** * 若上一步是当前录像指令,而下一步不是,触发此函数,一般用于连续步骤的后处理 * @returns 当此后处理执行完毕时兑现的 `Promise`,兑现值表示是否执行成功 */ - notExecuted?(): Promise; + finalize?(): Promise; } export interface IReplaySandboxHooks extends IHookBase { @@ -91,6 +121,11 @@ export interface IReplaySandbox extends IHookable { /** 当前是否正在播放,当调用 `play` 后,调用 `stop` 前,此值会是 `true` */ readonly playing: boolean; + /** + * 获取被动录像参数。如果下一步是被动录像,那么获取其参数,如果不是,返回 `null` + */ + getPassive(): IReplayPassiveHandler | null; + /** * 设置播放倍率 * @param speed 播放倍率 diff --git a/packages-user/data-state/src/replay/commands.test.ts b/packages-user/data-state/src/replay/commands.test.ts index c476da5..57f0129 100644 --- a/packages-user/data-state/src/replay/commands.test.ts +++ b/packages-user/data-state/src/replay/commands.test.ts @@ -122,7 +122,7 @@ describe('replay commands', () => { expect(start).not.toHaveBeenCalled(); let result: boolean | undefined; - const pending = command.notExecuted().then(value => { + const pending = command.finalize().then(value => { result = value; }); await Promise.resolve(); @@ -147,7 +147,7 @@ describe('replay commands', () => { (mover as unknown as { moving: boolean }).moving = false; vi.spyOn(mover, 'start').mockReturnValueOnce(null); - await expect(command.notExecuted()).resolves.toBe(false); + await expect(command.finalize()).resolves.toBe(false); expect(error).toHaveBeenCalledWith(2004); error.mockRestore(); }); diff --git a/packages-user/data-state/src/replay/commands.ts b/packages-user/data-state/src/replay/commands.ts index 00764c1..aa7f767 100644 --- a/packages-user/data-state/src/replay/commands.ts +++ b/packages-user/data-state/src/replay/commands.ts @@ -2,7 +2,9 @@ import { FaceDirection, IReplayStepHandler, IReplayCommand, - ReplayParamValue + ReplayParamValue, + ReplayCommandResult, + ReplayCommandType } from '@user/data-common'; import { IStateSystem } from '@user/data-system'; import { logger } from '@motajs/common'; @@ -18,6 +20,8 @@ export abstract class BaseReplayCommand implements IReplayCommand { /** 预期的参数类型列表 */ protected abstract readonly paramTypes: readonly string[]; + abstract readonly type: ReplayCommandType; + constructor(state: IStateSystem) { this.state = state; } @@ -37,16 +41,16 @@ export abstract class BaseReplayCommand implements IReplayCommand { const e = expect.length.toString(); const p = parameter.length.toString(); logger.error(2001, command, e, p); - return false; + return ReplayCommandResult.Failed; } return parameter.every((v, i) => { const type = typeof v; if (type === expect[i]) { - return true; + return ReplayCommandResult.Success; } else { logger.error(2002, command, i.toString(), expect[i], type); - return false; + return ReplayCommandResult.Failed; } }); } @@ -55,11 +59,13 @@ export abstract class BaseReplayCommand implements IReplayCommand { * 执行录像步,已进行必要的参数校验,内部仅包含录像逻辑,不必包含参数校验 * @param step 当前录像步信息 */ - abstract wrappedExecute(step: IReplayStepHandler): Promise; + abstract wrappedExecute( + step: IReplayStepHandler + ): Promise; - execute(step: IReplayStepHandler): Promise { + execute(step: IReplayStepHandler): Promise { if (!this.assertParameter(this.name, step.params, this.paramTypes)) { - return Promise.resolve(false); + return Promise.resolve(ReplayCommandResult.Failed); } else { return this.wrappedExecute(step); } @@ -71,6 +77,8 @@ export abstract class BaseReplayCommand implements IReplayCommand { //#region 移动指令 export class ReplayMove extends BaseReplayCommand implements IReplayCommand { + readonly type: ReplayCommandType = ReplayCommandType.Active; + protected readonly name: string = 'move'; protected readonly paramTypes: readonly string[] = []; @@ -81,27 +89,27 @@ export class ReplayMove extends BaseReplayCommand implements IReplayCommand { super(state); } - async wrappedExecute(): Promise { + async wrappedExecute(): Promise { // Parameter: [] const mover = this.state.hero.location.mover; if (mover.moving) { logger.error(2003); - return false; + return ReplayCommandResult.Failed; } mover.step(this.direction); - return true; + return ReplayCommandResult.Success; } - async notExecuted(): Promise { + async finalize(): Promise { const mover = this.state.hero.location.mover; const controller = mover.start(); if (!controller) { logger.error(2004); - return false; + return ReplayCommandResult.Failed; } await controller.onEnd; - return true; + return ReplayCommandResult.Success; } } @@ -113,19 +121,23 @@ export class ReplayTeleport extends BaseReplayCommand implements IReplayCommand { + readonly type: ReplayCommandType = ReplayCommandType.Active; + protected readonly name: string = 'teleport'; protected readonly paramTypes: readonly string[] = ['number', 'number']; - async wrappedExecute(step: IReplayStepHandler): Promise { + async wrappedExecute( + step: IReplayStepHandler + ): Promise { // Parameter: [int16 x, int16 y] const [x, y] = step.params as [number, number]; const result = this.state.pathfinding.teleportTo({ x, y }); if (!result) { logger.error(2005, x.toString(), y.toString()); - return false; + return ReplayCommandResult.Failed; } await result.controller.onEnd; - return true; + return ReplayCommandResult.Success; } } @@ -134,17 +146,21 @@ export class ReplayTeleport //#region 使用物品指令 export class ReplayUseItem extends BaseReplayCommand implements IReplayCommand { + readonly type: ReplayCommandType = ReplayCommandType.Active; + protected readonly name: string = 'use-item'; protected readonly paramTypes: readonly string[] = ['number']; - async wrappedExecute(step: IReplayStepHandler): Promise { + async wrappedExecute( + step: IReplayStepHandler + ): Promise { // Parameter: [int16 item] const item = step.params[0] as number; if (!this.state.hero.items.useItem(item)) { logger.error(2006, item.toString()); - return false; + return ReplayCommandResult.Failed; } - return true; + return ReplayCommandResult.Success; } } @@ -153,14 +169,18 @@ export class ReplayUseItem extends BaseReplayCommand implements IReplayCommand { //#region 装备指令 export class ReplayEquip extends BaseReplayCommand implements IReplayCommand { + readonly type: ReplayCommandType = ReplayCommandType.Active; + protected readonly name: string = 'equip'; protected readonly paramTypes: readonly string[] = [ 'number', 'number', - 'boolean' + 'ReplayCommandResult' ]; - async wrappedExecute(step: IReplayStepHandler): Promise { + async wrappedExecute( + step: IReplayStepHandler + ): Promise { // Parameter: [int16 uid, int8 slot, bool autoUnload] const [uid, slot, autoUnload] = step.params as [ number, @@ -171,9 +191,9 @@ export class ReplayEquip extends BaseReplayCommand implements IReplayCommand { equipment.equip(uid, slot, autoUnload); if (equipment.getEquipped(slot) !== uid) { logger.error(2007, uid.toString(), slot.toString()); - return false; + return ReplayCommandResult.Failed; } - return true; + return ReplayCommandResult.Success; } } @@ -182,19 +202,23 @@ export class ReplayEquip extends BaseReplayCommand implements IReplayCommand { //#region 卸下装备指令 export class ReplayUnequip extends BaseReplayCommand implements IReplayCommand { + readonly type: ReplayCommandType = ReplayCommandType.Active; + protected readonly name: string = 'unequip'; protected readonly paramTypes: readonly string[] = ['number']; - async wrappedExecute(step: IReplayStepHandler): Promise { + async wrappedExecute( + step: IReplayStepHandler + ): Promise { // Parameter: [int8 slot] const slot = step.params[0] as number; const equipment = this.state.hero.equip; equipment.unequip(slot); if (equipment.getEquipped(slot) !== void 0) { logger.error(2008, slot.toString()); - return false; + return ReplayCommandResult.Failed; } - return true; + return ReplayCommandResult.Success; } } diff --git a/packages-user/data-state/test/replayPlayback.test.ts b/packages-user/data-state/test/replayPlayback.test.ts index 9eb263c..67f34bb 100644 --- a/packages-user/data-state/test/replayPlayback.test.ts +++ b/packages-user/data-state/test/replayPlayback.test.ts @@ -417,7 +417,7 @@ describe('replay playback error codes 2001-2008', () => { (mover as unknown as { moving: boolean }).moving = false; vi.spyOn(mover, 'start').mockReturnValueOnce(null); - await expect(move.notExecuted()).resolves.toBe(false); + await expect(move.finalize()).resolves.toBe(false); expect(error).toHaveBeenCalledWith(2004); }); diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index c2da690..e57b548 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -70,6 +70,9 @@ "68": "Extra config identifier '$1' has conflicts. More than one extra config use this identifier, and the final load result is unpredictable since network decides which one to be loaded fast.", "69": "$1 core config not loaded for the next loading phase.", "70": "Cannot $1 replay param type $2 since they are not defined in this engine version.", + "71": "Passive replay command needs a ReplayCommandResult status transfering by IReplayPassiveHandler.next, but system got a undefined.", + "72": "Replay error when replaying $1 replay step, detailed error message will show together with this error info.", + "73": "No passive replay needed currently, but a 'getPassive' call detected, check if there's an issue on replay array or your replay logic.", "2001": "Replay($1): Replay parameter count mismatch: expected $2, got $3.", "2002": "Replay($1): Replay parameter type mismatch: index $2, expected $3, got $4.", "2003": "Replay(move): Expected hero to be stopped before execute moving step.",