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,368 @@
/**
* @zh 游戏主循环
* @en Game Main Loop
*
* @zh 协调玩家蓝图执行、意图处理和状态更新
* @en Coordinates player blueprint execution, intent processing, and state updates
*/
import type { IIntent } from '../intent/IntentTypes';
import type { IGameState } from '../vm/ServerExecutionContext';
import type { IMemoryStore } from '../persistence/IMemoryStore';
import type { TickScheduler } from './TickScheduler';
import type { IIntentProcessor } from './IIntentProcessor';
import type {
GameLoopConfig,
GameLoopState,
GameLoopEvents,
TickExecutionResult,
IntentProcessingResult
} from './types';
/**
* @zh 默认游戏循环配置
* @en Default game loop configuration
*/
export const DEFAULT_GAME_LOOP_CONFIG: Required<GameLoopConfig> = {
tickInterval: 1000,
maxCatchUpTicks: 5,
autoSaveMemory: true,
memorySaveInterval: 10
};
/**
* @zh 游戏循环统计
* @en Game loop statistics
*/
export interface GameLoopStats {
readonly currentTick: number;
readonly state: GameLoopState;
readonly totalTicksProcessed: number;
readonly averageTickDuration: number;
readonly lastTickDuration: number;
readonly skippedTicks: number;
}
/**
* @zh 游戏主循环
* @en Game Main Loop
*
* @zh 完整的游戏循环实现,协调各组件工作
* @en Complete game loop implementation, coordinating all components
*
* @typeParam TGameState - @zh 游戏状态类型 @en Game state type
* @typeParam TIntent - @zh 意图类型 @en Intent type
*
* @example
* ```typescript
* const gameLoop = new GameLoop<MyGameState, MyIntent>({
* scheduler,
* intentProcessor,
* memoryStore,
* getGameState: () => gameWorld.getState(),
* updateGameState: (state) => gameWorld.setState(state),
* config: { tickInterval: 1000 }
* });
*
* // 启动游戏循环
* await gameLoop.start();
*
* // 稍后停止
* await gameLoop.stop();
* ```
*/
export class GameLoop<
TGameState extends IGameState = IGameState,
TIntent extends IIntent = IIntent
> {
private readonly _scheduler: TickScheduler<TGameState, TIntent>;
private readonly _intentProcessor: IIntentProcessor<TGameState, TIntent>;
private readonly _memoryStore: IMemoryStore | null;
private readonly _getGameState: () => TGameState;
private readonly _updateGameState: (state: TGameState) => void;
private readonly _config: Required<GameLoopConfig>;
private readonly _events: GameLoopEvents<TGameState, TIntent>;
private _state: GameLoopState = 'idle';
private _currentTick: number = 0;
private _timerId: ReturnType<typeof setTimeout> | null = null;
private _lastTickTime: number = 0;
private _totalTicksProcessed: number = 0;
private _totalTickDuration: number = 0;
private _lastTickDuration: number = 0;
private _skippedTicks: number = 0;
private _ticksSinceLastSave: number = 0;
constructor(options: {
scheduler: TickScheduler<TGameState, TIntent>;
intentProcessor: IIntentProcessor<TGameState, TIntent>;
memoryStore?: IMemoryStore;
getGameState: () => TGameState;
updateGameState: (state: TGameState) => void;
config?: GameLoopConfig;
events?: GameLoopEvents<TGameState, TIntent>;
}) {
this._scheduler = options.scheduler;
this._intentProcessor = options.intentProcessor;
this._memoryStore = options.memoryStore ?? null;
this._getGameState = options.getGameState;
this._updateGameState = options.updateGameState;
this._config = { ...DEFAULT_GAME_LOOP_CONFIG, ...options.config };
this._events = options.events ?? {};
}
// =========================================================================
// 属性 | Properties
// =========================================================================
/**
* @zh 获取当前状态
* @en Get current state
*/
get state(): GameLoopState {
return this._state;
}
/**
* @zh 获取当前 tick
* @en Get current tick
*/
get currentTick(): number {
return this._currentTick;
}
/**
* @zh 获取调度器
* @en Get scheduler
*/
get scheduler(): TickScheduler<TGameState, TIntent> {
return this._scheduler;
}
// =========================================================================
// 生命周期 | Lifecycle
// =========================================================================
/**
* @zh 启动游戏循环
* @en Start game loop
*
* @param startTick - @zh 起始 tick可选@en Starting tick (optional)
*/
async start(startTick?: number): Promise<void> {
if (this._state === 'running') {
return;
}
if (startTick !== undefined) {
this._currentTick = startTick;
}
this._state = 'running';
this._lastTickTime = performance.now();
this._scheduleNextTick();
}
/**
* @zh 停止游戏循环
* @en Stop game loop
*/
async stop(): Promise<void> {
if (this._state !== 'running' && this._state !== 'paused') {
return;
}
this._state = 'stopping';
if (this._timerId !== null) {
clearTimeout(this._timerId);
this._timerId = null;
}
if (this._config.autoSaveMemory && this._memoryStore) {
await this._saveAllMemory();
}
this._state = 'idle';
}
/**
* @zh 暂停游戏循环
* @en Pause game loop
*/
pause(): void {
if (this._state === 'running') {
this._state = 'paused';
if (this._timerId !== null) {
clearTimeout(this._timerId);
this._timerId = null;
}
}
}
/**
* @zh 恢复游戏循环
* @en Resume game loop
*/
resume(): void {
if (this._state === 'paused') {
this._state = 'running';
this._lastTickTime = performance.now();
this._scheduleNextTick();
}
}
// =========================================================================
// 手动执行 | Manual Execution
// =========================================================================
/**
* @zh 手动执行单个 tick
* @en Manually execute a single tick
*
* @zh 用于测试或单步调试
* @en For testing or step-by-step debugging
*/
async executeSingleTick(): Promise<{
playerResults: TickExecutionResult<TIntent>;
intentResults: IntentProcessingResult<TGameState>;
}> {
const gameState = this._getGameState();
const stateWithTick = this._withTick(gameState, this._currentTick);
await this._events.onTickStart?.(this._currentTick);
const playerResults = this._scheduler.executeTick(stateWithTick);
await this._events.onPlayersExecuted?.(playerResults);
const intentResults = this._intentProcessor.process(stateWithTick, playerResults.allIntents);
await this._events.onIntentsProcessed?.(intentResults);
this._updateGameState(intentResults.gameState);
await this._events.onTickEnd?.(this._currentTick, intentResults.gameState);
this._currentTick++;
return { playerResults, intentResults };
}
// =========================================================================
// 统计 | Statistics
// =========================================================================
/**
* @zh 获取统计信息
* @en Get statistics
*/
getStats(): GameLoopStats {
return {
currentTick: this._currentTick,
state: this._state,
totalTicksProcessed: this._totalTicksProcessed,
averageTickDuration: this._totalTicksProcessed > 0
? this._totalTickDuration / this._totalTicksProcessed
: 0,
lastTickDuration: this._lastTickDuration,
skippedTicks: this._skippedTicks
};
}
// =========================================================================
// 私有方法 | Private Methods
// =========================================================================
private _scheduleNextTick(): void {
if (this._state !== 'running') {
return;
}
const now = performance.now();
const elapsed = now - this._lastTickTime;
const delay = Math.max(0, this._config.tickInterval - elapsed);
this._timerId = setTimeout(() => {
this._runTick().catch(error => {
this._events.onError?.(error, this._currentTick);
});
}, delay);
}
private async _runTick(): Promise<void> {
if (this._state !== 'running') {
return;
}
const tickStartTime = performance.now();
try {
await this.executeSingleTick();
this._lastTickDuration = performance.now() - tickStartTime;
this._totalTickDuration += this._lastTickDuration;
this._totalTicksProcessed++;
this._ticksSinceLastSave++;
if (
this._config.autoSaveMemory &&
this._memoryStore &&
this._ticksSinceLastSave >= this._config.memorySaveInterval
) {
await this._saveAllMemory();
this._ticksSinceLastSave = 0;
}
this._handleCatchUp(tickStartTime);
} catch (error) {
await this._events.onError?.(
error instanceof Error ? error : new Error(String(error)),
this._currentTick
);
}
this._lastTickTime = performance.now();
this._scheduleNextTick();
}
private _handleCatchUp(tickStartTime: number): void {
const tickEndTime = performance.now();
const tickDuration = tickEndTime - tickStartTime;
if (tickDuration > this._config.tickInterval) {
const missedTicks = Math.floor(tickDuration / this._config.tickInterval);
const ticksToSkip = Math.min(missedTicks, this._config.maxCatchUpTicks);
if (ticksToSkip > 0) {
this._skippedTicks += ticksToSkip;
this._currentTick += ticksToSkip;
}
}
}
private async _saveAllMemory(): Promise<void> {
if (!this._memoryStore) return;
const entries: Array<{ playerId: string; memory: Record<string, unknown> }> = [];
for (const playerId of this._scheduler.playerIds) {
const memory = this._scheduler.getPlayerMemory(playerId);
if (memory) {
entries.push({ playerId, memory: memory as Record<string, unknown> });
}
}
if (entries.length > 0) {
await this._memoryStore.savePlayerMemoryBatch(entries);
}
}
private _withTick(state: TGameState, tick: number): TGameState {
return {
...state,
tick,
deltaTime: this._config.tickInterval / 1000
};
}
}

