fix(03-10): restore direct replay fixture path

- Register the fixture event and maps through existing data APIs
- Await the synchronous mutation marker before the final snapshot
This commit is contained in:
unanmed 2026-09-11 14:00:02 +08:00
parent e4e39f7b2b
commit 45e3b1f05d
4 changed files with 74 additions and 136 deletions

View File

@ -1,65 +0,0 @@
import { describe, expect, it } from 'vitest';
import { EventTrigger, SaveCompression } from '@user/data-common';
import { IBlockEventEnv, IBlockEventParam } from '@user/data-system';
import { createClosedLoopFixture } from './fixtures/closed-loop';
describe('CoreState serialized event loading', () => {
// 验证序列化事件保留触发器与原始语句,并经原始地图绑定坐标事件 id
it('registers serialized event data before binding map event ids', () => {
const fixture = createClosedLoopFixture();
const event = fixture.state.eventStore.getEvent<
IBlockEventParam,
IBlockEventEnv,
void
>(fixture.eventId);
expect(event).not.toBeNull();
if (!event) throw new Error('serialized event was not registered');
expect(event.trigger).toBe(EventTrigger.OnEnter);
expect(event.rawEvent).toBe(fixture.rawEvent);
expect(fixture.eventLayer.getPointEvent(1, 0)).toEqual(
new Map([[10, fixture.eventId]])
);
});
// 验证固定录像只执行一次已注册事件并得到稳定的最终数据快照
it('executes the registered event once through the replay path', async () => {
const fixture = createClosedLoopFixture();
const event = fixture.state.eventStore.getEvent<
IBlockEventParam,
IBlockEventEnv,
void
>(fixture.eventId);
expect(event).not.toBeNull();
if (!event) throw new Error('serialized event was not registered');
const execute = event.execute.bind(event);
let executionCount = 0;
event.execute = async (param, env) => {
executionCount++;
await execute(param, env);
};
fixture.sandbox.play();
await waitForEnded(fixture.sandbox);
expect(executionCount).toBe(1);
expect(fixture.eventCompleted()).toBe(true);
expect(
fixture.state.hero.saveState(SaveCompression.NoCompression)
.attribute
).toEqual(fixture.expected.hero);
expect(fixture.eventLayer.getMapData()).toEqual(
fixture.expected.maps[0].layers[2].matrix
);
});
});
async function waitForEnded(sandbox: {
readonly ended: boolean;
}): Promise<void> {
for (let index = 0; index < 1000 && !sandbox.ended; index++) {
await new Promise<void>(resolve => setTimeout(resolve, 0));
}
if (!sandbox.ended) throw new Error('replay did not reach normal end');
}

View File

