mirror of
https://github.com/motajs/template.git
synced 2026-09-12 17:58:51 +08:00
fix(03-14): restore synchronous replay command handling
- Remove Promise settlement from replay safety restoration - Adapt command booleans to the existing Promise boundary - Preserve stable replay interfaces and user-owned decorator placement
This commit is contained in:
parent
d9a7faa5b9
commit
7ffc6c4171
@ -5,7 +5,7 @@
|
||||
本节晚于初始 replay checkpoint,优先于下方关于异步 command completion 和 decorator
|
||||
placement 的旧记录:
|
||||
|
||||
- replay command 的状态操作必须同步完成;command 不等待移动控制器、事件链或其他 Promise,不能用异步恢复 collection context。
|
||||
- replay command 的状态操作必须同步完成;command 不等待移动控制器、事件链或其他 Promise,collection context 在装饰方法返回时同步恢复。
|
||||
- 既有 `ReplaySystem`、route 和 sandbox 只做使同步 command 正常运行所需的最小兼容调整,不重新设计录像系统。
|
||||
- `@shouldReplay()` 不在本次 correction 中移动或新增;其最终位置由用户自行放到真正改变最终状态的方法上。
|
||||
|
||||
@ -86,18 +86,20 @@ contract.
|
||||
## Completion boundaries
|
||||
|
||||
- Four-direction movement appends one direction to the hero mover, starts it,
|
||||
and awaits the returned `mover controller.onEnd` Promise.
|
||||
and returns `true` when the controller starts; it does not await
|
||||
`mover controller.onEnd`.
|
||||
- Auto-pathfind calls the existing `PathfindingSystem.moveTo({ x, y })`; a null
|
||||
result is `false`, and a non-null result is complete only after its returned
|
||||
`controller.onEnd` Promise settles.
|
||||
- Item and equipment calls are synchronous under the current interfaces; their
|
||||
result is `false`, and a non-null result returns `true` immediately without
|
||||
awaiting its controller.
|
||||
- Item and equipment calls remain synchronous under the current interfaces; their
|
||||
boolean/undefined result is converted to the command's success boolean.
|
||||
- Replay safety collection remains active across every decorated Promise until
|
||||
that Promise settles, including nested decorated calls. Synchronous queries,
|
||||
pure calculations, and internal helpers are outside the decoration boundary.
|
||||
- A command never advances replay completion before its complete action Promise
|
||||
settles. First-divergence thrown diagnostics remain the responsibility of
|
||||
Plan 04 and do not change the replay boolean interface.
|
||||
- Replay safety collection is restored to its previous context immediately after
|
||||
every decorated method returns, including nested synchronous decorated calls.
|
||||
Synchronous queries, pure calculations, and internal helpers are outside the
|
||||
decoration boundary.
|
||||
- `IReplayCommand.execute()` continues to return the existing `Promise<boolean>`
|
||||
boundary, but command implementations adapt their immediate boolean result with
|
||||
`Promise.resolve` rather than adding asynchronous command sequencing.
|
||||
|
||||
## Explicit exclusions
|
||||
|
||||
|
||||
@ -15,13 +15,6 @@ interface IReplaySafetyDetailQueue {
|
||||
readonly collection: IReplaySafetyCollection;
|
||||
}
|
||||
|
||||
interface IPromiseLike {
|
||||
then(
|
||||
onFulfilled?: (value: unknown) => unknown,
|
||||
onRejected?: (reason: unknown) => unknown
|
||||
): unknown;
|
||||
}
|
||||
|
||||
type ReplayMethod<This, Args extends unknown[], Return> = (
|
||||
this: This,
|
||||
...args: Args
|
||||
@ -54,13 +47,6 @@ let detailCode = 0;
|
||||
/** 录像收集详细信息队列,保留 50 个以确保可以在控制台重复输出 */
|
||||
const detailQueue: IReplaySafetyDetailQueue[] = [];
|
||||
|
||||
function isPromiseLike(value: unknown): value is IPromiseLike {
|
||||
if (!value || (typeof value !== 'object' && typeof value !== 'function')) {
|
||||
return false;
|
||||
}
|
||||
return typeof (value as { then?: unknown }).then === 'function';
|
||||
}
|
||||
|
||||
function resetReplaySafetyCollection(): void {
|
||||
replaySystem = null;
|
||||
collecting = false;
|
||||
@ -184,18 +170,7 @@ export function shouldReplay(message: string): ReplayDecorator {
|
||||
|
||||
currentCollection = newCollection;
|
||||
const result = method.apply(this, args);
|
||||
if (isPromiseLike(result)) {
|
||||
Promise.resolve(result).then(
|
||||
() => {
|
||||
currentCollection = before;
|
||||
},
|
||||
() => {
|
||||
currentCollection = before;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
currentCollection = before;
|
||||
}
|
||||
currentCollection = before;
|
||||
return result;
|
||||
};
|
||||
};
|
||||
|
||||
@ -42,14 +42,14 @@ function resolveSlot(
|
||||
}
|
||||
|
||||
interface IReplayCommandEntrances {
|
||||
/** 执行一个方向的勇士移动,并等待移动控制器完成 */
|
||||
moveHero(direction: FaceDirection): Promise<boolean>;
|
||||
/** 执行一个方向的勇士移动 */
|
||||
moveHero(direction: FaceDirection): boolean;
|
||||
|
||||
/** 执行一次指定目标点的自动寻路移动,并等待移动控制器完成 */
|
||||
moveToPoint(x: number, y: number): Promise<boolean>;
|
||||
/** 执行一次指定目标点的自动寻路移动 */
|
||||
moveToPoint(x: number, y: number): boolean;
|
||||
|
||||
/** 执行既有勇士道具使用入口 */
|
||||
useItem(item: number | string): Promise<boolean>;
|
||||
useItem(item: number | string): boolean;
|
||||
|
||||
/** 执行既有勇士装备入口 */
|
||||
equip(
|
||||
@ -57,10 +57,10 @@ interface IReplayCommandEntrances {
|
||||
slot: number | string,
|
||||
slotIndex: number,
|
||||
autoUnload: boolean | undefined
|
||||
): Promise<boolean>;
|
||||
): boolean;
|
||||
|
||||
/** 执行既有勇士卸下装备入口 */
|
||||
unequip(slot: number): Promise<boolean>;
|
||||
unequip(slot: number): boolean;
|
||||
}
|
||||
|
||||
class ReplayCommandEntrances implements IReplayCommandEntrances {
|
||||
@ -74,7 +74,7 @@ class ReplayCommandEntrances implements IReplayCommandEntrances {
|
||||
(
|
||||
this: ReplayCommandEntrances,
|
||||
direction: FaceDirection
|
||||
) => Promise<boolean>
|
||||
) => boolean
|
||||
>
|
||||
);
|
||||
this.moveToPoint = shouldReplay('replay command: pathfind hero')(
|
||||
@ -83,21 +83,14 @@ class ReplayCommandEntrances implements IReplayCommandEntrances {
|
||||
name: 'moveToPoint'
|
||||
} as ClassMethodDecoratorContext<
|
||||
ReplayCommandEntrances,
|
||||
(
|
||||
this: ReplayCommandEntrances,
|
||||
x: number,
|
||||
y: number
|
||||
) => Promise<boolean>
|
||||
(this: ReplayCommandEntrances, x: number, y: number) => boolean
|
||||
>
|
||||
);
|
||||
this.useItem = shouldReplay('replay command: use item')(this.useItem, {
|
||||
name: 'useItem'
|
||||
} as ClassMethodDecoratorContext<
|
||||
ReplayCommandEntrances,
|
||||
(
|
||||
this: ReplayCommandEntrances,
|
||||
item: number | string
|
||||
) => Promise<boolean>
|
||||
(this: ReplayCommandEntrances, item: number | string) => boolean
|
||||
>);
|
||||
this.equip = shouldReplay('replay command: equip item')(this.equip, {
|
||||
name: 'equip'
|
||||
@ -109,7 +102,7 @@ class ReplayCommandEntrances implements IReplayCommandEntrances {
|
||||
slot: number | string,
|
||||
slotIndex: number,
|
||||
autoUnload: boolean | undefined
|
||||
) => Promise<boolean>
|
||||
) => boolean
|
||||
>);
|
||||
this.unequip = shouldReplay('replay command: unequip item')(
|
||||
this.unequip,
|
||||
@ -117,38 +110,36 @@ class ReplayCommandEntrances implements IReplayCommandEntrances {
|
||||
name: 'unequip'
|
||||
} as ClassMethodDecoratorContext<
|
||||
ReplayCommandEntrances,
|
||||
(this: ReplayCommandEntrances, slot: number) => Promise<boolean>
|
||||
(this: ReplayCommandEntrances, slot: number) => boolean
|
||||
>
|
||||
);
|
||||
}
|
||||
|
||||
async moveHero(direction: FaceDirection): Promise<boolean> {
|
||||
moveHero(direction: FaceDirection): boolean {
|
||||
const mover = this.state.hero.location.mover;
|
||||
if (mover.moving) return false;
|
||||
mover.step(direction);
|
||||
const controller = mover.start();
|
||||
if (!controller) return false;
|
||||
await controller.onEnd;
|
||||
return true;
|
||||
}
|
||||
|
||||
async moveToPoint(x: number, y: number): Promise<boolean> {
|
||||
moveToPoint(x: number, y: number): boolean {
|
||||
const result = this.state.pathfinding.moveTo({ x, y });
|
||||
if (!result) return false;
|
||||
await result.controller.onEnd;
|
||||
return true;
|
||||
}
|
||||
|
||||
async useItem(item: number | string): Promise<boolean> {
|
||||
useItem(item: number | string): boolean {
|
||||
return this.state.hero.items.useItem(item);
|
||||
}
|
||||
|
||||
async equip(
|
||||
equip(
|
||||
uid: number,
|
||||
slot: number | string,
|
||||
slotIndex: number,
|
||||
autoUnload: boolean | undefined
|
||||
): Promise<boolean> {
|
||||
): boolean {
|
||||
if (this.state.hero.equip.getEquipped(slotIndex) === uid) return true;
|
||||
if (
|
||||
this.state.hero.equip.canEquipTo(uid, slot) ===
|
||||
@ -160,7 +151,7 @@ class ReplayCommandEntrances implements IReplayCommandEntrances {
|
||||
return this.state.hero.equip.getEquipped(slotIndex) === uid;
|
||||
}
|
||||
|
||||
async unequip(slot: number): Promise<boolean> {
|
||||
unequip(slot: number): boolean {
|
||||
if (this.state.hero.equip.getEquipped(slot) === undefined) {
|
||||
return false;
|
||||
}
|
||||
@ -176,7 +167,7 @@ function createMoveCommand(
|
||||
return {
|
||||
execute: (step: IReplayStepHandler): Promise<boolean> => {
|
||||
if (step.params.length !== 0) return Promise.resolve(false);
|
||||
return entries.moveHero(direction);
|
||||
return Promise.resolve(entries.moveHero(direction));
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -213,7 +204,7 @@ export function createReplayCommandItems(
|
||||
if (!isNumber(x) || !isNumber(y)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return entries.moveToPoint(x, y);
|
||||
return Promise.resolve(entries.moveToPoint(x, y));
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -224,7 +215,7 @@ export function createReplayCommandItems(
|
||||
if (step.params.length !== 1) return Promise.resolve(false);
|
||||
const item = step.params[0];
|
||||
if (!isItem(item)) return Promise.resolve(false);
|
||||
return entries.useItem(item);
|
||||
return Promise.resolve(entries.useItem(item));
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -250,7 +241,9 @@ export function createReplayCommandItems(
|
||||
}
|
||||
const slotIndex = resolveSlot(state, slot);
|
||||
if (slotIndex === null) return Promise.resolve(false);
|
||||
return entries.equip(uid, slot, slotIndex, autoUnload);
|
||||
return Promise.resolve(
|
||||
entries.equip(uid, slot, slotIndex, autoUnload)
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -269,7 +262,7 @@ export function createReplayCommandItems(
|
||||
) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return entries.unequip(slot);
|
||||
return Promise.resolve(entries.unequip(slot));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user