View File

@@ -0,0 +1,248 @@
/**
* @zh 意图处理器接口
* @en Intent Processor Interface
*
* @zh 定义意图处理的抽象接口,由游戏项目实现
* @en Defines abstract interface for intent processing, implemented by game projects
*/
import type { IIntent } from '../intent/IntentTypes';
import type { IGameState } from '../vm/ServerExecutionContext';
import type { IntentProcessingResult } from './types';
/**
* @zh 意图处理器接口
* @en Intent processor interface
*
* @zh 游戏项目实现此接口以处理玩家蓝图产生的意图
* @en Game projects implement this interface to process intents from player blueprints
*
* @typeParam TGameState - @zh 游戏状态类型 @en Game state type
* @typeParam TIntent - @zh 意图类型 @en Intent type
*
* @example
* ```typescript
* // 游戏项目中实现 | Implement in game project
* class MyIntentProcessor implements IIntentProcessor<MyGameState, MyIntent> {
* process(gameState: MyGameState, intents: MyIntent[]): IntentProcessingResult<MyGameState> {
* let newState = gameState;
* let processedCount = 0;
* let rejectedCount = 0;
* const errors: string[] = [];
*
* for (const intent of intents) {
* const result = this.processIntent(newState, intent);
* if (result.success) {
* newState = result.state;
* processedCount++;
* } else {
* rejectedCount++;
* errors.push(result.error);
* }
* }
*
* return { gameState: newState, processedCount, rejectedCount, errors };
* }
* }
* ```
*/
export interface IIntentProcessor<
TGameState extends IGameState = IGameState,
TIntent extends IIntent = IIntent
> {
/**
* @zh 处理一批意图
* @en Process a batch of intents
*
* @param gameState - @zh 当前游戏状态 @en Current game state
* @param intents - @zh 要处理的意图列表 @en Intents to process
* @returns @zh 处理结果 @en Processing result
*/
process(gameState: TGameState, intents: readonly TIntent[]): IntentProcessingResult<TGameState>;
}
/**
* @zh 单个意图处理结果
* @en Single intent processing result
*
* @typeParam TGameState - @zh 游戏状态类型 @en Game state type
*/
export type SingleIntentResult<TGameState extends IGameState = IGameState> =
| { readonly success: true; readonly state: TGameState }
| { readonly success: false; readonly error: string };
/**
* @zh 意图处理器基类
* @en Intent processor base class
*
* @zh 提供常用的意图处理模式
* @en Provides common intent processing patterns
*
* @typeParam TGameState - @zh 游戏状态类型 @en Game state type
* @typeParam TIntent - @zh 意图类型 @en Intent type
*
* @example
* ```typescript
* class MyProcessor extends IntentProcessorBase<MyGameState, MyIntent> {
* protected processIntent(state: MyGameState, intent: MyIntent): SingleIntentResult<MyGameState> {
* switch (intent.type) {
* case 'unit.move':
* return this.processUnitMove(state, intent);
* case 'unit.attack':
* return this.processUnitAttack(state, intent);
* default:
* return { success: false, error: `Unknown intent type: ${intent.type}` };
* }
* }
* }
* ```
*/
export abstract class IntentProcessorBase<
TGameState extends IGameState = IGameState,
TIntent extends IIntent = IIntent
> implements IIntentProcessor<TGameState, TIntent> {
/**
* @zh 处理一批意图
* @en Process a batch of intents
*/
process(gameState: TGameState, intents: readonly TIntent[]): IntentProcessingResult<TGameState> {
let currentState = gameState;
let processedCount = 0;
let rejectedCount = 0;
const errors: string[] = [];
const sortedIntents = this.sortIntents(intents);
for (const intent of sortedIntents) {
if (!this.validateIntent(currentState, intent)) {
rejectedCount++;
errors.push(`Intent validation failed: ${intent.type}`);
continue;
}
const result = this.processIntent(currentState, intent);
if (result.success) {
currentState = result.state;
processedCount++;
} else {
rejectedCount++;
errors.push(result.error);
}
}
return {
gameState: currentState,
processedCount,
rejectedCount,
errors
};
}
/**
* @zh 处理单个意图
* @en Process a single intent
*
* @zh 子类必须实现此方法
* @en Subclasses must implement this method
*/
protected abstract processIntent(
state: TGameState,
intent: TIntent
): SingleIntentResult<TGameState>;
/**
* @zh 验证意图是否有效
* @en Validate whether intent is valid
*
* @zh 子类可以覆盖此方法添加验证逻辑
* @en Subclasses can override this method to add validation logic
*/
protected validateIntent(_state: TGameState, _intent: TIntent): boolean {
return true;
}
/**
* @zh 对意图进行排序
* @en Sort intents
*
* @zh 子类可以覆盖此方法定义处理顺序
* @en Subclasses can override this method to define processing order
*/
protected sortIntents(intents: readonly TIntent[]): readonly TIntent[] {
return intents;
}
}
/**
* @zh 意图处理器注册表
* @en Intent processor registry
*
* @zh 按意图类型注册处理器
* @en Register processors by intent type
*
* @typeParam TGameState - @zh 游戏状态类型 @en Game state type
* @typeParam TIntent - @zh 意图类型 @en Intent type
*/
export class IntentProcessorRegistry<
TGameState extends IGameState = IGameState,
TIntent extends IIntent = IIntent
> implements IIntentProcessor<TGameState, TIntent> {
private readonly _handlers = new Map<
string,
(state: TGameState, intent: TIntent) => SingleIntentResult<TGameState>
>();
/**
* @zh 注册意图处理器
* @en Register intent handler
*
* @param intentType - @zh 意图类型 @en Intent type
* @param handler - @zh 处理函数 @en Handler function
*/
register(
intentType: string,
handler: (state: TGameState, intent: TIntent) => SingleIntentResult<TGameState>
): this {
this._handlers.set(intentType, handler);
return this;
}
/**
* @zh 处理一批意图
* @en Process a batch of intents
*/
process(gameState: TGameState, intents: readonly TIntent[]): IntentProcessingResult<TGameState> {
let currentState = gameState;
let processedCount = 0;
let rejectedCount = 0;
const errors: string[] = [];
for (const intent of intents) {
const handler = this._handlers.get(intent.type);
if (!handler) {
rejectedCount++;
errors.push(`No handler for intent type: ${intent.type}`);
continue;
}
const result = handler(currentState, intent);
if (result.success) {
currentState = result.state;
processedCount++;
} else {
rejectedCount++;
errors.push(result.error);
}
}
return {
gameState: currentState,
processedCount,
rejectedCount,
errors
};
}
}

