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:
368
packages/engine/script-runtime/src/server/GameLoop.ts
Normal file
368
packages/engine/script-runtime/src/server/GameLoop.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
248
packages/engine/script-runtime/src/server/IIntentProcessor.ts
Normal file
248
packages/engine/script-runtime/src/server/IIntentProcessor.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
294
packages/engine/script-runtime/src/server/PlayerSession.ts
Normal file
294
packages/engine/script-runtime/src/server/PlayerSession.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
419
packages/engine/script-runtime/src/server/TickScheduler.ts
Normal file
419
packages/engine/script-runtime/src/server/TickScheduler.ts
Normal 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;
|
||||
}
|
||||
30
packages/engine/script-runtime/src/server/index.ts
Normal file
30
packages/engine/script-runtime/src/server/index.ts
Normal 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';
|
||||
233
packages/engine/script-runtime/src/server/types.ts
Normal file
233
packages/engine/script-runtime/src/server/types.ts
Normal 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>;
|
||||
}
|
||||
Reference in New Issue
Block a user