import { logger } from '@motajs/common'; import { SaveCompression } from '../common'; import { ILayerState, ILayerStateSave, IMapLayer, IMapLayerSave, IMapStore, IMapStoreSave, MapArea } from './types'; import { LayerState } from './layerState'; import { uniq } from 'lodash-es'; export class MapStore implements IMapStore { /** 楼层 id 到状态对象的映射 */ private readonly mapData: Map = new Map(); /** 所有楼层 id 的有序数组 */ readonly maps: string[] = []; /** 差分压缩参考基准,首次 compareWith 后设置,之后不再更新 */ private refData: Map> | null = null; /** 分区列表 */ private areaList: Set = new Set(); /** 上一次调用 notifyEnterFloor 传入的楼层 id */ private lastFloorId: string | null = null; /** 自动分区激活器开关 */ private autoActivitorEnabled: boolean = false; //#region 楼层管理 createLayerState(id: string, width: number, height: number): ILayerState { if (this.mapData.has(id)) { logger.warn(121, id); } else { this.maps.push(id); } const state = new LayerState(width, height); // 若 refData 已存在,新楼层直接视为全脏 if (this.refData !== null) { state.setDirty(true); } this.mapData.set(id, state); return state; } setMapList(maps: string[]): void { this.maps.length = 0; this.maps.push(...uniq(maps)); } useManualOrder(sort: (arr: string[]) => string[]): void { const copy = this.maps.slice(); const sorted = sort(copy); const oldSet = new Set(this.maps); const newSet = new Set(sorted); if (oldSet.size !== newSet.size || !newSet.isSubsetOf(oldSet)) { logger.warn(125); return; } this.maps.length = 0; this.maps.push(...uniq(sorted)); } getLayerState(id: string): ILayerState | null { return this.mapData.get(id) ?? null; } getActiveMap(id: string): ILayerState | null { const state = this.mapData.get(id); if (!state || !state.active) return null; return state; } //#endregion //#region 分区管理 setArea(areas: Set): void { this.areaList = areas; } activeArea(id: string): void { const idx = this.maps.indexOf(id); if (idx === -1) return; const area = this.findAreaByIndex(idx); if (!area) return; this.setAreaActive(area, true); } deactiveArea(id: string): void { const idx = this.maps.indexOf(id); if (idx === -1) return; const area = this.findAreaByIndex(idx); if (!area) return; this.setAreaActive(area, false); } useAutoActivitor(enable: boolean): void { this.autoActivitorEnabled = enable; } notifyEnterFloor(id: string): void { if (!this.autoActivitorEnabled) return; const idx = this.maps.indexOf(id); if (idx === -1) return; const area = this.findAreaByIndex(idx); if (!area) return; if (this.lastFloorId !== null) { this.deactiveArea(this.lastFloorId); } this.activeArea(id); this.lastFloorId = id; } private findAreaByIndex(idx: number): MapArea | null { for (const area of this.areaList) { for (const interval of area) { if (idx >= interval.start && idx <= interval.end) { return area; } } } return null; } private setAreaActive(area: MapArea, active: boolean): void { for (const interval of area) { for (let i = interval.start; i <= interval.end; i++) { const floorId = this.maps[i]; if (floorId !== undefined) { this.setMapActiveStatus(floorId, active); } } } } //#endregion //#region active 管理 isMapActive(id: string): boolean { return this.mapData.get(id)?.active ?? false; } setMapActiveStatus(id: string, active: boolean): void { this.mapData.get(id)?.setActiveStatus(active); } *iterateActiveMaps(): Iterable<[string, ILayerState]> { for (const [id, state] of this.mapData) { if (state.active) yield [id, state]; } } *iterateInactiveMaps(): Iterable<[string, ILayerState]> { for (const [id, state] of this.mapData) { if (!state.active) yield [id, state]; } } iterateAllMaps(): Iterable<[string, ILayerState]> { return this.mapData; } //#endregion //#region 存档及压缩 compareWith(ref: Map>): void { if (this.refData !== null) return; this.refData = ref; for (const [id, state] of this.mapData) { const refFloor = ref.get(id); if (!refFloor) { state.setDirty(true); continue; } let dirty = false; for (const layer of state.layerList) { const refArray = refFloor.get(layer.zIndex); if (!refArray) { dirty = true; break; } const cur = layer.getMapRef().array; if (cur.length !== refArray.length) { dirty = true; break; } if (cur.some((v, i) => refArray[i] !== v)) { dirty = true; break; } } state.setDirty(dirty); } } private saveNoCompression(): IMapStoreSave { const floors = new Map(); for (const [id, state] of this.mapData) { if (!state.active) continue; floors.set(id, this.saveLayerStateFull(state)); } return { floors }; } private saveLowCompression(): IMapStoreSave { const floors = new Map(); for (const [id, state] of this.mapData) { if (!state.active) continue; // 非 dirty 或 dirty 但与参考基准完全一致 → 空 layers(读档时从参考基准恢复) if ( !state.isDirty() || (this.refData && this.isStateEqualToRef(id, state)) ) { floors.set(id, { background: state.getBackground(), layers: new Map() }); } else { floors.set(id, this.saveLayerStateFull(state)); } } return { floors }; } private saveHighCompression(): IMapStoreSave { const floors = new Map(); for (const [id, state] of this.mapData) { if (!state.active) continue; if (!state.isDirty()) { floors.set(id, { background: state.getBackground(), layers: new Map() }); continue; } const refFloor = this.refData?.get(id); const layersMap = new Map(); for (const layer of state.layerList) { const refArray = refFloor?.get(layer.zIndex); const rows = this.diffRows(layer, refArray); if (rows.size === 0 && refArray) continue; // 与参考完全一致 layersMap.set(layer.zIndex, { width: layer.width, height: layer.height, rows }); } floors.set(id, { background: state.getBackground(), layers: layersMap }); } return { floors }; } /** * NoCompression 读档:每个图层均有 fullMap,直接转移所有权,无需参考基准。 */ private loadNoCompression(state: IMapStoreSave): void { for (const [id, cur] of this.mapData) { cur.setActiveStatus(state.floors.has(id)); } for (const [id, layerStateSave] of state.floors) { const cur = this.mapData.get(id); if (!cur) { logger.warn(122, id); continue; } cur.setBackground(layerStateSave.background); for (const layer of cur.layerList) { const layerSave = layerStateSave.layers.get(layer.zIndex); if (!layerSave?.fullMap) continue; layer.setMapRef(new Uint32Array(layerSave.fullMap)); } cur.setDirty(false); } } /** * LowCompression 读档: * - layers 有数据(dirty 楼层)→ fullMap 直接转移所有权 * - layers 为空(非 dirty 楼层)→ 从参考基准恢复 */ private loadLowCompression(state: IMapStoreSave): void { if (!this.refData) { logger.error(55); return; } for (const [id, cur] of this.mapData) { cur.setActiveStatus(state.floors.has(id)); } for (const [id, layerStateSave] of state.floors) { const cur = this.mapData.get(id); const refFloor = this.refData.get(id); if (!cur) { logger.warn(122, id); continue; } if (!refFloor) { logger.warn(124, id); continue; } cur.setBackground(layerStateSave.background); for (const layer of cur.layerList) { const layerSave = layerStateSave.layers.get(layer.zIndex); if (layerSave?.fullMap) { layer.setMapRef(layerSave.fullMap); } else { const refArray = refFloor?.get(layer.zIndex); if (!refArray) { logger.warn(124, id); return; } layer.setMapRef(new Uint32Array(refArray)); } } cur.setDirty(false); } } /** * HighCompression 读档: * - layers 有数据(dirty 楼层)→ 以参考基准为底,叠加差分行 * - layers 为空(非 dirty 楼层)或图层无变化(rows 缺失)→ 从参考基准恢复 */ private loadHighCompression(state: IMapStoreSave): void { if (!this.refData) { logger.error(55); return; } for (const [id, cur] of this.mapData) { cur.setActiveStatus(state.floors.has(id)); } for (const [id, layerStateSave] of state.floors) { const cur = this.mapData.get(id); const refFloor = this.refData.get(id); if (!cur) { logger.warn(122, id); continue; } if (!refFloor) { logger.warn(124, id); continue; } cur.setBackground(layerStateSave.background); let isMapDirty = true; for (const layer of cur.layerList) { const refArray = refFloor.get(layer.zIndex); if (!refArray) { logger.warn(124, id); continue; } const layerSave = layerStateSave.layers.get(layer.zIndex); if (!layerSave?.rows || layerSave.rows.size === 0) { // 图层无变化或非 dirty 楼层,从参考基准恢复 layer.setMapRef(new Uint32Array(refArray)); } else { // 以参考基准为底,叠加差分行 isMapDirty = false; const size = layer.width * layer.height; const buf = new Uint32Array(size); if (refArray) buf.set(refArray.subarray(0, size)); for (const [rowIdx, rowData] of layerSave.rows) { buf.set( rowData.subarray(0, layer.width), rowIdx * layer.width ); } layer.setMapRef(buf); } } cur.setDirty(isMapDirty); } } saveState(compression: SaveCompression): IMapStoreSave { if (compression === SaveCompression.HighCompression) { return this.saveHighCompression(); } else if (compression === SaveCompression.LowCompression) { return this.saveLowCompression(); } else { return this.saveNoCompression(); } } loadState(state: IMapStoreSave, compression: SaveCompression): void { if (compression === SaveCompression.HighCompression) { this.loadHighCompression(state); } else if (compression === SaveCompression.LowCompression) { this.loadLowCompression(state); } else { this.loadNoCompression(state); } } //#region 内部方法 /** * 将楼层所有图层全量序列化(NoCompression / LowCompression 用) */ private saveLayerStateFull(state: LayerState): ILayerStateSave { const layersMap = new Map(); for (const layer of state.layerList) { const arr = layer.getMapRef().array; layersMap.set(layer.zIndex, { width: layer.width, height: layer.height, fullMap: new Uint32Array(arr) }); } return { background: state.getBackground(), layers: layersMap }; } /** * 仅返回与参考基准不同的行(HighCompression 用) */ private diffRows( layer: IMapLayer, refArray?: Uint32Array ): Map { const rows = new Map(); const arr = layer.getMapRef().array; if (refArray) { for (let row = 0; row < layer.height; row++) { const start = row * layer.width; const end = start + layer.width; const slice = arr.subarray(start, end); const refSlice = refArray.subarray(start, end); const same = refSlice.every((v, i) => slice[i] === v); if (!same) { rows.set(row, new Uint32Array(slice)); } } } else { for (let row = 0; row < layer.height; row++) { const start = row * layer.width; const end = start + layer.width; rows.set(row, new Uint32Array(arr.subarray(start, end))); } } return rows; } /** * 判断楼层所有图层是否与参考基准完全一致(LowCompression 去误判用) */ private isStateEqualToRef(id: string, state: LayerState): boolean { const refFloor = this.refData?.get(id); if (!refFloor) return false; for (const layer of state.layerList) { const refArray = refFloor.get(layer.zIndex); if (!refArray) return false; const cur = layer.getMapRef().array; if (cur.length !== refArray.length) return false; for (let i = 0; i < cur.length; i++) { if (cur[i] !== refArray[i]) return false; } } return true; } //#endregion }