From 75a39ad5735da0cf52288021edf8ebe7972c548a Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Sun, 4 Oct 2026 15:37:26 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E8=A2=AB=E5=8A=A8=E5=BD=95?= =?UTF-8?q?=E5=83=8F=E6=AD=A5=E5=BA=94=E5=BD=93=E5=9C=A8=E4=B8=BB=E5=8A=A8?= =?UTF-8?q?=E5=BD=95=E5=83=8F=E6=AD=A5=E6=89=A7=E8=A1=8C=E6=9C=9F=E9=97=B4?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages-user/data-common/src/replay/array.ts | 4 - .../data-common/src/replay/sandbox.ts | 307 ++++++++++-------- packages-user/data-common/src/replay/types.ts | 10 +- packages/common/src/logger.json | 1 + 4 files changed, 186 insertions(+), 136 deletions(-) diff --git a/packages-user/data-common/src/replay/array.ts b/packages-user/data-common/src/replay/array.ts index 1896d3b..b7bd885 100644 --- a/packages-user/data-common/src/replay/array.ts +++ b/packages-user/data-common/src/replay/array.ts @@ -24,10 +24,6 @@ interface INormalizedParam { * - 9: string,带 int32 长度前缀 * - 10 ~ 31: 留空,以备后续新增类型 * - 32 ~ 255: n - 32 长度的字符串 - * - * 该类型码表属于录像参数编解码格式的一部分,遵循只增不改的约定:新语义必须分配 - * 新的类型码,禁止复用既有类型码。历史上 5/6/7/8 与短字符串基址(长度 + 9)曾 - * 发生一次性重新分配,项目未发布,旧录像不在兼容范围内。 */ readonly paramType: number; diff --git a/packages-user/data-common/src/replay/sandbox.ts b/packages-user/data-common/src/replay/sandbox.ts index 0f97bbc..c7c9903 100644 --- a/packages-user/data-common/src/replay/sandbox.ts +++ b/packages-user/data-common/src/replay/sandbox.ts @@ -6,7 +6,6 @@ import { } from '@motajs/common'; import { IReplayArray, - IReplayCommand, IReplayPassiveHandler, IReplayReadStream, IReplaySandbox, @@ -16,7 +15,16 @@ import { ReplayCommandResult, ReplayCommandType } from './types'; -import { isNil } from 'lodash-es'; + +interface IReplayStepResult { + /** 录像是否播放成功 */ + readonly success: boolean; + /** + * 当前正在播放的录像步,注意如果是 finalize 失败, + * 那么这里也应是还未播放的那个本来应该播放的录像步,而不是需要 finalize 的录像步 + */ + readonly step: IReplayStepHandler | null; +} export class ReplaySandbox extends Hookable @@ -35,17 +43,15 @@ export class ReplaySandbox /** 暂停 `Promise` 的 `resolve` 函数 */ private pauseResolve: () => void = () => {}; - /** 被动录像步兑现函数 */ - private passiveResolve: (status: ReplayCommandResult) => void = () => {}; - /** 下一步被动录像步的 `StepHandler`,注意不是 `PassiveHandler` */ - private passiveHandler: IReplayStepHandler | null = null; - /** 录像的流式读取器 */ private reader: Readonly; - /** 上一步播放的指令 */ + /** 上一个主动录像步指令 */ private last: number = -1; + /** 等待下一步播放的录像步,包括主动和被动 */ + private appendingStep: IReplayStepHandler | null = null; + constructor( readonly route: IReplayArray, readonly system: IReplaySystem, @@ -61,20 +67,6 @@ 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)); @@ -84,11 +76,162 @@ export class ReplaySandbox return this.reader.index; } + //#region 录像步 + + getPassive(): IReplayPassiveHandler | null { + const curr = this.appendingStep; + if (!curr) { + logger.error(73); + return null; + } + const command = this.system.getCommand(curr.command); + if (!command) { + logger.warn(157, curr.command.toString()); + return null; + } + if (command.type === ReplayCommandType.Active) { + logger.error(73); + return null; + } + + const handler: IReplayPassiveHandler = { + step: curr, + next: async status => { + const result = this.checkReplayStatus(status, 'passive'); + if (result) { + this.appendingStep = this.reader.read(); + await Promise.all( + this.forEachHook(hook => hook.onStep?.(curr)) + ); + } + } + }; + return handler; + } + + /** + * 触发上一步指令的连续步骤后处理 + */ + 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 ReplayCommandResult.Failed; + } + return last.finalize?.() ?? ReplayCommandResult.Success; + } + + /** + * 检查录像执行状态,进行适当的控制台输出 + * @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; + } + } + + private async replayStep(): Promise { + if (!this.playing || this.ended) { + return { + step: null, + success: false + }; + } + if (this.reader.expired) { + logger.warn(156); + return { + step: null, + success: false + }; + } + const curr = this.appendingStep; + + // 当录像步已经放完时,对最后一个录像步进行 finalize 处理 + if (!curr) { + const ne = await this.finalizeLast(); + const success = this.checkReplayStatus(ne, 'finalize'); + if (success) { + this.last = -1; + this.ending = true; + } + return { + step: null, + success: false + }; + } + + const command = this.system.getCommand(curr.command); + if (!command) { + logger.warn(157, curr.command.toString()); + return { + step: null, + success: false + }; + } + + // 如果当前录像步是被动录像步,说明它没有在上一个主动录像步期间完成,则忽略并进入后续播放 + if (command.type !== ReplayCommandType.Active) { + logger.warn(194, curr.command.toString(), this.last.toString()); + this.appendingStep = this.reader.read(); + return { + step: curr, + success: true + }; + } + + // 如果当前录像步指令与上一个主动录像步不同,那么进行 finalize 后处理 + if (curr.command !== this.last) { + const ne = await this.finalizeLast(); + const success = this.checkReplayStatus(ne, 'finalize'); + if (!success) { + return { + step: curr, + success: false + }; + } + } + this.last = curr.command; + + // 在执行前获取下一个录像步,因为被动录像步一定会在其紧跟着的前面的主动录像步中进行读取, + // 所以在此时必须确定下一个录像步是什么,这样 `getPassive` 才能判断是否真的是被动录像步。 + this.appendingStep = this.reader.read(); + + // 然后执行当前录像步,此时一定是主动录像步,可以直接调 + const status = await command.execute(curr); + + const success = this.checkReplayStatus( + status, + command.type === ReplayCommandType.Active ? 'execute' : 'passive' + ); + + return { + step: curr, + success + }; + } + + //#endregion + + //#region 录像控制 + /** * 开始录像重播循环 */ private async startReplayLoop() { - if (this.ended) return; + if (this.ended || this.pausing) return; if (this.reader.expired) { logger.warn(156); return; @@ -105,11 +248,15 @@ export class ReplaySandbox logger.warn(156); break; } - const success = await this.step(); - if (!success) break; + const res = await this.replayStep(); + if (!res.success) break; + await Promise.all( + this.forEachHook(hook => hook.onStep?.(res.step!)) + ); } if (this.ending) { this.ended = true; + this.forEachHook(hook => hook.onStopReplay?.()); } } @@ -144,109 +291,15 @@ export class ReplaySandbox this.forEachHook(hook => hook.onStopReplay?.()); } - /** - * 触发上一步指令的连续步骤后处理 - */ - 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 ReplayCommandResult.Failed; - } - 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; - } - } - async step(): Promise { - if (!this.playing || this.ended) return false; - if (this.reader.expired) { - logger.warn(156); - return false; - } - const next = this.reader.read(); - - // finalize 执行 - if (!next || next.command !== this.last) { - const ne = await this.finalizeLast(); - 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; - - // 获取指令本身 - const command = this.system.getCommand(next.command); - if (!command) { - logger.warn(157, next.command.toString()); - return false; - } - - 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; + const res = await this.replayStep(); + if (res.success) { + await Promise.all( + this.forEachHook(hook => hook.onStep?.(res.step!)) + ); } + return res.success; } + + //#endregion } diff --git a/packages-user/data-common/src/replay/types.ts b/packages-user/data-common/src/replay/types.ts index 739483c..be6c35b 100644 --- a/packages-user/data-common/src/replay/types.ts +++ b/packages-user/data-common/src/replay/types.ts @@ -56,7 +56,7 @@ export interface IReplayPassiveHandler { * 本被动录像步执行完毕,进入下一个录像步 * @param status 被动录像步执行情况 */ - next(status: ReplayCommandResult): void; + next(status: ReplayCommandResult): Promise; } export interface IReplayCommand { @@ -122,7 +122,8 @@ export interface IReplaySandbox extends IHookable { readonly playing: boolean; /** - * 获取被动录像参数。如果下一步是被动录像,那么获取其参数,如果不是,返回 `null` + * 获取被动录像参数。如果下一步是被动录像,那么获取其参数,如果不是,返回 `null`。 + * 当调用 `IReplayPassiveHandler` 后,才会进入下一个录像步,否则多次调用 `getPassive`,会得到相同的结果。 */ getPassive(): IReplayPassiveHandler | null; @@ -159,7 +160,7 @@ export interface IReplaySandbox extends IHookable { /** * 单步播放一步,返回的 Promise 在该步渲染完成后兑现 - * @returns 当前步是否正常播放完成 + * @returns 当前步的播放结果 */ step(): Promise; } @@ -188,8 +189,7 @@ export interface IReplayReadStream { expired: boolean; /** - * 读取当前指针位置的录像步并将指针推进到下一步 - * 超出录像长度时返回 null + * 读取当前指针位置的录像步,并将录像指针推进到下一步,超出录像长度时返回 `null` */ read(): IReplayStepHandler | null; diff --git a/packages/common/src/logger.json b/packages/common/src/logger.json index 980acd4..1729be5 100644 --- a/packages/common/src/logger.json +++ b/packages/common/src/logger.json @@ -276,6 +276,7 @@ "191": "Cannot cache autotile since no its frame or type data stored.", "192": "A FaceGroup.Dir8 face handler registration is expected.", "193": "Cannot load item state since no item id registration for item num $1.", + "194": "Expected passive replay command executed during the last active replay command executing, but a passive command $1 was got after active replay command $2 executing. Replay will continue and ignore this issue.", "1001": "Event(setBlock): Unknown tile '$1' when setting block." } }