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:
39
packages/engine/script-runtime/module.json
Normal file
39
packages/engine/script-runtime/module.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"id": "script-runtime",
|
||||
"name": "@esengine/script-runtime",
|
||||
"globalKey": "scriptRuntime",
|
||||
"displayName": "Script Runtime",
|
||||
"description": "Server-side blueprint execution for programmable strategy games | 服务器端蓝图执行,用于可编程策略游戏",
|
||||
"version": "1.0.0",
|
||||
"category": "Other",
|
||||
"icon": "Code",
|
||||
"tags": [
|
||||
"scripting",
|
||||
"server",
|
||||
"blueprint",
|
||||
"sandbox",
|
||||
"programmable"
|
||||
],
|
||||
"isCore": false,
|
||||
"defaultEnabled": false,
|
||||
"isEngineModule": true,
|
||||
"canContainContent": false,
|
||||
"platforms": [
|
||||
"desktop"
|
||||
],
|
||||
"dependencies": [
|
||||
"core",
|
||||
"blueprint"
|
||||
],
|
||||
"exports": {
|
||||
"other": [
|
||||
"ServerBlueprintVM",
|
||||
"IntentCollector",
|
||||
"CPULimiter",
|
||||
"FileMemoryStore"
|
||||
]
|
||||
},
|
||||
"requiresWasm": false,
|
||||
"outputPath": "dist/index.js",
|
||||
"pluginExport": "ScriptRuntimePlugin"
|
||||
}
|
||||
56
packages/engine/script-runtime/package.json
Normal file
56
packages/engine/script-runtime/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@esengine/script-runtime",
|
||||
"version": "1.0.0",
|
||||
"description": "Server-side blueprint execution for programmable strategy games | 服务器端蓝图执行,用于可编程策略游戏",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"module.json"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"script-runtime",
|
||||
"screeps",
|
||||
"server-side",
|
||||
"game-engine"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/blueprint": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"@types/node": "^20.19.17",
|
||||
"rimraf": "^5.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/esengine.git",
|
||||
"directory": "packages/script-runtime"
|
||||
}
|
||||
}
|
||||
143
packages/engine/script-runtime/src/index.ts
Normal file
143
packages/engine/script-runtime/src/index.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @esengine/script-runtime
|
||||
*
|
||||
* Server-side blueprint execution for programmable strategy games
|
||||
* 服务器端蓝图执行,用于可编程策略游戏
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// VM | Virtual Machine
|
||||
// =============================================================================
|
||||
|
||||
export { ServerBlueprintVM } from './vm/ServerBlueprintVM';
|
||||
export type { ServerVMConfig, TickResult } from './vm/ServerBlueprintVM';
|
||||
|
||||
export { ServerExecutionContext } from './vm/ServerExecutionContext';
|
||||
export type { IGameState, LogEntry } from './vm/ServerExecutionContext';
|
||||
|
||||
export { CPULimiter, DEFAULT_CPU_CONFIG } from './vm/CPULimiter';
|
||||
export type { CPULimiterConfig, CPUStats } from './vm/CPULimiter';
|
||||
|
||||
// =============================================================================
|
||||
// Intent System | 意图系统
|
||||
// =============================================================================
|
||||
|
||||
export { IntentCollector } from './intent/IntentCollector';
|
||||
export type { IIntentCollector } from './intent/IntentCollector';
|
||||
|
||||
export type { IIntent, IntentKeyExtractor, Direction } from './intent/IntentTypes';
|
||||
|
||||
export { defaultIntentKeyExtractor } from './intent/IntentTypes';
|
||||
|
||||
// Result constants
|
||||
export {
|
||||
OK,
|
||||
ERR_GENERIC,
|
||||
ERR_NOT_OWNER,
|
||||
ERR_INVALID_TARGET,
|
||||
ERR_NOT_IN_RANGE,
|
||||
ERR_NOT_ENOUGH_RESOURCES,
|
||||
ERR_BUSY,
|
||||
ERR_INVALID_ARGS,
|
||||
// Direction constants
|
||||
TOP,
|
||||
TOP_RIGHT,
|
||||
RIGHT,
|
||||
BOTTOM_RIGHT,
|
||||
BOTTOM,
|
||||
BOTTOM_LEFT,
|
||||
LEFT,
|
||||
TOP_LEFT
|
||||
} from './intent/IntentTypes';
|
||||
|
||||
// =============================================================================
|
||||
// Persistence | 持久化
|
||||
// =============================================================================
|
||||
|
||||
export { FileMemoryStore } from './persistence/FileMemoryStore';
|
||||
export type { FileMemoryStoreConfig } from './persistence/FileMemoryStore';
|
||||
|
||||
export type {
|
||||
IMemoryStore,
|
||||
PlayerMemory,
|
||||
WorldState,
|
||||
MemoryStoreStats
|
||||
} from './persistence/IMemoryStore';
|
||||
|
||||
// =============================================================================
|
||||
// Service Tokens | 服务令牌
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
ScriptRuntimeServiceToken,
|
||||
MemoryStoreToken
|
||||
} from './tokens';
|
||||
|
||||
export type { IScriptRuntimeService } from './tokens';
|
||||
|
||||
// =============================================================================
|
||||
// Server | 服务器端
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
// PlayerSession
|
||||
PlayerSession,
|
||||
// TickScheduler
|
||||
TickScheduler,
|
||||
// IntentProcessor
|
||||
IntentProcessorBase,
|
||||
IntentProcessorRegistry,
|
||||
// GameLoop
|
||||
GameLoop,
|
||||
DEFAULT_GAME_LOOP_CONFIG
|
||||
} from './server';
|
||||
|
||||
export type {
|
||||
// Types
|
||||
PlayerTickResult,
|
||||
TickExecutionResult,
|
||||
IntentProcessingResult,
|
||||
GameLoopConfig,
|
||||
GameLoopState,
|
||||
GameLoopEvents,
|
||||
// PlayerSession
|
||||
PlayerSessionConfig,
|
||||
PlayerSessionState,
|
||||
// TickScheduler
|
||||
TickSchedulerConfig,
|
||||
SchedulerStats,
|
||||
// IntentProcessor
|
||||
IIntentProcessor,
|
||||
SingleIntentResult,
|
||||
// GameLoop
|
||||
GameLoopStats
|
||||
} from './server';
|
||||
|
||||
// =============================================================================
|
||||
// Nodes | 蓝图节点
|
||||
// =============================================================================
|
||||
|
||||
export {
|
||||
// Registration
|
||||
registerScriptRuntimeNodes,
|
||||
AllNodeDefinitions,
|
||||
// Memory Nodes
|
||||
GetMemoryTemplate,
|
||||
SetMemoryTemplate,
|
||||
HasMemoryKeyTemplate,
|
||||
DeleteMemoryTemplate,
|
||||
MemoryNodeDefinitions,
|
||||
// Log Nodes
|
||||
LogTemplate,
|
||||
WarnTemplate,
|
||||
ErrorTemplate,
|
||||
LogNodeDefinitions,
|
||||
// Game Info Nodes
|
||||
GetTickTemplate,
|
||||
GetPlayerIdTemplate,
|
||||
GetDeltaTimeTemplate,
|
||||
GetGameStateTemplate,
|
||||
GameInfoNodeDefinitions
|
||||
} from './nodes';
|
||||
214
packages/engine/script-runtime/src/intent/IntentCollector.ts
Normal file
214
packages/engine/script-runtime/src/intent/IntentCollector.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* @zh 意图收集器
|
||||
* @en Intent Collector
|
||||
*
|
||||
* @zh 收集玩家蓝图执行过程中产生的所有意图
|
||||
* @en Collects all intents generated during player blueprint execution
|
||||
*/
|
||||
|
||||
import type { IIntent, IntentKeyExtractor } from './IntentTypes';
|
||||
import { defaultIntentKeyExtractor } from './IntentTypes';
|
||||
|
||||
/**
|
||||
* @zh 意图收集器接口
|
||||
* @en Intent collector interface
|
||||
*/
|
||||
export interface IIntentCollector<T extends IIntent = IIntent> {
|
||||
/**
|
||||
* @zh 添加一个意图
|
||||
* @en Add an intent
|
||||
*/
|
||||
addIntent(intent: T): boolean;
|
||||
|
||||
/**
|
||||
* @zh 获取所有收集的意图
|
||||
* @en Get all collected intents
|
||||
*/
|
||||
getIntents(): T[];
|
||||
|
||||
/**
|
||||
* @zh 按类型获取意图
|
||||
* @en Get intents by type
|
||||
*/
|
||||
getIntentsByType(type: string): T[];
|
||||
|
||||
/**
|
||||
* @zh 清除所有意图
|
||||
* @en Clear all intents
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* @zh 获取意图数量
|
||||
* @en Get intent count
|
||||
*/
|
||||
readonly count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 意图收集器
|
||||
* @en Intent Collector
|
||||
*
|
||||
* @zh 在蓝图执行过程中收集玩家的操作意图,执行完成后由服务器统一处理
|
||||
* @en Collects player operation intents during blueprint execution, processed by server after execution
|
||||
*
|
||||
* @typeParam T - @zh 意图类型,必须继承 IIntent @en Intent type, must extend IIntent
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 游戏项目中定义意图类型 | Define intent types in game project
|
||||
* interface MyGameIntent extends IIntent {
|
||||
* readonly type: 'unit.move' | 'unit.attack';
|
||||
* unitId: string;
|
||||
* }
|
||||
*
|
||||
* // 创建收集器时提供键提取器 | Provide key extractor when creating collector
|
||||
* const collector = new IntentCollector<MyGameIntent>('player1', {
|
||||
* keyExtractor: (intent) => `${intent.type}:${intent.unitId}`
|
||||
* });
|
||||
*
|
||||
* collector.addIntent({ type: 'unit.move', unitId: 'unit1' });
|
||||
* ```
|
||||
*/
|
||||
export class IntentCollector<T extends IIntent = IIntent> implements IIntentCollector<T> {
|
||||
/**
|
||||
* @zh 玩家 ID
|
||||
* @en Player ID
|
||||
*/
|
||||
private readonly _playerId: string;
|
||||
|
||||
/**
|
||||
* @zh 当前 tick
|
||||
* @en Current tick
|
||||
*/
|
||||
private _currentTick: number = 0;
|
||||
|
||||
/**
|
||||
* @zh 收集的意图列表
|
||||
* @en Collected intents list
|
||||
*/
|
||||
private _intents: T[] = [];
|
||||
|
||||
/**
|
||||
* @zh 按类型索引的意图
|
||||
* @en Intents indexed by type
|
||||
*/
|
||||
private _intentsByType: Map<string, T[]> = new Map();
|
||||
|
||||
/**
|
||||
* @zh 已添加的意图键(防止重复)
|
||||
* @en Added intent keys (prevent duplicates)
|
||||
*/
|
||||
private _intentKeys: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* @zh 意图键提取器
|
||||
* @en Intent key extractor
|
||||
*/
|
||||
private readonly _keyExtractor: IntentKeyExtractor<T>;
|
||||
|
||||
/**
|
||||
* @param playerId - @zh 玩家 ID @en Player ID
|
||||
* @param options - @zh 配置选项 @en Configuration options
|
||||
*/
|
||||
constructor(
|
||||
playerId: string,
|
||||
options: {
|
||||
keyExtractor?: IntentKeyExtractor<T>;
|
||||
} = {}
|
||||
) {
|
||||
this._playerId = playerId;
|
||||
this._keyExtractor = options.keyExtractor ?? (defaultIntentKeyExtractor as IntentKeyExtractor<T>);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取意图数量
|
||||
* @en Get intent count
|
||||
*/
|
||||
get count(): number {
|
||||
return this._intents.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取玩家 ID
|
||||
* @en Get player ID
|
||||
*/
|
||||
get playerId(): string {
|
||||
return this._playerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置当前 tick
|
||||
* @en Set current tick
|
||||
*/
|
||||
setTick(tick: number): void {
|
||||
this._currentTick = tick;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加一个意图
|
||||
* @en Add an intent
|
||||
*
|
||||
* @param intent - @zh 要添加的意图 @en Intent to add
|
||||
* @returns @zh 是否添加成功(重复意图返回 false)@en Whether added successfully (duplicate returns false)
|
||||
*/
|
||||
addIntent(intent: T): boolean {
|
||||
const key = this._keyExtractor(intent);
|
||||
|
||||
if (this._intentKeys.has(key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 添加元数据 | Add metadata
|
||||
const intentWithMeta: T = {
|
||||
...intent,
|
||||
playerId: this._playerId,
|
||||
tick: this._currentTick
|
||||
};
|
||||
|
||||
this._intents.push(intentWithMeta);
|
||||
this._intentKeys.add(key);
|
||||
|
||||
// 按类型索引 | Index by type
|
||||
if (!this._intentsByType.has(intent.type)) {
|
||||
this._intentsByType.set(intent.type, []);
|
||||
}
|
||||
this._intentsByType.get(intent.type)!.push(intentWithMeta);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有收集的意图
|
||||
* @en Get all collected intents
|
||||
*/
|
||||
getIntents(): T[] {
|
||||
return [...this._intents];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 按类型获取意图
|
||||
* @en Get intents by type
|
||||
*/
|
||||
getIntentsByType(type: string): T[] {
|
||||
return [...(this._intentsByType.get(type) ?? [])];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否已有指定键的意图
|
||||
* @en Check if intent with specified key exists
|
||||
*/
|
||||
hasIntentKey(key: string): boolean {
|
||||
return this._intentKeys.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清除所有意图
|
||||
* @en Clear all intents
|
||||
*/
|
||||
clear(): void {
|
||||
this._intents = [];
|
||||
this._intentsByType.clear();
|
||||
this._intentKeys.clear();
|
||||
}
|
||||
}
|
||||
138
packages/engine/script-runtime/src/intent/IntentTypes.ts
Normal file
138
packages/engine/script-runtime/src/intent/IntentTypes.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @zh 意图类型定义(引擎基础接口)
|
||||
* @en Intent type definitions (engine base interface)
|
||||
*
|
||||
* @zh 引擎只提供基础的 IIntent 接口,具体的意图类型由游戏项目定义
|
||||
* @en Engine only provides base IIntent interface, specific intent types are defined by game projects
|
||||
*/
|
||||
|
||||
// =============================================================================
|
||||
// 基础意图接口 | Base Intent Interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 基础意图接口
|
||||
* @en Base intent interface
|
||||
*
|
||||
* @zh 所有游戏意图都应该继承这个接口
|
||||
* @en All game intents should extend this interface
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 游戏项目中定义具体意图 | Define specific intents in game project
|
||||
* interface UnitMoveIntent extends IIntent {
|
||||
* readonly type: 'unit.move';
|
||||
* unitId: string;
|
||||
* direction: number;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface IIntent {
|
||||
/**
|
||||
* @zh 意图类型(唯一标识符)
|
||||
* @en Intent type (unique identifier)
|
||||
*/
|
||||
readonly type: string;
|
||||
|
||||
/**
|
||||
* @zh 发起者玩家 ID(由 IntentCollector 自动填充)
|
||||
* @en Originator player ID (auto-filled by IntentCollector)
|
||||
*/
|
||||
playerId?: string;
|
||||
|
||||
/**
|
||||
* @zh 产生时的 tick(由 IntentCollector 自动填充)
|
||||
* @en Tick when generated (auto-filled by IntentCollector)
|
||||
*/
|
||||
tick?: number;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 意图键提取器 | Intent Key Extractor
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 意图键提取器函数类型
|
||||
* @en Intent key extractor function type
|
||||
*
|
||||
* @zh 用于从意图中提取唯一键,防止同一对象重复操作
|
||||
* @en Used to extract unique key from intent, preventing duplicate operations on same object
|
||||
*/
|
||||
export type IntentKeyExtractor<T extends IIntent = IIntent> = (intent: T) => string;
|
||||
|
||||
/**
|
||||
* @zh 默认的意图键提取器(只使用 type)
|
||||
* @en Default intent key extractor (uses type only)
|
||||
*/
|
||||
export const defaultIntentKeyExtractor: IntentKeyExtractor = (intent) => intent.type;
|
||||
|
||||
// =============================================================================
|
||||
// 通用结果常量 | Common Result Constants
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 操作成功
|
||||
* @en Operation successful
|
||||
*/
|
||||
export const OK = 0;
|
||||
|
||||
/**
|
||||
* @zh 通用错误
|
||||
* @en Generic error
|
||||
*/
|
||||
export const ERR_GENERIC = -1;
|
||||
|
||||
/**
|
||||
* @zh 不是所有者
|
||||
* @en Not the owner
|
||||
*/
|
||||
export const ERR_NOT_OWNER = -2;
|
||||
|
||||
/**
|
||||
* @zh 目标无效
|
||||
* @en Invalid target
|
||||
*/
|
||||
export const ERR_INVALID_TARGET = -3;
|
||||
|
||||
/**
|
||||
* @zh 不在范围内
|
||||
* @en Not in range
|
||||
*/
|
||||
export const ERR_NOT_IN_RANGE = -4;
|
||||
|
||||
/**
|
||||
* @zh 资源不足
|
||||
* @en Not enough resources
|
||||
*/
|
||||
export const ERR_NOT_ENOUGH_RESOURCES = -5;
|
||||
|
||||
/**
|
||||
* @zh 正忙
|
||||
* @en Currently busy
|
||||
*/
|
||||
export const ERR_BUSY = -6;
|
||||
|
||||
/**
|
||||
* @zh 参数无效
|
||||
* @en Invalid arguments
|
||||
*/
|
||||
export const ERR_INVALID_ARGS = -7;
|
||||
|
||||
// =============================================================================
|
||||
// 通用方向常量 | Common Direction Constants
|
||||
// =============================================================================
|
||||
|
||||
export const TOP = 1;
|
||||
export const TOP_RIGHT = 2;
|
||||
export const RIGHT = 3;
|
||||
export const BOTTOM_RIGHT = 4;
|
||||
export const BOTTOM = 5;
|
||||
export const BOTTOM_LEFT = 6;
|
||||
export const LEFT = 7;
|
||||
export const TOP_LEFT = 8;
|
||||
|
||||
/**
|
||||
* @zh 方向类型
|
||||
* @en Direction type
|
||||
*/
|
||||
export type Direction = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
|
||||
201
packages/engine/script-runtime/src/nodes/GameInfoNodes.ts
Normal file
201
packages/engine/script-runtime/src/nodes/GameInfoNodes.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* @zh 游戏信息节点
|
||||
* @en Game Info Nodes
|
||||
*
|
||||
* @zh 提供获取游戏状态信息的能力
|
||||
* @en Provides access to game state information
|
||||
*/
|
||||
|
||||
import type { BlueprintNodeTemplate, BlueprintNode } from '@esengine/blueprint';
|
||||
import type { INodeExecutor, ExecutionResult } from '@esengine/blueprint';
|
||||
import type { IGameState } from '../vm/ServerExecutionContext';
|
||||
|
||||
// =============================================================================
|
||||
// 扩展的执行上下文接口 | Extended Execution Context Interface
|
||||
// =============================================================================
|
||||
|
||||
interface ServerContext {
|
||||
gameState: IGameState | null;
|
||||
playerId: string;
|
||||
deltaTime: number;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GetTick Node | 获取 Tick 节点
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 获取 Tick 节点模板
|
||||
* @en Get Tick node template
|
||||
*/
|
||||
export const GetTickTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetTick',
|
||||
title: 'Get Tick',
|
||||
category: 'time',
|
||||
description: 'Get the current game tick / 获取当前游戏 tick',
|
||||
keywords: ['tick', 'time', 'frame', 'turn'],
|
||||
menuPath: ['Game', 'Get Tick'],
|
||||
isPure: true,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'tick',
|
||||
displayName: 'Tick',
|
||||
type: 'int'
|
||||
}
|
||||
],
|
||||
color: '#1e6b8b'
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 获取 Tick 节点执行器
|
||||
* @en Get Tick node executor
|
||||
*/
|
||||
export class GetTickExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ServerContext;
|
||||
const tick = ctx.gameState?.tick ?? 0;
|
||||
|
||||
return {
|
||||
outputs: { tick }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GetPlayerId Node | 获取玩家 ID 节点
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 获取玩家 ID 节点模板
|
||||
* @en Get Player ID node template
|
||||
*/
|
||||
export const GetPlayerIdTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetPlayerId',
|
||||
title: 'Get Player ID',
|
||||
category: 'entity',
|
||||
description: 'Get the current player ID / 获取当前玩家 ID',
|
||||
keywords: ['player', 'id', 'owner', 'me'],
|
||||
menuPath: ['Game', 'Get Player ID'],
|
||||
isPure: true,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'playerId',
|
||||
displayName: 'Player ID',
|
||||
type: 'string'
|
||||
}
|
||||
],
|
||||
color: '#1e5a8b'
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 获取玩家 ID 节点执行器
|
||||
* @en Get Player ID node executor
|
||||
*/
|
||||
export class GetPlayerIdExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ServerContext;
|
||||
|
||||
return {
|
||||
outputs: { playerId: ctx.playerId }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GetDeltaTime Node | 获取增量时间节点
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 获取增量时间节点模板
|
||||
* @en Get Delta Time node template
|
||||
*/
|
||||
export const GetDeltaTimeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetDeltaTime',
|
||||
title: 'Get Delta Time',
|
||||
category: 'time',
|
||||
description: 'Get the time since last tick (seconds) / 获取距上次 tick 的时间(秒)',
|
||||
keywords: ['delta', 'time', 'dt', 'interval'],
|
||||
menuPath: ['Game', 'Get Delta Time'],
|
||||
isPure: true,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'deltaTime',
|
||||
displayName: 'Delta Time',
|
||||
type: 'float'
|
||||
}
|
||||
],
|
||||
color: '#1e6b8b'
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 获取增量时间节点执行器
|
||||
* @en Get Delta Time node executor
|
||||
*/
|
||||
export class GetDeltaTimeExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ServerContext;
|
||||
|
||||
return {
|
||||
outputs: { deltaTime: ctx.deltaTime }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GetGameState Node | 获取游戏状态节点
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 获取游戏状态节点模板
|
||||
* @en Get Game State node template
|
||||
*/
|
||||
export const GetGameStateTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetGameState',
|
||||
title: 'Get Game State',
|
||||
category: 'entity',
|
||||
description: 'Get the current game state object / 获取当前游戏状态对象',
|
||||
keywords: ['game', 'state', 'world', 'data'],
|
||||
menuPath: ['Game', 'Get Game State'],
|
||||
isPure: true,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'state',
|
||||
displayName: 'State',
|
||||
type: 'object'
|
||||
}
|
||||
],
|
||||
color: '#1e5a8b'
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 获取游戏状态节点执行器
|
||||
* @en Get Game State node executor
|
||||
*/
|
||||
export class GetGameStateExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ServerContext;
|
||||
|
||||
return {
|
||||
outputs: { state: ctx.gameState }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 节点定义集合 | Node Definition Collection
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 游戏信息节点定义
|
||||
* @en Game info node definitions
|
||||
*/
|
||||
export const GameInfoNodeDefinitions = [
|
||||
{ template: GetTickTemplate, executor: new GetTickExecutor() },
|
||||
{ template: GetPlayerIdTemplate, executor: new GetPlayerIdExecutor() },
|
||||
{ template: GetDeltaTimeTemplate, executor: new GetDeltaTimeExecutor() },
|
||||
{ template: GetGameStateTemplate, executor: new GetGameStateExecutor() }
|
||||
];
|
||||
200
packages/engine/script-runtime/src/nodes/LogNodes.ts
Normal file
200
packages/engine/script-runtime/src/nodes/LogNodes.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* @zh 日志节点
|
||||
* @en Log Nodes
|
||||
*
|
||||
* @zh 提供日志记录能力
|
||||
* @en Provides logging capabilities
|
||||
*/
|
||||
|
||||
import type { BlueprintNodeTemplate, BlueprintNode } from '@esengine/blueprint';
|
||||
import type { INodeExecutor, ExecutionResult } from '@esengine/blueprint';
|
||||
|
||||
// =============================================================================
|
||||
// 扩展的执行上下文接口 | Extended Execution Context Interface
|
||||
// =============================================================================
|
||||
|
||||
interface ServerContext {
|
||||
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown;
|
||||
log: (message: string) => void;
|
||||
warn: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Log Node | 日志节点
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 日志节点模板
|
||||
* @en Log node template
|
||||
*/
|
||||
export const LogTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Log',
|
||||
title: 'Log',
|
||||
category: 'debug',
|
||||
description: 'Log a message / 记录日志消息',
|
||||
keywords: ['log', 'print', 'debug', 'console'],
|
||||
menuPath: ['Debug', 'Log'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
displayName: '',
|
||||
type: 'exec'
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
displayName: 'Message',
|
||||
type: 'string',
|
||||
defaultValue: ''
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
displayName: '',
|
||||
type: 'exec'
|
||||
}
|
||||
],
|
||||
color: '#5a5a5a'
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 日志节点执行器
|
||||
* @en Log node executor
|
||||
*/
|
||||
export class LogExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ServerContext;
|
||||
const message = ctx.evaluateInput(node.id, 'message', '') as string;
|
||||
|
||||
ctx.log(String(message));
|
||||
|
||||
return {
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Warn Node | 警告节点
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 警告节点模板
|
||||
* @en Warn node template
|
||||
*/
|
||||
export const WarnTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Warn',
|
||||
title: 'Warn',
|
||||
category: 'debug',
|
||||
description: 'Log a warning message / 记录警告消息',
|
||||
keywords: ['warn', 'warning', 'debug', 'console'],
|
||||
menuPath: ['Debug', 'Warn'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
displayName: '',
|
||||
type: 'exec'
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
displayName: 'Message',
|
||||
type: 'string',
|
||||
defaultValue: ''
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
displayName: '',
|
||||
type: 'exec'
|
||||
}
|
||||
],
|
||||
color: '#8b8b1e'
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 警告节点执行器
|
||||
* @en Warn node executor
|
||||
*/
|
||||
export class WarnExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ServerContext;
|
||||
const message = ctx.evaluateInput(node.id, 'message', '') as string;
|
||||
|
||||
ctx.warn(String(message));
|
||||
|
||||
return {
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Error Node | 错误节点
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 错误节点模板
|
||||
* @en Error node template
|
||||
*/
|
||||
export const ErrorTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Error',
|
||||
title: 'Error',
|
||||
category: 'debug',
|
||||
description: 'Log an error message / 记录错误消息',
|
||||
keywords: ['error', 'debug', 'console'],
|
||||
menuPath: ['Debug', 'Error'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
displayName: '',
|
||||
type: 'exec'
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
displayName: 'Message',
|
||||
type: 'string',
|
||||
defaultValue: ''
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
displayName: '',
|
||||
type: 'exec'
|
||||
}
|
||||
],
|
||||
color: '#8b1e1e'
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 错误节点执行器
|
||||
* @en Error node executor
|
||||
*/
|
||||
export class ErrorExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ServerContext;
|
||||
const message = ctx.evaluateInput(node.id, 'message', '') as string;
|
||||
|
||||
ctx.error(String(message));
|
||||
|
||||
return {
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 节点定义集合 | Node Definition Collection
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 日志节点定义
|
||||
* @en Log node definitions
|
||||
*/
|
||||
export const LogNodeDefinitions = [
|
||||
{ template: LogTemplate, executor: new LogExecutor() },
|
||||
{ template: WarnTemplate, executor: new WarnExecutor() },
|
||||
{ template: ErrorTemplate, executor: new ErrorExecutor() }
|
||||
];
|
||||
269
packages/engine/script-runtime/src/nodes/MemoryNodes.ts
Normal file
269
packages/engine/script-runtime/src/nodes/MemoryNodes.ts
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* @zh Memory 操作节点
|
||||
* @en Memory Operation Nodes
|
||||
*
|
||||
* @zh 提供玩家持久化数据的读写能力
|
||||
* @en Provides read/write access to player persistent data
|
||||
*/
|
||||
|
||||
import type { BlueprintNodeTemplate, BlueprintNode } from '@esengine/blueprint';
|
||||
import type { INodeExecutor, ExecutionResult } from '@esengine/blueprint';
|
||||
|
||||
// =============================================================================
|
||||
// 扩展的执行上下文接口 | Extended Execution Context Interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 服务器端执行上下文接口(用于节点)
|
||||
* @en Server-side execution context interface (for nodes)
|
||||
*/
|
||||
interface ServerContext {
|
||||
memory: Record<string, unknown>;
|
||||
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown;
|
||||
setOutputs(nodeId: string, outputs: Record<string, unknown>): void;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GetMemory Node | 获取 Memory 节点
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 获取 Memory 节点模板
|
||||
* @en Get Memory node template
|
||||
*/
|
||||
export const GetMemoryTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetMemory',
|
||||
title: 'Get Memory',
|
||||
category: 'variable',
|
||||
description: 'Get a value from player Memory / 从玩家 Memory 获取值',
|
||||
keywords: ['memory', 'get', 'read', 'load', 'data'],
|
||||
menuPath: ['Memory', 'Get Memory'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{
|
||||
name: 'key',
|
||||
displayName: 'Key',
|
||||
type: 'string',
|
||||
defaultValue: ''
|
||||
},
|
||||
{
|
||||
name: 'defaultValue',
|
||||
displayName: 'Default',
|
||||
type: 'any',
|
||||
defaultValue: null
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'value',
|
||||
displayName: 'Value',
|
||||
type: 'any'
|
||||
}
|
||||
],
|
||||
color: '#8b5a8b'
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 获取 Memory 节点执行器
|
||||
* @en Get Memory node executor
|
||||
*/
|
||||
export class GetMemoryExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ServerContext;
|
||||
const key = ctx.evaluateInput(node.id, 'key', '') as string;
|
||||
const defaultValue = ctx.evaluateInput(node.id, 'defaultValue', null);
|
||||
|
||||
const value = ctx.memory[key] ?? defaultValue;
|
||||
|
||||
return {
|
||||
outputs: { value }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// SetMemory Node | 设置 Memory 节点
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 设置 Memory 节点模板
|
||||
* @en Set Memory node template
|
||||
*/
|
||||
export const SetMemoryTemplate: BlueprintNodeTemplate = {
|
||||
type: 'SetMemory',
|
||||
title: 'Set Memory',
|
||||
category: 'variable',
|
||||
description: 'Set a value in player Memory / 在玩家 Memory 中设置值',
|
||||
keywords: ['memory', 'set', 'write', 'save', 'data'],
|
||||
menuPath: ['Memory', 'Set Memory'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
displayName: '',
|
||||
type: 'exec'
|
||||
},
|
||||
{
|
||||
name: 'key',
|
||||
displayName: 'Key',
|
||||
type: 'string',
|
||||
defaultValue: ''
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
displayName: 'Value',
|
||||
type: 'any',
|
||||
defaultValue: null
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
displayName: '',
|
||||
type: 'exec'
|
||||
}
|
||||
],
|
||||
color: '#8b5a8b'
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 设置 Memory 节点执行器
|
||||
* @en Set Memory node executor
|
||||
*/
|
||||
export class SetMemoryExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ServerContext;
|
||||
const key = ctx.evaluateInput(node.id, 'key', '') as string;
|
||||
const value = ctx.evaluateInput(node.id, 'value', null);
|
||||
|
||||
if (key) {
|
||||
ctx.memory[key] = value;
|
||||
}
|
||||
|
||||
return {
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// HasMemoryKey Node | 检查 Memory 键节点
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 检查 Memory 键节点模板
|
||||
* @en Has Memory Key node template
|
||||
*/
|
||||
export const HasMemoryKeyTemplate: BlueprintNodeTemplate = {
|
||||
type: 'HasMemoryKey',
|
||||
title: 'Has Memory Key',
|
||||
category: 'variable',
|
||||
description: 'Check if a key exists in Memory / 检查 Memory 中是否存在某个键',
|
||||
keywords: ['memory', 'has', 'exists', 'check', 'key'],
|
||||
menuPath: ['Memory', 'Has Key'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{
|
||||
name: 'key',
|
||||
displayName: 'Key',
|
||||
type: 'string',
|
||||
defaultValue: ''
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exists',
|
||||
displayName: 'Exists',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
color: '#8b5a8b'
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 检查 Memory 键节点执行器
|
||||
* @en Has Memory Key node executor
|
||||
*/
|
||||
export class HasMemoryKeyExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ServerContext;
|
||||
const key = ctx.evaluateInput(node.id, 'key', '') as string;
|
||||
|
||||
const exists = key in ctx.memory;
|
||||
|
||||
return {
|
||||
outputs: { exists }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DeleteMemory Node | 删除 Memory 节点
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 删除 Memory 节点模板
|
||||
* @en Delete Memory node template
|
||||
*/
|
||||
export const DeleteMemoryTemplate: BlueprintNodeTemplate = {
|
||||
type: 'DeleteMemory',
|
||||
title: 'Delete Memory',
|
||||
category: 'variable',
|
||||
description: 'Delete a key from Memory / 从 Memory 中删除键',
|
||||
keywords: ['memory', 'delete', 'remove', 'clear', 'key'],
|
||||
menuPath: ['Memory', 'Delete'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
displayName: '',
|
||||
type: 'exec'
|
||||
},
|
||||
{
|
||||
name: 'key',
|
||||
displayName: 'Key',
|
||||
type: 'string',
|
||||
defaultValue: ''
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
displayName: '',
|
||||
type: 'exec'
|
||||
}
|
||||
],
|
||||
color: '#8b5a8b'
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 删除 Memory 节点执行器
|
||||
* @en Delete Memory node executor
|
||||
*/
|
||||
export class DeleteMemoryExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: unknown): ExecutionResult {
|
||||
const ctx = context as ServerContext;
|
||||
const key = ctx.evaluateInput(node.id, 'key', '') as string;
|
||||
|
||||
if (key) {
|
||||
delete ctx.memory[key];
|
||||
}
|
||||
|
||||
return {
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 节点定义集合 | Node Definition Collection
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh Memory 节点定义
|
||||
* @en Memory node definitions
|
||||
*/
|
||||
export const MemoryNodeDefinitions = [
|
||||
{ template: GetMemoryTemplate, executor: new GetMemoryExecutor() },
|
||||
{ template: SetMemoryTemplate, executor: new SetMemoryExecutor() },
|
||||
{ template: HasMemoryKeyTemplate, executor: new HasMemoryKeyExecutor() },
|
||||
{ template: DeleteMemoryTemplate, executor: new DeleteMemoryExecutor() }
|
||||
];
|
||||
84
packages/engine/script-runtime/src/nodes/index.ts
Normal file
84
packages/engine/script-runtime/src/nodes/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* @zh 蓝图节点模块
|
||||
* @en Blueprint Nodes Module
|
||||
*
|
||||
* @zh 提供服务器端蓝图执行所需的通用节点
|
||||
* @en Provides common nodes for server-side blueprint execution
|
||||
*/
|
||||
|
||||
import { NodeRegistry } from '@esengine/blueprint';
|
||||
|
||||
// Memory Nodes
|
||||
export {
|
||||
GetMemoryTemplate,
|
||||
GetMemoryExecutor,
|
||||
SetMemoryTemplate,
|
||||
SetMemoryExecutor,
|
||||
HasMemoryKeyTemplate,
|
||||
HasMemoryKeyExecutor,
|
||||
DeleteMemoryTemplate,
|
||||
DeleteMemoryExecutor,
|
||||
MemoryNodeDefinitions
|
||||
} from './MemoryNodes';
|
||||
|
||||
// Log Nodes
|
||||
export {
|
||||
LogTemplate,
|
||||
LogExecutor,
|
||||
WarnTemplate,
|
||||
WarnExecutor,
|
||||
ErrorTemplate,
|
||||
ErrorExecutor,
|
||||
LogNodeDefinitions
|
||||
} from './LogNodes';
|
||||
|
||||
// Game Info Nodes
|
||||
export {
|
||||
GetTickTemplate,
|
||||
GetTickExecutor,
|
||||
GetPlayerIdTemplate,
|
||||
GetPlayerIdExecutor,
|
||||
GetDeltaTimeTemplate,
|
||||
GetDeltaTimeExecutor,
|
||||
GetGameStateTemplate,
|
||||
GetGameStateExecutor,
|
||||
GameInfoNodeDefinitions
|
||||
} from './GameInfoNodes';
|
||||
|
||||
// =============================================================================
|
||||
// 节点注册 | Node Registration
|
||||
// =============================================================================
|
||||
|
||||
import { MemoryNodeDefinitions } from './MemoryNodes';
|
||||
import { LogNodeDefinitions } from './LogNodes';
|
||||
import { GameInfoNodeDefinitions } from './GameInfoNodes';
|
||||
|
||||
/**
|
||||
* @zh 所有节点定义
|
||||
* @en All node definitions
|
||||
*/
|
||||
export const AllNodeDefinitions = [
|
||||
...MemoryNodeDefinitions,
|
||||
...LogNodeDefinitions,
|
||||
...GameInfoNodeDefinitions
|
||||
];
|
||||
|
||||
/**
|
||||
* @zh 注册所有 script-runtime 节点到 NodeRegistry
|
||||
* @en Register all script-runtime nodes to NodeRegistry
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { registerScriptRuntimeNodes } from '@esengine/script-runtime';
|
||||
*
|
||||
* // 在应用启动时调用 | Call at application startup
|
||||
* registerScriptRuntimeNodes();
|
||||
* ```
|
||||
*/
|
||||
export function registerScriptRuntimeNodes(): void {
|
||||
const registry = NodeRegistry.instance;
|
||||
|
||||
for (const { template, executor } of AllNodeDefinitions) {
|
||||
registry.register(template, executor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
/**
|
||||
* @zh 文件系统 Memory 存储
|
||||
* @en File System Memory Store
|
||||
*
|
||||
* @zh 使用文件系统存储玩家 Memory 和世界状态
|
||||
* @en Uses file system to store player Memory and world state
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
|
||||
import type {
|
||||
IMemoryStore,
|
||||
PlayerMemory,
|
||||
WorldState,
|
||||
MemoryStoreStats
|
||||
} from './IMemoryStore';
|
||||
|
||||
/**
|
||||
* @zh 文件存储配置
|
||||
* @en File storage configuration
|
||||
*/
|
||||
export interface FileMemoryStoreConfig {
|
||||
/**
|
||||
* @zh 存储根目录
|
||||
* @en Storage root directory
|
||||
*/
|
||||
basePath: string;
|
||||
|
||||
/**
|
||||
* @zh 是否美化 JSON 输出
|
||||
* @en Whether to prettify JSON output
|
||||
*/
|
||||
prettyPrint?: boolean;
|
||||
|
||||
/**
|
||||
* @zh Memory 大小限制(字节)
|
||||
* @en Memory size limit (bytes)
|
||||
*/
|
||||
memorySizeLimit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 默认配置
|
||||
* @en Default configuration
|
||||
*/
|
||||
const DEFAULT_CONFIG: Required<FileMemoryStoreConfig> = {
|
||||
basePath: './data',
|
||||
prettyPrint: true,
|
||||
memorySizeLimit: 2 * 1024 * 1024 // 2MB
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh 文件系统 Memory 存储
|
||||
* @en File System Memory Store
|
||||
*
|
||||
* @zh 简单的文件存储实现,适合开发和小规模部署
|
||||
* @en Simple file storage implementation, suitable for development and small deployments
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const store = new FileMemoryStore({ basePath: './game-data' });
|
||||
* await store.init();
|
||||
*
|
||||
* // 保存玩家 Memory | Save player Memory
|
||||
* await store.savePlayerMemory('player1', { creeps: {} });
|
||||
*
|
||||
* // 加载玩家 Memory | Load player Memory
|
||||
* const memory = await store.loadPlayerMemory('player1');
|
||||
* ```
|
||||
*/
|
||||
export class FileMemoryStore implements IMemoryStore {
|
||||
/**
|
||||
* @zh 配置
|
||||
* @en Configuration
|
||||
*/
|
||||
private readonly _config: Required<FileMemoryStoreConfig>;
|
||||
|
||||
/**
|
||||
* @zh Memory 目录路径
|
||||
* @en Memory directory path
|
||||
*/
|
||||
private readonly _memoryPath: string;
|
||||
|
||||
/**
|
||||
* @zh 世界状态文件路径
|
||||
* @en World state file path
|
||||
*/
|
||||
private readonly _worldPath: string;
|
||||
|
||||
/**
|
||||
* @zh 是否已初始化
|
||||
* @en Whether initialized
|
||||
*/
|
||||
private _initialized: boolean = false;
|
||||
|
||||
constructor(config: Partial<FileMemoryStoreConfig> = {}) {
|
||||
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||
this._memoryPath = path.join(this._config.basePath, 'memory');
|
||||
this._worldPath = path.join(this._config.basePath, 'world.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 初始化存储(创建目录)
|
||||
* @en Initialize storage (create directories)
|
||||
*/
|
||||
async init(): Promise<void> {
|
||||
if (this._initialized) return;
|
||||
|
||||
try {
|
||||
await fs.mkdir(this._memoryPath, { recursive: true });
|
||||
this._initialized = true;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to initialize storage: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 确保已初始化
|
||||
* @en Ensure initialized
|
||||
*/
|
||||
private async _ensureInit(): Promise<void> {
|
||||
if (!this._initialized) {
|
||||
await this.init();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取玩家 Memory 文件路径
|
||||
* @en Get player Memory file path
|
||||
*/
|
||||
private _getPlayerMemoryPath(playerId: string): string {
|
||||
// 清理 playerId 以防止路径遍历 | Sanitize playerId to prevent path traversal
|
||||
const safeId = playerId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
return path.join(this._memoryPath, `${safeId}.json`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加载玩家 Memory
|
||||
* @en Load player Memory
|
||||
*/
|
||||
async loadPlayerMemory(playerId: string): Promise<PlayerMemory> {
|
||||
await this._ensureInit();
|
||||
|
||||
const filePath = this._getPlayerMemoryPath(playerId);
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
return JSON.parse(content) as PlayerMemory;
|
||||
} catch (error) {
|
||||
// 文件不存在则返回空对象 | Return empty object if file doesn't exist
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return {};
|
||||
}
|
||||
throw new Error(`Failed to load Memory for ${playerId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 保存玩家 Memory
|
||||
* @en Save player Memory
|
||||
*/
|
||||
async savePlayerMemory(playerId: string, memory: PlayerMemory): Promise<void> {
|
||||
await this._ensureInit();
|
||||
|
||||
const content = this._config.prettyPrint
|
||||
? JSON.stringify(memory, null, 2)
|
||||
: JSON.stringify(memory);
|
||||
|
||||
// 检查大小限制 | Check size limit
|
||||
if (content.length > this._config.memorySizeLimit) {
|
||||
throw new Error(
|
||||
`Memory size (${content.length} bytes) exceeds limit ` +
|
||||
`(${this._config.memorySizeLimit} bytes) for player ${playerId}`
|
||||
);
|
||||
}
|
||||
|
||||
const filePath = this._getPlayerMemoryPath(playerId);
|
||||
|
||||
try {
|
||||
await fs.writeFile(filePath, content, 'utf-8');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to save Memory for ${playerId}: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 批量保存玩家 Memory
|
||||
* @en Batch save player Memory
|
||||
*/
|
||||
async savePlayerMemoryBatch(
|
||||
entries: Array<{ playerId: string; memory: PlayerMemory }>
|
||||
): Promise<void> {
|
||||
await this._ensureInit();
|
||||
|
||||
// 并行保存所有玩家 | Save all players in parallel
|
||||
await Promise.all(
|
||||
entries.map(({ playerId, memory }) =>
|
||||
this.savePlayerMemory(playerId, memory)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 删除玩家 Memory
|
||||
* @en Delete player Memory
|
||||
*/
|
||||
async deletePlayerMemory(playerId: string): Promise<void> {
|
||||
await this._ensureInit();
|
||||
|
||||
const filePath = this._getPlayerMemoryPath(playerId);
|
||||
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (error) {
|
||||
// 文件不存在则忽略 | Ignore if file doesn't exist
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
throw new Error(`Failed to delete Memory for ${playerId}: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取所有玩家 ID
|
||||
* @en Get all player IDs
|
||||
*/
|
||||
async getAllPlayerIds(): Promise<string[]> {
|
||||
await this._ensureInit();
|
||||
|
||||
try {
|
||||
const files = await fs.readdir(this._memoryPath);
|
||||
return files
|
||||
.filter(f => f.endsWith('.json'))
|
||||
.map(f => f.slice(0, -5)); // 移除 .json 后缀 | Remove .json suffix
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 加载世界状态
|
||||
* @en Load world state
|
||||
*/
|
||||
async loadWorldState(): Promise<WorldState | null> {
|
||||
await this._ensureInit();
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(this._worldPath, 'utf-8');
|
||||
return JSON.parse(content) as WorldState;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`Failed to load world state: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 保存世界状态
|
||||
* @en Save world state
|
||||
*/
|
||||
async saveWorldState(state: WorldState): Promise<void> {
|
||||
await this._ensureInit();
|
||||
|
||||
const stateWithTimestamp: WorldState = {
|
||||
...state,
|
||||
lastSaveTime: Date.now()
|
||||
};
|
||||
|
||||
const content = this._config.prettyPrint
|
||||
? JSON.stringify(stateWithTimestamp, null, 2)
|
||||
: JSON.stringify(stateWithTimestamp);
|
||||
|
||||
try {
|
||||
await fs.writeFile(this._worldPath, content, 'utf-8');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to save world state: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取存储统计信息
|
||||
* @en Get storage statistics
|
||||
*/
|
||||
async getStats(): Promise<MemoryStoreStats> {
|
||||
await this._ensureInit();
|
||||
|
||||
const playerIds = await this.getAllPlayerIds();
|
||||
let totalSize = 0;
|
||||
let lastSaveTime = 0;
|
||||
|
||||
for (const playerId of playerIds) {
|
||||
try {
|
||||
const filePath = this._getPlayerMemoryPath(playerId);
|
||||
const stats = await fs.stat(filePath);
|
||||
totalSize += stats.size;
|
||||
if (stats.mtimeMs > lastSaveTime) {
|
||||
lastSaveTime = stats.mtimeMs;
|
||||
}
|
||||
} catch {
|
||||
// 忽略错误 | Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
// 加上世界状态文件大小 | Add world state file size
|
||||
try {
|
||||
const worldStats = await fs.stat(this._worldPath);
|
||||
totalSize += worldStats.size;
|
||||
if (worldStats.mtimeMs > lastSaveTime) {
|
||||
lastSaveTime = worldStats.mtimeMs;
|
||||
}
|
||||
} catch {
|
||||
// 忽略错误 | Ignore errors
|
||||
}
|
||||
|
||||
return {
|
||||
playerCount: playerIds.length,
|
||||
totalSize,
|
||||
lastSaveTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清除所有数据(慎用!)
|
||||
* @en Clear all data (use with caution!)
|
||||
*/
|
||||
async clearAll(): Promise<void> {
|
||||
await this._ensureInit();
|
||||
|
||||
const playerIds = await this.getAllPlayerIds();
|
||||
await Promise.all(playerIds.map(id => this.deletePlayerMemory(id)));
|
||||
|
||||
try {
|
||||
await fs.unlink(this._worldPath);
|
||||
} catch {
|
||||
// 忽略错误 | Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
131
packages/engine/script-runtime/src/persistence/IMemoryStore.ts
Normal file
131
packages/engine/script-runtime/src/persistence/IMemoryStore.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* @zh Memory 存储接口
|
||||
* @en Memory Store Interface
|
||||
*
|
||||
* @zh 定义玩家 Memory 和世界状态的持久化接口
|
||||
* @en Defines persistence interface for player Memory and world state
|
||||
*/
|
||||
|
||||
/**
|
||||
* @zh 玩家 Memory 类型
|
||||
* @en Player Memory type
|
||||
*/
|
||||
export type PlayerMemory = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* @zh 世界状态类型
|
||||
* @en World state type
|
||||
*/
|
||||
export interface WorldState {
|
||||
/**
|
||||
* @zh 当前 tick
|
||||
* @en Current tick
|
||||
*/
|
||||
tick: number;
|
||||
|
||||
/**
|
||||
* @zh 上次保存时间
|
||||
* @en Last save time
|
||||
*/
|
||||
lastSaveTime: number;
|
||||
|
||||
/**
|
||||
* @zh 实体状态
|
||||
* @en Entity states
|
||||
*/
|
||||
entities: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* @zh 房间状态
|
||||
* @en Room states
|
||||
*/
|
||||
rooms: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh Memory 存储接口
|
||||
* @en Memory store interface
|
||||
*/
|
||||
export interface IMemoryStore {
|
||||
/**
|
||||
* @zh 加载玩家 Memory
|
||||
* @en Load player Memory
|
||||
*
|
||||
* @param playerId - @zh 玩家 ID @en Player ID
|
||||
* @returns @zh 玩家 Memory @en Player Memory
|
||||
*/
|
||||
loadPlayerMemory(playerId: string): Promise<PlayerMemory>;
|
||||
|
||||
/**
|
||||
* @zh 保存玩家 Memory
|
||||
* @en Save player Memory
|
||||
*
|
||||
* @param playerId - @zh 玩家 ID @en Player ID
|
||||
* @param memory - @zh Memory 数据 @en Memory data
|
||||
*/
|
||||
savePlayerMemory(playerId: string, memory: PlayerMemory): Promise<void>;
|
||||
|
||||
/**
|
||||
* @zh 批量保存玩家 Memory
|
||||
* @en Batch save player Memory
|
||||
*
|
||||
* @param entries - @zh Memory 条目列表 @en Memory entry list
|
||||
*/
|
||||
savePlayerMemoryBatch(entries: Array<{ playerId: string; memory: PlayerMemory }>): Promise<void>;
|
||||
|
||||
/**
|
||||
* @zh 删除玩家 Memory
|
||||
* @en Delete player Memory
|
||||
*
|
||||
* @param playerId - @zh 玩家 ID @en Player ID
|
||||
*/
|
||||
deletePlayerMemory(playerId: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* @zh 获取所有玩家 ID
|
||||
* @en Get all player IDs
|
||||
*/
|
||||
getAllPlayerIds(): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* @zh 加载世界状态
|
||||
* @en Load world state
|
||||
*/
|
||||
loadWorldState(): Promise<WorldState | null>;
|
||||
|
||||
/**
|
||||
* @zh 保存世界状态
|
||||
* @en Save world state
|
||||
*/
|
||||
saveWorldState(state: WorldState): Promise<void>;
|
||||
|
||||
/**
|
||||
* @zh 获取存储统计信息
|
||||
* @en Get storage statistics
|
||||
*/
|
||||
getStats(): Promise<MemoryStoreStats>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 存储统计信息
|
||||
* @en Storage statistics
|
||||
*/
|
||||
export interface MemoryStoreStats {
|
||||
/**
|
||||
* @zh 玩家数量
|
||||
* @en Player count
|
||||
*/
|
||||
playerCount: number;
|
||||
|
||||
/**
|
||||
* @zh 总存储大小(字节)
|
||||
* @en Total storage size (bytes)
|
||||
*/
|
||||
totalSize: number;
|
||||
|
||||
/**
|
||||
* @zh 上次保存时间
|
||||
* @en Last save time
|
||||
*/
|
||||
lastSaveTime: number;
|
||||
}
|
||||
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>;
|
||||
}
|
||||
78
packages/engine/script-runtime/src/tokens.ts
Normal file
78
packages/engine/script-runtime/src/tokens.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* @zh Script Runtime 模块服务令牌
|
||||
* @en Script Runtime module service tokens
|
||||
*
|
||||
* @zh 遵循"谁定义接口,谁导出 Token"原则
|
||||
* @en Following "who defines interface, who exports Token" principle
|
||||
*/
|
||||
|
||||
import { createServiceToken } from '@esengine/ecs-framework';
|
||||
|
||||
import type { ServerBlueprintVM } from './vm/ServerBlueprintVM';
|
||||
import type { IMemoryStore } from './persistence/IMemoryStore';
|
||||
|
||||
// =============================================================================
|
||||
// 服务接口 | Service Interfaces
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 脚本运行时服务接口
|
||||
* @en Script runtime service interface
|
||||
*/
|
||||
export interface IScriptRuntimeService {
|
||||
/**
|
||||
* @zh 为玩家创建 VM
|
||||
* @en Create VM for player
|
||||
*/
|
||||
createPlayerVM(playerId: string, blueprintPath: string): Promise<ServerBlueprintVM>;
|
||||
|
||||
/**
|
||||
* @zh 获取玩家的 VM
|
||||
* @en Get player's VM
|
||||
*/
|
||||
getPlayerVM(playerId: string): ServerBlueprintVM | undefined;
|
||||
|
||||
/**
|
||||
* @zh 移除玩家的 VM
|
||||
* @en Remove player's VM
|
||||
*/
|
||||
removePlayerVM(playerId: string): void;
|
||||
|
||||
/**
|
||||
* @zh 获取所有活跃玩家 ID
|
||||
* @en Get all active player IDs
|
||||
*/
|
||||
getActivePlayerIds(): string[];
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 服务令牌 | Service Tokens
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 脚本运行时服务令牌
|
||||
* @en Script runtime service token
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { ScriptRuntimeServiceToken } from '@esengine/script-runtime';
|
||||
*
|
||||
* const service = context.services.get(ScriptRuntimeServiceToken);
|
||||
* const vm = await service.createPlayerVM('player1', 'blueprints/main.bp');
|
||||
* ```
|
||||
*/
|
||||
export const ScriptRuntimeServiceToken = createServiceToken<IScriptRuntimeService>('scriptRuntimeService');
|
||||
|
||||
/**
|
||||
* @zh Memory 存储服务令牌
|
||||
* @en Memory store service token
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { MemoryStoreToken } from '@esengine/script-runtime';
|
||||
*
|
||||
* const store = context.services.get(MemoryStoreToken);
|
||||
* const memory = await store.loadPlayerMemory('player1');
|
||||
* ```
|
||||
*/
|
||||
export const MemoryStoreToken = createServiceToken<IMemoryStore>('memoryStore');
|
||||
318
packages/engine/script-runtime/src/vm/CPULimiter.ts
Normal file
318
packages/engine/script-runtime/src/vm/CPULimiter.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* @zh CPU 限制器
|
||||
* @en CPU Limiter
|
||||
*
|
||||
* @zh 限制蓝图执行的 CPU 时间和执行步数
|
||||
* @en Limits CPU time and execution steps for blueprint execution
|
||||
*/
|
||||
|
||||
/**
|
||||
* @zh CPU 限制器配置
|
||||
* @en CPU limiter configuration
|
||||
*/
|
||||
export interface CPULimiterConfig {
|
||||
/**
|
||||
* @zh 每 tick 的 CPU 时间限制(毫秒)
|
||||
* @en CPU time limit per tick (milliseconds)
|
||||
*/
|
||||
cpuLimitMs: number;
|
||||
|
||||
/**
|
||||
* @zh 每 tick 的最大执行步数
|
||||
* @en Maximum execution steps per tick
|
||||
*/
|
||||
maxSteps: number;
|
||||
|
||||
/**
|
||||
* @zh CPU 桶最大值
|
||||
* @en CPU bucket maximum
|
||||
*/
|
||||
bucketMax: number;
|
||||
|
||||
/**
|
||||
* @zh 每 tick 恢复的 CPU 量
|
||||
* @en CPU amount recovered per tick
|
||||
*/
|
||||
bucketRecovery: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 默认 CPU 限制配置
|
||||
* @en Default CPU limit configuration
|
||||
*/
|
||||
export const DEFAULT_CPU_CONFIG: CPULimiterConfig = {
|
||||
cpuLimitMs: 100, // 100ms per tick
|
||||
maxSteps: 10000, // 10000 steps per tick
|
||||
bucketMax: 10000, // 10000ms max bucket
|
||||
bucketRecovery: 100 // 100ms per tick recovery
|
||||
};
|
||||
|
||||
/**
|
||||
* @zh CPU 使用统计
|
||||
* @en CPU usage statistics
|
||||
*/
|
||||
export interface CPUStats {
|
||||
/**
|
||||
* @zh 已使用的 CPU 时间(毫秒)
|
||||
* @en Used CPU time (milliseconds)
|
||||
*/
|
||||
used: number;
|
||||
|
||||
/**
|
||||
* @zh CPU 限制(毫秒)
|
||||
* @en CPU limit (milliseconds)
|
||||
*/
|
||||
limit: number;
|
||||
|
||||
/**
|
||||
* @zh 已执行的步数
|
||||
* @en Executed steps
|
||||
*/
|
||||
steps: number;
|
||||
|
||||
/**
|
||||
* @zh 最大步数
|
||||
* @en Maximum steps
|
||||
*/
|
||||
maxSteps: number;
|
||||
|
||||
/**
|
||||
* @zh 当前桶值
|
||||
* @en Current bucket value
|
||||
*/
|
||||
bucket: number;
|
||||
|
||||
/**
|
||||
* @zh 是否超出限制
|
||||
* @en Whether exceeded limit
|
||||
*/
|
||||
exceeded: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh CPU 限制器
|
||||
* @en CPU Limiter
|
||||
*
|
||||
* @zh 用于限制玩家蓝图的执行资源,类似 Screeps 的 CPU 系统
|
||||
* @en Used to limit player blueprint execution resources, similar to Screeps CPU system
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const limiter = new CPULimiter('player1', config);
|
||||
*
|
||||
* // 开始执行 | Start execution
|
||||
* limiter.start();
|
||||
*
|
||||
* // 在执行节点时检查 | Check during node execution
|
||||
* if (limiter.checkStep()) {
|
||||
* // 继续执行 | Continue execution
|
||||
* } else {
|
||||
* // 超出限制,停止执行 | Exceeded limit, stop execution
|
||||
* }
|
||||
*
|
||||
* // 结束执行 | End execution
|
||||
* limiter.end();
|
||||
* console.log(`Used: ${limiter.getUsed()}ms`);
|
||||
* ```
|
||||
*/
|
||||
export class CPULimiter {
|
||||
/**
|
||||
* @zh 玩家 ID
|
||||
* @en Player ID
|
||||
*/
|
||||
private readonly _playerId: string;
|
||||
|
||||
/**
|
||||
* @zh 配置
|
||||
* @en Configuration
|
||||
*/
|
||||
private readonly _config: CPULimiterConfig;
|
||||
|
||||
/**
|
||||
* @zh 开始时间
|
||||
* @en Start time
|
||||
*/
|
||||
private _startTime: number = 0;
|
||||
|
||||
/**
|
||||
* @zh 已使用的 CPU 时间
|
||||
* @en Used CPU time
|
||||
*/
|
||||
private _usedCpu: number = 0;
|
||||
|
||||
/**
|
||||
* @zh 已执行的步数
|
||||
* @en Executed steps
|
||||
*/
|
||||
private _steps: number = 0;
|
||||
|
||||
/**
|
||||
* @zh CPU 桶(累积的 CPU 配额)
|
||||
* @en CPU bucket (accumulated CPU quota)
|
||||
*/
|
||||
private _bucket: number;
|
||||
|
||||
/**
|
||||
* @zh 是否正在执行
|
||||
* @en Whether currently executing
|
||||
*/
|
||||
private _isRunning: boolean = false;
|
||||
|
||||
/**
|
||||
* @zh 是否超出限制
|
||||
* @en Whether exceeded limit
|
||||
*/
|
||||
private _exceeded: boolean = false;
|
||||
|
||||
constructor(playerId: string, config: Partial<CPULimiterConfig> = {}) {
|
||||
this._playerId = playerId;
|
||||
this._config = { ...DEFAULT_CPU_CONFIG, ...config };
|
||||
this._bucket = this._config.bucketMax;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取玩家 ID
|
||||
* @en Get player ID
|
||||
*/
|
||||
get playerId(): string {
|
||||
return this._playerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取当前 CPU 桶值
|
||||
* @en Get current CPU bucket value
|
||||
*/
|
||||
get bucket(): number {
|
||||
return this._bucket;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取 CPU 限制
|
||||
* @en Get CPU limit
|
||||
*/
|
||||
get limit(): number {
|
||||
return this._config.cpuLimitMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 是否超出限制
|
||||
* @en Whether exceeded limit
|
||||
*/
|
||||
get exceeded(): boolean {
|
||||
return this._exceeded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 开始计时
|
||||
* @en Start timing
|
||||
*/
|
||||
start(): void {
|
||||
this._startTime = performance.now();
|
||||
this._usedCpu = 0;
|
||||
this._steps = 0;
|
||||
this._exceeded = false;
|
||||
this._isRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 结束计时并更新桶
|
||||
* @en End timing and update bucket
|
||||
*/
|
||||
end(): void {
|
||||
if (!this._isRunning) return;
|
||||
|
||||
this._usedCpu = performance.now() - this._startTime;
|
||||
this._isRunning = false;
|
||||
|
||||
// 从桶中扣除使用的 CPU | Deduct used CPU from bucket
|
||||
this._bucket -= this._usedCpu;
|
||||
if (this._bucket < 0) {
|
||||
this._bucket = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取已使用的 CPU 时间
|
||||
* @en Get used CPU time
|
||||
*/
|
||||
getUsed(): number {
|
||||
if (this._isRunning) {
|
||||
return performance.now() - this._startTime;
|
||||
}
|
||||
return this._usedCpu;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取已执行的步数
|
||||
* @en Get executed steps
|
||||
*/
|
||||
getSteps(): number {
|
||||
return this._steps;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否可以继续执行一步
|
||||
* @en Check if can continue executing one step
|
||||
*
|
||||
* @returns @zh true 如果可以继续,false 如果超出限制 @en true if can continue, false if exceeded
|
||||
*/
|
||||
checkStep(): boolean {
|
||||
this._steps++;
|
||||
|
||||
// 检查步数限制 | Check step limit
|
||||
if (this._steps > this._config.maxSteps) {
|
||||
this._exceeded = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查 CPU 时间限制 | Check CPU time limit
|
||||
const currentTime = performance.now() - this._startTime;
|
||||
const effectiveLimit = Math.min(this._config.cpuLimitMs, this._bucket);
|
||||
|
||||
if (currentTime > effectiveLimit) {
|
||||
this._exceeded = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 每 tick 恢复桶
|
||||
* @en Recover bucket per tick
|
||||
*/
|
||||
recoverBucket(): void {
|
||||
this._bucket += this._config.bucketRecovery;
|
||||
if (this._bucket > this._config.bucketMax) {
|
||||
this._bucket = this._config.bucketMax;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取 CPU 统计信息
|
||||
* @en Get CPU statistics
|
||||
*/
|
||||
getStats(): CPUStats {
|
||||
return {
|
||||
used: this.getUsed(),
|
||||
limit: this._config.cpuLimitMs,
|
||||
steps: this._steps,
|
||||
maxSteps: this._config.maxSteps,
|
||||
bucket: this._bucket,
|
||||
exceeded: this._exceeded
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置限制器(用于测试)
|
||||
* @en Reset limiter (for testing)
|
||||
*/
|
||||
reset(): void {
|
||||
this._startTime = 0;
|
||||
this._usedCpu = 0;
|
||||
this._steps = 0;
|
||||
this._exceeded = false;
|
||||
this._isRunning = false;
|
||||
this._bucket = this._config.bucketMax;
|
||||
}
|
||||
}
|
||||
618
packages/engine/script-runtime/src/vm/ServerBlueprintVM.ts
Normal file
618
packages/engine/script-runtime/src/vm/ServerBlueprintVM.ts
Normal file
@@ -0,0 +1,618 @@
|
||||
/**
|
||||
* @zh 服务器端蓝图虚拟机
|
||||
* @en Server-side Blueprint Virtual Machine
|
||||
*
|
||||
* @zh 在服务器端执行玩家蓝图,支持 CPU 限制和意图收集
|
||||
* @en Executes player blueprints on server with CPU limiting and intent collection
|
||||
*/
|
||||
|
||||
import type {
|
||||
BlueprintAsset,
|
||||
BlueprintNode,
|
||||
BlueprintConnection
|
||||
} from '@esengine/blueprint';
|
||||
import { NodeRegistry } from '@esengine/blueprint';
|
||||
|
||||
import { ServerExecutionContext, type IGameState, type LogEntry } from './ServerExecutionContext';
|
||||
import { CPULimiter, type CPULimiterConfig, type CPUStats } from './CPULimiter';
|
||||
import { IntentCollector } from '../intent/IntentCollector';
|
||||
import type { IIntent, IntentKeyExtractor } from '../intent/IntentTypes';
|
||||
|
||||
/**
|
||||
* @zh 服务器 VM 配置
|
||||
* @en Server VM configuration
|
||||
*
|
||||
* @typeParam TIntent - @zh 意图类型 @en Intent type
|
||||
*/
|
||||
export interface ServerVMConfig<TIntent extends IIntent = IIntent> {
|
||||
/**
|
||||
* @zh CPU 限制配置
|
||||
* @en CPU limit configuration
|
||||
*/
|
||||
cpuConfig?: Partial<CPULimiterConfig>;
|
||||
|
||||
/**
|
||||
* @zh 调试模式
|
||||
* @en Debug mode
|
||||
*/
|
||||
debug?: boolean;
|
||||
|
||||
/**
|
||||
* @zh 意图键提取器
|
||||
* @en Intent key extractor
|
||||
*/
|
||||
intentKeyExtractor?: IntentKeyExtractor<TIntent>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh Tick 执行结果
|
||||
* @en Tick execution result
|
||||
*
|
||||
* @typeParam TIntent - @zh 意图类型 @en Intent type
|
||||
*/
|
||||
export interface TickResult<TIntent extends IIntent = IIntent> {
|
||||
/**
|
||||
* @zh 是否执行成功
|
||||
* @en Whether execution succeeded
|
||||
*/
|
||||
success: boolean;
|
||||
|
||||
/**
|
||||
* @zh CPU 使用统计
|
||||
* @en CPU usage statistics
|
||||
*/
|
||||
cpu: CPUStats;
|
||||
|
||||
/**
|
||||
* @zh 收集的意图列表
|
||||
* @en Collected intents list
|
||||
*/
|
||||
intents: TIntent[];
|
||||
|
||||
/**
|
||||
* @zh 日志列表
|
||||
* @en Log list
|
||||
*/
|
||||
logs: LogEntry[];
|
||||
|
||||
/**
|
||||
* @zh 错误列表
|
||||
* @en Error list
|
||||
*/
|
||||
errors: string[];
|
||||
|
||||
/**
|
||||
* @zh 更新后的 Memory
|
||||
* @en Updated Memory
|
||||
*/
|
||||
memory: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 待处理的延迟执行
|
||||
* @en Pending delayed execution
|
||||
*/
|
||||
interface PendingExecution {
|
||||
nodeId: string;
|
||||
execPin: string;
|
||||
resumeTick: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 节点执行结果
|
||||
* @en Node execution result
|
||||
*/
|
||||
interface ExecutionResult {
|
||||
nextExec?: string | null;
|
||||
outputs?: Record<string, unknown>;
|
||||
yield?: boolean;
|
||||
delay?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 服务器端蓝图虚拟机
|
||||
* @en Server-side Blueprint Virtual Machine
|
||||
*
|
||||
* @zh 专为服务器端设计的蓝图执行引擎,支持:
|
||||
* @en Blueprint execution engine designed for server-side, supporting:
|
||||
*
|
||||
* @zh - CPU 时间和步数限制
|
||||
* @en - CPU time and step limiting
|
||||
*
|
||||
* @zh - 意图收集(不直接执行操作)
|
||||
* @en - Intent collection (no direct execution)
|
||||
*
|
||||
* @zh - Memory 持久化
|
||||
* @en - Memory persistence
|
||||
*
|
||||
* @typeParam TGameState - @zh 游戏状态类型 @en Game state type
|
||||
* @typeParam TIntent - @zh 意图类型 @en Intent type
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 游戏项目中定义类型 | Define types in game project
|
||||
* interface MyGameState extends IGameState {
|
||||
* units: Map<string, IUnit>;
|
||||
* }
|
||||
*
|
||||
* interface MyIntent extends IIntent {
|
||||
* readonly type: 'unit.move' | 'unit.attack';
|
||||
* unitId: string;
|
||||
* }
|
||||
*
|
||||
* // 创建 VM | Create VM
|
||||
* const vm = new ServerBlueprintVM<MyGameState, MyIntent>('player1', blueprint, {
|
||||
* intentKeyExtractor: (intent) => `${intent.type}:${intent.unitId}`
|
||||
* });
|
||||
*
|
||||
* // 每个 tick 执行 | Execute each tick
|
||||
* const result = vm.executeTick(gameState, playerMemory);
|
||||
* ```
|
||||
*/
|
||||
export class ServerBlueprintVM<
|
||||
TGameState extends IGameState = IGameState,
|
||||
TIntent extends IIntent = IIntent
|
||||
> {
|
||||
/**
|
||||
* @zh 玩家 ID
|
||||
* @en Player ID
|
||||
*/
|
||||
private readonly _playerId: string;
|
||||
|
||||
/**
|
||||
* @zh 蓝图资产
|
||||
* @en Blueprint asset
|
||||
*/
|
||||
private readonly _blueprint: BlueprintAsset;
|
||||
|
||||
/**
|
||||
* @zh 执行上下文
|
||||
* @en Execution context
|
||||
*/
|
||||
private readonly _context: ServerExecutionContext<TGameState, TIntent>;
|
||||
|
||||
/**
|
||||
* @zh CPU 限制器
|
||||
* @en CPU limiter
|
||||
*/
|
||||
private readonly _cpuLimiter: CPULimiter;
|
||||
|
||||
/**
|
||||
* @zh 意图收集器
|
||||
* @en Intent collector
|
||||
*/
|
||||
private readonly _intentCollector: IntentCollector<TIntent>;
|
||||
|
||||
/**
|
||||
* @zh 事件节点缓存
|
||||
* @en Event nodes cache
|
||||
*/
|
||||
private readonly _eventNodes: Map<string, BlueprintNode[]> = new Map();
|
||||
|
||||
/**
|
||||
* @zh 连接查找表(按源)
|
||||
* @en Connection lookup (by source)
|
||||
*/
|
||||
private readonly _connectionsBySource: Map<string, BlueprintConnection[]> = new Map();
|
||||
|
||||
/**
|
||||
* @zh 连接查找表(按目标)
|
||||
* @en Connection lookup (by target)
|
||||
*/
|
||||
private readonly _connectionsByTarget: Map<string, BlueprintConnection[]> = new Map();
|
||||
|
||||
/**
|
||||
* @zh 待处理的延迟执行
|
||||
* @en Pending delayed executions
|
||||
*/
|
||||
private _pendingExecutions: PendingExecution[] = [];
|
||||
|
||||
/**
|
||||
* @zh 当前 tick
|
||||
* @en Current tick
|
||||
*/
|
||||
private _currentTick: number = 0;
|
||||
|
||||
/**
|
||||
* @zh 调试模式
|
||||
* @en Debug mode
|
||||
*/
|
||||
private readonly _debug: boolean;
|
||||
|
||||
/**
|
||||
* @zh 错误列表
|
||||
* @en Error list
|
||||
*/
|
||||
private _errors: string[] = [];
|
||||
|
||||
constructor(
|
||||
playerId: string,
|
||||
blueprint: BlueprintAsset,
|
||||
config: ServerVMConfig<TIntent> = {}
|
||||
) {
|
||||
this._playerId = playerId;
|
||||
this._blueprint = blueprint;
|
||||
this._debug = config.debug ?? false;
|
||||
|
||||
this._context = new ServerExecutionContext<TGameState, TIntent>(blueprint, playerId);
|
||||
|
||||
this._cpuLimiter = new CPULimiter(playerId, config.cpuConfig);
|
||||
|
||||
this._intentCollector = new IntentCollector<TIntent>(playerId, {
|
||||
keyExtractor: config.intentKeyExtractor
|
||||
});
|
||||
|
||||
this._context.setCPULimiter(this._cpuLimiter);
|
||||
this._context.setIntentCollector(this._intentCollector);
|
||||
|
||||
this._buildLookupTables();
|
||||
this._cacheEventNodes();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取玩家 ID
|
||||
* @en Get player ID
|
||||
*/
|
||||
get playerId(): string {
|
||||
return this._playerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取执行上下文
|
||||
* @en Get execution context
|
||||
*/
|
||||
get context(): ServerExecutionContext<TGameState, TIntent> {
|
||||
return this._context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取 CPU 限制器
|
||||
* @en Get CPU limiter
|
||||
*/
|
||||
get cpuLimiter(): CPULimiter {
|
||||
return this._cpuLimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取意图收集器
|
||||
* @en Get intent collector
|
||||
*/
|
||||
get intentCollector(): IntentCollector<TIntent> {
|
||||
return this._intentCollector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 构建连接查找表
|
||||
* @en Build connection lookup tables
|
||||
*/
|
||||
private _buildLookupTables(): void {
|
||||
for (const conn of this._blueprint.connections) {
|
||||
const sourceKey = `${conn.fromNodeId}.${conn.fromPin}`;
|
||||
if (!this._connectionsBySource.has(sourceKey)) {
|
||||
this._connectionsBySource.set(sourceKey, []);
|
||||
}
|
||||
this._connectionsBySource.get(sourceKey)!.push(conn);
|
||||
|
||||
const targetKey = `${conn.toNodeId}.${conn.toPin}`;
|
||||
if (!this._connectionsByTarget.has(targetKey)) {
|
||||
this._connectionsByTarget.set(targetKey, []);
|
||||
}
|
||||
this._connectionsByTarget.get(targetKey)!.push(conn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 缓存事件节点
|
||||
* @en Cache event nodes
|
||||
*/
|
||||
private _cacheEventNodes(): void {
|
||||
for (const node of this._blueprint.nodes) {
|
||||
if (node.type.startsWith('Event')) {
|
||||
const eventType = node.type;
|
||||
if (!this._eventNodes.has(eventType)) {
|
||||
this._eventNodes.set(eventType, []);
|
||||
}
|
||||
this._eventNodes.get(eventType)!.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 执行一个游戏 tick
|
||||
* @en Execute one game tick
|
||||
*
|
||||
* @param gameState - @zh 当前游戏状态 @en Current game state
|
||||
* @param memory - @zh 玩家 Memory @en Player Memory
|
||||
* @returns @zh Tick 执行结果 @en Tick execution result
|
||||
*/
|
||||
executeTick(gameState: TGameState, memory: Record<string, unknown> = {}): TickResult<TIntent> {
|
||||
this._currentTick = gameState.tick;
|
||||
this._errors = [];
|
||||
|
||||
this._intentCollector.clear();
|
||||
this._intentCollector.setTick(this._currentTick);
|
||||
this._context.clearLogs();
|
||||
this._context.clearOutputCache();
|
||||
|
||||
this._context.setGameState(gameState);
|
||||
this._context.setMemory(memory);
|
||||
|
||||
this._cpuLimiter.start();
|
||||
|
||||
try {
|
||||
this._processPendingExecutions();
|
||||
this._triggerEvent('EventTick');
|
||||
} catch (error) {
|
||||
this._errors.push(`Execution error: ${error}`);
|
||||
}
|
||||
|
||||
this._cpuLimiter.end();
|
||||
this._cpuLimiter.recoverBucket();
|
||||
|
||||
return {
|
||||
success: this._errors.length === 0,
|
||||
cpu: this._cpuLimiter.getStats(),
|
||||
intents: this._intentCollector.getIntents(),
|
||||
logs: this._context.getLogs(),
|
||||
errors: this._errors,
|
||||
memory: this._context.getMemory()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 触发事件
|
||||
* @en Trigger event
|
||||
*/
|
||||
private _triggerEvent(eventType: string, data?: Record<string, unknown>): void {
|
||||
const eventNodes = this._eventNodes.get(eventType);
|
||||
if (!eventNodes) return;
|
||||
|
||||
for (const node of eventNodes) {
|
||||
if (!this._context.checkCPU()) {
|
||||
this._errors.push('CPU limit exceeded');
|
||||
return;
|
||||
}
|
||||
this._executeFromNode(node, 'exec', data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 从节点开始执行
|
||||
* @en Execute from node
|
||||
*/
|
||||
private _executeFromNode(
|
||||
startNode: BlueprintNode,
|
||||
startPin: string,
|
||||
eventData?: Record<string, unknown>
|
||||
): void {
|
||||
if (eventData) {
|
||||
this._context.setOutputs(startNode.id, eventData);
|
||||
}
|
||||
|
||||
let currentNodeId: string | null = startNode.id;
|
||||
let currentPin: string = startPin;
|
||||
|
||||
while (currentNodeId) {
|
||||
if (!this._context.checkCPU()) {
|
||||
this._errors.push('CPU limit exceeded during execution');
|
||||
return;
|
||||
}
|
||||
|
||||
const connections = this._getConnectionsFromPin(currentNodeId, currentPin);
|
||||
|
||||
if (connections.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
const nextConn = connections[0];
|
||||
const result = this._executeNode(nextConn.toNodeId);
|
||||
|
||||
if (result.error) {
|
||||
this._errors.push(`Node ${nextConn.toNodeId}: ${result.error}`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.delay && result.delay > 0) {
|
||||
this._pendingExecutions.push({
|
||||
nodeId: nextConn.toNodeId,
|
||||
execPin: result.nextExec ?? 'exec',
|
||||
resumeTick: this._currentTick + Math.ceil(result.delay)
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.yield) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.nextExec === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentNodeId = nextConn.toNodeId;
|
||||
currentPin = result.nextExec ?? 'exec';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 执行单个节点
|
||||
* @en Execute single node
|
||||
*/
|
||||
private _executeNode(nodeId: string): ExecutionResult {
|
||||
const node = this._getNode(nodeId);
|
||||
if (!node) {
|
||||
return { error: `Node not found: ${nodeId}` };
|
||||
}
|
||||
|
||||
const executor = NodeRegistry.instance.getExecutor(node.type);
|
||||
if (!executor) {
|
||||
return { error: `No executor for node type: ${node.type}` };
|
||||
}
|
||||
|
||||
try {
|
||||
if (this._debug) {
|
||||
console.log(`[ServerVM] Executing: ${node.type} (${nodeId})`);
|
||||
}
|
||||
|
||||
const compatContext = this._createCompatibleContext() as unknown as Parameters<typeof executor.execute>[1];
|
||||
|
||||
const result = executor.execute(node, compatContext);
|
||||
|
||||
if (result.outputs) {
|
||||
this._context.setOutputs(nodeId, result.outputs);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return { error: `Execution error: ${error}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 创建与 Blueprint 兼容的执行上下文
|
||||
* @en Create Blueprint-compatible execution context
|
||||
*/
|
||||
private _createCompatibleContext(): {
|
||||
blueprint: BlueprintAsset;
|
||||
deltaTime: number;
|
||||
time: number;
|
||||
getNode: (id: string) => BlueprintNode | undefined;
|
||||
getConnectionsToPin: (nodeId: string, pinName: string) => BlueprintConnection[];
|
||||
getConnectionsFromPin: (nodeId: string, pinName: string) => BlueprintConnection[];
|
||||
evaluateInput: (nodeId: string, pinName: string, defaultValue?: unknown) => unknown;
|
||||
setOutputs: (nodeId: string, outputs: Record<string, unknown>) => void;
|
||||
getOutputs: (nodeId: string) => Record<string, unknown> | undefined;
|
||||
getVariable: (name: string) => unknown;
|
||||
setVariable: (name: string, value: unknown) => void;
|
||||
intentCollector: IntentCollector<TIntent>;
|
||||
gameState: TGameState | null;
|
||||
playerId: string;
|
||||
memory: Record<string, unknown>;
|
||||
log: (message: string) => void;
|
||||
warn: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
} {
|
||||
return {
|
||||
blueprint: this._blueprint,
|
||||
deltaTime: this._context.deltaTime,
|
||||
time: this._context.time,
|
||||
getNode: (id: string) => this._getNode(id),
|
||||
getConnectionsToPin: (nodeId: string, pinName: string) =>
|
||||
this._getConnectionsToPin(nodeId, pinName),
|
||||
getConnectionsFromPin: (nodeId: string, pinName: string) =>
|
||||
this._getConnectionsFromPin(nodeId, pinName),
|
||||
evaluateInput: (nodeId: string, pinName: string, defaultValue?: unknown) =>
|
||||
this._evaluateInput(nodeId, pinName, defaultValue),
|
||||
setOutputs: (nodeId: string, outputs: Record<string, unknown>) =>
|
||||
this._context.setOutputs(nodeId, outputs),
|
||||
getOutputs: (nodeId: string) => this._context.getOutputs(nodeId),
|
||||
getVariable: (name: string) => this._context.getVariable(name),
|
||||
setVariable: (name: string, value: unknown) => this._context.setVariable(name, value),
|
||||
intentCollector: this._intentCollector,
|
||||
gameState: this._context.getGameState(),
|
||||
playerId: this._playerId,
|
||||
memory: this._context.getMemory(),
|
||||
log: (message: string) => this._context.log(message),
|
||||
warn: (message: string) => this._context.warn(message),
|
||||
error: (message: string) => this._context.error(message)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 处理待处理的延迟执行
|
||||
* @en Process pending delayed executions
|
||||
*/
|
||||
private _processPendingExecutions(): void {
|
||||
const stillPending: PendingExecution[] = [];
|
||||
|
||||
for (const pending of this._pendingExecutions) {
|
||||
if (this._currentTick >= pending.resumeTick) {
|
||||
const node = this._getNode(pending.nodeId);
|
||||
if (node) {
|
||||
this._executeFromNode(node, pending.execPin);
|
||||
}
|
||||
} else {
|
||||
stillPending.push(pending);
|
||||
}
|
||||
}
|
||||
|
||||
this._pendingExecutions = stillPending;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取节点
|
||||
* @en Get node
|
||||
*/
|
||||
private _getNode(nodeId: string): BlueprintNode | undefined {
|
||||
return this._blueprint.nodes.find(n => n.id === nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取从源引脚的连接
|
||||
* @en Get connections from source pin
|
||||
*/
|
||||
private _getConnectionsFromPin(nodeId: string, pinName: string): BlueprintConnection[] {
|
||||
return this._connectionsBySource.get(`${nodeId}.${pinName}`) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取到目标引脚的连接
|
||||
* @en Get connections to target pin
|
||||
*/
|
||||
private _getConnectionsToPin(nodeId: string, pinName: string): BlueprintConnection[] {
|
||||
return this._connectionsByTarget.get(`${nodeId}.${pinName}`) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 计算输入引脚值
|
||||
* @en Evaluate input pin value
|
||||
*/
|
||||
private _evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown {
|
||||
const connections = this._getConnectionsToPin(nodeId, pinName);
|
||||
|
||||
if (connections.length === 0) {
|
||||
const node = this._getNode(nodeId);
|
||||
return node?.data[pinName] ?? defaultValue;
|
||||
}
|
||||
|
||||
const conn = connections[0];
|
||||
const cachedOutputs = this._context.getOutputs(conn.fromNodeId);
|
||||
|
||||
if (cachedOutputs && conn.fromPin in cachedOutputs) {
|
||||
return cachedOutputs[conn.fromPin];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取实例变量
|
||||
* @en Get instance variables
|
||||
*/
|
||||
getInstanceVariables(): Map<string, unknown> {
|
||||
return this._context.getInstanceVariables();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置实例变量
|
||||
* @en Set instance variables
|
||||
*/
|
||||
setInstanceVariables(variables: Map<string, unknown>): void {
|
||||
this._context.setInstanceVariables(variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 重置 VM
|
||||
* @en Reset VM
|
||||
*/
|
||||
reset(): void {
|
||||
this._pendingExecutions = [];
|
||||
this._currentTick = 0;
|
||||
this._errors = [];
|
||||
this._cpuLimiter.reset();
|
||||
this._intentCollector.clear();
|
||||
this._context.clearOutputCache();
|
||||
this._context.clearLogs();
|
||||
}
|
||||
}
|
||||
459
packages/engine/script-runtime/src/vm/ServerExecutionContext.ts
Normal file
459
packages/engine/script-runtime/src/vm/ServerExecutionContext.ts
Normal file
@@ -0,0 +1,459 @@
|
||||
/**
|
||||
* @zh 服务器端执行上下文
|
||||
* @en Server-side Execution Context
|
||||
*
|
||||
* @zh 扩展 Blueprint 的 ExecutionContext,添加服务器端特性
|
||||
* @en Extends Blueprint's ExecutionContext with server-side features
|
||||
*/
|
||||
|
||||
import type { BlueprintAsset } from '@esengine/blueprint';
|
||||
import type { IIntent } from '../intent/IntentTypes';
|
||||
import type { IIntentCollector } from '../intent/IntentCollector';
|
||||
import type { CPULimiter } from './CPULimiter';
|
||||
|
||||
// =============================================================================
|
||||
// 基础游戏状态接口 | Base Game State Interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 基础游戏状态接口(引擎级别)
|
||||
* @en Base game state interface (engine level)
|
||||
*
|
||||
* @zh 引擎只定义最基础的时间信息,具体的游戏状态由游戏项目扩展
|
||||
* @en Engine only defines basic timing info, specific game state is extended by game projects
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 游戏项目中扩展游戏状态 | Extend game state in game project
|
||||
* interface MyGameState extends IGameState {
|
||||
* units: Map<string, IUnitState>;
|
||||
* buildings: Map<string, IBuildingState>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export interface IGameState {
|
||||
/**
|
||||
* @zh 当前 tick
|
||||
* @en Current tick
|
||||
*/
|
||||
tick: number;
|
||||
|
||||
/**
|
||||
* @zh 自上次 tick 的时间间隔
|
||||
* @en Time interval since last tick
|
||||
*/
|
||||
deltaTime: number;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 日志接口 | Log Interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 日志条目
|
||||
* @en Log entry
|
||||
*/
|
||||
export interface LogEntry {
|
||||
level: 'log' | 'warn' | 'error';
|
||||
message: string;
|
||||
timestamp: number;
|
||||
tick: number;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 服务器执行上下文 | Server Execution Context
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* @zh 服务器端执行上下文
|
||||
* @en Server-side Execution Context
|
||||
*
|
||||
* @zh 提供蓝图执行时访问游戏状态和收集意图的能力
|
||||
* @en Provides ability to access game state and collect intents during blueprint execution
|
||||
*
|
||||
* @typeParam TGameState - @zh 游戏状态类型,由游戏项目定义 @en Game state type, defined by game project
|
||||
* @typeParam TIntent - @zh 意图类型,由游戏项目定义 @en Intent type, defined by game project
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 游戏项目中定义具体类型 | Define specific types in game project
|
||||
* interface MyGameState extends IGameState {
|
||||
* units: Map<string, IUnit>;
|
||||
* resources: Map<string, IResource>;
|
||||
* }
|
||||
*
|
||||
* interface MyIntent extends IIntent {
|
||||
* readonly type: 'unit.move' | 'unit.attack';
|
||||
* unitId: string;
|
||||
* }
|
||||
*
|
||||
* // 创建上下文 | Create context
|
||||
* const context = new ServerExecutionContext<MyGameState, MyIntent>(blueprint, 'player1');
|
||||
* ```
|
||||
*/
|
||||
export class ServerExecutionContext<
|
||||
TGameState extends IGameState = IGameState,
|
||||
TIntent extends IIntent = IIntent
|
||||
> {
|
||||
/**
|
||||
* @zh 蓝图资产
|
||||
* @en Blueprint asset
|
||||
*/
|
||||
readonly blueprint: BlueprintAsset;
|
||||
|
||||
/**
|
||||
* @zh 玩家 ID
|
||||
* @en Player ID
|
||||
*/
|
||||
readonly playerId: string;
|
||||
|
||||
/**
|
||||
* @zh 当前游戏状态
|
||||
* @en Current game state
|
||||
*/
|
||||
private _gameState: TGameState | null = null;
|
||||
|
||||
/**
|
||||
* @zh 意图收集器
|
||||
* @en Intent collector
|
||||
*/
|
||||
private _intentCollector: IIntentCollector<TIntent> | null = null;
|
||||
|
||||
/**
|
||||
* @zh CPU 限制器
|
||||
* @en CPU limiter
|
||||
*/
|
||||
private _cpuLimiter: CPULimiter | null = null;
|
||||
|
||||
/**
|
||||
* @zh 玩家持久化数据(Memory)
|
||||
* @en Player persistent data (Memory)
|
||||
*/
|
||||
private _memory: Record<string, unknown> = {};
|
||||
|
||||
/**
|
||||
* @zh 日志列表
|
||||
* @en Log list
|
||||
*/
|
||||
private _logs: LogEntry[] = [];
|
||||
|
||||
/**
|
||||
* @zh 帧增量时间
|
||||
* @en Frame delta time
|
||||
*/
|
||||
deltaTime: number = 0;
|
||||
|
||||
/**
|
||||
* @zh 自开始以来的总时间
|
||||
* @en Total time since start
|
||||
*/
|
||||
time: number = 0;
|
||||
|
||||
/**
|
||||
* @zh 节点输出缓存
|
||||
* @en Node output cache
|
||||
*/
|
||||
private _outputCache: Map<string, Record<string, unknown>> = new Map();
|
||||
|
||||
/**
|
||||
* @zh 实例变量
|
||||
* @en Instance variables
|
||||
*/
|
||||
private _instanceVariables: Map<string, unknown> = new Map();
|
||||
|
||||
/**
|
||||
* @zh 局部变量
|
||||
* @en Local variables
|
||||
*/
|
||||
private _localVariables: Map<string, unknown> = new Map();
|
||||
|
||||
/**
|
||||
* @zh 全局变量(所有玩家共享)
|
||||
* @en Global variables (shared by all players)
|
||||
*/
|
||||
private static _globalVariables: Map<string, unknown> = new Map();
|
||||
|
||||
constructor(blueprint: BlueprintAsset, playerId: string) {
|
||||
this.blueprint = blueprint;
|
||||
this.playerId = playerId;
|
||||
|
||||
for (const variable of blueprint.variables) {
|
||||
if (variable.scope === 'instance') {
|
||||
this._instanceVariables.set(variable.name, variable.defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 游戏状态访问 | Game State Access
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @zh 设置当前游戏状态
|
||||
* @en Set current game state
|
||||
*/
|
||||
setGameState(state: TGameState): void {
|
||||
this._gameState = state;
|
||||
this.deltaTime = state.deltaTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取当前游戏状态
|
||||
* @en Get current game state
|
||||
*/
|
||||
getGameState(): TGameState | null {
|
||||
return this._gameState;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取当前 tick
|
||||
* @en Get current tick
|
||||
*/
|
||||
getTick(): number {
|
||||
return this._gameState?.tick ?? 0;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 意图收集 | Intent Collection
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @zh 设置意图收集器
|
||||
* @en Set intent collector
|
||||
*/
|
||||
setIntentCollector(collector: IIntentCollector<TIntent>): void {
|
||||
this._intentCollector = collector;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取意图收集器
|
||||
* @en Get intent collector
|
||||
*/
|
||||
get intentCollector(): IIntentCollector<TIntent> | null {
|
||||
return this._intentCollector;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CPU 限制 | CPU Limiting
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @zh 设置 CPU 限制器
|
||||
* @en Set CPU limiter
|
||||
*/
|
||||
setCPULimiter(limiter: CPULimiter): void {
|
||||
this._cpuLimiter = limiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取 CPU 限制器
|
||||
* @en Get CPU limiter
|
||||
*/
|
||||
get cpuLimiter(): CPULimiter | null {
|
||||
return this._cpuLimiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 检查是否可以继续执行
|
||||
* @en Check if can continue execution
|
||||
*/
|
||||
checkCPU(): boolean {
|
||||
return this._cpuLimiter?.checkStep() ?? true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Memory 访问 | Memory Access
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @zh 设置玩家 Memory
|
||||
* @en Set player Memory
|
||||
*/
|
||||
setMemory(memory: Record<string, unknown>): void {
|
||||
this._memory = memory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取玩家 Memory
|
||||
* @en Get player Memory
|
||||
*/
|
||||
getMemory(): Record<string, unknown> {
|
||||
return this._memory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取 Memory 中的值
|
||||
* @en Get value from Memory
|
||||
*/
|
||||
getMemoryValue<T>(key: string): T | undefined {
|
||||
return this._memory[key] as T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置 Memory 中的值
|
||||
* @en Set value in Memory
|
||||
*/
|
||||
setMemoryValue(key: string, value: unknown): void {
|
||||
this._memory[key] = value;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 日志 | Logging
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @zh 添加日志
|
||||
* @en Add log
|
||||
*/
|
||||
log(message: string): void {
|
||||
this._logs.push({
|
||||
level: 'log',
|
||||
message,
|
||||
timestamp: Date.now(),
|
||||
tick: this.getTick()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加警告
|
||||
* @en Add warning
|
||||
*/
|
||||
warn(message: string): void {
|
||||
this._logs.push({
|
||||
level: 'warn',
|
||||
message,
|
||||
timestamp: Date.now(),
|
||||
tick: this.getTick()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 添加错误
|
||||
* @en Add error
|
||||
*/
|
||||
error(message: string): void {
|
||||
this._logs.push({
|
||||
level: 'error',
|
||||
message,
|
||||
timestamp: Date.now(),
|
||||
tick: this.getTick()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取日志列表
|
||||
* @en Get log list
|
||||
*/
|
||||
getLogs(): LogEntry[] {
|
||||
return [...this._logs];
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清除日志
|
||||
* @en Clear logs
|
||||
*/
|
||||
clearLogs(): void {
|
||||
this._logs = [];
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 变量管理 | Variable Management
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @zh 获取变量值
|
||||
* @en Get variable value
|
||||
*/
|
||||
getVariable(name: string): unknown {
|
||||
if (this._localVariables.has(name)) {
|
||||
return this._localVariables.get(name);
|
||||
}
|
||||
if (this._instanceVariables.has(name)) {
|
||||
return this._instanceVariables.get(name);
|
||||
}
|
||||
if (ServerExecutionContext._globalVariables.has(name)) {
|
||||
return ServerExecutionContext._globalVariables.get(name);
|
||||
}
|
||||
|
||||
const varDef = this.blueprint.variables.find(v => v.name === name);
|
||||
return varDef?.defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置变量值
|
||||
* @en Set variable value
|
||||
*/
|
||||
setVariable(name: string, value: unknown): void {
|
||||
const varDef = this.blueprint.variables.find(v => v.name === name);
|
||||
|
||||
if (!varDef) {
|
||||
this._localVariables.set(name, value);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (varDef.scope) {
|
||||
case 'local':
|
||||
this._localVariables.set(name, value);
|
||||
break;
|
||||
case 'instance':
|
||||
this._instanceVariables.set(name, value);
|
||||
break;
|
||||
case 'global':
|
||||
ServerExecutionContext._globalVariables.set(name, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取实例变量
|
||||
* @en Get instance variables
|
||||
*/
|
||||
getInstanceVariables(): Map<string, unknown> {
|
||||
return new Map(this._instanceVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 设置实例变量
|
||||
* @en Set instance variables
|
||||
*/
|
||||
setInstanceVariables(variables: Map<string, unknown>): void {
|
||||
this._instanceVariables = new Map(variables);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 输出缓存 | Output Cache
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* @zh 设置节点输出
|
||||
* @en Set node outputs
|
||||
*/
|
||||
setOutputs(nodeId: string, outputs: Record<string, unknown>): void {
|
||||
this._outputCache.set(nodeId, outputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 获取节点输出
|
||||
* @en Get node outputs
|
||||
*/
|
||||
getOutputs(nodeId: string): Record<string, unknown> | undefined {
|
||||
return this._outputCache.get(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清除输出缓存
|
||||
* @en Clear output cache
|
||||
*/
|
||||
clearOutputCache(): void {
|
||||
this._outputCache.clear();
|
||||
this._localVariables.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* @zh 清除全局变量
|
||||
* @en Clear global variables
|
||||
*/
|
||||
static clearGlobalVariables(): void {
|
||||
ServerExecutionContext._globalVariables.clear();
|
||||
}
|
||||
}
|
||||
22
packages/engine/script-runtime/tsconfig.build.json
Normal file
22
packages/engine/script-runtime/tsconfig.build.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
23
packages/engine/script-runtime/tsconfig.json
Normal file
23
packages/engine/script-runtime/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../framework/core"
|
||||
},
|
||||
{
|
||||
"path": "../../framework/blueprint"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
packages/engine/script-runtime/tsup.config.ts
Normal file
15
packages/engine/script-runtime/tsup.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
external: [
|
||||
'@esengine/ecs-framework',
|
||||
'@esengine/engine-core',
|
||||
'@esengine/blueprint'
|
||||
],
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
Reference in New Issue
Block a user