Files
esengine/packages/engine/script-runtime/src/server/PlayerSession.ts

295 lines
7.8 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
* @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
};
}
}