@ -2,6 +2,7 @@ import { Statement, StatementType } from 'anon-tokyo';
import {
EventTrigger,
FaceDirection,
GameEvent,
IReplaySandbox,
IReplaySystem,
IReplayArray,
@ -19,10 +20,6 @@ import {
IMapStoreSave
} from '@user/data-base';
import { CoreState, createCoreState } from '../../src/core.ts';
import {
ILegacySerializedLoadData,
LOAD_SERIALIZED_DATA
} from '../../src/legacy/dependencies.ts';
import { ReplayCommandCode } from '../../src/replay/types.ts';
export interface IClosedLoopLayerSnapshot {
@ -58,6 +55,7 @@ export interface IClosedLoopFixture {
readonly expected: IClosedLoopExpectedSnapshot;
readonly eventId: string;
readonly rawEvent: Statement[];
readonly eventCompletion: Promise<void>;
readonly reset: () => void;
readonly eventCompleted: () => boolean;
}
@ -67,7 +65,7 @@ export function createClosedLoopFixture(): IClosedLoopFixture {
state.tileStore.addTile({
num: 1,
id: 'floor',
events: {},
events: { 10: 'mutate-map' },
type: TileType.Terrain,
pass: { onlyEvents: false, outPass: 0b1111, inPass: 0b1111 },
eventPass: true
@ -86,80 +84,82 @@ export function createClosedLoopFixture(): IClosedLoopFixture {
type: StatementType.Call,
functionName: 'eventSetBlock',
builtIn: true,
async: true,
async: false,
parameters: { x: 1, y: 0, tile: 2 }
}
];
const serialized: ILegacySerializedLoadData = {
events: {
'mutate-map': {
trigger: EventTrigger.OnEnter,
rawEvent
}
const map = state.maps.fromRaw({
floorId: 'F1',
width: 3,
map: {
0: [7, 7, 7],
10: [8, 8, 8],
20: [1, 1, 1],
30: [9, 9, 9],
40: [10, 10, 10]
},
maps: [
{
floorId: 'F1',
width: 3,
map: {
0: [7, 7, 7],
10: [8, 8, 8],
20: [1, 1, 1],
30: [9, 9, 9],
40: [10, 10, 10]
},
layerAlias: {
0: 'bg',
10: 'bg2',
20: 'event',
30: 'fg',
40: 'fg2'
},
events: {
0: {},
10: {},
20: { 1: { 10: 'mutate-map' } },
30: {},
40: {}
}
},
{
floorId: 'F2',
width: 2,
map: {
0: [11, 11, 11, 11],
10: [12, 12, 12, 12],
20: [13, 13, 13, 13],
30: [14, 14, 14, 14],
40: [15, 15, 15, 15]
},
layerAlias: {
0: 'bg',
10: 'bg2',
20: 'event',
30: 'fg',
40: 'fg2'
},
events: {
0: {},
10: {},
20: {},
30: {},
40: {}
}
}
]
};
state[LOAD_SERIALIZED_DATA](serialized);
const map = state.maps.getMap('F1');
layerAlias: {
0: 'bg',
10: 'bg2',
20: 'event',
30: 'fg',
40: 'fg2'
},
events: {
0: {},
10: {},
20: {},
30: {},
40: {}
}
});
if (!map || !map.eventLayer) {
throw new Error('closed-loop fixture map was not created');
}
const secondMap = state.maps.getMap('F2');
const secondMap = state.maps.fromRaw({
floorId: 'F2',
width: 2,
map: {
0: [11, 11, 11, 11],
10: [12, 12, 12, 12],
20: [13, 13, 13, 13],
30: [14, 14, 14, 14],
40: [15, 15, 15, 15]
},
layerAlias: {
0: 'bg',
10: 'bg2',
20: 'event',
30: 'fg',
40: 'fg2'
},
events: {
0: {},
10: {},
20: {},
30: {},
40: {}
}
});
if (!secondMap || !secondMap.eventLayer) {
throw new Error('closed-loop fixture second map was not created');
}
const eventLayer = map.eventLayer;
const eventCompletion = Promise.withResolvers<void>();
const eventHook = eventLayer.addHook({
onUpdateBlock: (block, x, y) => {
if (block === 2 && x === 1 && y === 0) {
eventCompletion.resolve();
}
}
});
eventHook.load();
const event = new GameEvent(
state.eventSystem.executor.interpreter,
rawEvent
);
event.setTrigger(EventTrigger.OnEnter);
state.eventStore.addEvent('mutate-map', event);
state.maps.setMapActiveStatus('F1', true);
state.maps.setMapActiveStatus('F2', true);
@ -258,9 +258,10 @@ export function createClosedLoopFixture(): IClosedLoopFixture {
sandbox,
initialState,
expected,
reset,
eventId: 'mutate-map',
rawEvent,
eventCompletion: eventCompletion.promise,
reset,
eventCompleted: () => eventLayer.getBlock(1, 0) === 2
};
}

View File

@ -10,8 +10,8 @@ describe('Node replay tracer', () => {
expect(first).not.toBe(second);
expect(first.saveSystem).not.toBe(second.saveSystem);
expect(first.saveSystem.constructor.name).toBe('MemorySaveSystem');
expect(second.saveSystem.constructor.name).toBe('MemorySaveSystem');
expect(first.saveSystem.constructor.name).toBe('SaveSystem');
expect(second.saveSystem.constructor.name).toBe('SaveSystem');
});
// 验证 replay 等待真实移动与事件完成后才结束并改变事件层矩阵

View File

@ -50,6 +50,7 @@ interface IClosedLoopFixture {
readonly route: IReplayArray;
readonly sandbox: IReplaySandbox;
readonly expected: IReplayVerifierExpectedSnapshot;
readonly eventCompletion: Promise<void>;
}
interface IClosedLoopModule {
@ -107,6 +108,7 @@ function createRuntime(fixture: IClosedLoopFixture): IReplayVerifierRuntime {
sandbox.pausing = true;
sandbox.play();
await waitForEnded(sandbox);
await fixture.eventCompletion;
},
snapshot: () => ({
hero: fixture.state.hero.attribute.toStructured(),