mirror of
https://gitee.com/dgflash/oops-plugin-framework.git
synced 2026-06-23 19:22:47 +08:00
ecs 命名空间:统一导出所有 API,业务只需 import { ecs } from './ECS'
核心类型别名:ecs.Entity / ecs.Comp / ecs.RootSystem / ecs.ComblockSystem
接口再导出:ecs.IComp / ecs.IMatcher / ecs.IEntityEnterSystem / ecs.IEntityRemoveSystem / ecs.ISystemFirstUpdate / ecs.ISystemUpdate
实体工厂:ecs.getEntity(ctor, world?) — 创建或从对象池获取实体
动态查询:ecs.query(matcher, world?) — 按匹配器查询实体
过滤器工厂:ecs.allOf() / ecs.anyOf() / ecs.onlyOf() / ecs.excludeOf() — 组合匹配规则
单例组件:ecs.getSingleton() / ecs.addSingleton() — 全局唯一组件访问
子模块门面:ecs.world / ecs.pool / ecs.system / ecs.storage / ecs.network / ecs.serialize / ecs.entityRef
2. 驱动器 — ECSDriver.ts
封装默认世界的 init / execute / destroy 生命周期
add(system) — 向默认世界添加业务系统
init() — 初始化 ECS(并入 @ecs.register 注册的系统、拓扑排序、init)
execute(dt) — 驱动默认世界一帧
destroy() — 清理所有子系统
3. 实体 — ECSEntity.ts
组件容器:位掩码 mask + 按 tid 索引的密集数组,get/has 为 O(1)
双重重载 add():
add(组件类) — 从对象池取实例或恢复软移除缓存
add(组件实例) — 挂载外部实例(如 cc.Component),设 canRecycle=false
remove(ctor, isRecycle?):
isRecycle=true(默认):reset + 回池/释放 SoA 槽位
isRecycle=false:软移除,数据暂存 compTid2Obj,下次 add 同 tid 恢复
父子层级:addChild / removeChild,带循环引用检测
destroy():断开父子 → 移除全部组件 → 清理软移除缓存 → 回收实体 + 释放 eid
forEachComponent(cb):遍历当前挂载的所有组件
4. 组件 — ECSComp.ts
抽象基类,子类须实现 reset() 方法
canRecycle — 是否可回收(外部创建的组件设 false)
ent — 拥有该组件的实体引用
变更检测:markDirty() / isChangedSince(sinceEpoch) / lastWriteEpoch — 基于 epoch 的帧级脏标记
5. 位掩码 — ECSMask.ts
基于 Uint32Array 的位运算,支持 set / has / delete / and / or / bitCount
对象池复用,clearPool() 清理
6. 注册系统 — ECSRegister.ts + ECSTypeRegistry.ts
@ecs.register('Name') 类装饰器:自动识别注册类型
组件:分配 tid,写入 compCtors 表
实体:记录 ctor → name 映射
系统:注册到全局表或指定世界
ECSTypeRegistry:全局类型注册表(跨世界共享),记录组件/实体/系统元数据
7. 查询 — ECSMatcher.ts + ECSGroup.ts
四种规则:AllOf / AnyOf / OnlyOf / ExcludeOf,规则间为"与"关系
flyweight 缓存:相同组合共享同一 Matcher 实例
小规则优化:tid 数 ≤ 4 时直接 mask.has,否则分配 ECSMask
ECSGroup:SparseSet 实现,O(1) 增删(swap-pop),稳定快照遍历
进入/离开追踪:watchEntityEnterAndRemove 供系统帧内查询变化
8. 系统 — ECSComblockSystem.ts + ECSRootSystem.ts
ComblockSystem:业务系统基类
生命周期:init → entityEnter → firstUpdate → update → entityRemove → onDestroy
filter() — 声明实体匹配规则
interval — 执行间隔(0=每帧,>0=固定间隔)
被动系统:isPassiveSystem() 返回 true 时不参与每帧 update
构造时探测子类钩子,选定 execute 变体(零开销分支)
RootSystem:根系统,一世界一个
init() — 并入 @ecs.register 系统 + 拓扑排序 + 绑定世界 + init
execute(dt) — 切换当前世界 → 递增 epoch → tick 各系统 → flush 命令缓冲
9. 系统调度 — SystemScheduler.ts
声明式执行顺序:@ecs.system.executeBefore / @ecs.system.executeAfter / @ecs.system.inSet
拓扑排序(Kahn 算法):无约束系统保持原始顺序,循环依赖抛 CycleDependencyError
集合机制:inSet('名') 把系统加入虚拟分组,其他系统用 executeAfter('set:名') 批量依赖
10. 世界 — ECSWorld.ts + ECSWorldManager.ts
ECSWorld:运行期数据容器
entities — 活动实体表(eid → 实体)
groups — 响应式查询分组
singletons — 单例组件表
refs — @entityRef 引用追踪
commands — 延迟结构变更命令队列
epoch — 世代号(每帧递增,变更检测用)
getEntity(ctor) — 创建/从池获取实体
assignEid(entity, eid) — 反序列化时保持 eid 一致
ECSWorldManager(ecs.world):多世界管理
get(name?) / default() / current — 获取/切换世界
use(world) — 切换当前世界,返回切换前的世界
inWorld(world, fn) — 在指定世界中执行,自动还原
createSystems(world, ...ctors) — 批量装配系统并 init
defer(fn) / flushCommands() — 延迟结构变更
11. 命令缓冲 — ECSCommandBuffer.ts
延迟结构变更队列,帧末由 RootSystem.execute 统一 flush
push(fn) 入队,flush() 先快照再执行(避免本帧新入队命令在本帧执行)
12. 对象池 — ECSPoolManager.ts + ECSDynamicPool.ts
ecs.pool:统一管理实体/组件动态对象池
getPool(name, factory) — 获取或创建池
clearAll() / clearPools() — 清空池缓存(不触碰存活数据)
getAllMetrics() — 获取各池统计信息(命中/未命中/缓存/创建数)
13. 实体引用 — ECSEntityRef.ts + ECSReferenceTracker.ts
@ecs.entityRef() 属性装饰器:标记组件属性为实体引用
目标实体销毁时自动置 null,避免悬空引用
组件回收时 clearComponentEntityRefs 清理所有引用
14. SoA 列存储 — StorageSoA.ts + StorageDecorators.ts
@ecs.storage.enableSoA 类装饰器:为组件启用 SoA 列存储(默认 AoS,opt-in)
字段装饰器:float64 / float32 / int32 / uint32 / int16 / uint16 / int8 / uint8
数值字段按列存入 TypedArray,按 eid 分配槽位
acquire 返回 Proxy 视图:SoA 字段读写直接落到 TypedArray,非 SoA 字段走后备实例
槽位超出容量时 2 倍扩容,释放时入空闲栈
自注册机制:模块加载时注册到核心,删除 storage/ 目录后基础 ECS 仍可运行
15. 网络同步 — NetworkSync.ts + SyncDecorators.ts + SyncCodec.ts + ByteBuffer.ts
@ecs.network.sync(类型) 字段装饰器:标记组件字段参与网络同步
ecs.network.net 门面:
encodeWorld(op?) — 编码当前世界为紧凑二进制(Full 全量 / Delta 增量)
applyToWorld(bytes, onMissing?) — 将二进制同步数据应用到世界
track(entity, markAll?) — 为实体初始化变更追踪器
clearDirty() — 清除所有脏标记
SyncCodec:实体/组件级编解码器
ByteWriter / ByteReader:二进制读写缓冲(变长整数等)
ChangeTracker:字段级脏标记追踪
16. 序列化 — Serialization.ts + Incremental.ts
@ecs.serialize() 字段装饰器:标记组件需要持久化的字段
全量序列化:
ecs.serialize.serializeWorld(pretty?) — 序列化当前世界为 JSON
ecs.serialize.deserializeWorld(json) — 从 JSON 反序列化到当前世界
增量序列化:
ecs.serialize.snapshot() — 对当前世界拍快照(基线)
ecs.serialize.computeDelta(base) — 计算相对基线的增量(added / removed / changed)
ecs.serialize.applyDelta(delta) — 将增量应用到当前世界
保留实体层级(parentEid),两阶段重建(先建实体+组件,再重建父子)
17. 监控日志 — ECSMonitorLogger.ts
零入侵监控模块,通过控制台命令触发
注册全局命令:
ecsLog() — 打印所有监控表格
ecsWorldSummary() — 各世界实体/系统/组件缓存统计
ecsSummary() — 总体统计(所有世界合计)
ecsEntityPools() — 实体池明细(命中/未命中/缓存)
ecsCompPools() — 组件池明细(按 M/B/V/VC 分类排序)
ecsEntityCaches() — 实体软移除组件缓存明细
ecsHelp() — 显示帮助信息
402 lines
14 KiB
TypeScript
402 lines
14 KiB
TypeScript
/**
|
||
* ECS 对象数量监控日志模块
|
||
* 独立模块,零入侵现有 ECS 代码
|
||
* 通过控制台命令触发打印 ECS 对象统计表格
|
||
*/
|
||
|
||
import type { ECSWorld } from './world/ECSWorld';
|
||
import { registry } from './registry/ECSTypeRegistry';
|
||
import { ecsWorldManager } from './world/ECSWorldManager';
|
||
import { ecsPoolCoordinator } from './pool/ECSPoolManager';
|
||
|
||
/** 监控数据项 */
|
||
interface MonitorItem {
|
||
/** 对象类型 */
|
||
type: string;
|
||
/** 活跃数 */
|
||
active: number;
|
||
/** 缓存数 */
|
||
cached: number;
|
||
/** 总计 */
|
||
total: number;
|
||
}
|
||
|
||
/** 世界监控数据项 */
|
||
interface WorldMonitorItem {
|
||
/** 世界名称 */
|
||
worldName: string;
|
||
/** 活跃实体数 */
|
||
entities: number;
|
||
/** 活跃组件数(实体上已挂载) */
|
||
activeComponents: number;
|
||
/** 软移除缓存组件数(isRecycle=false) */
|
||
softCachedComponents: number;
|
||
/** 本世界 @ecs.register(world) 注册的系统类数 */
|
||
worldSystems: number;
|
||
}
|
||
|
||
/** 对象池缓存汇总 */
|
||
interface PoolCacheSummaryItem {
|
||
/** 缓存类别 */
|
||
category: string;
|
||
/** 池中闲置对象数 */
|
||
cached: number;
|
||
}
|
||
|
||
/** 池监控数据项 */
|
||
interface PoolMonitorItem {
|
||
/** 类型名 */
|
||
typeName: string;
|
||
/** 活跃数 */
|
||
active: number;
|
||
/** 缓存命中 */
|
||
hitCount: number;
|
||
/** 缓存未中 */
|
||
missCount: number;
|
||
/** 当前缓存 */
|
||
currentCache: number;
|
||
/** 总创建 */
|
||
totalCreated: number;
|
||
}
|
||
|
||
/** 实体缓存监控项 */
|
||
interface EntityCacheItem {
|
||
/** 所属世界 */
|
||
worldName: string;
|
||
/** 实体名 */
|
||
entityName: string;
|
||
/** 缓存组件数 */
|
||
cachedCompCount: number;
|
||
}
|
||
|
||
/**
|
||
* ECS 监控日志
|
||
* 通过控制台命令输出实体/组件/系统/对象池的统计表格,零入侵现有 ECS 逻辑
|
||
*/
|
||
export class ECSMonitorLogger {
|
||
/** 统计全局 registry.systems 中注册的系统类总数 */
|
||
private countGlobalSystemClasses(): number {
|
||
let count = 0;
|
||
registry.systems.forEach((ctors) => { count += ctors.length; });
|
||
return count;
|
||
}
|
||
|
||
/** 统计某世界 world.systems 中注册的系统类总数 */
|
||
private countWorldSystemClasses(world: ECSWorld): number {
|
||
return world.systems.count;
|
||
}
|
||
|
||
/** 收集单个世界的实体/组件/系统统计 */
|
||
private collectWorldStats(world: ECSWorld): WorldMonitorItem {
|
||
let activeComponents = 0;
|
||
let softCachedComponents = 0;
|
||
world.entities.forEach((entity) => {
|
||
activeComponents += entity.getMask().bitCount();
|
||
softCachedComponents += entity.getCachedComponentCount();
|
||
});
|
||
return {
|
||
worldName: world.name,
|
||
entities: world.entities.size,
|
||
activeComponents,
|
||
softCachedComponents,
|
||
worldSystems: this.countWorldSystemClasses(world),
|
||
};
|
||
}
|
||
|
||
/** 汇总对象池中实体与组件的闲置缓存数量 */
|
||
private collectPoolCacheStats(): PoolCacheSummaryItem[] {
|
||
const poolMetrics = ecsPoolCoordinator.getAllMetrics();
|
||
const entityNames = new Set(registry.entityCtors.values());
|
||
let entityPoolCached = 0;
|
||
let compPoolCached = 0;
|
||
|
||
poolMetrics.forEach((metrics, name) => {
|
||
if (entityNames.has(name)) {
|
||
entityPoolCached += metrics.currentSize;
|
||
}
|
||
else if (registry.compCtors.some((ctor) => ctor?.compName === name)) {
|
||
compPoolCached += metrics.currentSize;
|
||
}
|
||
});
|
||
|
||
return [
|
||
{ category: '实体池', cached: entityPoolCached },
|
||
{ category: '组件池', cached: compPoolCached },
|
||
{ category: '合计', cached: entityPoolCached + compPoolCached },
|
||
];
|
||
}
|
||
|
||
/** 遍历所有世界 */
|
||
private forEachWorld(fn: (world: ECSWorld) => void): void {
|
||
ecsWorldManager.worlds.forEach(fn);
|
||
}
|
||
|
||
/**
|
||
* 打印各世界实体/系统/组件统计,以及全局对象池组件缓存汇总
|
||
*/
|
||
printWorldSummary(): void {
|
||
const worldData: WorldMonitorItem[] = [];
|
||
this.forEachWorld((world) => {
|
||
worldData.push(this.collectWorldStats(world));
|
||
});
|
||
|
||
const globalSystems = this.countGlobalSystemClasses();
|
||
const poolCache = this.collectPoolCacheStats();
|
||
const totalSoftCache = worldData.reduce((sum, row) => sum + row.softCachedComponents, 0);
|
||
const totalPoolCompCache = poolCache.find((row) => row.category === '组件池')?.cached ?? 0;
|
||
|
||
console.log('%c[ECS Monitor] 各世界统计', 'color:#fff;background:#3a5fcd;padding:2px 8px;border-radius:4px;font-weight:bold;');
|
||
console.table(worldData);
|
||
|
||
console.log('%c[ECS Monitor] 全局注册系统(@ecs.register 未指定 world,RootSystem.init 时按世界实例化)', 'color:#888;');
|
||
console.table([{ globalSystemClasses: globalSystems }]);
|
||
|
||
console.log('%c[ECS Monitor] 对象池闲置缓存', 'color:#fff;background:#3a5fcd;padding:2px 8px;border-radius:4px;font-weight:bold;');
|
||
console.table(poolCache);
|
||
|
||
console.log(
|
||
`%c[ECS Monitor] 组件缓存合计:对象池 ${totalPoolCompCache} + 实体软移除 ${totalSoftCache} = ${totalPoolCompCache + totalSoftCache}`,
|
||
'color:#00aa00;font-weight:bold;'
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 打印 ECS 总体统计表格(汇总所有世界)
|
||
*/
|
||
printSummary(): void {
|
||
const data: MonitorItem[] = [];
|
||
let totalEntities = 0;
|
||
let totalActiveComps = 0;
|
||
let totalSoftCompCache = 0;
|
||
let totalGroups = 0;
|
||
|
||
this.forEachWorld((world) => {
|
||
totalEntities += world.entities.size;
|
||
totalGroups += world.groups.size;
|
||
world.entities.forEach((entity) => {
|
||
totalActiveComps += entity.getMask().bitCount();
|
||
totalSoftCompCache += entity.getCachedComponentCount();
|
||
});
|
||
});
|
||
|
||
const poolCache = this.collectPoolCacheStats();
|
||
const entityPoolCached = poolCache.find((row) => row.category === '实体池')?.cached ?? 0;
|
||
const compPoolCached = poolCache.find((row) => row.category === '组件池')?.cached ?? 0;
|
||
|
||
data.push({
|
||
type: 'ECSEntity',
|
||
active: totalEntities,
|
||
cached: entityPoolCached,
|
||
total: totalEntities + entityPoolCached,
|
||
});
|
||
|
||
data.push({
|
||
type: 'ECSComp',
|
||
active: totalActiveComps,
|
||
cached: compPoolCached + totalSoftCompCache,
|
||
total: totalActiveComps + compPoolCached + totalSoftCompCache,
|
||
});
|
||
|
||
data.push({
|
||
type: 'ECSGroup',
|
||
active: totalGroups,
|
||
cached: 0,
|
||
total: totalGroups,
|
||
});
|
||
|
||
let worldSystemClasses = 0;
|
||
this.forEachWorld((world) => {
|
||
worldSystemClasses += this.countWorldSystemClasses(world);
|
||
});
|
||
const globalSystemClasses = this.countGlobalSystemClasses();
|
||
|
||
data.push({
|
||
type: 'ECSSystem(世界注册)',
|
||
active: worldSystemClasses,
|
||
cached: 0,
|
||
total: worldSystemClasses,
|
||
});
|
||
|
||
data.push({
|
||
type: 'ECSSystem(全局注册)',
|
||
active: globalSystemClasses,
|
||
cached: 0,
|
||
total: globalSystemClasses,
|
||
});
|
||
|
||
console.log('%c[ECS Monitor] 总体统计(所有世界合计)', 'color:#fff;background:#3a5fcd;padding:2px 8px;border-radius:4px;font-weight:bold;');
|
||
console.table(data);
|
||
}
|
||
|
||
/**
|
||
* 打印实体池明细表格
|
||
*/
|
||
printEntityPools(): void {
|
||
const data: PoolMonitorItem[] = [];
|
||
const poolMetrics = ecsPoolCoordinator.getAllMetrics();
|
||
const entityNames = new Set(registry.entityCtors.values());
|
||
|
||
poolMetrics.forEach((metrics, name) => {
|
||
if (entityNames.has(name)) {
|
||
let activeCount = 0;
|
||
this.forEachWorld((world) => {
|
||
world.entities.forEach((entity) => {
|
||
if (entity.name === name) activeCount++;
|
||
});
|
||
});
|
||
data.push({
|
||
typeName: name,
|
||
active: activeCount,
|
||
hitCount: metrics.hitCount,
|
||
missCount: metrics.missCount,
|
||
currentCache: metrics.currentSize,
|
||
totalCreated: metrics.createCount,
|
||
});
|
||
}
|
||
});
|
||
|
||
if (data.length === 0) {
|
||
console.log('%c[ECS Monitor] 暂无实体池数据', 'color:#ee7700;');
|
||
return;
|
||
}
|
||
|
||
console.log('%c[ECS Monitor] 实体池明细', 'color:#fff;background:#3a5fcd;padding:2px 8px;border-radius:4px;font-weight:bold;');
|
||
console.table(data);
|
||
}
|
||
|
||
/**
|
||
* 获取组件分类排序权重
|
||
* @param compName 组件名
|
||
* @returns 排序权重(越小越靠前)
|
||
*/
|
||
private getCompSortWeight(compName: string): number {
|
||
if (compName.startsWith('M_')) return 1;
|
||
if (compName.startsWith('B_')) return 2;
|
||
if (compName.startsWith('V_')) return 3;
|
||
if (compName.startsWith('VC_')) return 4;
|
||
return 5;
|
||
}
|
||
|
||
/**
|
||
* 打印组件池明细表格(按 M/B/V/VC/其他 分类排序)
|
||
*/
|
||
printComponentPools(): void {
|
||
const data: PoolMonitorItem[] = [];
|
||
const poolMetrics = ecsPoolCoordinator.getAllMetrics();
|
||
const compDataList: { item: PoolMonitorItem; weight: number }[] = [];
|
||
|
||
registry.compCtors.forEach((ctor) => {
|
||
if (!ctor) return;
|
||
const metrics = poolMetrics.get(ctor.compName);
|
||
let activeCount = 0;
|
||
this.forEachWorld((world) => {
|
||
world.entities.forEach((entity) => {
|
||
if (entity.has(ctor.tid)) activeCount++;
|
||
});
|
||
});
|
||
|
||
const item: PoolMonitorItem = metrics
|
||
? {
|
||
typeName: ctor.compName,
|
||
active: activeCount,
|
||
hitCount: metrics.hitCount,
|
||
missCount: metrics.missCount,
|
||
currentCache: metrics.currentSize,
|
||
totalCreated: metrics.createCount,
|
||
}
|
||
: {
|
||
typeName: ctor.compName,
|
||
active: activeCount,
|
||
hitCount: 0,
|
||
missCount: 0,
|
||
currentCache: 0,
|
||
totalCreated: 0,
|
||
};
|
||
|
||
compDataList.push({
|
||
item,
|
||
weight: this.getCompSortWeight(ctor.compName),
|
||
});
|
||
});
|
||
|
||
compDataList.sort((a, b) => {
|
||
if (a.weight !== b.weight) return a.weight - b.weight;
|
||
return a.item.typeName.localeCompare(b.item.typeName);
|
||
});
|
||
|
||
compDataList.forEach(({ item }) => data.push(item));
|
||
|
||
console.log('%c[ECS Monitor] 组件池明细', 'color:#fff;background:#3a5fcd;padding:2px 8px;border-radius:4px;font-weight:bold;');
|
||
console.table(data);
|
||
}
|
||
|
||
/**
|
||
* 打印实体组件缓存明细(isRecycle=false 缓存的组件)
|
||
*/
|
||
printEntityCaches(): void {
|
||
const data: EntityCacheItem[] = [];
|
||
|
||
this.forEachWorld((world) => {
|
||
world.entities.forEach((entity) => {
|
||
const count = entity.getCachedComponentCount();
|
||
if (count > 0) {
|
||
data.push({
|
||
worldName: world.name,
|
||
entityName: `${entity.name}(eid:${entity.eid})`,
|
||
cachedCompCount: count,
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
if (data.length === 0) {
|
||
console.log('%c[ECS Monitor] 暂无实体组件缓存', 'color:#00aa00;');
|
||
return;
|
||
}
|
||
|
||
console.log('%c[ECS Monitor] 实体组件缓存明细 (isRecycle=false)', 'color:#fff;background:#ee7700;padding:2px 8px;border-radius:4px;font-weight:bold;');
|
||
console.table(data);
|
||
}
|
||
|
||
/**
|
||
* 打印所有监控表格
|
||
*/
|
||
printAll(): void {
|
||
console.log('%c══════════════ ECS 对象监控 ══════════════', 'color:#3a5fcd;font-weight:bold;font-size:14px;');
|
||
this.printWorldSummary();
|
||
this.printSummary();
|
||
this.printEntityPools();
|
||
this.printComponentPools();
|
||
this.printEntityCaches();
|
||
console.log('%c══════════════════════════════════════════', 'color:#3a5fcd;font-weight:bold;font-size:14px;');
|
||
}
|
||
}
|
||
|
||
/** ECS 监控日志全局实例 */
|
||
export const ecsMonitor = new ECSMonitorLogger();
|
||
|
||
// 注册全局控制台命令
|
||
if (typeof window !== 'undefined') {
|
||
Object.assign(window, {
|
||
ecsMonitor,
|
||
ecsLog: () => ecsMonitor.printAll(),
|
||
ecsWorldSummary: () => ecsMonitor.printWorldSummary(),
|
||
ecsSummary: () => ecsMonitor.printSummary(),
|
||
ecsEntityPools: () => ecsMonitor.printEntityPools(),
|
||
ecsCompPools: () => ecsMonitor.printComponentPools(),
|
||
ecsEntityCaches: () => ecsMonitor.printEntityCaches(),
|
||
ecsHelp: () => {
|
||
console.log('%c[ECS Monitor] 可用命令:', 'color:#fff;background:#3a5fcd;padding:2px 8px;border-radius:4px;font-weight:bold;');
|
||
console.table([
|
||
{ command: 'ecsLog()', description: '打印所有监控表格' },
|
||
{ command: 'ecsWorldSummary()', description: '打印各世界实体/系统/组件缓存统计' },
|
||
{ command: 'ecsSummary()', description: '打印总体统计(所有世界合计)' },
|
||
{ command: 'ecsEntityPools()', description: '打印实体池明细(命中/未命中/缓存统计)' },
|
||
{ command: 'ecsCompPools()', description: '打印组件池明细(命中/未命中/缓存统计)' },
|
||
{ command: 'ecsEntityCaches()', description: '打印实体软移除组件缓存明细' },
|
||
{ command: 'ecsHelp()', description: '显示此帮助信息' },
|
||
]);
|
||
},
|
||
});
|
||
}
|