Files
esengine/packages/script-runtime/src/server/TickScheduler.ts

420 lines
12 KiB
TypeScript
Raw Normal View History

feat(script-runtime): 服务器端蓝图执行模块 (#322) * feat(script-runtime): 添加服务器端蓝图执行模块 - ServerBlueprintVM: 服务器端蓝图虚拟机 - CPULimiter: CPU 时间和步数限制 - IntentCollector: 意图收集系统 - FileMemoryStore: 文件系统持久化 - ServerExecutionContext: 服务器执行上下文 * refactor(script-runtime): 分离引擎接口与游戏逻辑 - 重构 IntentTypes.ts 只保留基础 IIntent 接口和通用常量 - IntentCollector 改为泛型类,支持任意意图类型 - ServerExecutionContext 改为泛型类,支持任意游戏状态类型 - ServerBlueprintVM 改为泛型类,使用 TGameState 和 TIntent 类型参数 - 移除游戏特定类型(IUnitState, ISpawnerState 等),由游戏项目定义 - 添加 IntentKeyExtractor 机制用于防止重复意图 * feat(script-runtime): 添加服务器端游戏循环框架 - PlayerSession: 封装单个玩家的 VM、蓝图和 Memory 状态 - TickScheduler: 管理所有玩家会话,调度每 tick 的蓝图执行 - IIntentProcessor: 意图处理器接口,由游戏项目实现 - IntentProcessorBase: 意图处理器基类,提供常用处理模式 - IntentProcessorRegistry: 按类型注册意图处理器 - GameLoop: 完整的游戏主循环,协调各组件工作 * feat(script-runtime): 添加通用蓝图节点 Memory 节点: - GetMemory: 读取玩家 Memory - SetMemory: 写入玩家 Memory - HasMemoryKey: 检查键是否存在 - DeleteMemory: 删除 Memory 键 Log 节点: - Log: 记录日志 - Warn: 记录警告 - Error: 记录错误 Game 信息节点: - GetTick: 获取当前 tick - GetPlayerId: 获取玩家 ID - GetDeltaTime: 获取增量时间 - GetGameState: 获取游戏状态 提供 registerScriptRuntimeNodes() 用于批量注册节点 * fix(script-runtime): 修复 CI 构建错误 - 更新 tsconfig.json 继承 tsconfig.base.json - 添加 references 到 core 和 blueprint 包 - 更新 pnpm-lock.yaml * fix(script-runtime): 修复 DTS 构建错误 - 添加 tsconfig.build.json 用于 tsup 构建 - 更新 tsup.config.ts 使用 tsconfig.build.json - 分离构建配置和类型检查配置
2025-12-25 11:00:43 +08:00
/**
* @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;
}