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:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -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
}
}
}

View 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;
}