fix(07-13): clear existing dynamic tiles before loading

- add private clearDynamics() that snapshots the managed dynamic tiles and drops each via the deleteDynamic semantics (syncStaticEvent + removeTile + onDeleteDynamic), without awaiting the hook
- call it as the first line of loadState so all three compression tiers and the missing-dynamicBlocks case start from a clean slate (#06-17-8)
- add regressions: mapLayer hook/instance witness and the three-tier repeated-load non-accumulation case
This commit is contained in:
unanmed 2026-09-17 17:27:09 +08:00
parent c1df1e0116
commit 13234b762a
3 changed files with 57 additions and 0 deletions

View File

@ -9,6 +9,7 @@ import {
FaceGroup,
FaceManager,
RoleFaceBinder,
SaveCompression,
TileStore,
TileType
} from '@user/data-common';
@ -590,6 +591,30 @@ describe('MapLayer dynamic conversion', () => {
expect(result.info.map(info => info.code)).toContain(130);
await result.ret;
});
// 验证 loadState 先清空既有动态块并触发删除钩子(#06-17-8 读档不累积)
it('clears existing dynamic tiles before loading', () => {
const { layer } = createFixture([0, 0, 0, 0]);
const deleted: IDynamicTile[] = [];
layer
.addHook({
onDeleteDynamic: async tile => {
deleted.push(tile);
}
})
.load();
const first = layer.createDynamic(1, 0, 0);
const second = layer.createDynamic(1, 1, 0);
const saved = layer.saveState(SaveCompression.NoCompression);
const extra = layer.createDynamic(1, 0, 1);
layer.loadState(saved, SaveCompression.NoCompression);
expect([...layer.iterateDynamicTiles()]).toHaveLength(2);
expect([...layer.getDynamicTilesAt(0, 1)]).toHaveLength(0);
expect(deleted).toEqual([first, second, extra]);
expect([...layer.iterateDynamicTiles()]).not.toContain(first);
});
});
describe('MapLayer dirty state and doors', () => {

View File

@ -848,6 +848,19 @@ export class MapLayer
}
}
/**
* 清空本图层全部既有动态图块,使读档后的动态块与存档点一致
* 逐块复用删除语义,会触发 `onDeleteDynamic` 钩子,但读档为同步流程,不等待钩子完成
*/
private clearDynamics(): void {
const tiles = [...this.iterateDynamicTiles()];
for (const tile of tiles) {
this.syncStaticEvent(tile, false);
this.removeTile(tile);
this.forEachHook(hook => hook.onDeleteDynamic?.(tile));
}
}
/**
* 以无压缩方式读取当前图层
* @param save 图层存档
@ -920,6 +933,7 @@ export class MapLayer
}
loadState(save: IMapLayerSave, compression: SaveCompression): void {
this.clearDynamics();
if (compression === SaveCompression.HighCompression) {
this.loadHighCompression(save);
} else if (compression === SaveCompression.LowCompression) {

View File

@ -215,6 +215,24 @@ describe('MapLayer save and load round trips', () => {
expect(lowResult.info.map(info => info.code)).toContain(124);
expect(highResult.info.map(info => info.code)).toContain(124);
});
// 验证动态块多于存档时读档后等于存档,且重复读档不累积(#06-17-8)
it('does not accumulate dynamic tiles when loading fewer than present', () => {
for (const compression of SAVE_COMPRESSIONS) {
const { layer } = createMapFixture();
setLayerReference(layer);
layer.createDynamic(1, 0, 0);
const saved = layer.saveState(compression);
layer.createDynamic(1, 1, 0);
layer.createDynamic(1, 1, 0);
layer.loadState(saved, compression);
expect([...layer.iterateDynamicTiles()]).toHaveLength(1);
layer.loadState(saved, compression);
expect([...layer.iterateDynamicTiles()]).toHaveLength(1);
}
});
});
describe('GameMap save and load round trips', () => {