From 1b93deb8c814df53314269e15f70290d1e2ab208 Mon Sep 17 00:00:00 2001 From: unanmed <1319491857@qq.com> Date: Thu, 10 Sep 2026 20:13:51 +0800 Subject: [PATCH] test(03-07): prove serialized event replay path - Load the fixed replay event through the internal serialized boundary - Assert trigger, raw statements, coordinate binding, one execution, and final state --- .../test/coreSerializedEvents.test.ts | 66 ++++++++ .../data-state/test/fixtures/closed-loop.ts | 159 +++++++++--------- 2 files changed, 149 insertions(+), 76 deletions(-) create mode 100644 packages-user/data-state/test/coreSerializedEvents.test.ts diff --git a/packages-user/data-state/test/coreSerializedEvents.test.ts b/packages-user/data-state/test/coreSerializedEvents.test.ts new file mode 100644 index 0000000..4c7ba02 --- /dev/null +++ b/packages-user/data-state/test/coreSerializedEvents.test.ts @@ -0,0 +1,66 @@ +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 { + for (let index = 0; index < 1000 && !sandbox.ended; index++) { + await new Promise(resolve => setTimeout(resolve, 0)); + } + if (!sandbox.ended) throw new Error('replay did not reach normal end'); +} diff --git a/packages-user/data-state/test/fixtures/closed-loop.ts b/packages-user/data-state/test/fixtures/closed-loop.ts index 35aedfe..b72f880 100644 --- a/packages-user/data-state/test/fixtures/closed-loop.ts +++ b/packages-user/data-state/test/fixtures/closed-loop.ts @@ -1,4 +1,4 @@ -import { AnonTokyoInterpreter } from 'anon-tokyo'; +import { Statement, StatementType } from 'anon-tokyo'; import { EventTrigger, FaceDirection, @@ -18,8 +18,11 @@ import { IMapLayer, IMapStoreSave } from '@user/data-base'; -import { IReadonlyBlockEvent } from '@user/data-system'; 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 { @@ -53,6 +56,8 @@ export interface IClosedLoopFixture { readonly sandbox: IReplaySandbox; readonly initialState: IClosedLoopInitialState; readonly expected: IClosedLoopExpectedSnapshot; + readonly eventId: string; + readonly rawEvent: Statement[]; readonly reset: () => void; readonly eventCompleted: () => boolean; } @@ -62,7 +67,7 @@ export function createClosedLoopFixture(): IClosedLoopFixture { state.tileStore.addTile({ num: 1, id: 'floor', - events: { 10: 'mutate-map' }, + events: {}, type: TileType.Terrain, pass: { onlyEvents: false, outPass: 0b1111, inPass: 0b1111 }, eventPass: true @@ -76,87 +81,88 @@ export function createClosedLoopFixture(): IClosedLoopFixture { eventPass: true }); - 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] - }, - layerAlias: { - 0: 'bg', - 10: 'bg2', - 20: 'event', - 30: 'fg', - 40: 'fg2' - }, - events: { - 0: {}, - 10: {}, - 20: {}, - 30: {}, - 40: {} + const rawEvent: Statement[] = [ + { + type: StatementType.Call, + functionName: 'eventSetBlock', + builtIn: true, + async: true, + parameters: { x: 1, y: 0, tile: 2 } } - }); + ]; + const serialized: ILegacySerializedLoadData = { + events: { + 'mutate-map': { + trigger: EventTrigger.OnEnter, + rawEvent + } + }, + 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'); if (!map || !map.eventLayer) { throw new Error('closed-loop fixture map was not created'); } - const eventLayer = map.eventLayer; - 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: {} - } - }); + const secondMap = state.maps.getMap('F2'); if (!secondMap || !secondMap.eventLayer) { throw new Error('closed-loop fixture second map was not created'); } + const eventLayer = map.eventLayer; state.maps.setMapActiveStatus('F1', true); state.maps.setMapActiveStatus('F2', true); - let completed = false; - const event: IReadonlyBlockEvent = { - interpreter: new AnonTokyoInterpreter({ - builtInFunctions: [], - globalFunctions: [] - }), - trigger: EventTrigger.OnEnter, - rawEvent: [], - compiled: null, - compile: () => null, - execute: async (_param, env) => { - await Promise.resolve(); - if (!env.layer || !env.triggerLocator) { - throw new Error('closed-loop event lost its map source'); - } - env.layer.setBlock(2, env.triggerLocator.x, env.triggerLocator.y); - completed = true; - } - }; - state.eventStore.addEvent('mutate-map', event); - state.hero.location.setFloor('F1'); state.hero.location.setPos(0, 0); state.hero.location.mover.setFaceDir(FaceDirection.Right); @@ -237,7 +243,6 @@ export function createClosedLoopFixture(): IClosedLoopFixture { initialState.enemy, SaveCompression.NoCompression ); - completed = false; }; const sandbox = replay.createReplaySandbox({ route: replay.route, @@ -254,6 +259,8 @@ export function createClosedLoopFixture(): IClosedLoopFixture { initialState, expected, reset, - eventCompleted: () => completed + eventId: 'mutate-map', + rawEvent, + eventCompleted: () => eventLayer.getBlock(1, 0) === 2 }; }