refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* @zh 文件系统 Memory 存储
|
||||
* @en File System Memory Store
|
||||
*
|
||||
* @zh 使用文件系统存储玩家 Memory 和世界状态
|
||||
* @en Uses file system to store player Memory and world state
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
import type {
|
||||
IMemoryStore,
|
||||
PlayerMemory,
|
||||
WorldState,
|
||||
MemoryStoreStats
|
||||
} from './IMemoryStore';
|
||||
|
||||
/**
|
||||
* @zh 文件存储配置
|
||||
* @en File storage configuration
|
||||
*/
|
||||
export interface FileMemoryStoreConfig {
|
||||
/**
|
||||
* @zh 存储根目录
|
||||
* @en Storage root directory
|
||||
*/
|
||||
basePath: string;
|
||||
|
||||
/**
|
||||
* @zh 是否美化 JSON 输出
|
||||
* @en Whether to prettify JSON output
|
||||
*/
|
||||
prettyPrint?: boolean;
|
||||
|
||||
/**
|
||||
* @zh Memory 大小限制(字节)
|
||||
* @en Memory size limit (bytes)
|
||||
*/
|
||||
memorySizeLimit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 默认配置
|
||||
* @en Default configuration
|
||||
*/
|
||||
const DEFAULT_CONFIG: Required<FileMemoryStoreConfig> = {
|
||||
basePath: './data',
|
||||
prettyPrint: true,
|
||||
memorySizeLimit: 2 * 1024 * 1024 // 2MB
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 文件系统 Memory 存储
|
||||
* @en File System Memory Store
|
||||
*
|
||||
* @zh 简单的文件存储实现,适合开发和小规模部署
|
||||
* @en Simple file storage implementation, suitable for development and small deployments
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const store = new FileMemoryStore({ basePath: './game-data' });
|
||||
* await store.init();
|
||||
*
|
||||
* // 保存玩家 Memory | Save player Memory
|
||||
* await store.savePlayerMemory('player1', { creeps: {} });
|
||||
*
|
||||
* // 加载玩家 Memory | Load player Memory
|
||||
* const memory = await store.loadPlayerMemory('player1');
|
||||
* ```
|
||||
*/
|
||||
export class FileMemoryStore implements IMemoryStore {
|
||||
/**
|
||||
* @zh 配置
|
||||
* @en Configuration
|
||||
*/
|
||||
private readonly _config: Required<FileMemoryStoreConfig>;
|
||||
|
||||
/**
|
||||
* @zh Memory 目录路径
|
||||
* @en Memory directory path
|
||||
*/
|
||||
private readonly _memoryPath: string;
|
||||
|
||||
/**
|
||||
* @zh 世界状态文件路径
|
||||
* @en World state file path
|
||||
*/
|
||||
private readonly _worldPath: string;
|
||||
|
||||
/**
|
||||
* @zh 是否已初始化
|
||||
* @en Whether initialized
|
||||
*/
|
||||
private _initialized: boolean = false;
|
||||
|
||||
constructor(config: Partial<FileMemoryStoreConfig> = {}) {
|
||||
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||
this._memoryPath = path.join(this._config.basePath, 'memory');
|
||||
this._worldPath = path.join(this._config.basePath, 'world.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 初始化存储(创建目录)
|
||||
* @en Initialize storage (create directories)
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
if (this._initialized) return;
|
||||
|
||||
try {
|
||||
await fs.mkdir(this._memoryPath, { recursive: true });
|
||||
this._initialized = true;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to initialize storage: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 确保已初始化
|
||||
* @en Ensure initialized
|
||||
*/
|
||||
private async _ensureInit(): Promise<void> {
|
||||
if (!this._initialized) {
|
||||
await this.init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取玩家 Memory 文件路径
|
||||
* @en Get player Memory file path
|
||||
*/
|
||||
private _getPlayerMemoryPath(playerId: string): string {
|
||||
// 清理 playerId 以防止路径遍历 | Sanitize playerId to prevent path traversal
|
||||
const safeId = playerId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
return path.join(this._memoryPath, `${safeId}.json`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加载玩家 Memory
|
||||
* @en Load player Memory
|
||||
*/
|
||||
async loadPlayerMemory(playerId: string): Promise<PlayerMemory> {
|
||||
await this._ensureInit();
|
||||
|
||||
const filePath = this._getPlayerMemoryPath(playerId);
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
return JSON.parse(content) as PlayerMemory;
|
||||
} catch (error) {
|
||||
// 文件不存在则返回空对象 | Return empty object if file doesn't exist
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return {};
|
||||
}
|
||||
throw new Error(`Failed to load Memory for ${playerId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 保存玩家 Memory
|
||||
* @en Save player Memory
|
||||
*/
|
||||
async savePlayerMemory(playerId: string, memory: PlayerMemory): Promise<void> {
|
||||
await this._ensureInit();
|
||||
|
||||
const content = this._config.prettyPrint
|
||||
? JSON.stringify(memory, null, 2)
|
||||
: JSON.stringify(memory);
|
||||
|
||||
// 检查大小限制 | Check size limit
|
||||
if (content.length > this._config.memorySizeLimit) {
|
||||
throw new Error(
|
||||
`Memory size (${content.length} bytes) exceeds limit ` +
|
||||
`(${this._config.memorySizeLimit} bytes) for player ${playerId}`
|
||||
);
|
||||
}
|
||||
|
||||
const filePath = this._getPlayerMemoryPath(playerId);
|
||||
|
||||
try {
|
||||
await fs.writeFile(filePath, content, 'utf-8');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to save Memory for ${playerId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 批量保存玩家 Memory
|
||||
* @en Batch save player Memory
|
||||
*/
|
||||
async savePlayerMemoryBatch(
|
||||
entries: Array<{ playerId: string; memory: PlayerMemory }>
|
||||
): Promise<void> {
|
||||
await this._ensureInit();
|
||||
|
||||
// 并行保存所有玩家 | Save all players in parallel
|
||||
await Promise.all(
|
||||
entries.map(({ playerId, memory }) =>
|
||||
this.savePlayerMemory(playerId, memory)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 删除玩家 Memory
|
||||
* @en Delete player Memory
|
||||
*/
|
||||
async deletePlayerMemory(playerId: string): Promise<void> {
|
||||
await this._ensureInit();
|
||||
|
||||
const filePath = this._getPlayerMemoryPath(playerId);
|
||||
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (error) {
|
||||
// 文件不存在则忽略 | Ignore if file doesn't exist
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw new Error(`Failed to delete Memory for ${playerId}: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有玩家 ID
|
||||
* @en Get all player IDs
|
||||
*/
|
||||
async getAllPlayerIds(): Promise<string[]> {
|
||||
await this._ensureInit();
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(this._memoryPath);
|
||||
return files
|
||||
.filter(f => f.endsWith('.json'))
|
||||
.map(f => f.slice(0, -5)); // 移除 .json 后缀 | Remove .json suffix
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加载世界状态
|
||||
* @en Load world state
|
||||
*/
|
||||
async loadWorldState(): Promise<WorldState | null> {
|
||||
await this._ensureInit();
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(this._worldPath, 'utf-8');
|
||||
return JSON.parse(content) as WorldState;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`Failed to load world state: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 保存世界状态
|
||||
* @en Save world state
|
||||
*/
|
||||
async saveWorldState(state: WorldState): Promise<void> {
|
||||
await this._ensureInit();
|
||||
|
||||
const stateWithTimestamp: WorldState = {
|
||||
...state,
|
||||
lastSaveTime: Date.now()
|
||||
};
|
||||
|
||||
const content = this._config.prettyPrint
|
||||
? JSON.stringify(stateWithTimestamp, null, 2)
|
||||
: JSON.stringify(stateWithTimestamp);
|
||||
|
||||
try {
|
||||
await fs.writeFile(this._worldPath, content, 'utf-8');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to save world state: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取存储统计信息
|
||||
* @en Get storage statistics
|
||||
*/
|
||||
async getStats(): Promise<MemoryStoreStats> {
|
||||
await this._ensureInit();
|
||||
|
||||
const playerIds = await this.getAllPlayerIds();
|
||||
let totalSize = 0;
|
||||
let lastSaveTime = 0;
|
||||
|
||||
for (const playerId of playerIds) {
|
||||
try {
|
||||
const filePath = this._getPlayerMemoryPath(playerId);
|
||||
const stats = await fs.stat(filePath);
|
||||
totalSize += stats.size;
|
||||
if (stats.mtimeMs > lastSaveTime) {
|
||||
lastSaveTime = stats.mtimeMs;
|
||||
}
|
||||
} catch {
|
||||
// 忽略错误 | Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
// 加上世界状态文件大小 | Add world state file size
|
||||
try {
|
||||
const worldStats = await fs.stat(this._worldPath);
|
||||
totalSize += worldStats.size;
|
||||
if (worldStats.mtimeMs > lastSaveTime) {
|
||||
lastSaveTime = worldStats.mtimeMs;
|
||||
}
|
||||
} catch {
|
||||
// 忽略错误 | Ignore errors
|
||||
}
|
||||
|
||||
return {
|
||||
playerCount: playerIds.length,
|
||||
totalSize,
|
||||
lastSaveTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清除所有数据(慎用!)
|
||||
* @en Clear all data (use with caution!)
|
||||
*/
|
||||
async clearAll(): Promise<void> {
|
||||
await this._ensureInit();
|
||||
|
||||
const playerIds = await this.getAllPlayerIds();
|
||||
await Promise.all(playerIds.map(id => this.deletePlayerMemory(id)));
|
||||
|
||||
try {
|
||||
await fs.unlink(this._worldPath);
|
||||
} catch {
|
||||
// 忽略错误 | Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
131
packages/engine/script-runtime/src/persistence/IMemoryStore.ts
Normal file
131
packages/engine/script-runtime/src/persistence/IMemoryStore.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* @zh Memory 存储接口
|
||||
* @en Memory Store Interface
|
||||
*
|
||||
* @zh 定义玩家 Memory 和世界状态的持久化接口
|
||||
* @en Defines persistence interface for player Memory and world state
|
||||
*/
|
||||
|
||||
/**
|
||||
* @zh 玩家 Memory 类型
|
||||
* @en Player Memory type
|
||||
*/
|
||||
export type PlayerMemory = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* @zh 世界状态类型
|
||||
* @en World state type
|
||||
*/
|
||||
export interface WorldState {
|
||||
/**
|
||||
* @zh 当前 tick
|
||||
* @en Current tick
|
||||
*/
|
||||
tick: number;
|
||||
|
||||
/**
|
||||
* @zh 上次保存时间
|
||||
* @en Last save time
|
||||
*/
|
||||
lastSaveTime: number;
|
||||
|
||||
/**
|
||||
* @zh 实体状态
|
||||
* @en Entity states
|
||||
*/
|
||||
entities: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* @zh 房间状态
|
||||
* @en Room states
|
||||
*/
|
||||
rooms: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh Memory 存储接口
|
||||
* @en Memory store interface
|
||||
*/
|
||||
export interface IMemoryStore {
|
||||
/**
|
||||
* @zh 加载玩家 Memory
|
||||
* @en Load player Memory
|
||||
*
|
||||
* @param playerId - @zh 玩家 ID @en Player ID
|
||||
* @returns @zh 玩家 Memory @en Player Memory
|
||||
*/
|
||||
loadPlayerMemory(playerId: string): Promise<PlayerMemory>;
|
||||
|
||||
/**
|
||||
* @zh 保存玩家 Memory
|
||||
* @en Save player Memory
|
||||
*
|
||||
* @param playerId - @zh 玩家 ID @en Player ID
|
||||
* @param memory - @zh Memory 数据 @en Memory data
|
||||
*/
|
||||
savePlayerMemory(playerId: string, memory: PlayerMemory): Promise<void>;
|
||||
|
||||
/**
|
||||
* @zh 批量保存玩家 Memory
|
||||
* @en Batch save player Memory
|
||||
*
|
||||
* @param entries - @zh Memory 条目列表 @en Memory entry list
|
||||
*/
|
||||
savePlayerMemoryBatch(entries: Array<{ playerId: string; memory: PlayerMemory }>): Promise<void>;
|
||||
|
||||
/**
|
||||
* @zh 删除玩家 Memory
|
||||
* @en Delete player Memory
|
||||
*
|
||||
* @param playerId - @zh 玩家 ID @en Player ID
|
||||
*/
|
||||
deletePlayerMemory(playerId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* @zh 获取所有玩家 ID
|
||||
* @en Get all player IDs
|
||||
*/
|
||||
getAllPlayerIds(): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* @zh 加载世界状态
|
||||
* @en Load world state
|
||||
*/
|
||||
loadWorldState(): Promise<WorldState | null>;
|
||||
|
||||
/**
|
||||
* @zh 保存世界状态
|
||||
* @en Save world state
|
||||
*/
|
||||
saveWorldState(state: WorldState): Promise<void>;
|
||||
|
||||
/**
|
||||
* @zh 获取存储统计信息
|
||||
* @en Get storage statistics
|
||||
*/
|
||||
getStats(): Promise<MemoryStoreStats>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 存储统计信息
|
||||
* @en Storage statistics
|
||||
*/
|
||||
export interface MemoryStoreStats {
|
||||
/**
|
||||
* @zh 玩家数量
|
||||
* @en Player count
|
||||
*/
|
||||
playerCount: number;
|
||||
|
||||
/**
|
||||
* @zh 总存储大小(字节)
|
||||
* @en Total storage size (bytes)
|
||||
*/
|
||||
totalSize: number;
|
||||
|
||||
/**
|
||||
* @zh 上次保存时间
|
||||
* @en Last save time
|
||||
*/
|
||||
lastSaveTime: number;
|
||||
}
|
||||
Reference in New Issue
Block a user