mirror of
https://github.com/motajs/template.git
synced 2026-09-25 20:10:17 +08:00
test(06-09): cover map, replay and flag save/load round trips
- add map/saveLoad.test.ts: StaticTile/DynamicTile/MapLayer/GameMap/MapState round trips, codes 55/122/124 - add replay/saveLoad.test.ts: ReplayArray and ReplaySystem same-instance round trips - add flag/saveLoad.test.ts: FlagSystem round trip and separated snapshot
This commit is contained in:
parent
3e29548bfa
commit
c707c87add
78
packages-user/data-base/src/flag/saveLoad.test.ts
Normal file
78
packages-user/data-base/src/flag/saveLoad.test.ts
Normal file
@ -0,0 +1,78 @@
|
||||
// 测试全局 Flag 系统存读档:同实例往返、占用状态与存档克隆
|
||||
import { afterAll, describe, expect, it, vi } from 'vitest';
|
||||
import { FlagSystem } from './system';
|
||||
|
||||
vi.hoisted(() => {
|
||||
vi.stubGlobal('main', { replayChecking: true });
|
||||
vi.stubGlobal('location', { origin: 'http://localhost' });
|
||||
Map.prototype.getOrInsert ??= function <K, V>(
|
||||
this: Map<K, V>,
|
||||
key: K,
|
||||
value: V
|
||||
): V {
|
||||
const existing = this.get(key);
|
||||
if (existing !== undefined) return existing;
|
||||
this.set(key, value);
|
||||
return value;
|
||||
};
|
||||
Map.prototype.getOrInsertComputed ??= function <K, V>(
|
||||
this: Map<K, V>,
|
||||
key: K,
|
||||
callback: (key: K) => V
|
||||
): V {
|
||||
const existing = this.get(key);
|
||||
if (existing !== undefined) return existing;
|
||||
const value = callback(key);
|
||||
this.set(key, value);
|
||||
return value;
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('FlagSystem save and load round trips', () => {
|
||||
// 验证字段数值与占用状态在同实例上恢复
|
||||
it('restores field values and occupancy on the same instance', () => {
|
||||
const system = new FlagSystem();
|
||||
system.setFieldValue('score', 7);
|
||||
system.addFieldValue('score', 5);
|
||||
system.setFieldValue('name', 'hero');
|
||||
|
||||
const saved = system.saveState();
|
||||
system.setFieldValue('score', 0);
|
||||
system.deleteField('name');
|
||||
system.loadState(saved);
|
||||
|
||||
expect(system.getFieldValue<number>('score')).toBe(12);
|
||||
expect(system.occupied('score')).toBe(true);
|
||||
expect(system.getFieldValue<string>('name')).toBe('hero');
|
||||
expect(system.occupied('name')).toBe(true);
|
||||
});
|
||||
|
||||
// 验证 saveState 返回分离克隆,修改存档不会影响系统
|
||||
it('returns a separated clone from saveState', () => {
|
||||
const system = new FlagSystem();
|
||||
system.setFieldValue('score', 7);
|
||||
|
||||
const saved = system.saveState();
|
||||
saved.fields.set('score', 999);
|
||||
saved.fields.set('extra', 1);
|
||||
|
||||
expect(system.getFieldValue<number>('score')).toBe(7);
|
||||
expect(system.occupied('extra')).toBe(false);
|
||||
});
|
||||
|
||||
// 验证 loadState 会重建字段对象并保留恢复值
|
||||
it('rebuilds field objects on loadState', () => {
|
||||
const system = new FlagSystem();
|
||||
const before = system.insertField('score', 7);
|
||||
|
||||
const saved = system.saveState();
|
||||
system.loadState(saved);
|
||||
|
||||
expect(system.getField('score')).not.toBe(before);
|
||||
expect(system.getFieldValue<number>('score')).toBe(7);
|
||||
});
|
||||
});
|
||||
292
packages-user/data-base/src/map/saveLoad.test.ts
Normal file
292
packages-user/data-base/src/map/saveLoad.test.ts
Normal file
@ -0,0 +1,292 @@
|
||||
// 测试地图系统存读档:tile/MapLayer/GameMap/MapState 同实例往返、压缩档与码 55/122/124
|
||||
import { afterAll, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
type IDataCommon,
|
||||
Dir8FaceHandler,
|
||||
FaceGroup,
|
||||
FaceManager,
|
||||
RoleFaceBinder,
|
||||
SaveCompression,
|
||||
TileStore,
|
||||
TileType
|
||||
} from '@user/data-common';
|
||||
import { DirectionMapper, logger } from '@motajs/common';
|
||||
import { type IGameMap, type IResizableMapLayer } from './types';
|
||||
import { MapState } from './mapState';
|
||||
|
||||
vi.hoisted(() => {
|
||||
vi.stubGlobal('main', { replayChecking: true });
|
||||
vi.stubGlobal('location', { origin: 'http://localhost' });
|
||||
Map.prototype.getOrInsert ??= function <K, V>(
|
||||
this: Map<K, V>,
|
||||
key: K,
|
||||
value: V
|
||||
): V {
|
||||
const existing = this.get(key);
|
||||
if (existing !== undefined) return existing;
|
||||
this.set(key, value);
|
||||
return value;
|
||||
};
|
||||
Map.prototype.getOrInsertComputed ??= function <K, V>(
|
||||
this: Map<K, V>,
|
||||
key: K,
|
||||
callback: (key: K) => V
|
||||
): V {
|
||||
const existing = this.get(key);
|
||||
if (existing !== undefined) return existing;
|
||||
const value = callback(key);
|
||||
this.set(key, value);
|
||||
return value;
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
/** 存读档测试覆盖的三档压缩级别 */
|
||||
const SAVE_COMPRESSIONS = [
|
||||
SaveCompression.NoCompression,
|
||||
SaveCompression.LowCompression,
|
||||
SaveCompression.HighCompression
|
||||
] as const;
|
||||
|
||||
interface MapFixture {
|
||||
mapState: MapState;
|
||||
map: IGameMap;
|
||||
layer: IResizableMapLayer;
|
||||
}
|
||||
|
||||
/** 构造一个含事件层的最小地图状态与楼层,可传入点事件与初始图块 */
|
||||
function createMapFixture(
|
||||
pointEvents: Record<number, Record<number, string>> = {},
|
||||
blocks: number[] = [1, 1, 1, 1]
|
||||
): MapFixture {
|
||||
const tileStore = new TileStore();
|
||||
tileStore.addTile({
|
||||
num: 1,
|
||||
id: 'base',
|
||||
events: { 10: 'base-event' },
|
||||
type: TileType.Terrain,
|
||||
pass: { onlyEvents: false, outPass: 15, inPass: 15 },
|
||||
eventPass: true
|
||||
});
|
||||
tileStore.addTile({
|
||||
num: 2,
|
||||
id: 'alternate',
|
||||
events: { 20: 'alternate-event' },
|
||||
type: TileType.Terrain,
|
||||
pass: { onlyEvents: false, outPass: 15, inPass: 15 },
|
||||
eventPass: true
|
||||
});
|
||||
const faceManager = new FaceManager();
|
||||
faceManager.register(FaceGroup.Dir8, new Dir8FaceHandler());
|
||||
const state: IDataCommon = {
|
||||
tileStore,
|
||||
itemStore: {},
|
||||
mapStore: {},
|
||||
eventStore: {},
|
||||
roleFace: new RoleFaceBinder(),
|
||||
faceManager,
|
||||
directionMapper: new DirectionMapper(),
|
||||
saveSystem: {}
|
||||
} as never;
|
||||
const mapState = new MapState(tileStore, state);
|
||||
const map = mapState.fromRaw({
|
||||
floorId: 'F1',
|
||||
width: 2,
|
||||
map: { 0: blocks },
|
||||
layerAlias: { 0: 'event' },
|
||||
events: { 0: pointEvents }
|
||||
});
|
||||
return {
|
||||
mapState,
|
||||
map: map!,
|
||||
layer: map!.getLayerByAlias('event')! as IResizableMapLayer
|
||||
};
|
||||
}
|
||||
|
||||
/** 用当前图层矩阵作为参考基准,使 Low/High 压缩档具备可恢复基准 */
|
||||
function setLayerReference(layer: IResizableMapLayer): void {
|
||||
layer.compareWith(new Uint32Array(layer.getMapData()));
|
||||
}
|
||||
|
||||
describe('StaticTile save and load round trips', () => {
|
||||
// 验证静态图块覆盖事件在同实例上恢复且保存返回分离的事件映射
|
||||
it('restores covered events on the same instance', () => {
|
||||
const { layer } = createMapFixture();
|
||||
const tile = layer.getTile(0, 0)!;
|
||||
tile.tileEvent().set(30, 'override-event');
|
||||
|
||||
const saved = tile.saveState(SaveCompression.NoCompression);
|
||||
const snapshot = new Map(saved.events!);
|
||||
tile.tileEvent().set(30, 'changed-after-save');
|
||||
|
||||
expect(saved.events).toEqual(snapshot);
|
||||
|
||||
tile.loadState(saved, SaveCompression.NoCompression);
|
||||
|
||||
expect(tile.tileEvent().get()).toEqual(
|
||||
new Map([
|
||||
[10, 'base-event'],
|
||||
[30, 'override-event']
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DynamicTile save and load round trips', () => {
|
||||
// 验证动态图块覆盖事件在同实例上恢复,存档记录当前图块数字
|
||||
it('restores covered events on the same instance', () => {
|
||||
const { layer } = createMapFixture();
|
||||
const tile = layer.createDynamic(1, 1, 0);
|
||||
tile.tileEvent().set(30, 'override-event');
|
||||
|
||||
const saved = tile.saveState(SaveCompression.NoCompression);
|
||||
tile.tileEvent().set(30, 'changed-after-save');
|
||||
tile.loadState(saved, SaveCompression.NoCompression);
|
||||
|
||||
expect(saved.num).toBe(1);
|
||||
expect(tile.num()).toBe(1);
|
||||
expect(tile.tileEvent().get()).toEqual(
|
||||
new Map([
|
||||
[10, 'base-event'],
|
||||
[30, 'override-event']
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
// 疑似 bug:loadState 不恢复存档中的图块数字,详见 06-TEST-FINDINGS.md #06-09-3
|
||||
it.skip('restores the tile num on the same instance', () => {
|
||||
const { layer } = createMapFixture();
|
||||
const tile = layer.createDynamic(1, 1, 0);
|
||||
|
||||
const saved = tile.saveState(SaveCompression.NoCompression);
|
||||
tile.set(2);
|
||||
tile.loadState(saved, SaveCompression.NoCompression);
|
||||
|
||||
expect(tile.num()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MapLayer save and load round trips', () => {
|
||||
// 验证矩阵与点事件在三个压缩档下同实例恢复
|
||||
it('restores the matrix and point events across all compressions', () => {
|
||||
for (const compression of SAVE_COMPRESSIONS) {
|
||||
const { layer } = createMapFixture({ 1: { 5: 'raw-point' } });
|
||||
setLayerReference(layer);
|
||||
const point = layer.event(1, 0)!;
|
||||
point.set(7, 'saved-point');
|
||||
layer.setBlock(9, 0, 0);
|
||||
|
||||
const saved = layer.saveState(compression);
|
||||
point.set(7, 'changed-after-save');
|
||||
layer.setBlock(0, 0, 0);
|
||||
layer.loadState(saved, compression);
|
||||
|
||||
expect(layer.getBlock(0, 0)).toBe(9);
|
||||
expect(point.get()).toEqual(
|
||||
new Map([
|
||||
[5, 'raw-point'],
|
||||
[7, 'saved-point']
|
||||
])
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 验证缺少参考基准的压缩档经 logger.catch 观测到警告码 124
|
||||
it('warns code 124 when the compression reference is missing', () => {
|
||||
const dirty = createMapFixture();
|
||||
const lowSave = dirty.layer.saveState(SaveCompression.LowCompression);
|
||||
|
||||
const lowResult = logger.catch(() =>
|
||||
dirty.layer.loadState(lowSave, SaveCompression.LowCompression)
|
||||
);
|
||||
|
||||
const highSave = dirty.layer.saveState(SaveCompression.HighCompression);
|
||||
const highResult = logger.catch(() =>
|
||||
dirty.layer.loadState(highSave, SaveCompression.HighCompression)
|
||||
);
|
||||
|
||||
expect(lowResult.info.map(info => info.code)).toContain(124);
|
||||
expect(highResult.info.map(info => info.code)).toContain(124);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GameMap save and load round trips', () => {
|
||||
// 验证背景与图层矩阵在三个压缩档下同实例恢复
|
||||
it('restores background and layer matrices across all compressions', () => {
|
||||
for (const compression of SAVE_COMPRESSIONS) {
|
||||
const { map, layer } = createMapFixture();
|
||||
setLayerReference(layer);
|
||||
map.setBackground(7);
|
||||
layer.setBlock(9, 0, 0);
|
||||
|
||||
const saved = map.saveState(compression);
|
||||
map.setBackground(0);
|
||||
layer.setBlock(0, 0, 0);
|
||||
map.loadState(saved, compression);
|
||||
|
||||
expect(map.getBackground()).toBe(7);
|
||||
expect(layer.getBlock(0, 0)).toBe(9);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('MapState save and load round trips', () => {
|
||||
// 验证激活楼层矩阵在三个压缩档下同实例恢复
|
||||
it('restores active floor matrices across all compressions', () => {
|
||||
for (const compression of SAVE_COMPRESSIONS) {
|
||||
const { mapState, map, layer } = createMapFixture();
|
||||
map.setActiveStatus(true);
|
||||
mapState.compareWith(
|
||||
new Map([
|
||||
['F1', new Map([[0, new Uint32Array(layer.getMapData())]])]
|
||||
])
|
||||
);
|
||||
layer.setBlock(9, 0, 0);
|
||||
|
||||
const saved = mapState.saveState(compression);
|
||||
layer.setBlock(0, 0, 0);
|
||||
mapState.loadState(saved, compression);
|
||||
|
||||
expect(mapState.isMapActive('F1')).toBe(true);
|
||||
expect(layer.getBlock(0, 0)).toBe(9);
|
||||
}
|
||||
});
|
||||
|
||||
// 验证非无压缩读档且未比较基准时经 logger.catch 观测到错误码 55
|
||||
it('warns code 55 when loading a compressed MapState without a reference', () => {
|
||||
const { mapState } = createMapFixture();
|
||||
|
||||
const result = logger.catch(() =>
|
||||
mapState.loadState(
|
||||
{ floors: new Map() },
|
||||
SaveCompression.LowCompression
|
||||
)
|
||||
);
|
||||
|
||||
expect(result.info.map(info => info.code)).toContain(55);
|
||||
});
|
||||
|
||||
// 验证存档中缺失楼层时经 logger.catch 观测到警告码 122
|
||||
it('warns code 122 when a floor is missing during loadState', () => {
|
||||
const { mapState, map } = createMapFixture();
|
||||
|
||||
const result = logger.catch(() =>
|
||||
mapState.loadState(
|
||||
{
|
||||
floors: new Map([
|
||||
[
|
||||
'missing',
|
||||
map.saveState(SaveCompression.NoCompression)
|
||||
]
|
||||
])
|
||||
},
|
||||
SaveCompression.NoCompression
|
||||
)
|
||||
);
|
||||
|
||||
expect(result.info.map(info => info.code)).toContain(122);
|
||||
});
|
||||
});
|
||||
124
packages-user/data-common/src/replay/saveLoad.test.ts
Normal file
124
packages-user/data-common/src/replay/saveLoad.test.ts
Normal file
@ -0,0 +1,124 @@
|
||||
// 测试录像存读档:ReplayArray 与 ReplaySystem 同实例往返与多类型参数读回
|
||||
import { afterAll, describe, expect, it, vi } from 'vitest';
|
||||
import { ReplayArray } from './array';
|
||||
import { ReplaySystem } from './system';
|
||||
import { ReplayCommandWidth } from './types';
|
||||
|
||||
vi.hoisted(() => {
|
||||
vi.stubGlobal('main', { replayChecking: true });
|
||||
vi.stubGlobal('location', { origin: 'http://localhost' });
|
||||
Map.prototype.getOrInsertComputed ??= function <K, V>(
|
||||
this: Map<K, V>,
|
||||
key: K,
|
||||
callback: (key: K) => V
|
||||
): V {
|
||||
const existing = this.get(key);
|
||||
if (existing !== undefined) return existing;
|
||||
const value = callback(key);
|
||||
this.set(key, value);
|
||||
return value;
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
interface IArrayOverrides {
|
||||
initCommandLength?: number;
|
||||
initParamLength?: number;
|
||||
commandExpandMultiplier?: number;
|
||||
paramExpandMultiplier?: number;
|
||||
commandWidth?: ReplayCommandWidth;
|
||||
commandMaxLength?: number;
|
||||
paramMaxLength?: number;
|
||||
}
|
||||
|
||||
/** 与 system.ts 初始化保持一致的构造入口,默认给足初始容量避免无关扩容 */
|
||||
function createArray(overrides: IArrayOverrides = {}): ReplayArray {
|
||||
return new ReplayArray({
|
||||
initCommandLength: 32,
|
||||
initParamLength: 128,
|
||||
commandExpandMultiplier: 2,
|
||||
paramExpandMultiplier: 2,
|
||||
commandWidth: ReplayCommandWidth.Uint8,
|
||||
commandMaxLength: 64,
|
||||
paramMaxLength: 512,
|
||||
...overrides
|
||||
});
|
||||
}
|
||||
|
||||
/** 构造一个独立的录像系统实例 */
|
||||
function createSystem(): ReplaySystem {
|
||||
return new ReplaySystem();
|
||||
}
|
||||
|
||||
describe('ReplayArray save and load round trips', () => {
|
||||
// 验证指令位宽与首步命令及多类型参数在同实例上读回一致
|
||||
it('restores the command width and reads back the first step on the same instance', () => {
|
||||
const array = createArray();
|
||||
array.add(3, [true, -128, 32768, 'hello', 5]);
|
||||
|
||||
const saved = array.saveState();
|
||||
array.add(9, [1]);
|
||||
array.loadState(saved);
|
||||
|
||||
expect(array.commandWidth).toBe(saved.metadata.commandWidth);
|
||||
expect(array.get(0)).toEqual({
|
||||
command: 3,
|
||||
params: [true, -128, 32768, 'hello', 5],
|
||||
index: 0
|
||||
});
|
||||
});
|
||||
|
||||
// 疑似 bug:loadState 未恢复录像长度与索引数组,详见 06-TEST-FINDINGS.md #06-09-4
|
||||
it.skip('restores the recorded length on the same instance', () => {
|
||||
const array = createArray();
|
||||
array.add(1, [10]);
|
||||
|
||||
const saved = array.saveState();
|
||||
array.add(2, [20]);
|
||||
array.loadState(saved);
|
||||
|
||||
expect(array.length).toBe(1);
|
||||
expect(array.get(0)).toEqual({ command: 1, params: [10], index: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('ReplaySystem save and load round trips', () => {
|
||||
// 验证已注册指令与录像步在同实例上恢复到存档点
|
||||
it('restores the route and registered commands on the same instance', () => {
|
||||
const system = createSystem();
|
||||
system.registerCommand(3, { execute: async () => true });
|
||||
system.record(3, 1, true, 'param');
|
||||
|
||||
const snapshot = system.saveState();
|
||||
system.record(4, 2);
|
||||
system.replaying = true;
|
||||
system.loadState(snapshot);
|
||||
|
||||
expect(system.route.length).toBe(1);
|
||||
expect(system.route.commandWidth).toBe(snapshot.commandWidth);
|
||||
expect(system.route.get(0)).toEqual({
|
||||
command: 3,
|
||||
params: [1, true, 'param'],
|
||||
index: 0
|
||||
});
|
||||
expect(system.getCommand(3)).not.toBeNull();
|
||||
expect(system.getCommand(4)).toBeNull();
|
||||
});
|
||||
|
||||
// 验证读档后命令与参数数组与快照关键内容一致
|
||||
it('keeps the command and param arrays consistent with the snapshot', () => {
|
||||
const system = createSystem();
|
||||
system.record(2, 7);
|
||||
|
||||
const snapshot = system.saveState();
|
||||
system.record(5, 9);
|
||||
system.loadState(snapshot);
|
||||
|
||||
expect(system.route.getCommandArray()).toBe(snapshot.commandArray);
|
||||
expect(system.route.getParamArray()).toBe(snapshot.paramArray);
|
||||
expect(system.route.get(0).command).toBe(2);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user