Files
esengine/packages/script-runtime/src/server/TickScheduler.ts
YHH 6b8b65ae16 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

420 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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;
}