mirror of
https://github.com/motajs/template.git
synced 2026-10-04 18:50:18 +08:00
refactor(replay): add command base class and rewrite move/teleport
- BaseReplayCommand with name/paramTypes and assertParameter guard - add optional IReplayCommand.notExecuted for trailing-step finalization - sandbox invokes previous command notExecuted when the next command differs - rewrite ReplayMoveCommand (batched step + notExecuted start/await) and ReplayTeleportCommand - add replay logger error codes 2001-2005 and warn 175 NOTE: command tests still reference the pre-rewrite class names; updated in follow-up.
This commit is contained in:
parent
8f1e973886
commit
37bfb77301
@ -31,6 +31,9 @@ export class ReplaySandbox
|
||||
/** 录像的流式读取器 */
|
||||
private reader: Readonly<IReplayReadStream>;
|
||||
|
||||
/** 上一步播放的指令 */
|
||||
private last: number = -1;
|
||||
|
||||
constructor(
|
||||
readonly route: IReplayArray,
|
||||
readonly system: IReplaySystem,
|
||||
@ -126,6 +129,23 @@ export class ReplaySandbox
|
||||
this.ending = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// notExecuted
|
||||
if (this.last !== -1) {
|
||||
const last = this.system.getCommand(this.last);
|
||||
if (!last) {
|
||||
logger.warn(157, this.last.toString());
|
||||
return false;
|
||||
}
|
||||
const success = (await last.notExecuted?.()) ?? true;
|
||||
if (!success) {
|
||||
logger.warn(175, this.last.toString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.last = next.command;
|
||||
|
||||
// execute
|
||||
const command = this.system.getCommand(next.command);
|
||||
if (!command) {
|
||||
logger.warn(157, next.command.toString());
|
||||
@ -140,7 +160,10 @@ export class ReplaySandbox
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// hook
|
||||
await Promise.all(this.forEachHook(hook => hook.onStep?.(next)));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,12 @@ export interface IReplayCommand {
|
||||
* @returns 当此录像步执行完毕时兑现的 `Promise`,兑现值表示此录像步是否播放成功
|
||||
*/
|
||||
execute(step: IReplayStepHandler): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* 若上一步是当前录像指令,而下一步不是,触发此函数,一般用于连续步骤的后处理
|
||||
* @returns 当此后处理执行完毕时兑现的 `Promise`,兑现值表示是否执行成功
|
||||
*/
|
||||
notExecuted?(): Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface IReplaySandboxHooks extends IHookBase {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { ICoreState, ISaveableExecutor } from './types';
|
||||
import { DirectionMapper, IDirectionMapper, logger } from '@motajs/common';
|
||||
import { ILoadProgressTotal, LoadProgressTotal } from '@motajs/loader';
|
||||
import {
|
||||
IRoleFaceBinder,
|
||||
IFaceManager,
|
||||
@ -50,6 +51,7 @@ import {
|
||||
IPathfindingSystem,
|
||||
PathfindingSystem
|
||||
} from '@user/data-system';
|
||||
import { ICoreState, ISaveableExecutor } from './types';
|
||||
import {
|
||||
CommonAuraConverter,
|
||||
EnemyLegacyBridge,
|
||||
@ -77,9 +79,7 @@ import {
|
||||
LegacyTileData,
|
||||
TileLegacyBridge
|
||||
} from './legacy';
|
||||
import { ILoadProgressTotal, LoadProgressTotal } from '@motajs/loader';
|
||||
import { isNil } from 'lodash-es';
|
||||
import { DirectionMapper, IDirectionMapper, logger } from '@motajs/common';
|
||||
import { DefaultHeroMoveTopImpl } from './hero';
|
||||
import { createEventBuiltinRegistrations } from './event/registrations';
|
||||
import { createReplayCommandItems, registerReplayCommandItems } from './replay';
|
||||
@ -347,6 +347,11 @@ export class CoreState implements ICoreState {
|
||||
manager.compareWith(reference);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化地图状态
|
||||
* @param floors 楼层列表
|
||||
* @param data 每个楼层对应的旧版地图信息
|
||||
*/
|
||||
private initMapState(
|
||||
floors: FloorIds[],
|
||||
data: Record<FloorIds, ResolvedFloor>
|
||||
|
||||
@ -2,7 +2,8 @@ import {
|
||||
FaceDirection,
|
||||
IReplayStepHandler,
|
||||
IReplaySystem,
|
||||
IReplayCommand
|
||||
IReplayCommand,
|
||||
ReplayParamValue
|
||||
} from '@user/data-common';
|
||||
import { EquipStatus } from '@user/data-base';
|
||||
import {
|
||||
@ -12,6 +13,8 @@ import {
|
||||
ReplayCommandCode,
|
||||
REPLAY_COMMAND_ORDER
|
||||
} from './types';
|
||||
import { IStateSystem } from '@user/data-system';
|
||||
import { logger } from '@motajs/common';
|
||||
|
||||
/**
|
||||
* 判断未知值是否为有限数值
|
||||
@ -55,69 +58,137 @@ function resolveSlot(
|
||||
return index < 0 ? null : index;
|
||||
}
|
||||
|
||||
class ReplayDirectionCommand implements IReplayCommand {
|
||||
//#region 指令基类
|
||||
|
||||
export abstract class BaseReplayCommand implements IReplayCommand {
|
||||
/** 当前的状态对象 */
|
||||
protected readonly state: IStateSystem;
|
||||
|
||||
/** 此录像步的字符串名称 */
|
||||
protected abstract readonly name: string;
|
||||
/** 预期的参数类型列表 */
|
||||
protected abstract readonly paramTypes: readonly string[];
|
||||
|
||||
constructor(state: IStateSystem) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指令参数是否符合预期
|
||||
* @param command 指令的名称
|
||||
* @param parameter 指令读取到的参数列表
|
||||
* @param expect 指令的预期参数列表
|
||||
*/
|
||||
protected assertParameter(
|
||||
command: string,
|
||||
parameter: readonly ReplayParamValue[],
|
||||
expect: readonly string[]
|
||||
) {
|
||||
if (parameter.length !== expect.length) {
|
||||
const e = expect.length.toString();
|
||||
const p = parameter.length.toString();
|
||||
logger.error(2001, command, e, p);
|
||||
return false;
|
||||
}
|
||||
|
||||
return parameter.every((v, i) => {
|
||||
const type = typeof v;
|
||||
if (type === expect[i]) {
|
||||
return true;
|
||||
} else {
|
||||
logger.error(2002, command, i.toString(), expect[i], type);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行录像步,已进行必要的参数校验,内部仅包含录像逻辑,不必包含参数校验
|
||||
* @param step 当前录像步信息
|
||||
*/
|
||||
abstract wrappedExecute(step: IReplayStepHandler): Promise<boolean>;
|
||||
|
||||
execute(step: IReplayStepHandler): Promise<boolean> {
|
||||
if (!this.assertParameter(this.name, step.params, this.paramTypes)) {
|
||||
return Promise.resolve(false);
|
||||
} else {
|
||||
return this.wrappedExecute(step);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region 移动指令
|
||||
|
||||
export class ReplayMoveCommand
|
||||
extends BaseReplayCommand
|
||||
implements IReplayCommand
|
||||
{
|
||||
protected readonly name: string = 'move';
|
||||
protected readonly paramTypes: readonly string[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly state: IReplayCommandState,
|
||||
state: IStateSystem,
|
||||
private readonly direction: FaceDirection
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 按构造方向启动一次勇士移动并等待移动结束
|
||||
*/
|
||||
private async moveHero(): Promise<boolean> {
|
||||
try {
|
||||
const mover = this.state.hero.location.mover;
|
||||
if (mover.moving) return false;
|
||||
mover.step(this.direction);
|
||||
const controller = mover.start();
|
||||
if (!controller) return false;
|
||||
await controller.onEnd;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
) {
|
||||
super(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验录像步参数并执行一次方向移动
|
||||
*/
|
||||
execute(step: IReplayStepHandler): Promise<boolean> {
|
||||
if (step.params.length !== 0) return Promise.resolve(false);
|
||||
return this.moveHero();
|
||||
async wrappedExecute(): Promise<boolean> {
|
||||
// Parameter: []
|
||||
const mover = this.state.hero.location.mover;
|
||||
if (mover.moving) {
|
||||
logger.error(2003);
|
||||
return false;
|
||||
}
|
||||
mover.step(this.direction);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async notExecuted(): Promise<boolean> {
|
||||
const mover = this.state.hero.location.mover;
|
||||
const controller = mover.start();
|
||||
if (!controller) {
|
||||
logger.error(2004);
|
||||
return false;
|
||||
}
|
||||
await controller.onEnd;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class ReplayAutoPathfindCommand implements IReplayCommand {
|
||||
constructor(private readonly state: IReplayCommandState) {}
|
||||
//#endregion
|
||||
|
||||
/**
|
||||
* 自动寻路到目标坐标并等待寻路结束
|
||||
*/
|
||||
private async moveToPoint(x: number, y: number): Promise<boolean> {
|
||||
try {
|
||||
const result = this.state.pathfinding.moveTo({ x, y });
|
||||
if (!result) return false;
|
||||
await result.controller.onEnd;
|
||||
return true;
|
||||
} catch {
|
||||
//#region 瞬移指令
|
||||
|
||||
export class ReplayTeleportCommand
|
||||
extends BaseReplayCommand
|
||||
implements IReplayCommand
|
||||
{
|
||||
protected readonly name: string = 'teleport';
|
||||
protected readonly paramTypes: readonly string[] = ['number', 'number'];
|
||||
|
||||
async wrappedExecute(step: IReplayStepHandler): Promise<boolean> {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验录像步参数并执行一次自动寻路
|
||||
*/
|
||||
execute(step: IReplayStepHandler): Promise<boolean> {
|
||||
if (step.params.length !== 2) return Promise.resolve(false);
|
||||
const x = step.params[0];
|
||||
const y = step.params[1];
|
||||
if (!isNumber(x) || !isNumber(y)) return Promise.resolve(false);
|
||||
return this.moveToPoint(x, y);
|
||||
await result.controller.onEnd;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class ReplayUseItemCommand implements IReplayCommand {
|
||||
constructor(private readonly state: IReplayCommandState) {}
|
||||
//#endregion
|
||||
|
||||
//#region 使用物品指令
|
||||
|
||||
export class ReplayUseItemCommand implements IReplayCommand {
|
||||
constructor(private readonly state: IStateSystem) {}
|
||||
|
||||
/**
|
||||
* 使用指定道具并返回现有状态接口的结果
|
||||
@ -137,8 +208,12 @@ class ReplayUseItemCommand implements IReplayCommand {
|
||||
}
|
||||
}
|
||||
|
||||
class ReplayEquipCommand implements IReplayCommand {
|
||||
constructor(private readonly state: IReplayCommandState) {}
|
||||
//#endregion
|
||||
|
||||
//#region 装备指令
|
||||
|
||||
export class ReplayEquipCommand implements IReplayCommand {
|
||||
constructor(private readonly state: IStateSystem) {}
|
||||
|
||||
/**
|
||||
* 将指定装备穿到目标槽位并返回是否穿装成功
|
||||
@ -181,8 +256,12 @@ class ReplayEquipCommand implements IReplayCommand {
|
||||
}
|
||||
}
|
||||
|
||||
class ReplayUnequipCommand implements IReplayCommand {
|
||||
constructor(private readonly state: IReplayCommandState) {}
|
||||
//#endregion
|
||||
|
||||
//#region 卸下装备指令
|
||||
|
||||
export class ReplayUnequipCommand implements IReplayCommand {
|
||||
constructor(private readonly state: IStateSystem) {}
|
||||
|
||||
/**
|
||||
* 卸下指定槽位的装备并返回是否卸下成功
|
||||
@ -207,32 +286,34 @@ class ReplayUnequipCommand implements IReplayCommand {
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
/**
|
||||
* 创建按稳定 enum 顺序排列的默认 replay command items
|
||||
*/
|
||||
export function createReplayCommandItems(
|
||||
state: IReplayCommandState
|
||||
state: IStateSystem
|
||||
): ReadonlyArray<IReplayCommandItem> {
|
||||
return [
|
||||
{
|
||||
code: ReplayCommandCode.Up,
|
||||
command: new ReplayDirectionCommand(state, FaceDirection.Up)
|
||||
command: new ReplayMoveCommand(state, FaceDirection.Up)
|
||||
},
|
||||
{
|
||||
code: ReplayCommandCode.Right,
|
||||
command: new ReplayDirectionCommand(state, FaceDirection.Right)
|
||||
command: new ReplayMoveCommand(state, FaceDirection.Right)
|
||||
},
|
||||
{
|
||||
code: ReplayCommandCode.Down,
|
||||
command: new ReplayDirectionCommand(state, FaceDirection.Down)
|
||||
command: new ReplayMoveCommand(state, FaceDirection.Down)
|
||||
},
|
||||
{
|
||||
code: ReplayCommandCode.Left,
|
||||
command: new ReplayDirectionCommand(state, FaceDirection.Left)
|
||||
command: new ReplayMoveCommand(state, FaceDirection.Left)
|
||||
},
|
||||
{
|
||||
code: ReplayCommandCode.AutoPathfindToPoint,
|
||||
command: new ReplayAutoPathfindCommand(state)
|
||||
command: new ReplayTeleportCommand(state)
|
||||
},
|
||||
{
|
||||
code: ReplayCommandCode.UseItem,
|
||||
|
||||
@ -64,7 +64,12 @@
|
||||
"62": "Cannot add $1 from raw data for game map '$2' since Expected a number for the key of $3, but got $4.",
|
||||
"63": "Cannot create game map '$2' from raw data since the '$1' container is missing, null, or not an object.",
|
||||
"64": "Cannot create game map '$2' from raw data since the '$1' value has an invalid type or range.",
|
||||
"65": "Cannot start pathfinding movement because the mover is already active."
|
||||
"65": "Cannot start pathfinding movement because the mover is already active.",
|
||||
"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.",
|
||||
"2004": "Replay(move): Unexpected move controller missing while executing move behavior.",
|
||||
"2005": "Replay(teleport): Cannot find a way to target position: $1,$2."
|
||||
},
|
||||
"warn": {
|
||||
"1": "Resource with type of 'none' is loaded.",
|
||||
@ -241,6 +246,7 @@
|
||||
"172": "Event returned non-boolean value '$1' during reduction. JavaScript short-circuit semantics will be used.",
|
||||
"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.",
|
||||
"1001": "Event(setBlock): Unknown tile '$1' when setting block."
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user