diff --git a/packages-user/data-common/src/common/__test__/face.test.ts b/packages-user/data-common/src/common/__test__/face.test.ts index c7fd35f..d9e46c5 100644 --- a/packages-user/data-common/src/common/__test__/face.test.ts +++ b/packages-user/data-common/src/common/__test__/face.test.ts @@ -78,7 +78,7 @@ describe('RoleFaceBinder malloc and bind', () => { expect(result.info.map(info => info.code)).toContain(43); expect(binder.getMainFace(2)).toBeNull(); - expect(binder.getFaceDirection(2)).toBeUndefined(); + expect(binder.getFaceDirection(2)).toBe(FaceDirection.Unknown); }); // 验证绑定与主朝向相同朝向时经 logger 观测错误码 44 且不建立绑定 @@ -92,7 +92,7 @@ describe('RoleFaceBinder malloc and bind', () => { expect(result.info.map(info => info.code)).toContain(44); expect(binder.getMainFace(2)).toBeNull(); - expect(binder.getFaceDirection(2)).toBeUndefined(); + expect(binder.getFaceDirection(2)).toBe(FaceDirection.Unknown); }); }); @@ -106,11 +106,11 @@ describe('RoleFaceBinder queries', () => { expect(binder.getFaceOf(99, FaceDirection.Down)).toBeNull(); }); - // 验证 getFaceDirection 对未注册图块返回 undefined - it('returns undefined direction for an unknown block', () => { + // 验证 getFaceDirection 对未注册图块返回 FaceDirection.Unknown + it('returns Unknown direction for an unknown block', () => { const binder = createBinder(); - expect(binder.getFaceDirection(42)).toBeUndefined(); + expect(binder.getFaceDirection(42)).toBe(FaceDirection.Unknown); }); // 验证 getMainFace 对未注册图块返回 null diff --git a/packages-user/data-common/src/common/__test__/utils.test.ts b/packages-user/data-common/src/common/__test__/utils.test.ts deleted file mode 100644 index 3146ae7..0000000 --- a/packages-user/data-common/src/common/__test__/utils.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -// 测试 L0 common 朝向工具纯函数:坐标偏移、降级、旋转与字符串解析 -import { describe, expect, it } from 'vitest'; -import { FaceDirection } from '../types'; -import { - degradeFace, - fromDirectionString, - getFaceMovement, - nextFaceDirection -} from '../utils'; - -/** 全部朝向枚举值,用于校验方向表的完整覆盖 */ -const ALL_DIRECTIONS: readonly FaceDirection[] = [ - FaceDirection.Unknown, - FaceDirection.Left, - FaceDirection.Up, - FaceDirection.Right, - FaceDirection.Down, - FaceDirection.LeftUp, - FaceDirection.RightUp, - FaceDirection.LeftDown, - FaceDirection.RightDown -]; - -/** 每个朝向对应的单步坐标偏移量 */ -const FACE_MOVEMENTS: readonly [FaceDirection, Loc][] = [ - [FaceDirection.Unknown, { x: 0, y: 0 }], - [FaceDirection.Left, { x: -1, y: 0 }], - [FaceDirection.Up, { x: 0, y: -1 }], - [FaceDirection.Right, { x: 1, y: 0 }], - [FaceDirection.Down, { x: 0, y: 1 }], - [FaceDirection.LeftUp, { x: -1, y: -1 }], - [FaceDirection.RightUp, { x: 1, y: -1 }], - [FaceDirection.LeftDown, { x: -1, y: 1 }], - [FaceDirection.RightDown, { x: 1, y: 1 }] -]; - -describe('getFaceMovement', () => { - // 验证全部朝向(含未知)都返回唯一且正确的坐标偏移量 - it('returns the coordinate offset for every face direction', () => { - for (const [dir, movement] of FACE_MOVEMENTS) { - expect(getFaceMovement(dir)).toEqual(movement); - } - expect(FACE_MOVEMENTS.map(item => item[0]).sort()).toEqual( - [...ALL_DIRECTIONS].sort() - ); - }); -}); - -describe('degradeFace', () => { - // 验证对角朝向降级为对应的左/右,正交朝向原样返回 - it('degrades diagonals into their orthogonal direction', () => { - expect(degradeFace(FaceDirection.LeftUp)).toBe(FaceDirection.Left); - expect(degradeFace(FaceDirection.LeftDown)).toBe(FaceDirection.Left); - expect(degradeFace(FaceDirection.RightUp)).toBe(FaceDirection.Right); - expect(degradeFace(FaceDirection.RightDown)).toBe(FaceDirection.Right); - expect(degradeFace(FaceDirection.Left)).toBe(FaceDirection.Left); - expect(degradeFace(FaceDirection.Right)).toBe(FaceDirection.Right); - expect(degradeFace(FaceDirection.Up)).toBe(FaceDirection.Up); - expect(degradeFace(FaceDirection.Down)).toBe(FaceDirection.Down); - }); - - // 验证未知朝向默认返回未知,且可经 unknown 参数自定义返回值 - it('maps unknown to the configurable fallback', () => { - expect(degradeFace(FaceDirection.Unknown)).toBe(FaceDirection.Unknown); - expect(degradeFace(FaceDirection.Unknown, FaceDirection.Down)).toBe( - FaceDirection.Down - ); - }); -}); - -describe('nextFaceDirection', () => { - // 验证四方向顺时针旋转顺序为上→右→下→左 - it('rotates four directions clockwise', () => { - expect(nextFaceDirection(FaceDirection.Up)).toBe(FaceDirection.Right); - expect(nextFaceDirection(FaceDirection.Right)).toBe(FaceDirection.Down); - expect(nextFaceDirection(FaceDirection.Down)).toBe(FaceDirection.Left); - expect(nextFaceDirection(FaceDirection.Left)).toBe(FaceDirection.Up); - }); - - // 验证四方向逆时针旋转顺序为上→左→下→右 - it('rotates four directions anticlockwise', () => { - expect(nextFaceDirection(FaceDirection.Up, true)).toBe( - FaceDirection.Left - ); - expect(nextFaceDirection(FaceDirection.Left, true)).toBe( - FaceDirection.Down - ); - expect(nextFaceDirection(FaceDirection.Down, true)).toBe( - FaceDirection.Right - ); - expect(nextFaceDirection(FaceDirection.Right, true)).toBe( - FaceDirection.Up - ); - }); - - // 验证四方向模式下对角朝向同样按九十度旋转 - it('rotates diagonal directions in four-way mode', () => { - expect(nextFaceDirection(FaceDirection.LeftUp)).toBe( - FaceDirection.RightUp - ); - expect(nextFaceDirection(FaceDirection.RightUp)).toBe( - FaceDirection.RightDown - ); - expect(nextFaceDirection(FaceDirection.RightDown)).toBe( - FaceDirection.LeftDown - ); - expect(nextFaceDirection(FaceDirection.LeftDown)).toBe( - FaceDirection.LeftUp - ); - }); - - // 验证八方向顺时针按四十五度依次推进一整圈 - it('rotates eight directions clockwise', () => { - expect(nextFaceDirection(FaceDirection.Up, false, true)).toBe( - FaceDirection.RightUp - ); - expect(nextFaceDirection(FaceDirection.RightUp, false, true)).toBe( - FaceDirection.Right - ); - expect(nextFaceDirection(FaceDirection.Right, false, true)).toBe( - FaceDirection.RightDown - ); - expect(nextFaceDirection(FaceDirection.RightDown, false, true)).toBe( - FaceDirection.Down - ); - expect(nextFaceDirection(FaceDirection.Down, false, true)).toBe( - FaceDirection.LeftDown - ); - expect(nextFaceDirection(FaceDirection.LeftDown, false, true)).toBe( - FaceDirection.Left - ); - expect(nextFaceDirection(FaceDirection.Left, false, true)).toBe( - FaceDirection.LeftUp - ); - expect(nextFaceDirection(FaceDirection.LeftUp, false, true)).toBe( - FaceDirection.Up - ); - }); - - // 验证八方向逆时针按四十五度依次回退一整圈 - it('rotates eight directions anticlockwise', () => { - expect(nextFaceDirection(FaceDirection.Up, true, true)).toBe( - FaceDirection.LeftUp - ); - expect(nextFaceDirection(FaceDirection.LeftUp, true, true)).toBe( - FaceDirection.Left - ); - expect(nextFaceDirection(FaceDirection.Left, true, true)).toBe( - FaceDirection.LeftDown - ); - expect(nextFaceDirection(FaceDirection.LeftDown, true, true)).toBe( - FaceDirection.Down - ); - expect(nextFaceDirection(FaceDirection.Down, true, true)).toBe( - FaceDirection.RightDown - ); - expect(nextFaceDirection(FaceDirection.RightDown, true, true)).toBe( - FaceDirection.Right - ); - expect(nextFaceDirection(FaceDirection.Right, true, true)).toBe( - FaceDirection.RightUp - ); - expect(nextFaceDirection(FaceDirection.RightUp, true, true)).toBe( - FaceDirection.Up - ); - }); - - // 验证未知朝向在所有旋转模式下都原样透传 - it('passes unknown through every rotation mode', () => { - expect(nextFaceDirection(FaceDirection.Unknown)).toBe( - FaceDirection.Unknown - ); - expect(nextFaceDirection(FaceDirection.Unknown, true)).toBe( - FaceDirection.Unknown - ); - expect(nextFaceDirection(FaceDirection.Unknown, false, true)).toBe( - FaceDirection.Unknown - ); - expect(nextFaceDirection(FaceDirection.Unknown, true, true)).toBe( - FaceDirection.Unknown - ); - }); -}); - -describe('fromDirectionString', () => { - // 验证每个合法朝向字符串都解析为对应枚举值 - it('parses every direction string', () => { - expect(fromDirectionString('left')).toBe(FaceDirection.Left); - expect(fromDirectionString('right')).toBe(FaceDirection.Right); - expect(fromDirectionString('up')).toBe(FaceDirection.Up); - expect(fromDirectionString('down')).toBe(FaceDirection.Down); - expect(fromDirectionString('leftup')).toBe(FaceDirection.LeftUp); - expect(fromDirectionString('rightup')).toBe(FaceDirection.RightUp); - expect(fromDirectionString('leftdown')).toBe(FaceDirection.LeftDown); - expect(fromDirectionString('rightdown')).toBe(FaceDirection.RightDown); - }); - - // 验证无法识别的字符串回退为未知朝向 - it('falls back to unknown for an unrecognized string', () => { - const invalid = 'sideways' as Dir2; - expect(fromDirectionString(invalid)).toBe(FaceDirection.Unknown); - }); -}); diff --git a/packages-user/data-common/src/replay/__test__/array.test.ts b/packages-user/data-common/src/replay/__test__/array.test.ts index 2939b6c..b3fc5c5 100644 --- a/packages-user/data-common/src/replay/__test__/array.test.ts +++ b/packages-user/data-common/src/replay/__test__/array.test.ts @@ -572,13 +572,13 @@ describe('ReplayArray param codec', () => { ]); }); - // 验证短字符串参数使用内联类型 token(长度 + 9)并读回一致 + // 验证短字符串参数使用内联类型 token(长度 + 32)并读回一致 it('round-trips a short string with an inline type token', () => { const array = createArray(); array.add(0, ['hi']); expect(array.get(0).params).toEqual(['hi']); - expect(firstParamToken(array)).toBe(11); + expect(firstParamToken(array)).toBe(34); }); // 验证超过内联长度的字符串参数使用 type 9 并读回一致 @@ -591,13 +591,13 @@ describe('ReplayArray param codec', () => { expect(firstParamToken(array)).toBe(9); }); - // 验证空字符串参数回退到 type 9 并读回为空串 + // 验证空字符串参数按内联类型码 32(长度 0)并读回为空串 it('round-trips an empty string', () => { const array = createArray(); array.add(0, ['']); expect(array.get(0).params).toEqual(['']); - expect(firstParamToken(array)).toBe(9); + expect(firstParamToken(array)).toBe(32); }); // 验证单条录像步的多个不同类型参数按顺序完整读回 @@ -791,7 +791,7 @@ describe('ReplayArray param codec', () => { }); describe('ReplayArray param type table', () => { - // 参数类型码表只增不改:锚定 float=6、非负 bigint=7、负 bigint=8、短字符串基址 9、负 int64=5 + // 参数类型码表只增不改:锚定 float=6、非负 bigint=7、负 bigint=8、短字符串基址 32、负 int64=5 it('keeps the param type code mapping stable', () => { const floatArray = createArray(); floatArray.add(0, [1.5]); @@ -811,7 +811,7 @@ describe('ReplayArray param type table', () => { const shortStringArray = createArray(); shortStringArray.add(0, ['hi']); - expect(firstParamToken(shortStringArray)).toBe(11); + expect(firstParamToken(shortStringArray)).toBe(34); const longStringArray = createArray(); longStringArray.add(0, ['a'.repeat(300)]); diff --git a/packages-user/data-common/src/replay/__test__/sandbox.test.ts b/packages-user/data-common/src/replay/__test__/sandbox.test.ts index 140b914..3f52598 100644 --- a/packages-user/data-common/src/replay/__test__/sandbox.test.ts +++ b/packages-user/data-common/src/replay/__test__/sandbox.test.ts @@ -2,7 +2,13 @@ import { logger } from '@motajs/common'; import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'; import { ReplaySystem } from '../system'; -import { IReplayCommand, IReplaySandbox, IReplayStepHandler } from '../types'; +import { + IReplayCommand, + IReplaySandbox, + IReplayStepHandler, + ReplayCommandResult, + ReplayCommandType +} from '../types'; vi.hoisted(() => { vi.stubGlobal('main', { replayChecking: true }); @@ -22,12 +28,14 @@ interface IManualReplaySandbox extends IReplaySandbox { pausing: boolean; } -// 构造一个可注入执行与收尾行为的录像命令 +// 构造一个可注入执行与收尾行为的主动录像命令 function createCommand( - execute: (step: IReplayStepHandler) => Promise, - notExecuted?: () => Promise + execute: (step: IReplayStepHandler) => Promise, + finalize?: () => Promise ): IReplayCommand { - return notExecuted ? { execute, finalize: notExecuted } : { execute }; + return finalize + ? { type: ReplayCommandType.Active, execute, finalize } + : { type: ReplayCommandType.Active, execute }; } // 用录像系统构造一个可手动驱动的录像沙箱 @@ -53,7 +61,7 @@ async function waitForEnded(sandbox: IReplaySandbox): Promise { // 生成一个可外部兑现的异步结果 function createDeferred() { - return Promise.withResolvers(); + return Promise.withResolvers(); } describe('ReplaySandbox stepping', () => { @@ -65,19 +73,17 @@ describe('ReplaySandbox stepping', () => { 1, createCommand(async step => { executed.push(step.index); - return true; + return ReplayCommandResult.Success; }) ); system.record(1, 5); const sandbox = createSandbox(system); const stepped: IReplayStepHandler[] = []; - sandbox - .addHook({ - onStep: async step => { - stepped.push(step); - } - }) - .load(); + sandbox.addHook({ + onStep: async step => { + stepped.push(step); + } + }); start(sandbox); await expect(sandbox.step()).resolves.toBe(true); @@ -88,23 +94,23 @@ describe('ReplaySandbox stepping', () => { expect(sandbox.getReplayed()).toBe(1); }); - // 验证执行失败的命令触发告警码 158 并停止后续步骤 - it('warns code 158 and stops when a command returns false', async () => { - const warn = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + // 验证执行失败的命令触发错误码 72 并停止后续步骤 + it('errors code 72 and stops when a command returns false', async () => { + const error = vi.spyOn(logger, 'error').mockImplementation(() => {}); const executed: number[] = []; const system = new ReplaySystem(); system.registerCommand( 1, createCommand(async () => { executed.push(1); - return false; + return ReplayCommandResult.Failed; }) ); system.registerCommand( 2, createCommand(async () => { executed.push(2); - return true; + return ReplayCommandResult.Success; }) ); system.record(1); @@ -115,8 +121,8 @@ describe('ReplaySandbox stepping', () => { await expect(sandbox.step()).resolves.toBe(false); expect(executed).toEqual([1]); - expect(warn).toHaveBeenCalledWith(158, '1', '[]'); - expect(sandbox.getReplayed()).toBe(1); + expect(error).toHaveBeenCalledWith(72, 'execute'); + expect(sandbox.getReplayed()).toBe(2); }); // 验证未知指令触发告警码 157 并返回 false @@ -141,11 +147,11 @@ describe('ReplaySandbox stepping', () => { createCommand( async () => { order.push('exec1'); - return true; + return ReplayCommandResult.Success; }, async () => { order.push('fin1'); - return true; + return ReplayCommandResult.Success; } ) ); @@ -153,7 +159,7 @@ describe('ReplaySandbox stepping', () => { 2, createCommand(async () => { order.push('exec2'); - return true; + return ReplayCommandResult.Success; }) ); system.record(1); @@ -167,20 +173,20 @@ describe('ReplaySandbox stepping', () => { expect(order).toEqual(['exec1', 'fin1', 'exec2']); }); - // 验证 notExecuted 收尾失败触发告警码 175 并停止 - it('warns code 175 when notExecuted fails', async () => { - const warn = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + // 验证收尾失败触发错误码 72 并停止 + it('errors code 72 when finalize fails', async () => { + const error = vi.spyOn(logger, 'error').mockImplementation(() => {}); const system = new ReplaySystem(); system.registerCommand( 1, createCommand( - async () => true, - async () => false + async () => ReplayCommandResult.Success, + async () => ReplayCommandResult.Failed ) ); system.registerCommand( 2, - createCommand(async () => true) + createCommand(async () => ReplayCommandResult.Success) ); system.record(1); system.record(2); @@ -190,7 +196,7 @@ describe('ReplaySandbox stepping', () => { await expect(sandbox.step()).resolves.toBe(true); await expect(sandbox.step()).resolves.toBe(false); - expect(warn).toHaveBeenCalledWith(175, '1'); + expect(error).toHaveBeenCalledWith(72, 'finalize'); }); // 验证读取流过期时触发告警码 156 并返回 false @@ -199,7 +205,7 @@ describe('ReplaySandbox stepping', () => { const system = new ReplaySystem(); system.registerCommand( 1, - createCommand(async () => true) + createCommand(async () => ReplayCommandResult.Success) ); system.record(1); const sandbox = createSandbox(system); @@ -219,7 +225,7 @@ describe('ReplaySandbox stepping', () => { 1, createCommand(async step => { executed.push(step.index); - return true; + return ReplayCommandResult.Success; }) ); system.record(1); @@ -247,14 +253,14 @@ describe('ReplaySandbox playback control', () => { 1, createCommand(async step => { order.push(step.index); - return true; + return ReplayCommandResult.Success; }) ); system.record(1); system.record(1); const sandbox = createSandbox(system); const startHook = vi.fn(); - sandbox.addHook({ onStartReplay: startHook }).load(); + sandbox.addHook({ onStartReplay: startHook }); sandbox.play(); await waitForEnded(sandbox); @@ -276,11 +282,11 @@ describe('ReplaySandbox playback control', () => { system.record(1); const sandbox = createSandbox(system); const startHook = vi.fn(); - sandbox.addHook({ onStartReplay: startHook }).load(); + sandbox.addHook({ onStartReplay: startHook }); sandbox.play(); sandbox.play(); - deferred.resolve(true); + deferred.resolve(ReplayCommandResult.Success); await waitForEnded(sandbox); expect(startHook).toHaveBeenCalledTimes(1); @@ -291,12 +297,12 @@ describe('ReplaySandbox playback control', () => { const system = new ReplaySystem(); system.registerCommand( 1, - createCommand(async () => true) + createCommand(async () => ReplayCommandResult.Success) ); system.record(1); const sandbox = createSandbox(system); const startHook = vi.fn(); - sandbox.addHook({ onStartReplay: startHook }).load(); + sandbox.addHook({ onStartReplay: startHook }); sandbox.play(); await waitForEnded(sandbox); @@ -315,7 +321,9 @@ describe('ReplaySandbox playback control', () => { 1, createCommand(step => { order.push(step.index); - return step.index === 1 ? first.promise : Promise.resolve(true); + return step.index === 1 + ? first.promise + : Promise.resolve(ReplayCommandResult.Success); }) ); system.record(1); @@ -323,16 +331,14 @@ describe('ReplaySandbox playback control', () => { const sandbox = createSandbox(system); const pauseHook = vi.fn(); const resumeHook = vi.fn(); - sandbox - .addHook({ - onPauseReplay: pauseHook, - onResumeReplay: resumeHook - }) - .load(); + sandbox.addHook({ + onPauseReplay: pauseHook, + onResumeReplay: resumeHook + }); sandbox.play(); const paused = sandbox.pause(); - first.resolve(true); + first.resolve(ReplayCommandResult.Success); await paused; expect(pauseHook).toHaveBeenCalledTimes(1); @@ -359,16 +365,14 @@ describe('ReplaySandbox playback control', () => { const sandbox = createSandbox(system); const pauseHook = vi.fn(); const stopHook = vi.fn(); - sandbox - .addHook({ - onPauseReplay: pauseHook, - onStopReplay: stopHook - }) - .load(); + sandbox.addHook({ + onPauseReplay: pauseHook, + onStopReplay: stopHook + }); sandbox.play(); const stopped = sandbox.stop(); - first.resolve(true); + first.resolve(ReplayCommandResult.Success); await stopped; expect(pauseHook).toHaveBeenCalledTimes(1); @@ -382,7 +386,7 @@ describe('ReplaySandbox playback control', () => { const system = new ReplaySystem(); system.registerCommand( 1, - createCommand(async () => true) + createCommand(async () => ReplayCommandResult.Success) ); system.record(1); const sandbox = createSandbox(system); @@ -400,7 +404,7 @@ describe('ReplaySandbox playback control', () => { const system = new ReplaySystem(); const sandbox = createSandbox(system); const speedHook = vi.fn(); - sandbox.addHook({ onSpeedSet: speedHook }).load(); + sandbox.addHook({ onSpeedSet: speedHook }); sandbox.setSpeed(2); diff --git a/packages-user/data-common/src/replay/__test__/system.test.ts b/packages-user/data-common/src/replay/__test__/system.test.ts index 2b46039..b57fd97 100644 --- a/packages-user/data-common/src/replay/__test__/system.test.ts +++ b/packages-user/data-common/src/replay/__test__/system.test.ts @@ -75,13 +75,11 @@ describe('ReplaySystem registration and recording', () => { const recorded: Array< readonly [number, number, readonly ReplayParamValue[]] > = []; - system - .addHook({ - onRecordCommand: (code, index, params) => { - recorded.push([code, index, params]); - } - }) - .load(); + system.addHook({ + onRecordCommand: (code, index, params) => { + recorded.push([code, index, params]); + } + }); system.record(5, 1, true); @@ -91,7 +89,7 @@ describe('ReplaySystem registration and recording', () => { params: [1, true], index: 0 }); - expect(recorded).toEqual([[5, 1, [1, true]]]); + expect(recorded).toEqual([[5, 0, [1, true]]]); }); // 验证每个 ReplaySystem 实例的注册与录像相互独立 @@ -113,13 +111,11 @@ describe('ReplaySystem sandbox lifecycle', () => { const system = new ReplaySystem(); const { calls, reseter } = createReseter(); const created: IReplaySandbox[] = []; - system - .addHook({ - onCreateSandbox: sandbox => { - created.push(sandbox); - } - }) - .load(); + system.addHook({ + onCreateSandbox: sandbox => { + created.push(sandbox); + } + }); const save = new Map([['hp', 10]]); const sandbox = system.createReplaySandbox({ diff --git a/packages-user/data-common/src/replay/sandbox.ts b/packages-user/data-common/src/replay/sandbox.ts index c7c9903..aed2ac6 100644 --- a/packages-user/data-common/src/replay/sandbox.ts +++ b/packages-user/data-common/src/replay/sandbox.ts @@ -264,6 +264,9 @@ export class ReplaySandbox if (this.playing || this.ended) return; this.pausing = false; this.playing = true; + if (!this.appendingStep && !this.reader.expired) { + this.appendingStep = this.reader.read(); + } this.forEachHook(hook => hook.onStartReplay?.()); this.startReplayLoop(); } @@ -292,6 +295,14 @@ export class ReplaySandbox } async step(): Promise { + if ( + this.playing && + !this.appendingStep && + !this.reader.expired && + !this.ended + ) { + this.appendingStep = this.reader.read(); + } const res = await this.replayStep(); if (res.success) { await Promise.all(