refactor(event): centralize point event references

This commit is contained in:
AncTe committed 2026-09-09 12:53:37 +08:00
1 parent a297311ea6
commit 64efcf2555
14 files changed
+617 -167

No files matched your search

+6 -18
View File
@@ -45,20 +45,6 @@ export class DynamicTile
this.restoreDefaultEvents();
}
/**
* 根据当前图块原始数据恢复默认事件并建立干净基准
*/
private restoreDefaultEvents(): void {
this.tileEvent().clear();
const data = this.raw();
if (data) {
for (const [priority, id] of Object.entries(data.events)) {
this.tileEvent().set(Number(priority), id);
}
}
this.tileEvent().markPure();
}
num(): number {
return this.tileNum;
}
@@ -115,10 +101,11 @@ export class DynamicTile
saveState(): Readonly<IDynamicBlockSave> {
let save: IDynamicBlockSave;
if (this.tileEvent().dirty()) {
const eventView = this.tileEvent();
if (eventView.dirty()) {
save = {
num: this.num(),
events: new Map(this.tileEvent().get())
events: new Map(eventView.get())
};
} else {
save = {
@@ -131,9 +118,10 @@ export class DynamicTile
loadState(save: Readonly<IDynamicBlockSave>): void {
this.restoreDefaultEvents();
if (save.events) {
this.tileEvent().clear();
const eventView = this.tileEvent();
eventView.clear();
for (const [priority, id] of save.events) {
this.tileEvent().set(priority, id);
eventView.set(priority, id);
}
}
}
@@ -61,6 +61,9 @@ describe('MapState raw event path', () => {
expect(layer?.getPointEvent(1, 0)).toEqual(
new Map([[5, 'point-event']])
);
expect(layer?.event(1, 0)?.ref()).toEqual(
new Map([[5, 'point-event']])
);
expect(layer?.getLocationData(1, 0)?.static.tileEvent().get()).toEqual(
new Map()
);
@@ -0,0 +1,48 @@
import { describe, expect, it, vi } from 'vitest';
import { LayerEventView } from './eventView';
vi.hoisted(() => {
vi.stubGlobal('main', { replayChecking: true });
vi.stubGlobal('location', { origin: 'http://localhost' });
});
describe('LayerEventView reference tracking', () => {
it('keeps dirty state in sync with set, delete, and clear', () => {
const view = new LayerEventView();
expect(view.ref()).toEqual(new Map());
expect(view.dirty()).toBe(false);
view.set(10, 'first');
expect(view.dirty()).toBe(true);
view.delete(10);
expect(view.dirty()).toBe(false);
view.set(10, 'first');
view.markPure();
view.set(20, 'second');
expect(view.dirty()).toBe(true);
view.clear();
expect(view.dirty()).toBe(true);
view.set(10, 'first');
expect(view.dirty()).toBe(false);
});
it('exposes a stable reference snapshot and restores it without markPure', () => {
const view = new LayerEventView();
view.set(10, 'first');
view.markPure();
const reference = view.ref();
view.set(10, 'changed');
view.clear();
for (const [priority, id] of reference) {
view.set(priority, id);
}
expect(view.ref()).toBe(reference);
expect(view.ref()).toEqual(new Map([[10, 'first']]));
expect(view.get()).toEqual(new Map([[10, 'first']]));
expect(view.dirty()).toBe(false);
});
});
+40 -14
View File
@@ -5,44 +5,70 @@ export class LayerEventView implements ILayerEventView {
/** 当前绑定的事件 */
private readonly store: Map<number, string> = new Map();
/** 用于判断事件是否变化的参考基准 */
private reference: Map<number, string> | null = null;
private reference: Map<number, string> = new Map();
/** 当前存储与参考基准不一致的条目数量 */
private dirtyEntries: number = 0;
get(): ReadonlyMap<number, string> {
return this.store;
}
ref(): ReadonlyMap<number, string> {
return this.reference;
}
/**
* 判断指定优先级当前是否偏离参考基准
* @param priority 事件优先级
*/
private isEntryDirty(priority: number): boolean {
const storeHas = this.store.has(priority);
const referenceHas = this.reference.has(priority);
return (
storeHas !== referenceHas ||
(storeHas &&
this.store.get(priority) !== this.reference.get(priority))
);
}
/**
* 根据指定条目的变更更新脏条目数量
* @param priority 事件优先级
* @param before 条目变更前是否脏
*/
private updateDirtyEntry(priority: number, before: boolean): void {
const after = this.isEntryDirty(priority);
if (before !== after) {
this.dirtyEntries += after ? 1 : -1;
}
}
set(priority: number, event: string): void {
if (this.store.has(priority)) {
logger.warn(136, priority.toString());
}
const before = this.isEntryDirty(priority);
this.store.set(priority, event);
this.updateDirtyEntry(priority, before);
}
delete(priority: number): void {
const before = this.isEntryDirty(priority);
this.store.delete(priority);
this.updateDirtyEntry(priority, before);
}
clear(): void {
this.store.clear();
this.dirtyEntries = this.reference.size;
}
markPure(): void {
this.reference = new Map(this.store);
this.dirtyEntries = 0;
}
dirty(): boolean {
const reference = this.reference;
if (!reference) {
return this.store.size > 0;
}
if (this.store.size !== reference.size) {
return true;
}
for (const [priority, event] of this.store) {
if (reference.get(priority) !== event) {
return true;
}
}
return false;
return this.dirtyEntries > 0;
}
}
+62 -99
View File
@@ -47,14 +47,6 @@ export class MapLayer
/** 坐标到动态图块集合的映射,外层 key = y,内层 key = x */
private readonly tilePosMap: Map<number, Map<number, Set<IDynamicTile>>> =
new Map();
/** 点事件视图,外层 key = y,内层 key = x */
private readonly pointEvents: Map<number, Map<number, ILayerEventView>> =
new Map();
/** 点事件的原始纯基准,外层 key = y,内层 key = x */
private readonly pointEventBaselines: Map<
number,
Map<number, ReadonlyMap<number, string>>
> = new Map();
/** 动态图块到其当前坐标的映射 */
private readonly posTileMap: Map<IDynamicTile, ITileLocator> = new Map();
/** 图层脏标记 */
@@ -62,6 +54,11 @@ export class MapLayer
/** 图层参考基准,用于存档压缩对比 */
private refArray: Uint32Array | null = null;
//#region 点事件操作
/** 点事件视图,key = y * width + x */
private readonly pointEvents: Map<number, ILayerEventView> = new Map();
constructor(
array: Uint32Array,
width: number,
@@ -138,80 +135,45 @@ export class MapLayer
if (!staticTile) return;
staticTile.set(staticTile.num());
if (keepEvent) {
staticTile.tileEvent().clear();
for (const [priority, id] of tile.tileEvent().get()) {
staticTile.tileEvent().set(priority, id);
const staticEvent = staticTile.tileEvent();
const dynamicEvent = tile.tileEvent();
staticEvent.clear();
for (const [priority, id] of dynamicEvent.get()) {
staticEvent.set(priority, id);
}
}
}
/**
* 记录尚未保存过的点事件纯基准
* @param x 点事件横坐标
* @param y 点事件纵坐标
* @param eventView 点事件视图
*/
private capturePointEventBaseline(
x: number,
y: number,
eventView: ILayerEventView
): void {
const yMap = this.pointEventBaselines.get(y);
if (yMap?.has(x)) return;
if (eventView.dirty()) return;
const target = yMap ?? new Map<number, ReadonlyMap<number, string>>();
target.set(x, new Map(eventView.get()));
if (!yMap) this.pointEventBaselines.set(y, target);
}
/**
* 将所有已创建的点事件视图恢复到原始纯基准
*/
private resetPointEvents(): void {
for (const [y, xMap] of this.pointEvents) {
for (const [x, eventView] of xMap) {
this.capturePointEventBaseline(x, y, eventView);
}
}
for (const [y, xMap] of this.pointEvents) {
for (const [x, eventView] of xMap) {
eventView.clear();
const baseline = this.pointEventBaselines.get(y)?.get(x);
if (baseline) {
for (const [priority, id] of baseline) {
eventView.set(priority, id);
}
}
eventView.markPure();
for (const eventView of this.pointEvents.values()) {
const reference = eventView.ref();
eventView.clear();
for (const [priority, id] of reference) {
eventView.set(priority, id);
}
}
}
/**
* 裁剪超出新图层范围的点事件及其纯基准
* 裁剪超出新图层范围的点事件,并按新宽度重建索引
* @param width 新图层宽度
* @param height 新图层高度
*/
private cropPointEvents(width: number, height: number): void {
for (const [y, xMap] of this.pointEvents) {
if (y >= height) {
this.pointEvents.delete(y);
continue;
const pointEvents = new Map<number, ILayerEventView>();
for (const [index, eventView] of this.pointEvents) {
const x = index % this.width;
const y = Math.floor(index / this.width);
if (x < width && y < height) {
pointEvents.set(y * width + x, eventView);
}
for (const x of xMap.keys()) {
if (x >= width) xMap.delete(x);
}
if (xMap.size === 0) this.pointEvents.delete(y);
}
for (const [y, xMap] of this.pointEventBaselines) {
if (y >= height) {
this.pointEventBaselines.delete(y);
continue;
}
for (const x of xMap.keys()) {
if (x >= width) xMap.delete(x);
}
if (xMap.size === 0) this.pointEventBaselines.delete(y);
this.pointEvents.clear();
for (const [index, eventView] of pointEvents) {
this.pointEvents.set(index, eventView);
}
}
@@ -220,12 +182,9 @@ export class MapLayer
*/
private savePointEvents(): Map<number, ReadonlyMap<number, string>> {
const pointEvents = new Map<number, ReadonlyMap<number, string>>();
for (const [y, xMap] of this.pointEvents) {
for (const [x, eventView] of xMap) {
if (!eventView.dirty()) continue;
const index = this.map.indexer.locToIndex(x, y);
pointEvents.set(index, new Map(eventView.get()));
}
for (const [index, eventView] of this.pointEvents) {
if (!eventView.dirty()) continue;
pointEvents.set(index, new Map(eventView.get()));
}
return pointEvents;
}
@@ -240,7 +199,8 @@ export class MapLayer
this.resetPointEvents();
if (!save) return;
for (const [index, events] of save) {
const { x, y } = this.map.indexer.indexToLocator(index);
const x = index % this.width;
const y = Math.floor(index / this.width);
const eventView = this.event(x, y);
if (!eventView) continue;
eventView.clear();
@@ -250,6 +210,31 @@ export class MapLayer
}
}
event(x: number, y: number): ILayerEventView | null {
if (!this.inMap(x, y)) return null;
const index = y * this.width + x;
const getOrInsertComputed = this.pointEvents.getOrInsertComputed;
if (getOrInsertComputed) {
return getOrInsertComputed.call(
this.pointEvents,
index,
() => new LayerEventView()
);
}
let eventView = this.pointEvents.get(index);
if (!eventView) {
eventView = new LayerEventView();
this.pointEvents.set(index, eventView);
}
return eventView;
}
getPointEvent(x: number, y: number): ReadonlyMap<number, string> | null {
return this.event(x, y)?.get() ?? null;
}
//#endregion
//#region 静态图层操作
inMap(x: number, y: number): boolean {
@@ -432,9 +417,11 @@ export class MapLayer
const tile = new DynamicTile(num, x, y, this);
const location = this.getLocationData(x, y);
if (location) {
tile.tileEvent().clear();
for (const [priority, id] of location.static.tileEvent().get()) {
tile.tileEvent().set(priority, id);
const tileEvent = tile.tileEvent();
const staticEvent = location.static.tileEvent();
tileEvent.clear();
for (const [priority, id] of staticEvent.get()) {
tileEvent.set(priority, id);
}
}
this.addTileToPosMap(tile, x, y);
@@ -580,27 +567,6 @@ export class MapLayer
this.setBlock(num, x, y);
}
event(x: number, y: number): ILayerEventView | null {
if (!this.inMap(x, y)) return null;
let xMap = this.pointEvents.get(y);
if (!xMap) {
xMap = new Map();
this.pointEvents.set(y, xMap);
}
let eventView = xMap.get(x);
if (!eventView) {
eventView = new LayerEventView();
xMap.set(x, eventView);
} else {
this.capturePointEventBaseline(x, y, eventView);
}
return eventView;
}
getPointEvent(x: number, y: number): ReadonlyMap<number, string> | null {
return this.event(x, y)?.get() ?? null;
}
//#endregion
//#region 图层操作
@@ -616,10 +582,8 @@ export class MapLayer
dirty(): boolean {
if (this.layerDirty) return true;
for (const xMap of this.pointEvents.values()) {
for (const eventView of xMap.values()) {
if (eventView.dirty()) return true;
}
for (const eventView of this.pointEvents.values()) {
if (eventView.dirty()) return true;
}
return false;
}
@@ -693,7 +657,6 @@ export class MapLayer
resize2(width: number, height: number): void {
this.layerDirty = true;
this.pointEvents.clear();
this.pointEventBaselines.clear();
if (this.width === width && this.height === height) {
this.empty = true;
this.mapArray.fill(0);
@@ -267,4 +267,33 @@ describe('MapLayer point event lifecycle', () => {
layer.resize2(1, 1);
expect(layer.getPointEvent(0, 0)).toEqual(new Map());
});
it('reindexes preserved point events when the layer width changes', () => {
const { layer } = createMapState({
1: { 1: 'top-right' },
3: { 1: 'bottom-right' }
});
layer.resize(3, 2);
expect(layer.getPointEvent(1, 0)).toEqual(new Map([[1, 'top-right']]));
expect(layer.getPointEvent(1, 1)).toEqual(
new Map([[1, 'bottom-right']])
);
layer.event(1, 1)!.set(2, 'runtime-event');
expect(
layer.saveState(SaveCompression.NoCompression).pointEvents
).toEqual(
new Map([
[
4,
new Map([
[1, 'bottom-right'],
[2, 'runtime-event']
])
]
])
);
});
});
+8 -19
View File
@@ -16,20 +16,6 @@ export class StaticTile
this.restoreDefaultEvents();
}
/**
* 根据当前图块原始数据恢复默认事件并建立干净基准
*/
private restoreDefaultEvents(): void {
this.tileEvent().clear();
const data = this.raw();
if (data) {
for (const [priority, id] of Object.entries(data.events)) {
this.tileEvent().set(Number(priority), id);
}
}
this.tileEvent().markPure();
}
num(): number {
return this.layer.getBlock(this.locator.x, this.locator.y);
}
@@ -48,14 +34,16 @@ export class StaticTile
}
shouldSave(): boolean {
return this.tileEvent().dirty();
const eventView = this.tileEvent();
return eventView.dirty();
}
saveState(): Readonly<IStaticBlockSave> {
let save: IStaticBlockSave;
if (this.tileEvent().dirty()) {
const eventView = this.tileEvent();
if (eventView.dirty()) {
save = {
events: new Map(this.tileEvent().get())
events: new Map(eventView.get())
};
} else {
save = {};
@@ -66,9 +54,10 @@ export class StaticTile
loadState(save: Readonly<IStaticBlockSave>): void {
this.restoreDefaultEvents();
if (save.events) {
this.tileEvent().clear();
const eventView = this.tileEvent();
eventView.clear();
for (const [priority, id] of save.events) {
this.tileEvent().set(priority, id);
eventView.set(priority, id);
}
}
}
+15
View File
@@ -37,6 +37,21 @@ export abstract class MapTileBase<TSave extends IMapBlockSaveBase>
abstract set(num: number): void;
/**
* 根据当前图块原始数据恢复默认事件并建立干净基准
*/
protected restoreDefaultEvents(): void {
const eventView = this.tileEvent();
eventView.clear();
const data = this.raw();
if (data) {
for (const [priority, id] of Object.entries(data.events)) {
eventView.set(Number(priority), id);
}
}
eventView.markPure();
}
setFaceDirection(direction: FaceDirection): number {
const cur = this.num();
const next = this.layer.faceBinder.getFaceOf(cur, direction);