mirror of
https://github.com/motajs/template.git
synced 2026-06-14 10:41:12 +08:00
473 lines
15 KiB
TypeScript
473 lines
15 KiB
TypeScript
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<string, LayerState> = new Map();
|
||
|
||
/** 所有楼层 id 的有序数组 */
|
||
readonly maps: string[] = [];
|
||
|
||
/** 差分压缩参考基准,首次 compareWith 后设置,之后不再更新 */
|
||
private refData: Map<string, Map<number, Uint32Array>> | null = null;
|
||
|
||
/** 分区列表 */
|
||
private areaList: Set<MapArea> = 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<MapArea>): 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<string, Map<number, Uint32Array>>): 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<string, ILayerStateSave>();
|
||
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<string, ILayerStateSave>();
|
||
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<string, ILayerStateSave>();
|
||
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<number, IMapLayerSave>();
|
||
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<number, IMapLayerSave>();
|
||
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<number, Uint32Array> {
|
||
const rows = new Map<number, Uint32Array>();
|
||
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
|
||
}
|