refactor: 被动录像步应当在主动录像步执行期间完成

This commit is contained in:
unanmed 2026-10-04 15:37:26 +08:00
parent e25ef3fa93
commit 75a39ad573
4 changed files with 186 additions and 136 deletions

View File

@ -24,10 +24,6 @@ interface INormalizedParam {
* - 9: string,带 int32 长度前缀
* - 10 ~ 31: 留空,以备后续新增类型
* - 32 ~ 255: n - 32 长度的字符串
*
* 该类型码表属于录像参数编解码格式的一部分,遵循只增不改的约定:新语义必须分配
* 新的类型码,禁止复用既有类型码。历史上 5/6/7/8 与短字符串基址(长度 + 9)曾
* 发生一次性重新分配,项目未发布,旧录像不在兼容范围内。
*/
readonly paramType: number;

View File

@ -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<IReplaySandboxHooks>
@ -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<IReplayReadStream>;
/** 上一步播放的指令 */
/** 上一个主动录像步指令 */
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<ReplayCommandResult> {
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<IReplayStepResult> {
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<ReplayCommandResult> {
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<ReplayCommandResult> {
if (command.type === ReplayCommandType.Passive) {
// 被动录像步
const { promise, resolve } =
Promise.withResolvers<ReplayCommandResult>();
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<boolean> {
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
}

View File

@ -56,7 +56,7 @@ export interface IReplayPassiveHandler {
* 本被动录像步执行完毕,进入下一个录像步
* @param status 被动录像步执行情况
*/
next(status: ReplayCommandResult): void;
next(status: ReplayCommandResult): Promise<void>;
}
export interface IReplayCommand {
@ -122,7 +122,8 @@ export interface IReplaySandbox extends IHookable<IReplaySandboxHooks> {
readonly playing: boolean;
/**
* 获取被动录像参数。如果下一步是被动录像,那么获取其参数,如果不是,返回 `null`
* 获取被动录像参数。如果下一步是被动录像,那么获取其参数,如果不是,返回 `null`。
* 当调用 `IReplayPassiveHandler` 后,才会进入下一个录像步,否则多次调用 `getPassive`,会得到相同的结果。
*/
getPassive(): IReplayPassiveHandler | null;
@ -159,7 +160,7 @@ export interface IReplaySandbox extends IHookable<IReplaySandboxHooks> {
/**
* 单步播放一步,返回的 Promise 在该步渲染完成后兑现
* @returns 当前步是否正常播放完成
* @returns 当前步的播放结果
*/
step(): Promise<boolean>;
}
@ -188,8 +189,7 @@ export interface IReplayReadStream {
expired: boolean;
/**
* 读取当前指针位置的录像步并将指针推进到下一步
* 超出录像长度时返回 null
* 读取当前指针位置的录像步,并将录像指针推进到下一步,超出录像长度时返回 `null`
*/
read(): IReplayStepHandler | null;

View File

@ -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."
}
}