View File

@@ -0,0 +1,294 @@
/**
* @zh 玩家会话
* @en Player Session
*
* @zh 封装单个玩家的 VM 实例、蓝图和 Memory 状态
* @en Encapsulates a single player's VM instance, blueprint and Memory state
*/
import type { BlueprintAsset } from '@esengine/blueprint';
import type { IIntent, IntentKeyExtractor } from '../intent/IntentTypes';
import type { IGameState } from '../vm/ServerExecutionContext';
import type { CPULimiterConfig } from '../vm/CPULimiter';
import { ServerBlueprintVM } from '../vm/ServerBlueprintVM';
import type { PlayerTickResult } from './types';
/**
* @zh 玩家会话配置
* @en Player session configuration
*
* @typeParam TIntent - @zh 意图类型 @en Intent type
*/
export interface PlayerSessionConfig<TIntent extends IIntent = IIntent> {
/**
* @zh CPU 限制配置
* @en CPU limit configuration
*/
readonly cpuConfig?: Partial<CPULimiterConfig>;
/**
* @zh 意图键提取器
* @en Intent key extractor
*/
readonly intentKeyExtractor?: IntentKeyExtractor<TIntent>;
/**
* @zh 调试模式
* @en Debug mode
*/
readonly debug?: boolean;
}
/**
* @zh 玩家会话状态
* @en Player session state
*/
export type PlayerSessionState = 'active' | 'suspended' | 'error';
/**
* @zh 玩家会话
* @en Player Session
*
* @zh 管理单个玩家的蓝图执行环境
* @en Manages a single player's blueprint execution environment
*
* @typeParam TGameState - @zh 游戏状态类型 @en Game state type
* @typeParam TIntent - @zh 意图类型 @en Intent type
*
* @example
* ```typescript
* const session = new PlayerSession<MyGameState, MyIntent>(
* 'player1',
* playerBlueprint,
* { cpuConfig: { maxCpuTime: 50 } }
* );
*
* const result = session.executeTick(gameState);
* ```
*/
export class PlayerSession<
TGameState extends IGameState = IGameState,
TIntent extends IIntent = IIntent
> {
private readonly _playerId: string;
private readonly _vm: ServerBlueprintVM<TGameState, TIntent>;
private _memory: Record<string, unknown>;
private _state: PlayerSessionState = 'active';
private _lastError: string | null = null;
private _totalCpuUsed: number = 0;
private _ticksExecuted: number = 0;
/**
* @param playerId - @zh 玩家 ID @en Player ID
* @param blueprint - @zh 蓝图资产 @en Blueprint asset
* @param config - @zh 配置选项 @en Configuration options
*/
constructor(
playerId: string,
blueprint: BlueprintAsset,
config: PlayerSessionConfig<TIntent> = {}
) {
this._playerId = playerId;
this._memory = {};
this._vm = new ServerBlueprintVM<TGameState, TIntent>(playerId, blueprint, {
cpuConfig: config.cpuConfig,
intentKeyExtractor: config.intentKeyExtractor,
debug: config.debug
});
}
// =========================================================================
// 属性 | Properties
// =========================================================================
/**
* @zh 获取玩家 ID
* @en Get player ID
*/
get playerId(): string {
return this._playerId;
}
/**
* @zh 获取会话状态
* @en Get session state
*/
get state(): PlayerSessionState {
return this._state;
}
/**
* @zh 获取最后一次错误
* @en Get last error
*/
get lastError(): string | null {
return this._lastError;
}
/**
* @zh 获取累计 CPU 使用时间
* @en Get total CPU time used
*/
get totalCpuUsed(): number {
return this._totalCpuUsed;
}
/**
* @zh 获取已执行的 tick 数
* @en Get number of ticks executed
*/
get ticksExecuted(): number {
return this._ticksExecuted;
}
/**
* @zh 获取当前 Memory
* @en Get current Memory
*/
get memory(): Readonly<Record<string, unknown>> {
return this._memory;
}
/**
* @zh 获取底层 VM 实例
* @en Get underlying VM instance
*/
get vm(): ServerBlueprintVM<TGameState, TIntent> {
return this._vm;
}
// =========================================================================
// Memory 管理 | Memory Management
// =========================================================================
/**
* @zh 设置 Memory
* @en Set Memory
*/
setMemory(memory: Record<string, unknown>): void {
this._memory = { ...memory };
}
/**
* @zh 更新 Memory合并
* @en Update Memory (merge)
*/
updateMemory(updates: Record<string, unknown>): void {
this._memory = { ...this._memory, ...updates };
}
/**
* @zh 清空 Memory
* @en Clear Memory
*/
clearMemory(): void {
this._memory = {};
}
// =========================================================================
// 执行 | Execution
// =========================================================================
/**
* @zh 执行一个 tick
* @en Execute one tick
*
* @param gameState - @zh 当前游戏状态 @en Current game state
* @returns @zh 执行结果 @en Execution result
*/
executeTick(gameState: TGameState): PlayerTickResult<TIntent> {
if (this._state === 'suspended') {
return this._createSkippedResult('Session is suspended');
}
try {
const result = this._vm.executeTick(gameState, this._memory);
this._memory = result.memory;
this._totalCpuUsed += result.cpu.used;
this._ticksExecuted++;
if (!result.success && result.errors.length > 0) {
this._lastError = result.errors[0];
}
return {
playerId: this._playerId,
success: result.success,
cpu: result.cpu,
intents: result.intents,
logs: result.logs,
errors: result.errors,
memory: result.memory
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this._state = 'error';
this._lastError = errorMessage;
return this._createSkippedResult(errorMessage);
}
}
// =========================================================================
// 状态管理 | State Management
// =========================================================================
/**
* @zh 暂停会话
* @en Suspend session
*/
suspend(): void {
this._state = 'suspended';
}
/**
* @zh 恢复会话
* @en Resume session
*/
resume(): void {
if (this._state === 'suspended') {
this._state = 'active';
}
}
/**
* @zh 重置会话
* @en Reset session
*/
reset(): void {
this._vm.reset();
this._state = 'active';
this._lastError = null;
this._totalCpuUsed = 0;
this._ticksExecuted = 0;
}
/**
* @zh 从错误状态恢复
* @en Recover from error state
*/
recover(): void {
if (this._state === 'error') {
this._vm.reset();
this._state = 'active';
this._lastError = null;
}
}
// =========================================================================
// 私有方法 | Private Methods
// =========================================================================
private _createSkippedResult(error: string): PlayerTickResult<TIntent> {
return {
playerId: this._playerId,
success: false,
cpu: { used: 0, limit: 0, bucket: 0, steps: 0, maxSteps: 0, exceeded: false },
intents: [],
logs: [],
errors: [error],
memory: this._memory
};
}
}

View File

@@ -0,0 +1,419 @@
/**
* @zh Tick 调度器
* @en Tick Scheduler
*
* @zh 管理所有玩家会话的执行调度
* @en Manages execution scheduling for all player sessions
*/
import type { BlueprintAsset } from '@esengine/blueprint';
import type { IIntent, IntentKeyExtractor } from '../intent/IntentTypes';
import type { IGameState } from '../vm/ServerExecutionContext';
import type { CPULimiterConfig } from '../vm/CPULimiter';
import { PlayerSession, type PlayerSessionConfig } from './PlayerSession';
import type { TickExecutionResult, PlayerTickResult } from './types';
/**
* @zh 调度器配置
* @en Scheduler configuration
*
* @typeParam TIntent - @zh 意图类型 @en Intent type
*/
export interface TickSchedulerConfig<TIntent extends IIntent = IIntent> {
/**
* @zh 默认 CPU 配置
* @en Default CPU configuration
*/
readonly defaultCpuConfig?: Partial<CPULimiterConfig>;
/**
* @zh 默认意图键提取器
* @en Default intent key extractor
*/
readonly defaultIntentKeyExtractor?: IntentKeyExtractor<TIntent>;
/**
* @zh 是否并行执行玩家蓝图
* @en Whether to execute player blueprints in parallel
*
* @default false
*/
readonly parallel?: boolean;
/**
* @zh 调试模式
* @en Debug mode
*/
readonly debug?: boolean;
}
/**
* @zh Tick 调度器
* @en Tick Scheduler
*
* @zh 负责管理玩家会话并在每个 tick 执行所有玩家的蓝图
* @en Responsible for managing player sessions and executing all player blueprints each tick
*
* @typeParam TGameState - @zh 游戏状态类型 @en Game state type
* @typeParam TIntent - @zh 意图类型 @en Intent type
*
* @example
* ```typescript
* const scheduler = new TickScheduler<MyGameState, MyIntent>({
* defaultCpuConfig: { maxCpuTime: 50 },
* defaultIntentKeyExtractor: (i) => `${i.type}:${i.unitId}`
* });
*
* // 添加玩家
* scheduler.addPlayer('player1', blueprint1);
* scheduler.addPlayer('player2', blueprint2);
*
* // 执行 tick
* const result = scheduler.executeTick(gameState);
* ```
*/
export class TickScheduler<
TGameState extends IGameState = IGameState,
TIntent extends IIntent = IIntent
> {
private readonly _sessions: Map<string, PlayerSession<TGameState, TIntent>> = new Map();
private readonly _config: TickSchedulerConfig<TIntent>;
constructor(config: TickSchedulerConfig<TIntent> = {}) {
this._config = config;
}
// =========================================================================
// 属性 | Properties
// =========================================================================
/**
* @zh 获取玩家数量
* @en Get player count
*/
get playerCount(): number {
return this._sessions.size;
}
/**
* @zh 获取所有玩家 ID
* @en Get all player IDs
*/
get playerIds(): string[] {
return Array.from(this._sessions.keys());
}
// =========================================================================
// 玩家管理 | Player Management
// =========================================================================
/**
* @zh 添加玩家
* @en Add player
*
* @param playerId - @zh 玩家 ID @en Player ID
* @param blueprint - @zh 蓝图资产 @en Blueprint asset
* @param config - @zh 会话配置(可选,覆盖默认配置)@en Session config (optional, overrides defaults)
* @returns @zh 创建的会话 @en Created session
*/
addPlayer(
playerId: string,
blueprint: BlueprintAsset,
config?: PlayerSessionConfig<TIntent>
): PlayerSession<TGameState, TIntent> {
if (this._sessions.has(playerId)) {
throw new Error(`Player ${playerId} already exists`);
}
const sessionConfig: PlayerSessionConfig<TIntent> = {
cpuConfig: config?.cpuConfig ?? this._config.defaultCpuConfig,
intentKeyExtractor: config?.intentKeyExtractor ?? this._config.defaultIntentKeyExtractor,
debug: config?.debug ?? this._config.debug
};
const session = new PlayerSession<TGameState, TIntent>(playerId, blueprint, sessionConfig);
this._sessions.set(playerId, session);
return session;
}
/**
* @zh 移除玩家
* @en Remove player
*
* @param playerId - @zh 玩家 ID @en Player ID
* @returns @zh 是否成功移除 @en Whether removed successfully
*/
removePlayer(playerId: string): boolean {
return this._sessions.delete(playerId);
}
/**
* @zh 获取玩家会话
* @en Get player session
*
* @param playerId - @zh 玩家 ID @en Player ID
* @returns @zh 玩家会话(如果存在)@en Player session (if exists)
*/
getSession(playerId: string): PlayerSession<TGameState, TIntent> | undefined {
return this._sessions.get(playerId);
}
/**
* @zh 检查玩家是否存在
* @en Check if player exists
*/
hasPlayer(playerId: string): boolean {
return this._sessions.has(playerId);
}
/**
* @zh 更新玩家蓝图
* @en Update player blueprint
*
* @param playerId - @zh 玩家 ID @en Player ID
* @param blueprint - @zh 新蓝图资产 @en New blueprint asset
*/
updatePlayerBlueprint(
playerId: string,
blueprint: BlueprintAsset,
config?: PlayerSessionConfig<TIntent>
): void {
const existingSession = this._sessions.get(playerId);
const memory = existingSession?.memory ?? {};
this._sessions.delete(playerId);
const newSession = this.addPlayer(playerId, blueprint, config);
newSession.setMemory(memory as Record<string, unknown>);
}
// =========================================================================
// Memory 管理 | Memory Management
// =========================================================================
/**
* @zh 设置玩家 Memory
* @en Set player Memory
*/
setPlayerMemory(playerId: string, memory: Record<string, unknown>): void {
const session = this._sessions.get(playerId);
if (session) {
session.setMemory(memory);
}
}
/**
* @zh 获取玩家 Memory
* @en Get player Memory
*/
getPlayerMemory(playerId: string): Readonly<Record<string, unknown>> | undefined {
return this._sessions.get(playerId)?.memory;
}
/**
* @zh 获取所有玩家的 Memory
* @en Get all players' Memory
*/
getAllMemories(): Map<string, Readonly<Record<string, unknown>>> {
const result = new Map<string, Readonly<Record<string, unknown>>>();
for (const [playerId, session] of this._sessions) {
result.set(playerId, session.memory);
}
return result;
}
// =========================================================================
// 执行 | Execution
// =========================================================================
/**
* @zh 执行一个 tick
* @en Execute one tick
*
* @param gameState - @zh 当前游戏状态 @en Current game state
* @returns @zh Tick 执行结果 @en Tick execution result
*/
executeTick(gameState: TGameState): TickExecutionResult<TIntent> {
const startTime = performance.now();
const playerResults = new Map<string, PlayerTickResult<TIntent>>();
const allIntents: TIntent[] = [];
let successCount = 0;
let failureCount = 0;
for (const [playerId, session] of this._sessions) {
if (session.state === 'active') {
const result = session.executeTick(gameState);
playerResults.set(playerId, result);
if (result.success) {
successCount++;
allIntents.push(...result.intents);
} else {
failureCount++;
}
}
}
const duration = performance.now() - startTime;
return {
tick: gameState.tick,
duration,
playerResults,
allIntents,
successCount,
failureCount
};
}
/**
* @zh 为指定玩家构建游戏状态视图
* @en Build game state view for specified player
*
* @zh 由游戏项目实现,用于过滤玩家可见的游戏状态
* @en Implemented by game project, used to filter player-visible game state
*/
buildPlayerView?(gameState: TGameState, playerId: string): TGameState;
/**
* @zh 使用玩家视图执行 tick
* @en Execute tick with player views
*
* @zh 如果提供了 buildPlayerView则为每个玩家构建独立视图
* @en If buildPlayerView is provided, builds independent view for each player
*/
executeTickWithViews(
gameState: TGameState,
buildView: (gameState: TGameState, playerId: string) => TGameState
): TickExecutionResult<TIntent> {
const startTime = performance.now();
const playerResults = new Map<string, PlayerTickResult<TIntent>>();
const allIntents: TIntent[] = [];
let successCount = 0;
let failureCount = 0;
for (const [playerId, session] of this._sessions) {
if (session.state === 'active') {
const playerView = buildView(gameState, playerId);
const result = session.executeTick(playerView);
playerResults.set(playerId, result);
if (result.success) {
successCount++;
allIntents.push(...result.intents);
} else {
failureCount++;
}
}
}
const duration = performance.now() - startTime;
return {
tick: gameState.tick,
duration,
playerResults,
allIntents,
successCount,
failureCount
};
}
// =========================================================================
// 状态管理 | State Management
// =========================================================================
/**
* @zh 暂停所有玩家
* @en Suspend all players
*/
suspendAll(): void {
for (const session of this._sessions.values()) {
session.suspend();
}
}
/**
* @zh 恢复所有玩家
* @en Resume all players
*/
resumeAll(): void {
for (const session of this._sessions.values()) {
session.resume();
}
}
/**
* @zh 重置所有玩家
* @en Reset all players
*/
resetAll(): void {
for (const session of this._sessions.values()) {
session.reset();
}
}
/**
* @zh 清空所有玩家
* @en Clear all players
*/
clear(): void {
this._sessions.clear();
}
// =========================================================================
// 统计 | Statistics
// =========================================================================
/**
* @zh 获取调度器统计信息
* @en Get scheduler statistics
*/
getStats(): SchedulerStats {
let totalCpuUsed = 0;
let totalTicksExecuted = 0;
let activeCount = 0;
let suspendedCount = 0;
let errorCount = 0;
for (const session of this._sessions.values()) {
totalCpuUsed += session.totalCpuUsed;
totalTicksExecuted += session.ticksExecuted;
switch (session.state) {
case 'active':
activeCount++;
break;
case 'suspended':
suspendedCount++;
break;
case 'error':
errorCount++;
break;
}
}
return {
playerCount: this._sessions.size,
activeCount,
suspendedCount,
errorCount,
totalCpuUsed,
totalTicksExecuted,
averageCpuPerPlayer: this._sessions.size > 0 ? totalCpuUsed / this._sessions.size : 0
};
}
}
/**
* @zh 调度器统计信息
* @en Scheduler statistics
*/
export interface SchedulerStats {
readonly playerCount: number;
readonly activeCount: number;
readonly suspendedCount: number;
readonly errorCount: number;
readonly totalCpuUsed: number;
readonly totalTicksExecuted: number;
readonly averageCpuPerPlayer: number;
}

View File

@@ -0,0 +1,30 @@
/**
* @zh 服务器端模块
* @en Server-side Module
*/
// Types
export type {
PlayerTickResult,
TickExecutionResult,
IntentProcessingResult,
GameLoopConfig,
GameLoopState,
GameLoopEvents
} from './types';
// PlayerSession
export { PlayerSession } from './PlayerSession';
export type { PlayerSessionConfig, PlayerSessionState } from './PlayerSession';
// TickScheduler
export { TickScheduler } from './TickScheduler';
export type { TickSchedulerConfig, SchedulerStats } from './TickScheduler';
// IntentProcessor
export type { IIntentProcessor, SingleIntentResult } from './IIntentProcessor';
export { IntentProcessorBase, IntentProcessorRegistry } from './IIntentProcessor';
// GameLoop
export { GameLoop, DEFAULT_GAME_LOOP_CONFIG } from './GameLoop';
export type { GameLoopStats } from './GameLoop';

View File

@@ -0,0 +1,233 @@
/**
* @zh 服务器端类型定义
* @en Server-side type definitions
*/
import type { IIntent } from '../intent/IntentTypes';
import type { IGameState, LogEntry } from '../vm/ServerExecutionContext';
import type { CPUStats } from '../vm/CPULimiter';
// =============================================================================
// 玩家会话类型 | Player Session Types
// =============================================================================
/**
* @zh 玩家执行结果
* @en Player execution result
*
* @typeParam TIntent - @zh 意图类型 @en Intent type
*/
export interface PlayerTickResult<TIntent extends IIntent = IIntent> {
/**
* @zh 玩家 ID
* @en Player ID
*/
readonly playerId: string;
/**
* @zh 是否执行成功
* @en Whether execution succeeded
*/
readonly success: boolean;
/**
* @zh CPU 使用统计
* @en CPU usage statistics
*/
readonly cpu: CPUStats;
/**
* @zh 收集的意图列表
* @en Collected intents list
*/
readonly intents: readonly TIntent[];
/**
* @zh 日志列表
* @en Log list
*/
readonly logs: readonly LogEntry[];
/**
* @zh 错误列表
* @en Error list
*/
readonly errors: readonly string[];
/**
* @zh 更新后的 Memory
* @en Updated Memory
*/
readonly memory: Record<string, unknown>;
}
// =============================================================================
// Tick 调度类型 | Tick Scheduler Types
// =============================================================================
/**
* @zh Tick 执行结果
* @en Tick execution result
*
* @typeParam TIntent - @zh 意图类型 @en Intent type
*/
export interface TickExecutionResult<TIntent extends IIntent = IIntent> {
/**
* @zh 当前 tick
* @en Current tick
*/
readonly tick: number;
/**
* @zh 执行耗时(毫秒)
* @en Execution duration (milliseconds)
*/
readonly duration: number;
/**
* @zh 所有玩家的执行结果
* @en All players' execution results
*/
readonly playerResults: ReadonlyMap<string, PlayerTickResult<TIntent>>;
/**
* @zh 所有收集的意图(合并后)
* @en All collected intents (merged)
*/
readonly allIntents: readonly TIntent[];
/**
* @zh 成功执行的玩家数
* @en Number of successfully executed players
*/
readonly successCount: number;
/**
* @zh 执行失败的玩家数
* @en Number of failed players
*/
readonly failureCount: number;
}
// =============================================================================
// 意图处理类型 | Intent Processing Types
// =============================================================================
/**
* @zh 意图处理结果
* @en Intent processing result
*
* @typeParam TGameState - @zh 游戏状态类型 @en Game state type
*/
export interface IntentProcessingResult<TGameState extends IGameState = IGameState> {
/**
* @zh 更新后的游戏状态
* @en Updated game state
*/
readonly gameState: TGameState;
/**
* @zh 处理的意图数量
* @en Number of processed intents
*/
readonly processedCount: number;
/**
* @zh 被拒绝的意图数量
* @en Number of rejected intents
*/
readonly rejectedCount: number;
/**
* @zh 处理错误
* @en Processing errors
*/
readonly errors: readonly string[];
}
// =============================================================================
// 游戏循环类型 | Game Loop Types
// =============================================================================
/**
* @zh 游戏循环配置
* @en Game loop configuration
*/
export interface GameLoopConfig {
/**
* @zh Tick 间隔(毫秒)
* @en Tick interval (milliseconds)
*
* @default 1000
*/
readonly tickInterval?: number;
/**
* @zh 最大追赶 tick 数(如果落后太多)
* @en Maximum catch-up ticks (if falling behind)
*
* @default 5
*/
readonly maxCatchUpTicks?: number;
/**
* @zh 是否在 tick 之间保存 Memory
* @en Whether to save Memory between ticks
*
* @default true
*/
readonly autoSaveMemory?: boolean;
/**
* @zh Memory 保存间隔tick 数)
* @en Memory save interval (in ticks)
*
* @default 10
*/
readonly memorySaveInterval?: number;
}
/**
* @zh 游戏循环状态
* @en Game loop state
*/
export type GameLoopState = 'idle' | 'running' | 'paused' | 'stopping';
/**
* @zh 游戏循环事件
* @en Game loop events
*/
export interface GameLoopEvents<
TGameState extends IGameState = IGameState,
TIntent extends IIntent = IIntent
> {
/**
* @zh Tick 开始前
* @en Before tick starts
*/
onTickStart?: (tick: number) => void | Promise<void>;
/**
* @zh 玩家蓝图执行完成后
* @en After player blueprints executed
*/
onPlayersExecuted?: (result: TickExecutionResult<TIntent>) => void | Promise<void>;
/**
* @zh 意图处理完成后
* @en After intents processed
*/
onIntentsProcessed?: (result: IntentProcessingResult<TGameState>) => void | Promise<void>;
/**
* @zh Tick 结束后
* @en After tick ends
*/
onTickEnd?: (tick: number, gameState: TGameState) => void | Promise<void>;
/**
* @zh 发生错误时
* @en When error occurs
*/
onError?: (error: Error, tick: number) => void | Promise<void>;
}