refactor: 地图存储

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
AncTeandCopilot committed 2026-04-26 13:33:49 +08:00
1 parent 476f735adc
commit 70a58ef4dc
9 files changed
+853 -14

No files matched your search

+1 -1
View File
@@ -10,7 +10,7 @@ import {
SpecialCreation,
IEnemySaveState
} from './types';
import { SaveCompression } from '../common/types';
import { SaveCompression } from '../common';
export class EnemyManager<TAttr> implements IEnemyManager<TAttr> {
/** 特殊属性注册表,code -> 创建函数 */
+1
View File
@@ -1,3 +1,4 @@
export * from './layerState';
export * from './mapLayer';
export * from './mapStore';
export * from './types';
@@ -29,6 +29,12 @@ export class LayerState
/** 图层钩子映射 */
private layerHookMap: Map<IMapLayer, IMapLayerHookController> = new Map();
/** 楼层是否处于激活状态 */
active: boolean = false;
/** 楼层级脏标记 */
private dirty: boolean = false;
addLayer(width: number, height: number): IMapLayer {
const array = new Uint32Array(width * height);
const layer = new MapLayer(array, width, height);
@@ -106,6 +112,18 @@ export class LayerState
return this.backgroundTile;
}
setActiveStatus(active: boolean): void {
this.active = active;
}
isDirty(): boolean {
return this.dirty;
}
setDirty(dirty: boolean): void {
this.dirty = dirty;
}
protected createController(
hook: Partial<ILayerStateHooks>
): IHookController<ILayerStateHooks> {
@@ -120,18 +138,21 @@ class StateMapLayerHook implements Partial<IMapLayerHooks> {
) {}
onUpdateArea(x: number, y: number, width: number, height: number): void {
this.state.setDirty(true);
this.state.forEachHook(hook => {
hook.onUpdateLayerArea?.(this.layer, x, y, width, height);
});
}
onUpdateBlock(block: number, x: number, y: number): void {
this.state.setDirty(true);
this.state.forEachHook(hook => {
hook.onUpdateLayerBlock?.(this.layer, block, x, y);
});
}
onResize(width: number, height: number): void {
this.state.setDirty(true);
this.state.forEachHook(hook => {
hook.onResizeLayer?.(this.layer, width, height);
});
+26 -7
View File
@@ -186,14 +186,12 @@ export class MapLayer
}
const res = new Uint32Array(width * height);
const arr = this.mapArray;
const nr = Math.min(r, w);
const nb = Math.min(b, h);
for (let nx = x; nx < nr; nx++) {
for (let ny = y; ny < nb; ny++) {
const origin = ny * w + nx;
const target = (ny - y) * width + (nx - x);
res[target] = arr[origin];
}
for (let ny = y; ny < nb; ny++) {
const lineStart = ny * w + x;
const lineEnd = lineStart + width;
const dy = ny - y;
res.set(arr.subarray(lineStart, lineEnd), dy * width);
}
return res;
}
@@ -205,6 +203,27 @@ export class MapLayer
return this.mapData;
}
setMapRef(array: Uint32Array): void {
if (array.length !== this.width * this.height) {
logger.warn(
123,
array.length.toString(),
(this.width * this.height).toString()
);
return;
}
this.mapData.expired = true;
this.mapArray = array;
this.mapData = {
expired: false,
array: this.mapArray
};
this.empty = !array.some(v => v !== 0);
this.forEachHook(hook => {
hook.onUpdateArea?.(0, 0, this.width, this.height);
});
}
protected createController(
hook: Partial<IMapLayerHooks>
): IMapLayerHookController {
+383
View File
@@ -0,0 +1,383 @@
import { logger } from '@motajs/common';
import { SaveCompression } from '../common';
import {
ILayerState,
ILayerStateSave,
IMapLayer,
IMapLayerSave,
IMapStore,
IMapStoreSave
} from './types';
import { LayerState } from './layerState';
export class MapStore implements IMapStore {
/** 楼层 id 到状态对象的映射 */
private readonly mapData: Map<string, LayerState> = new Map();
/** 所有楼层 id 的只读集合视图 */
readonly maps: Set<string> = new Set();
/** 差分压缩参考基准,首次 compareWith 后设置,之后不再更新 */
private refData: Map<string, Map<number, Uint32Array>> | null = null;
//#region 楼层访问
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 楼层管理
createLayerState(id: string): ILayerState {
if (this.mapData.has(id)) {
logger.warn(121, id);
}
const state = new LayerState();
// 若 refData 已存在,新楼层直接视为全脏
if (this.refData !== null) {
state.setDirty(true);
}
this.mapData.set(id, state);
this.maps.add(id);
return state;
}
//#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
}
+120 -4
View File
@@ -1,4 +1,5 @@
import { IHookable, IHookBase, IHookController } from '@motajs/common';
import { ISaveableContent } from '../common';
export interface IMapLayerData {
/** 当前引用是否过期,当地图图层内部的地图数组引用更新时,此项会变为 `true` */
@@ -48,8 +49,7 @@ export interface IMapLayerHooks extends IHookBase {
onCloseDoor(num: number, x: number, y: number): Promise<void>;
}
export interface IMapLayerHookController
extends IHookController<IMapLayerHooks> {
export interface IMapLayerHookController extends IHookController<IMapLayerHooks> {
/** 拓展所属的图层对象 */
readonly layer: IMapLayer;
@@ -59,8 +59,10 @@ export interface IMapLayerHookController
getMapData(): Readonly<IMapLayerData>;
}
export interface IMapLayer
extends IHookable<IMapLayerHooks, IMapLayerHookController> {
export interface IMapLayer extends IHookable<
IMapLayerHooks,
IMapLayerHookController
> {
/** 地图宽度 */
readonly width: number;
/** 地图高度 */
@@ -155,6 +157,15 @@ export interface IMapLayer
* @param y 门纵坐标
*/
closeDoor(num: number, x: number, y: number): Promise<void>;
/**
* 直接替换内部图块数组引用,跳过拷贝,高性能但风险较高。
* 一般仅供 `MapStore` 读档时内部使用,外部正常情况下不应调用。
* 调用方需确保传入数组的长度与 `width * height` 匹配,
* 且调用后不得再持有或修改传入的数组。
* @param array 地图数组,会直接替换内部引用
*/
setMapRef(array: Uint32Array): void;
}
export interface ILayerStateHooks extends IHookBase {
@@ -212,6 +223,8 @@ export interface ILayerStateHooks extends IHookBase {
export interface ILayerState extends IHookable<ILayerStateHooks> {
/** 地图列表 */
readonly layerList: Set<IMapLayer>;
/** 此楼层是否处于激活状态 */
readonly active: boolean;
/**
* 添加图层
@@ -275,4 +288,107 @@ export interface ILayerState extends IHookable<ILayerStateHooks> {
* 获取背景图块数字,如果没有设置过,则返回 0
*/
getBackground(): number;
/**
* 设置楼层激活状态
* @param active 激活状态
*/
setActiveStatus(active: boolean): void;
/**
* 楼层是否被修改过(相对于参考基准)
*/
isDirty(): boolean;
/**
* 设置楼层脏标记
*/
setDirty(dirty: boolean): void;
}
/** 单个 MapLayer 的存档数据 */
export interface IMapLayerSave {
readonly width: number;
readonly height: number;
/**
* key = 行索引,value = 该行完整的 Uint32Array 数据;
* HighCompression 时使用此接口,仅包含与参考基准不同的行;
* 读档时,不在此 Map 中的行从参考基准还原。
*/
readonly rows?: ReadonlyMap<number, Uint32Array>;
/** 完整地图,当使用 `NoCompression` 和 `LowCompression` 时使用此接口 */
readonly fullMap?: Uint32Array;
}
/** 单个楼层的存档数据 */
export interface ILayerStateSave {
readonly background: number;
/** key = zIndex,value = 对应图层存档数据 */
readonly layers: ReadonlyMap<number, IMapLayerSave>;
}
/** 整个 MapStore 的存档数据 */
export interface IMapStoreSave {
/** key = 楼层 id,只包含 active 的楼层,inactive 的楼层不写入,读档时无需处理 */
readonly floors: ReadonlyMap<string, ILayerStateSave>;
}
export interface IMapStore extends ISaveableContent<IMapStoreSave> {
/** 所有楼层的 id 集合 */
readonly maps: ReadonlySet<string>;
/**
* 获取指定 id 的楼层状态,不存在则返回 null
* @param id 楼层 id
*/
getLayerState(id: string): ILayerState | null;
/**
* 获取指定 id 的楼层状态,要求楼层必须是 active 的,否则返回 null
* @param id 楼层 id
*/
getActiveMap(id: string): ILayerState | null;
/**
* 创建并注册一个空白楼层,若 id 已存在则警告并覆盖,返回楼层状态对象
* @param id 楼层 id
*/
createLayerState(id: string): ILayerState;
/**
* 获取指定 id 的楼层是否激活,不存在的 id 返回 false
* @param id 楼层 id
*/
isMapActive(id: string): boolean;
/**
* 设置指定 id 楼层的激活状态
* @param id 楼层 id
* @param active 激活状态
*/
setMapActiveStatus(id: string, active: boolean): void;
/**
* 迭代所有 active 的楼层,yield [id, ILayerState]
*/
iterateActiveMaps(): Iterable<[string, ILayerState]>;
/**
* 迭代所有 inactive 的楼层,yield [id, ILayerState]
*/
iterateInactiveMaps(): Iterable<[string, ILayerState]>;
/**
* 迭代所有楼层,yield [id, ILayerState]
*/
iterateAllMaps(): Iterable<[string, ILayerState]>;
/**
* 设置压缩参考基准,以首次调用为唯一基准,再次调用不更新。
* @param ref 外层 key = 楼层 id,内层 key = zIndex,value = 图层完整图块数据
*/
compareWith(ref: Map<string, Map<number, Uint32Array>>): void;
}
+2 -2
View File
@@ -2,7 +2,7 @@ import { IHeroFollower, IHeroState } from './hero';
import { IEnemyManager } from './enemy';
import { IFlagSystem } from './flag';
import { IRoleFaceBinder, ISaveableContent } from './common';
import { ILayerState } from './map';
import { IMapStore } from './map';
export interface IStateSaveData {
/** 跟随者列表 */
@@ -18,7 +18,7 @@ export interface IStateBase<TEnemy, THero> {
readonly numberIdMap: Map<number, string>;
/** 地图状态 */
readonly layer: ILayerState;
readonly layer: IMapStore;
/** 勇士状态 */
readonly hero: IHeroState<THero>;