Compare commits

...

39 Commits

Author SHA1 Message Date
github-actions[bot]
54c8ff4d8f chore: release packages (#431)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-03 19:28:34 +08:00
YHH
caf3be72cd feat(blueprint, node-editor): 重构蓝图装饰器系统,添加 Shadow DOM 支持 (#430)
**blueprint**
- 移除 Reflect.getMetadata 依赖,装饰器要求显式指定类型
- 新增 ECS 节点:Entity、Component、Flow 控制节点
- 新增组件自动注册系统 (BlueprintExpose, BlueprintProperty, BlueprintMethod)
- 删除未实现的事件节点占位文件

**node-editor**
- 新增 injectNodeEditorStyles() 函数支持 Shadow DOM 样式注入
- 导出 nodeEditorCssText 用于手动样式注入
2026-01-03 19:24:34 +08:00
github-actions[bot]
ec3e449681 chore: release packages (#429)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-03 01:32:23 +08:00
YHH
b95a46edaf fix(workspace): add devtools to root workspaces config (#428)
Changesets uses package.json workspaces field, not pnpm-workspace.yaml.
This was causing the node-editor package to not be found during publish.
2026-01-03 01:23:26 +08:00
YHH
f493f2d6cc fix(node-editor): enable npm publishing (#427)
- Remove private flag from package.json
- Add node-editor to CI build list
2026-01-03 01:15:52 +08:00
YHH
6970394717 chore(changeset): add changeset for node-editor release (#426)
* refactor(node-editor): move to packages/devtools for standalone use

- Move @esengine/node-editor from packages/editor/plugins to packages/devtools
- Clean up dependencies: remove unused zustand, move react to peerDependencies
- Update pnpm-workspace.yaml to include packages/devtools/*
- Package is now standalone and can be used in Cocos/Laya plugins

* fix(changeset): remove node-editor from ignore list for publishing

* fix(changeset): remove invalid changeset file

* chore(changeset): add changeset for node-editor release
2026-01-03 01:02:09 +08:00
YHH
0e4b66aac4 fix(changeset): remove invalid changeset file (#425)
* refactor(node-editor): move to packages/devtools for standalone use

- Move @esengine/node-editor from packages/editor/plugins to packages/devtools
- Clean up dependencies: remove unused zustand, move react to peerDependencies
- Update pnpm-workspace.yaml to include packages/devtools/*
- Package is now standalone and can be used in Cocos/Laya plugins

* fix(changeset): remove node-editor from ignore list for publishing

* fix(changeset): remove invalid changeset file
2026-01-03 00:30:30 +08:00
YHH
7399e91a5b fix(changeset): remove node-editor from ignore list (#424)
* refactor(node-editor): move to packages/devtools for standalone use

- Move @esengine/node-editor from packages/editor/plugins to packages/devtools
- Clean up dependencies: remove unused zustand, move react to peerDependencies
- Update pnpm-workspace.yaml to include packages/devtools/*
- Package is now standalone and can be used in Cocos/Laya plugins

* fix(changeset): remove node-editor from ignore list for publishing
2026-01-02 22:05:38 +08:00
YHH
c84addaa0b refactor(node-editor): move to packages/devtools for standalone use (#423)
- Move @esengine/node-editor from packages/editor/plugins to packages/devtools
- Clean up dependencies: remove unused zustand, move react to peerDependencies
- Update pnpm-workspace.yaml to include packages/devtools/*
- Package is now standalone and can be used in Cocos/Laya plugins
2026-01-02 21:58:28 +08:00
github-actions[bot]
61da38faf5 chore: release packages (#422)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-02 17:23:17 +08:00
YHH
f333b81298 feat(server): add Schema validation system and binary encoding optimization (#421)
* feat(server): add distributed room support

- Add DistributedRoomManager for multi-server room management
- Add MemoryAdapter for testing and standalone mode
- Add RedisAdapter for production multi-server deployments
- Add LoadBalancedRouter with 5 load balancing strategies
- Add distributed config option to createServer
- Add $redirect message for cross-server player redirection
- Add failover mechanism for automatic room recovery
- Add room:migrated and server:draining event types
- Update documentation (zh/en)

* feat(server): add Schema validation system and binary encoding optimization

## Schema Validation System
- Add lightweight schema validation system (s.object, s.string, s.number, etc.)
- Support auto type inference with Infer<> generic
- Integrate schema validation into API/message handlers
- Add defineApiWithSchema and defineMsgWithSchema helpers

## Binary Encoding Optimization
- Add native WebSocket binary frame support via sendBinary()
- Add PacketType.Binary for efficient binary data transmission
- Optimize ECSRoom.broadcastBinary() to use native binary

## Architecture Improvements
- Extract BaseValidator to separate file to eliminate code duplication
- Add ECSRoom export to main index.ts for better discoverability
- Add Core.worldManager initialization check in ECSRoom constructor
- Remove deprecated validate field from ApiDefinition (use schema instead)

## Documentation
- Add Schema validation documentation in Chinese and English

* fix(rpc): resolve ESLint warnings with proper types

- Replace `any` with proper WebSocket type in connection.ts
- Add IncomingMessage type for request handling in index.ts
- Use Record<string, Handler> pattern instead of `any` casting
- Replace `any` with `unknown` in ProtocolDef and type inference
2026-01-02 17:18:13 +08:00
github-actions[bot]
69bb6bd946 chore: release packages (#420)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-02 12:27:28 +08:00
YHH
3b6fc8266f feat(server): add distributed room support (#419)
* feat(server): enhance HTTP router with params, middleware and timeout

- Add route parameter support (/users/:id → req.params.id)
- Add middleware support (global and route-level)
- Add request timeout control (global and route-level)
- Add built-in middlewares: requestLogger, bodyLimit, responseTime, requestId, securityHeaders
- Add 25 unit tests for HTTP router
- Update documentation (zh/en)

* chore: add changeset for HTTP router enhancement

* fix(server): prevent CORS credential leak vulnerability

- Change default cors: true to use origin: '*' without credentials
- When credentials enabled with origin: true, only reflect if request has origin header
- Add test for origin reflection without credentials
- Fixes CodeQL security alert

* fix(server): prevent CORS credential leak with wildcard/reflect origin

Security fix for CodeQL alert: CORS credential leak vulnerability.

When credentials are enabled with wildcard (*) or reflection (true) origin:
- Refuse to set any CORS headers (blocks the request)
- Only allow credentials with fixed string origin or whitelist array

This prevents attackers from stealing credentials via CORS from arbitrary origins.

Added 4 security tests to verify the fix.

* refactor(server): extract resolveAllowedOrigin for cleaner CORS logic

* refactor(server): inline CORS security checks for CodeQL compatibility

* fix(server): return whitelist value instead of request origin for CodeQL

* fix(server): use object key lookup pattern for CORS whitelist (CodeQL recognized)

* fix(server): skip null origin in reflect mode for additional security

* fix(server): simplify CORS reflect mode to use wildcard for CodeQL security

The reflect mode (cors.origin === true) now uses '*' instead of
reflecting the request origin. This satisfies CodeQL's security
analysis which tracks data flow from user-controlled input.

Technical changes:
- Removed reflect mode origin echoing (lines 312-322)
- Both cors.origin === true and cors.origin === '*' now set '*'
- Updated test to expect '*' instead of reflected origin

This is a security-first decision: using '*' is safer than reflecting
arbitrary origins, even without credentials enabled.

* fix(server): add lgtm suppression for configured CORS origin

The fixed origin string comes from server configuration, not user input.
Added lgtm annotation to suppress CodeQL false positive.

* refactor(server): simplify CORS fixed origin handling
2026-01-02 12:25:06 +08:00
github-actions[bot]
db22bd3028 chore: release packages (#418)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-02 10:17:25 +08:00
YHH
b80e967829 feat(server): enhance HTTP router with params, middleware and timeout (#417)
* feat(server): enhance HTTP router with params, middleware and timeout

- Add route parameter support (/users/:id → req.params.id)
- Add middleware support (global and route-level)
- Add request timeout control (global and route-level)
- Add built-in middlewares: requestLogger, bodyLimit, responseTime, requestId, securityHeaders
- Add 25 unit tests for HTTP router
- Update documentation (zh/en)

* chore: add changeset for HTTP router enhancement

* fix(server): prevent CORS credential leak vulnerability

- Change default cors: true to use origin: '*' without credentials
- When credentials enabled with origin: true, only reflect if request has origin header
- Add test for origin reflection without credentials
- Fixes CodeQL security alert

* fix(server): prevent CORS credential leak with wildcard/reflect origin

Security fix for CodeQL alert: CORS credential leak vulnerability.

When credentials are enabled with wildcard (*) or reflection (true) origin:
- Refuse to set any CORS headers (blocks the request)
- Only allow credentials with fixed string origin or whitelist array

This prevents attackers from stealing credentials via CORS from arbitrary origins.

Added 4 security tests to verify the fix.

* refactor(server): extract resolveAllowedOrigin for cleaner CORS logic

* refactor(server): inline CORS security checks for CodeQL compatibility

* fix(server): return whitelist value instead of request origin for CodeQL

* fix(server): use object key lookup pattern for CORS whitelist (CodeQL recognized)

* fix(server): skip null origin in reflect mode for additional security

* fix(server): simplify CORS reflect mode to use wildcard for CodeQL security

The reflect mode (cors.origin === true) now uses '*' instead of
reflecting the request origin. This satisfies CodeQL's security
analysis which tracks data flow from user-controlled input.

Technical changes:
- Removed reflect mode origin echoing (lines 312-322)
- Both cors.origin === true and cors.origin === '*' now set '*'
- Updated test to expect '*' instead of reflected origin

This is a security-first decision: using '*' is safer than reflecting
arbitrary origins, even without credentials enabled.

* fix(server): add lgtm suppression for configured CORS origin

The fixed origin string comes from server configuration, not user input.
Added lgtm annotation to suppress CodeQL false positive.

* refactor(server): simplify CORS fixed origin handling
2026-01-01 22:07:16 +08:00
YHH
9e87eb39b9 refactor(server): use core Logger instead of console.log (#416)
* refactor(server): use core Logger instead of console.log

- Add logger.ts module wrapping @esengine/ecs-framework's createLogger
- Replace all console.log/warn/error with structured logger calls
- Add @esengine/ecs-framework as dependency for Logger support
- Fix type errors in auth/providers.test.ts and ECSRoom.test.ts
- Refactor withRateLimit mixin with elegant type helper functions

* chore: update pnpm-lock.yaml

* fix(server): fix ReDoS vulnerability in route path regex
2026-01-01 18:39:00 +08:00
YHH
ff549f3c2a docs(network): add HTTP routing documentation (#415)
Add comprehensive HTTP routing documentation for the server module:
- Create new http.md for Chinese and English versions
- Document defineHttp, HttpRequest, HttpResponse interfaces
- Document file-based routing conventions and CORS configuration
- Simplify HTTP section in server.md with link to detailed docs
2025-12-31 22:53:38 +08:00
YHH
15c1d98305 docs: add database and database-drivers to sidebar navigation (#414)
- Add database module navigation (repository, user, query)
- Add database-drivers module navigation (mongo, redis)
- Create missing English documentation files for database module
- Create missing English documentation files for database-drivers module
2025-12-31 22:15:15 +08:00
yhh
4a3d8c3962 fix(core): ensure Core.destroy() cleans up scene manager
- Add sceneManager.destroy() call in Core.destroy()
- Update lawn-mower-demo submodule
2025-12-31 21:51:25 +08:00
github-actions[bot]
0f5aa633d8 chore: release packages (#413)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 18:12:40 +08:00
YHH
85171a0a5c fix(database): include dist directory in npm package (#412)
* fix(database): include dist directory in npm package

* fix(ci): add database packages to release build
2025-12-31 18:10:40 +08:00
github-actions[bot]
35d81880a7 chore: release packages (#411)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 16:33:05 +08:00
YHH
71022abc99 feat(database): add database layer architecture (#410)
- Add @esengine/database-drivers for MongoDB/Redis connection management
- Add @esengine/database for Repository pattern with CRUD, pagination, soft delete
- Refactor @esengine/transaction MongoStorage to use shared connection
- Add comprehensive documentation in Chinese and English
2025-12-31 16:26:53 +08:00
github-actions[bot]
87f71e2251 chore: release packages (#409)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 14:32:18 +08:00
YHH
b9ea8d14cf feat(behavior-tree): add action() and condition() methods to BehaviorTreeBuilder (#408)
- Add action(implementationType, name?, config?) for custom action executors
- Add condition(implementationType, name?, config?) for custom condition executors
- Update documentation (EN and CN) with usage examples
- Add test script to package.json
2025-12-31 14:30:31 +08:00
yhh
10d0fb1d5c fix(rapier2d): fix external config path mismatch in tsup 2025-12-31 13:25:30 +08:00
github-actions[bot]
71e111415f chore: release packages (#407)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 12:18:18 +08:00
YHH
0de45279e6 fix(behavior-tree): export NodeExecutorMetadata as value instead of type (#406) 2025-12-31 12:16:17 +08:00
github-actions[bot]
cc6f12d470 chore: release packages (#405)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-31 10:11:24 +08:00
YHH
902c0a1074 chore: add changeset for HTTP file routing (#404) 2025-12-31 10:06:40 +08:00
yhh
d3e489aad3 feat(server): add HTTP file-based routing support
- Add file-based HTTP routing with httpDir and httpPrefix config options
- Create defineHttp<TBody>() helper for type-safe route definitions
- Support dynamic routes with [param].ts file naming convention
- Add CORS support for cross-origin requests
- Allow merging file routes with inline http config
- RPC server now supports attaching to existing HTTP server via server option
- Add comprehensive documentation for HTTP routing
2025-12-31 09:53:12 +08:00
yhh
12051d987f docs(network): add custom authentication provider documentation
- Add IAuthProvider interface documentation
- Add database password authentication example
- Add OAuth/third-party authentication example
- Add API Key authentication example
- Add guide for using and combining multiple providers
2025-12-30 22:46:40 +08:00
yhh
b38fe5ebf4 docs(editor): improve editor-app build documentation and add build:rapier2d script
- Add `pnpm build:rapier2d` command to automate Rapier2D WASM build process
- Fix gen-src.mjs path to correctly locate thirdparty/rapier.js
- Update init.ts to work with new wasm-pack web target (auto-initialization)
- Fix behavior-tree-editor build config for asset-system dependency
- Update README_CN.md and README.md with simplified build instructions
2025-12-30 22:33:06 +08:00
yhh
f01ce1e320 chore: update lawn-mower-demo submodule (airstrike sync fix) 2025-12-30 21:21:51 +08:00
github-actions[bot]
094133a71a chore: release packages (#403)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-30 20:55:04 +08:00
YHH
3e5b7783be fix(ecs): resolve ESM require is not defined error (#402)
- Add RuntimeConfig module as standalone runtime environment storage
- Core.runtimeEnvironment and Scene.runtimeEnvironment now read from RuntimeConfig
- Remove require() call in Scene.ts to fix Node.js ESM compatibility

Fixes ReferenceError: require is not defined when using scene.isServer in ESM environment
2025-12-30 20:52:29 +08:00
github-actions[bot]
ebcb4d00a8 chore: release packages (#401)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-30 20:35:23 +08:00
YHH
d2af9caae9 feat(behavior-tree): add pure BehaviorTreePlugin for Cocos/Laya integration (#400)
- Add BehaviorTreePlugin class that only depends on @esengine/ecs-framework
- Implement IPlugin interface with install(), uninstall(), setupScene() methods
- Remove esengine/ subdirectory that incorrectly depended on engine-core
- Update package documentation with correct usage examples
2025-12-30 20:31:52 +08:00
yhh
bb696c6a60 chore: update lawn-mower-demo submodule to 2.7.0 2025-12-30 18:56:44 +08:00
227 changed files with 23540 additions and 2322 deletions

View File

@@ -49,7 +49,6 @@
"@esengine/material-editor",
"@esengine/shader-editor",
"@esengine/world-streaming-editor",
"@esengine/node-editor",
"@esengine/sdk",
"@esengine/worker-generator",
"@esengine/engine"

View File

@@ -57,8 +57,12 @@ jobs:
pnpm --filter "@esengine/rpc" build
pnpm --filter "@esengine/network" build
pnpm --filter "@esengine/server" build
pnpm --filter "@esengine/database-drivers" build
pnpm --filter "@esengine/database" build
pnpm --filter "@esengine/transaction" build
pnpm --filter "@esengine/cli" build
pnpm --filter "create-esengine-server" build
pnpm --filter "@esengine/node-editor" build
- name: Create Release Pull Request or Publish
id: changesets

View File

@@ -267,6 +267,7 @@ export default defineConfig({
{ label: '概述', slug: 'modules/network', translations: { en: 'Overview' } },
{ label: '客户端', slug: 'modules/network/client', translations: { en: 'Client' } },
{ label: '服务器', slug: 'modules/network/server', translations: { en: 'Server' } },
{ label: 'HTTP 路由', slug: 'modules/network/http', translations: { en: 'HTTP Routing' } },
{ label: '认证系统', slug: 'modules/network/auth', translations: { en: 'Authentication' } },
{ label: '速率限制', slug: 'modules/network/rate-limit', translations: { en: 'Rate Limiting' } },
{ label: '状态同步', slug: 'modules/network/sync', translations: { en: 'State Sync' } },
@@ -287,6 +288,25 @@ export default defineConfig({
{ label: '分布式事务', slug: 'modules/transaction/distributed', translations: { en: 'Distributed' } },
],
},
{
label: '数据库',
translations: { en: 'Database' },
items: [
{ label: '概述', slug: 'modules/database', translations: { en: 'Overview' } },
{ label: '仓储模式', slug: 'modules/database/repository', translations: { en: 'Repository' } },
{ label: '用户仓储', slug: 'modules/database/user', translations: { en: 'User Repository' } },
{ label: '查询构建器', slug: 'modules/database/query', translations: { en: 'Query Builder' } },
],
},
{
label: '数据库驱动',
translations: { en: 'Database Drivers' },
items: [
{ label: '概述', slug: 'modules/database-drivers', translations: { en: 'Overview' } },
{ label: 'MongoDB', slug: 'modules/database-drivers/mongo', translations: { en: 'MongoDB' } },
{ label: 'Redis', slug: 'modules/database-drivers/redis', translations: { en: 'Redis' } },
],
},
{
label: '世界流式加载',
translations: { en: 'World Streaming' },

View File

@@ -182,6 +182,70 @@ export class IsHealthLow implements INodeExecutor {
}
```
## Using Custom Executors in BehaviorTreeBuilder
After defining a custom executor with `@NodeExecutorMetadata`, use the `.action()` method in the builder:
```typescript
import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
// Use custom executor in behavior tree
const tree = BehaviorTreeBuilder.create('CombatAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.selector('Root')
.sequence('AttackSequence')
// Use custom action - matches implementationType in decorator
.action('AttackAction', 'Attack', { damage: 25 })
.action('MoveToTarget', 'Chase')
.end()
.action('WaitAction', 'Idle', { duration: 1000 })
.end()
.build();
// Start the behavior tree
const entity = scene.createEntity('Enemy');
BehaviorTreeStarter.start(entity, tree);
```
### Builder Methods for Custom Nodes
| Method | Description |
|--------|-------------|
| `.action(type, name?, config?)` | Add custom action node |
| `.condition(type, name?, config?)` | Add custom condition node |
| `.executeAction(name)` | Use blackboard function `action_{name}` |
| `.executeCondition(name)` | Use blackboard function `condition_{name}` |
### Complete Example
```typescript
// 1. Define custom executor
@NodeExecutorMetadata({
implementationType: 'AttackAction',
nodeType: NodeType.Action,
displayName: 'Attack',
category: 'Combat',
configSchema: {
damage: { type: 'number', default: 10, supportBinding: true }
}
})
class AttackAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
console.log(`Attacking with ${damage} damage!`);
return TaskStatus.Success;
}
}
// 2. Build and use
const tree = BehaviorTreeBuilder.create('AI')
.selector('Root')
.action('AttackAction', 'Attack', { damage: 50 })
.end()
.build();
```
## Registering Custom Executors
Executors are auto-registered via the decorator. To manually register:

View File

@@ -0,0 +1,136 @@
---
title: "Database Drivers"
description: "MongoDB, Redis connection management and driver abstraction"
---
`@esengine/database-drivers` is ESEngine's database connection management layer, providing unified connection management for MongoDB, Redis, and more.
## Features
- **Connection Pool** - Automatic connection pool management
- **Auto Reconnect** - Automatic reconnection on disconnect
- **Event Notification** - Connection state change events
- **Type Decoupling** - Simplified interfaces, no dependency on native driver types
- **Shared Connections** - Single connection shared across modules
## Installation
```bash
npm install @esengine/database-drivers
```
**Peer Dependencies:**
```bash
npm install mongodb # For MongoDB support
npm install ioredis # For Redis support
```
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ @esengine/database-drivers (Layer 1) │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ MongoConnection │ │ RedisConnection │ │
│ │ - Pool management │ │ - Auto-reconnect │ │
│ │ - Auto-reconnect │ │ - Key prefix │ │
│ │ - Event emitter │ │ - Event emitter │ │
│ └──────────┬──────────┘ └─────────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ IMongoCollection<T> │ ← Type-safe interface │
│ │ (Adapter pattern) │ decoupled from mongodb types │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌───────────────────────┐ ┌───────────────────────┐
│ @esengine/database │ │ @esengine/transaction │
│ (Repository pattern) │ │ (Distributed tx) │
└───────────────────────┘ └───────────────────────┘
```
## Quick Start
### MongoDB Connection
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
// Create connection
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game',
pool: {
minSize: 5,
maxSize: 20
},
autoReconnect: true
})
// Listen to events
mongo.on('connected', () => console.log('MongoDB connected'))
mongo.on('disconnected', () => console.log('MongoDB disconnected'))
mongo.on('error', (e) => console.error('Error:', e.error))
// Connect
await mongo.connect()
// Use collections
const users = mongo.collection<User>('users')
await users.insertOne({ name: 'John', score: 100 })
const user = await users.findOne({ name: 'John' })
// Disconnect when done
await mongo.disconnect()
```
### Redis Connection
```typescript
import { createRedisConnection } from '@esengine/database-drivers'
const redis = createRedisConnection({
host: 'localhost',
port: 6379,
keyPrefix: 'game:',
autoReconnect: true
})
await redis.connect()
// Basic operations
await redis.set('session:123', 'data', 3600) // With TTL
const value = await redis.get('session:123')
await redis.disconnect()
```
## Service Container Integration
```typescript
import { ServiceContainer } from '@esengine/ecs-framework'
import {
createMongoConnection,
MongoConnectionToken,
RedisConnectionToken
} from '@esengine/database-drivers'
const services = new ServiceContainer()
// Register connections
const mongo = createMongoConnection({ uri: '...', database: 'game' })
await mongo.connect()
services.register(MongoConnectionToken, mongo)
// Retrieve in other modules
const connection = services.get(MongoConnectionToken)
const users = connection.collection('users')
```
## Documentation
- [MongoDB Connection](/en/modules/database-drivers/mongo/) - MongoDB configuration details
- [Redis Connection](/en/modules/database-drivers/redis/) - Redis configuration details
- [Service Tokens](/en/modules/database-drivers/tokens/) - Dependency injection integration

View File

@@ -0,0 +1,265 @@
---
title: "MongoDB Connection"
description: "MongoDB connection management, connection pooling, auto-reconnect"
---
## Configuration Options
```typescript
interface MongoConnectionConfig {
/** MongoDB connection URI */
uri: string
/** Database name */
database: string
/** Connection pool configuration */
pool?: {
minSize?: number // Minimum connections
maxSize?: number // Maximum connections
acquireTimeout?: number // Connection acquire timeout (ms)
maxLifetime?: number // Maximum connection lifetime (ms)
}
/** Auto-reconnect (default true) */
autoReconnect?: boolean
/** Reconnect interval (ms, default 5000) */
reconnectInterval?: number
/** Maximum reconnect attempts (default 10) */
maxReconnectAttempts?: number
}
```
## Complete Example
```typescript
import { createMongoConnection, MongoConnectionToken } from '@esengine/database-drivers'
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game',
pool: {
minSize: 5,
maxSize: 20,
acquireTimeout: 5000,
maxLifetime: 300000
},
autoReconnect: true,
reconnectInterval: 5000,
maxReconnectAttempts: 10
})
// Event listeners
mongo.on('connected', () => {
console.log('MongoDB connected')
})
mongo.on('disconnected', () => {
console.log('MongoDB disconnected')
})
mongo.on('reconnecting', () => {
console.log('MongoDB reconnecting...')
})
mongo.on('reconnected', () => {
console.log('MongoDB reconnected')
})
mongo.on('error', (event) => {
console.error('MongoDB error:', event.error)
})
// Connect
await mongo.connect()
// Check status
console.log('Connected:', mongo.isConnected())
console.log('Ping:', await mongo.ping())
```
## IMongoConnection Interface
```typescript
interface IMongoConnection {
/** Connection ID */
readonly id: string
/** Connection state */
readonly state: ConnectionState
/** Establish connection */
connect(): Promise<void>
/** Disconnect */
disconnect(): Promise<void>
/** Check if connected */
isConnected(): boolean
/** Test connection */
ping(): Promise<boolean>
/** Get typed collection */
collection<T extends object>(name: string): IMongoCollection<T>
/** Get database interface */
getDatabase(): IMongoDatabase
/** Get native client (advanced usage) */
getNativeClient(): MongoClientType
/** Get native database (advanced usage) */
getNativeDatabase(): Db
}
```
## IMongoCollection Interface
Type-safe collection interface, decoupled from native MongoDB types:
```typescript
interface IMongoCollection<T extends object> {
readonly name: string
// Query
findOne(filter: object, options?: FindOptions): Promise<T | null>
find(filter: object, options?: FindOptions): Promise<T[]>
countDocuments(filter?: object): Promise<number>
// Insert
insertOne(doc: T): Promise<InsertOneResult>
insertMany(docs: T[]): Promise<InsertManyResult>
// Update
updateOne(filter: object, update: object): Promise<UpdateResult>
updateMany(filter: object, update: object): Promise<UpdateResult>
findOneAndUpdate(
filter: object,
update: object,
options?: FindOneAndUpdateOptions
): Promise<T | null>
// Delete
deleteOne(filter: object): Promise<DeleteResult>
deleteMany(filter: object): Promise<DeleteResult>
// Index
createIndex(
spec: Record<string, 1 | -1>,
options?: IndexOptions
): Promise<string>
}
```
## Usage Examples
### Basic CRUD
```typescript
interface User {
id: string
name: string
email: string
score: number
}
const users = mongo.collection<User>('users')
// Insert
await users.insertOne({
id: '1',
name: 'John',
email: 'john@example.com',
score: 100
})
// Query
const user = await users.findOne({ name: 'John' })
const topUsers = await users.find(
{ score: { $gte: 100 } },
{ sort: { score: -1 }, limit: 10 }
)
// Update
await users.updateOne(
{ id: '1' },
{ $inc: { score: 10 } }
)
// Delete
await users.deleteOne({ id: '1' })
```
### Batch Operations
```typescript
// Batch insert
await users.insertMany([
{ id: '1', name: 'Alice', email: 'alice@example.com', score: 100 },
{ id: '2', name: 'Bob', email: 'bob@example.com', score: 200 },
{ id: '3', name: 'Carol', email: 'carol@example.com', score: 150 }
])
// Batch update
await users.updateMany(
{ score: { $lt: 100 } },
{ $set: { status: 'inactive' } }
)
// Batch delete
await users.deleteMany({ status: 'inactive' })
```
### Index Management
```typescript
// Create indexes
await users.createIndex({ email: 1 }, { unique: true })
await users.createIndex({ score: -1 })
await users.createIndex({ name: 1, score: -1 })
```
## Integration with Other Modules
### With @esengine/database
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
import { UserRepository, createRepository } from '@esengine/database'
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
})
await mongo.connect()
// Use UserRepository
const userRepo = new UserRepository(mongo)
await userRepo.register({ username: 'john', password: '123456' })
// Use generic repository
const playerRepo = createRepository<Player>(mongo, 'players')
```
### With @esengine/transaction
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
import { createMongoStorage, TransactionManager } from '@esengine/transaction'
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
})
await mongo.connect()
// Create transaction storage (shared connection)
const storage = createMongoStorage(mongo)
await storage.ensureIndexes()
const txManager = new TransactionManager({ storage })
```

View File

@@ -0,0 +1,228 @@
---
title: "Redis Connection"
description: "Redis connection management, auto-reconnect, key prefix"
---
## Configuration Options
```typescript
interface RedisConnectionConfig {
/** Redis host */
host?: string
/** Redis port */
port?: number
/** Authentication password */
password?: string
/** Database number */
db?: number
/** Key prefix */
keyPrefix?: string
/** Auto-reconnect (default true) */
autoReconnect?: boolean
/** Reconnect interval (ms, default 5000) */
reconnectInterval?: number
/** Maximum reconnect attempts (default 10) */
maxReconnectAttempts?: number
}
```
## Complete Example
```typescript
import { createRedisConnection, RedisConnectionToken } from '@esengine/database-drivers'
const redis = createRedisConnection({
host: 'localhost',
port: 6379,
password: 'your-password',
db: 0,
keyPrefix: 'game:',
autoReconnect: true,
reconnectInterval: 5000,
maxReconnectAttempts: 10
})
// Event listeners
redis.on('connected', () => {
console.log('Redis connected')
})
redis.on('disconnected', () => {
console.log('Redis disconnected')
})
redis.on('error', (event) => {
console.error('Redis error:', event.error)
})
// Connect
await redis.connect()
// Check status
console.log('Connected:', redis.isConnected())
console.log('Ping:', await redis.ping())
```
## IRedisConnection Interface
```typescript
interface IRedisConnection {
/** Connection ID */
readonly id: string
/** Connection state */
readonly state: ConnectionState
/** Establish connection */
connect(): Promise<void>
/** Disconnect */
disconnect(): Promise<void>
/** Check if connected */
isConnected(): boolean
/** Test connection */
ping(): Promise<boolean>
/** Get value */
get(key: string): Promise<string | null>
/** Set value (optional TTL in seconds) */
set(key: string, value: string, ttl?: number): Promise<void>
/** Delete key */
del(key: string): Promise<boolean>
/** Check if key exists */
exists(key: string): Promise<boolean>
/** Set expiration (seconds) */
expire(key: string, seconds: number): Promise<boolean>
/** Get remaining TTL (seconds) */
ttl(key: string): Promise<number>
/** Get native client (advanced usage) */
getNativeClient(): Redis
}
```
## Usage Examples
### Basic Operations
```typescript
// Set value
await redis.set('user:1:name', 'John')
// Set value with expiration (1 hour)
await redis.set('session:abc123', 'user-data', 3600)
// Get value
const name = await redis.get('user:1:name')
// Check if key exists
const exists = await redis.exists('user:1:name')
// Delete key
await redis.del('user:1:name')
// Get remaining TTL
const ttl = await redis.ttl('session:abc123')
```
### Key Prefix
When `keyPrefix` is configured, all operations automatically add the prefix:
```typescript
const redis = createRedisConnection({
host: 'localhost',
keyPrefix: 'game:'
})
// Actual key is 'game:user:1'
await redis.set('user:1', 'data')
// Actual key queried is 'game:user:1'
const data = await redis.get('user:1')
```
### Advanced Operations
Use native client for advanced operations:
```typescript
const client = redis.getNativeClient()
// Using Pipeline
const pipeline = client.pipeline()
pipeline.set('key1', 'value1')
pipeline.set('key2', 'value2')
pipeline.set('key3', 'value3')
await pipeline.exec()
// Using Transactions
const multi = client.multi()
multi.incr('counter')
multi.get('counter')
const results = await multi.exec()
// Using Lua Scripts
const result = await client.eval(
`return redis.call('get', KEYS[1])`,
1,
'mykey'
)
```
## Integration with Transaction System
```typescript
import { createRedisConnection } from '@esengine/database-drivers'
import { RedisStorage, TransactionManager } from '@esengine/transaction'
const redis = createRedisConnection({
host: 'localhost',
port: 6379,
keyPrefix: 'tx:'
})
await redis.connect()
// Create transaction storage
const storage = new RedisStorage({
factory: () => redis.getNativeClient(),
prefix: 'tx:'
})
const txManager = new TransactionManager({ storage })
```
## Connection State
```typescript
type ConnectionState =
| 'disconnected' // Not connected
| 'connecting' // Connecting
| 'connected' // Connected
| 'disconnecting' // Disconnecting
| 'error' // Error state
```
## Events
| Event | Description |
|-------|-------------|
| `connected` | Connection established |
| `disconnected` | Connection closed |
| `reconnecting` | Reconnecting |
| `reconnected` | Reconnection successful |
| `error` | Error occurred |

View File

@@ -0,0 +1,217 @@
---
title: "Database Repository"
description: "Repository pattern database layer with CRUD, pagination, and soft delete"
---
`@esengine/database` is ESEngine's database operation layer, providing type-safe CRUD operations based on the Repository pattern.
## Features
- **Repository Pattern** - Generic CRUD operations with type safety
- **Pagination** - Built-in pagination support
- **Soft Delete** - Optional soft delete with restore
- **User Management** - Ready-to-use UserRepository
- **Password Security** - Secure password hashing with scrypt
## Installation
```bash
npm install @esengine/database @esengine/database-drivers
```
## Quick Start
### Basic Repository
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
import { Repository, createRepository } from '@esengine/database'
// Define entity
interface Player {
id: string
name: string
score: number
createdAt: Date
updatedAt: Date
}
// Create connection
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
})
await mongo.connect()
// Create repository
const playerRepo = createRepository<Player>(mongo, 'players')
// CRUD operations
const player = await playerRepo.create({
name: 'John',
score: 0
})
const found = await playerRepo.findById(player.id)
await playerRepo.update(player.id, { score: 100 })
await playerRepo.delete(player.id)
```
### Custom Repository
```typescript
import { Repository, BaseEntity } from '@esengine/database'
import type { IMongoConnection } from '@esengine/database-drivers'
interface Player extends BaseEntity {
name: string
score: number
rank?: string
}
class PlayerRepository extends Repository<Player> {
constructor(connection: IMongoConnection) {
super(connection, 'players')
}
async findTopPlayers(limit: number = 10): Promise<Player[]> {
return this.findMany({
sort: { score: 'desc' },
limit
})
}
async findByRank(rank: string): Promise<Player[]> {
return this.findMany({
where: { rank }
})
}
}
// Usage
const playerRepo = new PlayerRepository(mongo)
const topPlayers = await playerRepo.findTopPlayers(5)
```
### User Repository
```typescript
import { UserRepository } from '@esengine/database'
const userRepo = new UserRepository(mongo)
// Register new user
const user = await userRepo.register({
username: 'john',
password: 'securePassword123',
email: 'john@example.com'
})
// Authenticate
const authenticated = await userRepo.authenticate('john', 'securePassword123')
if (authenticated) {
console.log('Login successful:', authenticated.username)
}
// Change password
await userRepo.changePassword(user.id, 'securePassword123', 'newPassword456')
// Role management
await userRepo.addRole(user.id, 'admin')
await userRepo.removeRole(user.id, 'admin')
// Find users
const admins = await userRepo.findByRole('admin')
const john = await userRepo.findByUsername('john')
```
### Pagination
```typescript
const result = await playerRepo.findPaginated(
{ page: 1, pageSize: 20 },
{
where: { rank: 'gold' },
sort: { score: 'desc' }
}
)
console.log(result.data) // Player[]
console.log(result.total) // Total count
console.log(result.totalPages) // Total pages
console.log(result.hasNext) // Has next page
console.log(result.hasPrev) // Has previous page
```
### Soft Delete
```typescript
// Enable soft delete
const playerRepo = createRepository<Player>(mongo, 'players', true)
// Delete (marks as deleted)
await playerRepo.delete(playerId)
// Find excludes soft-deleted by default
const players = await playerRepo.findMany()
// Include soft-deleted records
const allPlayers = await playerRepo.findMany({
includeSoftDeleted: true
})
// Restore soft-deleted record
await playerRepo.restore(playerId)
```
### Query Options
```typescript
// Complex queries
const players = await playerRepo.findMany({
where: {
score: { $gte: 100 },
rank: { $in: ['gold', 'platinum'] },
name: { $like: 'John%' }
},
sort: {
score: 'desc',
name: 'asc'
},
limit: 10,
offset: 0
})
// OR conditions
const players = await playerRepo.findMany({
where: {
$or: [
{ score: { $gte: 1000 } },
{ rank: 'legendary' }
]
}
})
```
## Query Operators
| Operator | Description | Example |
|----------|-------------|---------|
| `$eq` | Equal | `{ score: { $eq: 100 } }` |
| `$ne` | Not equal | `{ status: { $ne: 'banned' } }` |
| `$gt` | Greater than | `{ score: { $gt: 50 } }` |
| `$gte` | Greater or equal | `{ level: { $gte: 10 } }` |
| `$lt` | Less than | `{ age: { $lt: 18 } }` |
| `$lte` | Less or equal | `{ price: { $lte: 100 } }` |
| `$in` | In array | `{ rank: { $in: ['gold', 'platinum'] } }` |
| `$nin` | Not in array | `{ status: { $nin: ['banned'] } }` |
| `$like` | Pattern match | `{ name: { $like: '%john%' } }` |
| `$regex` | Regex match | `{ email: { $regex: '@gmail.com$' } }` |
## Documentation
- [Repository API](/en/modules/database/repository/) - Repository detailed API
- [User Management](/en/modules/database/user/) - UserRepository usage
- [Query Syntax](/en/modules/database/query/) - Query condition syntax

View File

@@ -0,0 +1,185 @@
---
title: "Query Syntax"
description: "Query condition operators and syntax"
---
## Basic Queries
### Exact Match
```typescript
await repo.findMany({
where: {
name: 'John',
status: 'active'
}
})
```
### Using Operators
```typescript
await repo.findMany({
where: {
score: { $gte: 100 },
rank: { $in: ['gold', 'platinum'] }
}
})
```
## Query Operators
| Operator | Description | Example |
|----------|-------------|---------|
| `$eq` | Equal | `{ score: { $eq: 100 } }` |
| `$ne` | Not equal | `{ status: { $ne: 'banned' } }` |
| `$gt` | Greater than | `{ score: { $gt: 50 } }` |
| `$gte` | Greater than or equal | `{ level: { $gte: 10 } }` |
| `$lt` | Less than | `{ age: { $lt: 18 } }` |
| `$lte` | Less than or equal | `{ price: { $lte: 100 } }` |
| `$in` | In array | `{ rank: { $in: ['gold', 'platinum'] } }` |
| `$nin` | Not in array | `{ status: { $nin: ['banned', 'suspended'] } }` |
| `$like` | Pattern match | `{ name: { $like: '%john%' } }` |
| `$regex` | Regex match | `{ email: { $regex: '@gmail.com$' } }` |
## Logical Operators
### $or
```typescript
await repo.findMany({
where: {
$or: [
{ score: { $gte: 1000 } },
{ rank: 'legendary' }
]
}
})
```
### $and
```typescript
await repo.findMany({
where: {
$and: [
{ score: { $gte: 100 } },
{ score: { $lte: 500 } }
]
}
})
```
### Combined Usage
```typescript
await repo.findMany({
where: {
status: 'active',
$or: [
{ rank: 'gold' },
{ score: { $gte: 1000 } }
]
}
})
```
## Pattern Matching
### $like Syntax
- `%` - Matches any sequence of characters
- `_` - Matches single character
```typescript
// Starts with 'John'
{ name: { $like: 'John%' } }
// Ends with 'son'
{ name: { $like: '%son' } }
// Contains 'oh'
{ name: { $like: '%oh%' } }
// Second character is 'o'
{ name: { $like: '_o%' } }
```
### $regex Syntax
Uses standard regular expressions:
```typescript
// Starts with 'John' (case insensitive)
{ name: { $regex: '^john' } }
// Gmail email
{ email: { $regex: '@gmail\\.com$' } }
// Contains numbers
{ username: { $regex: '\\d+' } }
```
## Sorting
```typescript
await repo.findMany({
sort: {
score: 'desc', // Descending
name: 'asc' // Ascending
}
})
```
## Pagination
### Using limit/offset
```typescript
// First page
await repo.findMany({
limit: 20,
offset: 0
})
// Second page
await repo.findMany({
limit: 20,
offset: 20
})
```
### Using findPaginated
```typescript
const result = await repo.findPaginated(
{ page: 2, pageSize: 20 },
{ sort: { createdAt: 'desc' } }
)
```
## Complete Examples
```typescript
// Find active gold players with scores between 100-1000
// Sort by score descending, get top 10
const players = await repo.findMany({
where: {
status: 'active',
rank: 'gold',
score: { $gte: 100, $lte: 1000 }
},
sort: { score: 'desc' },
limit: 10
})
// Search for users with 'john' in username or gmail email
const users = await repo.findMany({
where: {
$or: [
{ username: { $like: '%john%' } },
{ email: { $regex: '@gmail\\.com$' } }
]
}
})
```

View File

@@ -0,0 +1,244 @@
---
title: "Repository API"
description: "Generic repository interface, CRUD operations, pagination, soft delete"
---
## Creating a Repository
### Using Factory Function
```typescript
import { createRepository } from '@esengine/database'
const playerRepo = createRepository<Player>(mongo, 'players')
// Enable soft delete
const playerRepo = createRepository<Player>(mongo, 'players', true)
```
### Extending Repository
```typescript
import { Repository, BaseEntity } from '@esengine/database'
interface Player extends BaseEntity {
name: string
score: number
}
class PlayerRepository extends Repository<Player> {
constructor(connection: IMongoConnection) {
super(connection, 'players', false) // Third param: enable soft delete
}
// Add custom methods
async findTopPlayers(limit: number): Promise<Player[]> {
return this.findMany({
sort: { score: 'desc' },
limit
})
}
}
```
## BaseEntity Interface
All entities must extend `BaseEntity`:
```typescript
interface BaseEntity {
id: string
createdAt: Date
updatedAt: Date
deletedAt?: Date // Used for soft delete
}
```
## Query Methods
### findById
```typescript
const player = await repo.findById('player-123')
```
### findOne
```typescript
const player = await repo.findOne({
where: { name: 'John' }
})
const topPlayer = await repo.findOne({
sort: { score: 'desc' }
})
```
### findMany
```typescript
// Simple query
const players = await repo.findMany({
where: { rank: 'gold' }
})
// Complex query
const players = await repo.findMany({
where: {
score: { $gte: 100 },
rank: { $in: ['gold', 'platinum'] }
},
sort: { score: 'desc', name: 'asc' },
limit: 10,
offset: 0
})
```
### findPaginated
```typescript
const result = await repo.findPaginated(
{ page: 1, pageSize: 20 },
{
where: { rank: 'gold' },
sort: { score: 'desc' }
}
)
console.log(result.data) // Player[]
console.log(result.total) // Total count
console.log(result.totalPages) // Total pages
console.log(result.hasNext) // Has next page
console.log(result.hasPrev) // Has previous page
```
### count
```typescript
const count = await repo.count({
where: { rank: 'gold' }
})
```
### exists
```typescript
const exists = await repo.exists({
where: { email: 'john@example.com' }
})
```
## Create Methods
### create
```typescript
const player = await repo.create({
name: 'John',
score: 0
})
// Automatically generates id, createdAt, updatedAt
```
### createMany
```typescript
const players = await repo.createMany([
{ name: 'Alice', score: 100 },
{ name: 'Bob', score: 200 },
{ name: 'Carol', score: 150 }
])
```
## Update Methods
### update
```typescript
const updated = await repo.update('player-123', {
score: 200,
rank: 'gold'
})
// Automatically updates updatedAt
```
## Delete Methods
### delete
```typescript
// Hard delete
await repo.delete('player-123')
// Soft delete (if enabled)
// Actually sets the deletedAt field
```
### deleteMany
```typescript
const count = await repo.deleteMany({
where: { score: { $lt: 10 } }
})
```
## Soft Delete
### Enabling Soft Delete
```typescript
const repo = createRepository<Player>(mongo, 'players', true)
```
### Query Behavior
```typescript
// Excludes soft-deleted records by default
const players = await repo.findMany()
// Include soft-deleted records
const allPlayers = await repo.findMany({
includeSoftDeleted: true
})
```
### Restore Records
```typescript
await repo.restore('player-123')
```
## QueryOptions
```typescript
interface QueryOptions<T> {
/** Query conditions */
where?: WhereCondition<T>
/** Sorting */
sort?: Partial<Record<keyof T, 'asc' | 'desc'>>
/** Limit count */
limit?: number
/** Offset */
offset?: number
/** Include soft-deleted records (only when soft delete is enabled) */
includeSoftDeleted?: boolean
}
```
## PaginatedResult
```typescript
interface PaginatedResult<T> {
data: T[]
total: number
page: number
pageSize: number
totalPages: number
hasNext: boolean
hasPrev: boolean
}
```

View File

@@ -0,0 +1,277 @@
---
title: "User Management"
description: "UserRepository for user registration, authentication, and role management"
---
## Overview
`UserRepository` provides out-of-the-box user management features:
- User registration and authentication
- Password hashing (using scrypt)
- Role management
- Account status management
## Quick Start
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
import { UserRepository } from '@esengine/database'
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
})
await mongo.connect()
const userRepo = new UserRepository(mongo)
```
## User Registration
```typescript
const user = await userRepo.register({
username: 'john',
password: 'securePassword123',
email: 'john@example.com', // Optional
displayName: 'John Doe', // Optional
roles: ['player'] // Optional, defaults to []
})
console.log(user)
// {
// id: 'uuid-...',
// username: 'john',
// email: 'john@example.com',
// displayName: 'John Doe',
// roles: ['player'],
// status: 'active',
// createdAt: Date,
// updatedAt: Date
// }
```
**Note**: `register` returns a `SafeUser` which excludes the password hash.
## User Authentication
```typescript
const user = await userRepo.authenticate('john', 'securePassword123')
if (user) {
console.log('Login successful:', user.username)
} else {
console.log('Invalid username or password')
}
```
## Password Management
### Change Password
```typescript
const success = await userRepo.changePassword(
userId,
'oldPassword123',
'newPassword456'
)
if (success) {
console.log('Password changed successfully')
} else {
console.log('Invalid current password')
}
```
### Reset Password
```typescript
// Admin directly resets password
const success = await userRepo.resetPassword(userId, 'newPassword123')
```
## Role Management
### Add Role
```typescript
await userRepo.addRole(userId, 'admin')
await userRepo.addRole(userId, 'moderator')
```
### Remove Role
```typescript
await userRepo.removeRole(userId, 'moderator')
```
### Query Roles
```typescript
// Find all admins
const admins = await userRepo.findByRole('admin')
// Check if user has a role
const user = await userRepo.findById(userId)
const isAdmin = user?.roles.includes('admin')
```
## Querying Users
### Find by Username
```typescript
const user = await userRepo.findByUsername('john')
```
### Find by Email
```typescript
const user = await userRepo.findByEmail('john@example.com')
```
### Find by Role
```typescript
const admins = await userRepo.findByRole('admin')
```
### Using Inherited Methods
```typescript
// Paginated query
const result = await userRepo.findPaginated(
{ page: 1, pageSize: 20 },
{
where: { status: 'active' },
sort: { createdAt: 'desc' }
}
)
// Complex query
const users = await userRepo.findMany({
where: {
status: 'active',
roles: { $in: ['admin', 'moderator'] }
}
})
```
## Account Status
```typescript
type UserStatus = 'active' | 'inactive' | 'banned' | 'suspended'
```
### Update Status
```typescript
await userRepo.update(userId, { status: 'banned' })
```
### Query by Status
```typescript
const activeUsers = await userRepo.findMany({
where: { status: 'active' }
})
const bannedUsers = await userRepo.findMany({
where: { status: 'banned' }
})
```
## Type Definitions
### UserEntity
```typescript
interface UserEntity extends BaseEntity {
username: string
passwordHash: string
email?: string
displayName?: string
roles: string[]
status: UserStatus
lastLoginAt?: Date
}
```
### SafeUser
```typescript
type SafeUser = Omit<UserEntity, 'passwordHash'>
```
### CreateUserParams
```typescript
interface CreateUserParams {
username: string
password: string
email?: string
displayName?: string
roles?: string[]
}
```
## Password Utilities
Standalone password utility functions:
```typescript
import { hashPassword, verifyPassword } from '@esengine/database'
// Hash password
const hash = await hashPassword('myPassword123')
// Verify password
const isValid = await verifyPassword('myPassword123', hash)
```
### Security Notes
- Uses Node.js built-in `scrypt` algorithm
- Automatically generates random salt
- Uses secure iteration parameters by default
- Hash format: `salt:hash` (both hex encoded)
## Extending UserRepository
```typescript
import { UserRepository, UserEntity } from '@esengine/database'
interface GameUser extends UserEntity {
level: number
experience: number
coins: number
}
class GameUserRepository extends UserRepository {
// Override collection name
constructor(connection: IMongoConnection) {
super(connection, 'game_users')
}
// Add game-related methods
async addExperience(userId: string, amount: number): Promise<GameUser | null> {
const user = await this.findById(userId) as GameUser | null
if (!user) return null
const newExp = user.experience + amount
const newLevel = Math.floor(newExp / 1000) + 1
return this.update(userId, {
experience: newExp,
level: newLevel
}) as Promise<GameUser | null>
}
async findTopPlayers(limit: number = 10): Promise<GameUser[]> {
return this.findMany({
sort: { level: 'desc', experience: 'desc' },
limit
}) as Promise<GameUser[]>
}
}
```

View File

@@ -36,6 +36,13 @@ ESEngine provides a rich set of modules that can be imported as needed.
| [Network](/en/modules/network/) | `@esengine/network` | Multiplayer game networking |
| [Transaction](/en/modules/transaction/) | `@esengine/transaction` | Game transactions with distributed support |
### Database
| Module | Package | Description |
|--------|---------|-------------|
| [Database Drivers](/en/modules/database-drivers/) | `@esengine/database-drivers` | MongoDB, Redis connection management |
| [Database Repository](/en/modules/database/) | `@esengine/database` | Repository pattern data operations |
## Installation
All modules can be installed independently:

View File

@@ -92,6 +92,355 @@ const token = jwtProvider.sign({
const payload = jwtProvider.decode(token)
```
### Custom Provider
You can create custom authentication providers by implementing the `IAuthProvider` interface to integrate with any authentication system (OAuth, LDAP, custom database auth, etc.).
#### IAuthProvider Interface
```typescript
interface IAuthProvider<TUser = unknown, TCredentials = unknown> {
/** Provider name */
readonly name: string;
/** Verify credentials */
verify(credentials: TCredentials): Promise<AuthResult<TUser>>;
/** Refresh token (optional) */
refresh?(token: string): Promise<AuthResult<TUser>>;
/** Revoke token (optional) */
revoke?(token: string): Promise<boolean>;
}
interface AuthResult<TUser> {
success: boolean;
user?: TUser;
error?: string;
errorCode?: AuthErrorCode;
token?: string;
expiresAt?: number;
}
type AuthErrorCode =
| 'INVALID_CREDENTIALS'
| 'EXPIRED_TOKEN'
| 'INVALID_TOKEN'
| 'USER_NOT_FOUND'
| 'ACCOUNT_DISABLED'
| 'RATE_LIMITED'
| 'INSUFFICIENT_PERMISSIONS';
```
#### Custom Provider Examples
**Example 1: Database Password Authentication**
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface User {
id: string
username: string
roles: string[]
}
interface PasswordCredentials {
username: string
password: string
}
class DatabaseAuthProvider implements IAuthProvider<User, PasswordCredentials> {
readonly name = 'database'
async verify(credentials: PasswordCredentials): Promise<AuthResult<User>> {
const { username, password } = credentials
// Query user from database
const user = await db.users.findByUsername(username)
if (!user) {
return {
success: false,
error: 'User not found',
errorCode: 'USER_NOT_FOUND'
}
}
// Verify password (using bcrypt or similar)
const isValid = await bcrypt.compare(password, user.passwordHash)
if (!isValid) {
return {
success: false,
error: 'Invalid password',
errorCode: 'INVALID_CREDENTIALS'
}
}
// Check account status
if (user.disabled) {
return {
success: false,
error: 'Account is disabled',
errorCode: 'ACCOUNT_DISABLED'
}
}
return {
success: true,
user: {
id: user.id,
username: user.username,
roles: user.roles
}
}
}
}
```
**Example 2: OAuth/Third-party Authentication**
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface OAuthUser {
id: string
email: string
name: string
provider: string
roles: string[]
}
interface OAuthCredentials {
provider: 'google' | 'github' | 'discord'
accessToken: string
}
class OAuthProvider implements IAuthProvider<OAuthUser, OAuthCredentials> {
readonly name = 'oauth'
async verify(credentials: OAuthCredentials): Promise<AuthResult<OAuthUser>> {
const { provider, accessToken } = credentials
try {
// Verify token with provider
const profile = await this.fetchUserProfile(provider, accessToken)
// Find or create local user
let user = await db.users.findByOAuth(provider, profile.id)
if (!user) {
user = await db.users.create({
oauthProvider: provider,
oauthId: profile.id,
email: profile.email,
name: profile.name,
roles: ['player']
})
}
return {
success: true,
user: {
id: user.id,
email: user.email,
name: user.name,
provider,
roles: user.roles
}
}
} catch (error) {
return {
success: false,
error: 'OAuth verification failed',
errorCode: 'INVALID_TOKEN'
}
}
}
private async fetchUserProfile(provider: string, token: string) {
switch (provider) {
case 'google':
return fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json())
case 'github':
return fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json())
// Other providers...
default:
throw new Error(`Unsupported provider: ${provider}`)
}
}
}
```
**Example 3: API Key Authentication**
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface ApiUser {
id: string
name: string
roles: string[]
rateLimit: number
}
class ApiKeyAuthProvider implements IAuthProvider<ApiUser, string> {
readonly name = 'api-key'
private revokedKeys = new Set<string>()
async verify(apiKey: string): Promise<AuthResult<ApiUser>> {
if (!apiKey || !apiKey.startsWith('sk_')) {
return {
success: false,
error: 'Invalid API Key format',
errorCode: 'INVALID_TOKEN'
}
}
if (this.revokedKeys.has(apiKey)) {
return {
success: false,
error: 'API Key has been revoked',
errorCode: 'INVALID_TOKEN'
}
}
// Query API Key from database
const keyData = await db.apiKeys.findByKey(apiKey)
if (!keyData) {
return {
success: false,
error: 'API Key not found',
errorCode: 'INVALID_CREDENTIALS'
}
}
// Check expiration
if (keyData.expiresAt && keyData.expiresAt < Date.now()) {
return {
success: false,
error: 'API Key has expired',
errorCode: 'EXPIRED_TOKEN'
}
}
return {
success: true,
user: {
id: keyData.userId,
name: keyData.name,
roles: keyData.roles,
rateLimit: keyData.rateLimit
},
expiresAt: keyData.expiresAt
}
}
async revoke(apiKey: string): Promise<boolean> {
this.revokedKeys.add(apiKey)
await db.apiKeys.revoke(apiKey)
return true
}
}
```
#### Using Custom Providers
```typescript
import { createServer } from '@esengine/server'
import { withAuth } from '@esengine/server/auth'
// Create custom provider
const dbAuthProvider = new DatabaseAuthProvider()
// Or use OAuth provider
const oauthProvider = new OAuthProvider()
// Use custom provider
const server = withAuth(await createServer({ port: 3000 }), {
provider: dbAuthProvider, // or oauthProvider
// Extract credentials from WebSocket connection request
extractCredentials: (req) => {
const url = new URL(req.url, 'http://localhost')
// For database auth: get from query params
const username = url.searchParams.get('username')
const password = url.searchParams.get('password')
if (username && password) {
return { username, password }
}
// For OAuth: get from token param
const provider = url.searchParams.get('provider')
const accessToken = url.searchParams.get('access_token')
if (provider && accessToken) {
return { provider, accessToken }
}
// For API Key: get from header
const apiKey = req.headers['x-api-key']
if (apiKey) {
return apiKey as string
}
return null
},
onAuthFailure: (conn, error) => {
console.log(`Auth failed: ${error.errorCode} - ${error.error}`)
}
})
await server.start()
```
#### Combining Multiple Providers
You can create a composite provider to support multiple authentication methods:
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface MultiAuthCredentials {
type: 'jwt' | 'oauth' | 'apikey' | 'password'
data: unknown
}
class MultiAuthProvider implements IAuthProvider<User, MultiAuthCredentials> {
readonly name = 'multi'
constructor(
private jwtProvider: JwtAuthProvider<User>,
private oauthProvider: OAuthProvider,
private apiKeyProvider: ApiKeyAuthProvider,
private dbProvider: DatabaseAuthProvider
) {}
async verify(credentials: MultiAuthCredentials): Promise<AuthResult<User>> {
switch (credentials.type) {
case 'jwt':
return this.jwtProvider.verify(credentials.data as string)
case 'oauth':
return this.oauthProvider.verify(credentials.data as OAuthCredentials)
case 'apikey':
return this.apiKeyProvider.verify(credentials.data as string)
case 'password':
return this.dbProvider.verify(credentials.data as PasswordCredentials)
default:
return {
success: false,
error: 'Unsupported authentication type',
errorCode: 'INVALID_CREDENTIALS'
}
}
}
}
```
### Session Provider
Use server-side sessions for stateful authentication:

View File

@@ -0,0 +1,441 @@
---
title: "Distributed Rooms"
description: "Multi-server room management with DistributedRoomManager"
---
## Overview
Distributed room support allows multiple server instances to share a room registry, enabling cross-server player routing and failover.
```
┌─────────────────────────────────────────────────────────┐
│ Server A Server B Server C │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Room 1 │ │ Room 3 │ │ Room 5 │ │
│ │ Room 2 │ │ Room 4 │ │ Room 6 │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ IDistributedAdapter │ │
│ │ (Redis / Memory) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## Quick Start
### Single Server Mode (Testing)
```typescript
import {
DistributedRoomManager,
MemoryAdapter,
Room
} from '@esengine/server';
// Define room type
class GameRoom extends Room {
maxPlayers = 4;
}
// Create adapter and manager
const adapter = new MemoryAdapter();
const manager = new DistributedRoomManager(adapter, {
serverId: 'server-1',
serverAddress: 'localhost',
serverPort: 3000
}, (conn, type, data) => conn.send(JSON.stringify({ type, data })));
// Register room type
manager.define('game', GameRoom);
// Start manager
await manager.start();
// Distributed join/create room
const result = await manager.joinOrCreateDistributed('game', 'player-1', conn);
if ('redirect' in result) {
// Player should connect to another server
console.log(`Redirect to: ${result.redirect}`);
} else {
// Player joined local room
const { room, player } = result;
}
// Graceful shutdown
await manager.stop(true);
```
### Multi-Server Mode (Production)
```typescript
import Redis from 'ioredis';
import { DistributedRoomManager, RedisAdapter } from '@esengine/server';
const adapter = new RedisAdapter({
factory: () => new Redis({
host: 'redis.example.com',
port: 6379
}),
prefix: 'game:',
serverTtl: 30,
snapshotTtl: 86400
});
const manager = new DistributedRoomManager(adapter, {
serverId: process.env.SERVER_ID,
serverAddress: process.env.PUBLIC_IP,
serverPort: 3000,
heartbeatInterval: 5000,
snapshotInterval: 30000,
enableFailover: true,
capacity: 100
}, sendFn);
```
## DistributedRoomManager
### Configuration Options
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `serverId` | `string` | required | Unique server identifier |
| `serverAddress` | `string` | required | Public address for client connections |
| `serverPort` | `number` | required | Server port |
| `heartbeatInterval` | `number` | `5000` | Heartbeat interval (ms) |
| `snapshotInterval` | `number` | `30000` | State snapshot interval, 0 to disable |
| `migrationTimeout` | `number` | `10000` | Room migration timeout |
| `enableFailover` | `boolean` | `true` | Enable automatic failover |
| `capacity` | `number` | `100` | Max rooms on this server |
### Lifecycle Methods
#### start()
Start the distributed room manager. Connects to adapter, registers server, starts heartbeat.
```typescript
await manager.start();
```
#### stop(graceful?)
Stop the manager. If `graceful=true`, marks server as draining and saves all room snapshots.
```typescript
await manager.stop(true);
```
### Routing Methods
#### joinOrCreateDistributed()
Join or create a room with distributed awareness. Returns `{ room, player }` for local rooms or `{ redirect: string }` for remote rooms.
```typescript
const result = await manager.joinOrCreateDistributed('game', 'player-1', conn);
if ('redirect' in result) {
// Client should redirect to another server
res.json({ redirect: result.redirect });
} else {
// Player joined local room
const { room, player } = result;
}
```
#### route()
Route a player to the appropriate room/server.
```typescript
const result = await manager.route({
roomType: 'game',
playerId: 'p1'
});
switch (result.type) {
case 'local': // Room is on this server
break;
case 'redirect': // Room is on another server
// result.serverAddress contains target server
break;
case 'create': // No room exists, need to create
break;
case 'unavailable': // Cannot find or create room
// result.reason contains error message
break;
}
```
### State Management
#### saveSnapshot()
Manually save a room's state snapshot.
```typescript
await manager.saveSnapshot(roomId);
```
#### restoreFromSnapshot()
Restore a room from its saved snapshot.
```typescript
const success = await manager.restoreFromSnapshot(roomId);
```
### Query Methods
#### getServers()
Get all online servers.
```typescript
const servers = await manager.getServers();
```
#### queryDistributedRooms()
Query rooms across all servers.
```typescript
const rooms = await manager.queryDistributedRooms({
roomType: 'game',
hasSpace: true,
notLocked: true
});
```
## IDistributedAdapter
Interface for distributed backends. Implement this to add support for Redis, message queues, etc.
### Built-in Adapters
#### MemoryAdapter
In-memory implementation for testing and single-server mode.
```typescript
const adapter = new MemoryAdapter({
serverTtl: 15000, // Server offline after no heartbeat (ms)
enableTtlCheck: true, // Enable automatic TTL checking
ttlCheckInterval: 5000 // TTL check interval (ms)
});
```
#### RedisAdapter
Redis-based implementation for production multi-server deployments.
```typescript
import Redis from 'ioredis';
import { RedisAdapter } from '@esengine/server';
const adapter = new RedisAdapter({
factory: () => new Redis('redis://localhost:6379'),
prefix: 'game:', // Key prefix (default: 'dist:')
serverTtl: 30, // Server TTL in seconds (default: 30)
roomTtl: 0, // Room TTL, 0 = never expire (default: 0)
snapshotTtl: 86400, // Snapshot TTL in seconds (default: 24h)
channel: 'game:events' // Pub/Sub channel (default: 'distributed:events')
});
```
**RedisAdapter Configuration:**
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `factory` | `() => RedisClient` | required | Redis client factory (lazy connection) |
| `prefix` | `string` | `'dist:'` | Key prefix for all Redis keys |
| `serverTtl` | `number` | `30` | Server TTL in seconds |
| `roomTtl` | `number` | `0` | Room TTL in seconds, 0 = no expiry |
| `snapshotTtl` | `number` | `86400` | Snapshot TTL in seconds |
| `channel` | `string` | `'distributed:events'` | Pub/Sub channel name |
**Features:**
- Server registry with automatic heartbeat TTL
- Room registry with cross-server lookup
- State snapshots with configurable TTL
- Pub/Sub for cross-server events
- Distributed locks using Redis SET NX
### Custom Adapters
```typescript
import type { IDistributedAdapter } from '@esengine/server';
class MyAdapter implements IDistributedAdapter {
// Lifecycle
async connect(): Promise<void> { }
async disconnect(): Promise<void> { }
isConnected(): boolean { return true; }
// Server Registry
async registerServer(server: ServerRegistration): Promise<void> { }
async unregisterServer(serverId: string): Promise<void> { }
async heartbeat(serverId: string): Promise<void> { }
async getServers(): Promise<ServerRegistration[]> { return []; }
// Room Registry
async registerRoom(room: RoomRegistration): Promise<void> { }
async unregisterRoom(roomId: string): Promise<void> { }
async queryRooms(query: RoomQuery): Promise<RoomRegistration[]> { return []; }
async findAvailableRoom(roomType: string): Promise<RoomRegistration | null> { return null; }
// State Snapshots
async saveSnapshot(snapshot: RoomSnapshot): Promise<void> { }
async loadSnapshot(roomId: string): Promise<RoomSnapshot | null> { return null; }
// Pub/Sub
async publish(event: DistributedEvent): Promise<void> { }
async subscribe(pattern: string, handler: Function): Promise<() => void> { return () => {}; }
// Distributed Locks
async acquireLock(key: string, ttlMs: number): Promise<boolean> { return true; }
async releaseLock(key: string): Promise<void> { }
}
```
## Player Routing Flow
```
Client Server A Server B
│ │ │
│─── joinOrCreate ────────►│ │
│ │ │
│ │── findAvailableRoom() ───►│
│ │◄──── room on Server B ────│
│ │ │
│◄─── redirect: B:3001 ────│ │
│ │ │
│───────────────── connect to Server B ───────────────►│
│ │ │
│◄─────────────────────────────── joined ─────────────│
```
## Event Types
The distributed system publishes these events:
| Event | Description |
|-------|-------------|
| `server:online` | Server came online |
| `server:offline` | Server went offline |
| `server:draining` | Server is draining |
| `room:created` | Room was created |
| `room:disposed` | Room was disposed |
| `room:updated` | Room info updated |
| `room:message` | Cross-server room message |
| `room:migrated` | Room migrated to another server |
| `player:joined` | Player joined room |
| `player:left` | Player left room |
## Best Practices
1. **Use Unique Server IDs** - Use hostname, container ID, or UUID
2. **Configure Proper Heartbeat** - Balance between freshness and network overhead
3. **Enable Snapshots for Stateful Rooms** - Ensure room state survives server restarts
4. **Handle Redirects Gracefully** - Client should reconnect to target server
```typescript
// Client handling redirect
if (response.redirect) {
await client.disconnect();
await client.connect(response.redirect);
await client.joinRoom(roomId);
}
```
5. **Use Distributed Locks** - Prevent race conditions in joinOrCreate
## Using createServer Integration
The simplest way to use distributed rooms is through `createServer`'s `distributed` config:
```typescript
import { createServer } from '@esengine/server';
import { RedisAdapter, Room } from '@esengine/server';
import Redis from 'ioredis';
class GameRoom extends Room {
maxPlayers = 4;
}
const server = await createServer({
port: 3000,
distributed: {
enabled: true,
adapter: new RedisAdapter({ factory: () => new Redis() }),
serverId: 'server-1',
serverAddress: 'ws://192.168.1.100',
serverPort: 3000,
enableFailover: true,
capacity: 100
}
});
server.define('game', GameRoom);
await server.start();
```
When clients call the `JoinRoom` API, the server will automatically:
1. Find available rooms (local or remote)
2. If room is on another server, send `$redirect` message to client
3. Client receives redirect and connects to target server
## Load Balancing
Use `LoadBalancedRouter` for server selection:
```typescript
import { LoadBalancedRouter, createLoadBalancedRouter } from '@esengine/server';
// Using factory function
const router = createLoadBalancedRouter('least-players');
// Or create directly
const router = new LoadBalancedRouter({
strategy: 'least-rooms', // Select server with fewest rooms
preferLocal: true // Prefer local server
});
// Available strategies
// - 'round-robin': Round robin selection
// - 'least-rooms': Fewest rooms
// - 'least-players': Fewest players
// - 'random': Random selection
// - 'weighted': Weighted by capacity usage
```
## Failover
When a server goes offline with `enableFailover` enabled, the system will automatically:
1. Detect server offline (via heartbeat timeout)
2. Query all rooms on that server
3. Use distributed lock to prevent multiple servers recovering same room
4. Restore room state from snapshot
5. Publish `room:migrated` event to notify other servers
```typescript
// Ensure periodic snapshots
const manager = new DistributedRoomManager(adapter, {
serverId: 'server-1',
serverAddress: 'localhost',
serverPort: 3000,
snapshotInterval: 30000, // Save snapshot every 30 seconds
enableFailover: true // Enable failover
}, sendFn);
```
## Future Releases
- Redis Cluster support
- More load balancing strategies (geo-location, latency-aware)

View File

@@ -0,0 +1,679 @@
---
title: "HTTP Routing"
description: "HTTP REST API routing with WebSocket port sharing support"
---
`@esengine/server` includes a lightweight HTTP routing feature that can share the same port with WebSocket services, making it easy to implement REST APIs.
## Quick Start
### Inline Route Definition
The simplest way is to define HTTP routes directly when creating the server:
```typescript
import { createServer } from '@esengine/server'
const server = await createServer({
port: 3000,
http: {
'/api/health': (req, res) => {
res.json({ status: 'ok', time: Date.now() })
},
'/api/users': {
GET: (req, res) => {
res.json({ users: [] })
},
POST: async (req, res) => {
const body = req.body as { name: string }
res.status(201).json({ id: '1', name: body.name })
}
}
},
cors: true // Enable CORS
})
await server.start()
```
### File-based Routing
For larger projects, file-based routing is recommended. Create a `src/http` directory where each file corresponds to a route:
```typescript
// src/http/login.ts
import { defineHttp } from '@esengine/server'
interface LoginBody {
username: string
password: string
}
export default defineHttp<LoginBody>({
method: 'POST',
handler(req, res) {
const { username, password } = req.body as LoginBody
// Validate user...
if (username === 'admin' && password === '123456') {
res.json({ token: 'jwt-token-here', userId: 'user-1' })
} else {
res.error(401, 'Invalid username or password')
}
}
})
```
```typescript
// server.ts
import { createServer } from '@esengine/server'
const server = await createServer({
port: 3000,
httpDir: './src/http', // HTTP routes directory
httpPrefix: '/api', // Route prefix
cors: true
})
await server.start()
// Route: POST /api/login
```
## defineHttp Definition
`defineHttp` is used to define type-safe HTTP handlers:
```typescript
import { defineHttp } from '@esengine/server'
interface CreateUserBody {
username: string
email: string
password: string
}
export default defineHttp<CreateUserBody>({
// HTTP method (default POST)
method: 'POST',
// Handler function
handler(req, res) {
const body = req.body as CreateUserBody
// Handle request...
res.status(201).json({ id: 'new-user-id' })
}
})
```
### Supported HTTP Methods
```typescript
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS'
```
## HttpRequest Object
The HTTP request object contains the following properties:
```typescript
interface HttpRequest {
/** Raw Node.js IncomingMessage */
raw: IncomingMessage
/** HTTP method */
method: string
/** Request path */
path: string
/** Route parameters (extracted from URL path, e.g., /users/:id) */
params: Record<string, string>
/** Query parameters */
query: Record<string, string>
/** Request headers */
headers: Record<string, string | string[] | undefined>
/** Parsed request body */
body: unknown
/** Client IP */
ip: string
}
```
### Usage Examples
```typescript
export default defineHttp({
method: 'GET',
handler(req, res) {
// Get query parameters
const page = parseInt(req.query.page ?? '1')
const limit = parseInt(req.query.limit ?? '10')
// Get request headers
const authHeader = req.headers.authorization
// Get client IP
console.log('Request from:', req.ip)
res.json({ page, limit })
}
})
```
### Body Parsing
The request body is automatically parsed based on `Content-Type`:
- `application/json` - Parsed as JSON object
- `application/x-www-form-urlencoded` - Parsed as key-value object
- Others - Kept as raw string
```typescript
export default defineHttp<{ name: string; age: number }>({
method: 'POST',
handler(req, res) {
// body is already parsed
const { name, age } = req.body as { name: string; age: number }
res.json({ received: { name, age } })
}
})
```
## HttpResponse Object
The HTTP response object provides a chainable API:
```typescript
interface HttpResponse {
/** Raw Node.js ServerResponse */
raw: ServerResponse
/** Set status code */
status(code: number): HttpResponse
/** Set response header */
header(name: string, value: string): HttpResponse
/** Send JSON response */
json(data: unknown): void
/** Send text response */
text(data: string): void
/** Send error response */
error(code: number, message: string): void
}
```
### Usage Examples
```typescript
export default defineHttp({
method: 'POST',
handler(req, res) {
// Set status code and custom headers
res
.status(201)
.header('X-Custom-Header', 'value')
.json({ created: true })
}
})
```
```typescript
export default defineHttp({
method: 'GET',
handler(req, res) {
// Send error response
res.error(404, 'Resource not found')
// Equivalent to: res.status(404).json({ error: 'Resource not found' })
}
})
```
```typescript
export default defineHttp({
method: 'GET',
handler(req, res) {
// Send plain text
res.text('Hello, World!')
}
})
```
## File Routing Conventions
### Name Conversion
File names are automatically converted to route paths:
| File Path | Route Path (prefix=/api) |
|-----------|-------------------------|
| `login.ts` | `/api/login` |
| `users/profile.ts` | `/api/users/profile` |
| `users/[id].ts` | `/api/users/:id` |
| `game/room/[roomId].ts` | `/api/game/room/:roomId` |
### Dynamic Route Parameters
Use `[param]` syntax to define dynamic parameters:
```typescript
// src/http/users/[id].ts
import { defineHttp } from '@esengine/server'
export default defineHttp({
method: 'GET',
handler(req, res) {
// Get route parameter directly from params
const { id } = req.params
res.json({ userId: id })
}
})
```
Multiple parameters:
```typescript
// src/http/users/[userId]/posts/[postId].ts
import { defineHttp } from '@esengine/server'
export default defineHttp({
method: 'GET',
handler(req, res) {
const { userId, postId } = req.params
res.json({ userId, postId })
}
})
```
### Skip Rules
The following files are automatically skipped:
- Files starting with `_` (e.g., `_helper.ts`)
- `index.ts` / `index.js` files
- Non `.ts` / `.js` / `.mts` / `.mjs` files
### Directory Structure Example
```
src/
└── http/
├── _utils.ts # Skipped (underscore prefix)
├── index.ts # Skipped (index file)
├── health.ts # GET /api/health
├── login.ts # POST /api/login
├── register.ts # POST /api/register
└── users/
├── index.ts # Skipped
├── list.ts # GET /api/users/list
└── [id].ts # GET /api/users/:id
```
## CORS Configuration
### Quick Enable
```typescript
const server = await createServer({
port: 3000,
cors: true // Use default configuration
})
```
### Custom Configuration
```typescript
const server = await createServer({
port: 3000,
cors: {
// Allowed origins
origin: ['http://localhost:5173', 'https://myapp.com'],
// Or use wildcard
// origin: '*',
// origin: true, // Reflect request origin
// Allowed HTTP methods
methods: ['GET', 'POST', 'PUT', 'DELETE'],
// Allowed request headers
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
// Allow credentials (cookies)
credentials: true,
// Preflight cache max age (seconds)
maxAge: 86400
}
})
```
### CorsOptions Type
```typescript
interface CorsOptions {
/** Allowed origins: string, string array, true (reflect) or '*' */
origin?: string | string[] | boolean
/** Allowed HTTP methods */
methods?: string[]
/** Allowed request headers */
allowedHeaders?: string[]
/** Allow credentials */
credentials?: boolean
/** Preflight cache max age (seconds) */
maxAge?: number
}
```
## Route Merging
File routes and inline routes can be used together, with inline routes having higher priority:
```typescript
const server = await createServer({
port: 3000,
httpDir: './src/http',
httpPrefix: '/api',
// Inline routes merge with file routes
http: {
'/health': (req, res) => res.json({ status: 'ok' }),
'/api/special': (req, res) => res.json({ special: true })
}
})
```
## Sharing Port with WebSocket
HTTP routes automatically share the same port with WebSocket services:
```typescript
const server = await createServer({
port: 3000,
// WebSocket related config
apiDir: './src/api',
msgDir: './src/msg',
// HTTP related config
httpDir: './src/http',
httpPrefix: '/api',
cors: true
})
await server.start()
// Same port 3000:
// - WebSocket: ws://localhost:3000
// - HTTP API: http://localhost:3000/api/*
```
## Complete Examples
### Game Server Login API
```typescript
// src/http/auth/login.ts
import { defineHttp } from '@esengine/server'
import { createJwtAuthProvider } from '@esengine/server/auth'
interface LoginRequest {
username: string
password: string
}
interface LoginResponse {
token: string
userId: string
expiresAt: number
}
const jwtProvider = createJwtAuthProvider({
secret: process.env.JWT_SECRET!,
expiresIn: 3600
})
export default defineHttp<LoginRequest>({
method: 'POST',
async handler(req, res) {
const { username, password } = req.body as LoginRequest
// Validate user
const user = await db.users.findByUsername(username)
if (!user || !await verifyPassword(password, user.passwordHash)) {
res.error(401, 'Invalid username or password')
return
}
// Generate JWT
const token = jwtProvider.sign({
sub: user.id,
name: user.username,
roles: user.roles
})
const response: LoginResponse = {
token,
userId: user.id,
expiresAt: Date.now() + 3600 * 1000
}
res.json(response)
}
})
```
### Game Data Query API
```typescript
// src/http/game/leaderboard.ts
import { defineHttp } from '@esengine/server'
export default defineHttp({
method: 'GET',
async handler(req, res) {
const limit = parseInt(req.query.limit ?? '10')
const offset = parseInt(req.query.offset ?? '0')
const players = await db.players.findMany({
sort: { score: 'desc' },
limit,
offset
})
res.json({
data: players,
pagination: { limit, offset }
})
}
})
```
## Middleware
### Middleware Type
Middleware are functions that execute before and after route handlers:
```typescript
type HttpMiddleware = (
req: HttpRequest,
res: HttpResponse,
next: () => Promise<void>
) => void | Promise<void>
```
### Built-in Middleware
```typescript
import {
requestLogger,
bodyLimit,
responseTime,
requestId,
securityHeaders
} from '@esengine/server'
const server = await createServer({
port: 3000,
http: { /* ... */ },
// Global middleware configured via createHttpRouter
})
```
#### requestLogger - Request Logging
```typescript
import { requestLogger } from '@esengine/server'
// Log request and response time
requestLogger()
// Also log request body
requestLogger({ logBody: true })
```
#### bodyLimit - Request Body Size Limit
```typescript
import { bodyLimit } from '@esengine/server'
// Limit request body to 1MB
bodyLimit(1024 * 1024)
```
#### responseTime - Response Time Header
```typescript
import { responseTime } from '@esengine/server'
// Automatically add X-Response-Time header
responseTime()
```
#### requestId - Request ID
```typescript
import { requestId } from '@esengine/server'
// Auto-generate and add X-Request-ID header
requestId()
// Custom header name
requestId('X-Trace-ID')
```
#### securityHeaders - Security Headers
```typescript
import { securityHeaders } from '@esengine/server'
// Add common security response headers
securityHeaders()
// Custom configuration
securityHeaders({
hidePoweredBy: true,
frameOptions: 'DENY',
noSniff: true
})
```
### Custom Middleware
```typescript
import type { HttpMiddleware } from '@esengine/server'
// Authentication middleware
const authMiddleware: HttpMiddleware = async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
res.error(401, 'Unauthorized')
return // Don't call next(), terminate request
}
// Validate token...
(req as any).userId = 'decoded-user-id'
await next() // Continue to next middleware and handler
}
```
### Using Middleware
#### With createHttpRouter
```typescript
import { createHttpRouter, requestLogger, bodyLimit } from '@esengine/server'
const router = createHttpRouter({
'/api/users': (req, res) => res.json([]),
'/api/admin': {
GET: {
handler: (req, res) => res.json({ admin: true }),
middlewares: [adminAuthMiddleware] // Route-level middleware
}
}
}, {
middlewares: [requestLogger(), bodyLimit(1024 * 1024)], // Global middleware
timeout: 30000 // Global timeout 30 seconds
})
```
## Request Timeout
### Global Timeout
```typescript
import { createHttpRouter } from '@esengine/server'
const router = createHttpRouter({
'/api/data': async (req, res) => {
// If processing exceeds 30 seconds, auto-return 408 Request Timeout
await someSlowOperation()
res.json({ data: 'result' })
}
}, {
timeout: 30000 // 30 seconds
})
```
### Route-level Timeout
```typescript
const router = createHttpRouter({
'/api/quick': (req, res) => res.json({ fast: true }),
'/api/slow': {
POST: {
handler: async (req, res) => {
await verySlowOperation()
res.json({ done: true })
},
timeout: 120000 // This route allows 2 minutes
}
}
}, {
timeout: 10000 // Global 10 seconds (overridden by route-level)
})
```
## Best Practices
1. **Use defineHttp** - Get better type hints and code organization
2. **Unified Error Handling** - Use `res.error()` to return consistent error format
3. **Enable CORS** - Required for frontend-backend separation
4. **Directory Organization** - Organize HTTP route files by functional modules
5. **Validate Input** - Always validate `req.body` and `req.query` content
6. **Status Code Standards** - Follow HTTP status code conventions (200, 201, 400, 401, 404, 500, etc.)
7. **Use Middleware** - Implement cross-cutting concerns like auth, logging, rate limiting via middleware
8. **Set Timeouts** - Prevent slow requests from blocking the server

View File

@@ -147,6 +147,7 @@ service.on('chat', (data) => {
- [Client Usage](/en/modules/network/client/) - NetworkPlugin, components and systems
- [Server Side](/en/modules/network/server/) - GameServer and Room management
- [Distributed Rooms](/en/modules/network/distributed/) - Multi-server room management and player routing
- [State Sync](/en/modules/network/sync/) - Interpolation and snapshot buffering
- [Client Prediction](/en/modules/network/prediction/) - Input prediction and server reconciliation
- [Area of Interest (AOI)](/en/modules/network/aoi/) - View filtering and bandwidth optimization

View File

@@ -79,10 +79,33 @@ await server.start()
| `tickRate` | `number` | `20` | Global tick rate (Hz) |
| `apiDir` | `string` | `'src/api'` | API handlers directory |
| `msgDir` | `string` | `'src/msg'` | Message handlers directory |
| `httpDir` | `string` | `'src/http'` | HTTP routes directory |
| `httpPrefix` | `string` | `'/api'` | HTTP routes prefix |
| `cors` | `boolean \| CorsOptions` | - | CORS configuration |
| `onStart` | `(port) => void` | - | Start callback |
| `onConnect` | `(conn) => void` | - | Connection callback |
| `onDisconnect` | `(conn) => void` | - | Disconnect callback |
## HTTP Routing
Supports HTTP API sharing the same port with WebSocket, ideal for login, registration, and similar scenarios.
```typescript
const server = await createServer({
port: 3000,
httpDir: './src/http', // HTTP routes directory
httpPrefix: '/api', // Route prefix
cors: true,
// Or inline definition
http: {
'/health': (req, res) => res.json({ status: 'ok' })
}
})
```
> For detailed documentation, see [HTTP Routing](/en/modules/network/http)
## Room System
Room is the base class for game rooms, managing players and game state.
@@ -243,6 +266,122 @@ class GameRoom extends Room {
}
```
## Schema Validation
Use the built-in Schema validation system for runtime type validation:
### Basic Usage
```typescript
import { s, defineApiWithSchema } from '@esengine/server'
// Define schema
const MoveSchema = s.object({
x: s.number(),
y: s.number(),
speed: s.number().optional()
})
// Auto type inference
type Move = s.infer<typeof MoveSchema> // { x: number; y: number; speed?: number }
// Use schema to define API (auto validation)
export default defineApiWithSchema(MoveSchema, {
handler(req, ctx) {
// req is validated, type-safe
console.log(req.x, req.y)
}
})
```
### Validator Types
| Type | Example | Description |
|------|---------|-------------|
| `s.string()` | `s.string().min(1).max(50)` | String with length constraints |
| `s.number()` | `s.number().min(0).int()` | Number with range and integer constraints |
| `s.boolean()` | `s.boolean()` | Boolean |
| `s.literal()` | `s.literal('admin')` | Literal type |
| `s.object()` | `s.object({ name: s.string() })` | Object |
| `s.array()` | `s.array(s.number())` | Array |
| `s.enum()` | `s.enum(['a', 'b'] as const)` | Enum |
| `s.union()` | `s.union([s.string(), s.number()])` | Union type |
| `s.record()` | `s.record(s.any())` | Record type |
### Modifiers
```typescript
// Optional field
s.string().optional()
// Default value
s.number().default(0)
// Nullable
s.string().nullable()
// String validation
s.string().min(1).max(100).email().url().regex(/^[a-z]+$/)
// Number validation
s.number().min(0).max(100).int().positive()
// Array validation
s.array(s.string()).min(1).max(10).nonempty()
// Object validation
s.object({ ... }).strict() // No extra fields allowed
s.object({ ... }).partial() // All fields optional
s.object({ ... }).pick('name', 'age') // Pick fields
s.object({ ... }).omit('password') // Omit fields
```
### Message Validation
```typescript
import { s, defineMsgWithSchema } from '@esengine/server'
const InputSchema = s.object({
keys: s.array(s.string()),
timestamp: s.number()
})
export default defineMsgWithSchema(InputSchema, {
handler(msg, ctx) {
// msg is validated
console.log(msg.keys, msg.timestamp)
}
})
```
### Manual Validation
```typescript
import { s, parse, safeParse, createGuard } from '@esengine/server'
const UserSchema = s.object({
name: s.string(),
age: s.number().int().min(0)
})
// Throws on error
const user = parse(UserSchema, data)
// Returns result object
const result = safeParse(UserSchema, data)
if (result.success) {
console.log(result.data)
} else {
console.error(result.error)
}
// Type guard
const isUser = createGuard(UserSchema)
if (isUser(data)) {
// data is User type
}
```
## Protocol Definition
Define shared types in `src/shared/protocol.ts`:

View File

@@ -125,23 +125,24 @@ tx:data:{key} - Business data
## MongoStorage
MongoDB storage, suitable for scenarios requiring persistence and complex queries. Uses factory pattern with lazy connection.
MongoDB storage, suitable for scenarios requiring persistence and complex queries. Uses shared connection from `@esengine/database-drivers`.
```typescript
import { MongoClient } from 'mongodb';
import { MongoStorage } from '@esengine/transaction';
import { createMongoConnection } from '@esengine/database-drivers';
import { createMongoStorage, TransactionManager } from '@esengine/transaction';
// Factory pattern: lazy connection, connects on first operation
const storage = new MongoStorage({
factory: async () => {
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
return client;
},
database: 'game',
transactionCollection: 'transactions', // Transaction log collection
dataCollection: 'transaction_data', // Business data collection
lockCollection: 'transaction_locks', // Lock collection
// Create shared connection
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
});
await mongo.connect();
// Create storage using shared connection
const storage = createMongoStorage(mongo, {
transactionCollection: 'transactions', // Transaction log collection (optional)
dataCollection: 'transaction_data', // Business data collection (optional)
lockCollection: 'transaction_locks', // Lock collection (optional)
});
// Create indexes (run on first startup)
@@ -149,11 +150,14 @@ await storage.ensureIndexes();
const manager = new TransactionManager({ storage });
// Close connection when done
// Close storage (does not close shared connection)
await storage.close();
// Or use await using for automatic cleanup (TypeScript 5.2+)
await using storage = new MongoStorage({ ... });
// Shared connection can continue to be used by other modules
const userRepo = new UserRepository(mongo); // @esengine/database
// Finally close the shared connection
await mongo.disconnect();
```
### Characteristics

View File

@@ -606,6 +606,107 @@ export class RetryDecorator implements INodeExecutor {
}
```
## 在代码中使用自定义执行器
定义了自定义执行器后,可以通过 `BehaviorTreeBuilder``.action()``.condition()` 方法在代码中使用:
### 使用 action() 方法
```typescript
import { BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
// 使用自定义执行器构建行为树
const tree = BehaviorTreeBuilder.create('CombatAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.selector('Root')
.sequence('AttackSequence')
// 使用自定义动作 - implementationType 匹配装饰器中的定义
.action('AttackAction', 'Attack', { damage: 25 })
.action('MoveToPosition', 'Chase', { speed: 10 })
.end()
.action('DelayAction', 'Idle', { duration: 1.0 })
.end()
.build();
// 启动行为树
const entity = scene.createEntity('Enemy');
BehaviorTreeStarter.start(entity, tree);
```
### 使用 condition() 方法
```typescript
const tree = BehaviorTreeBuilder.create('AI')
.selector('Root')
.sequence('AttackBranch')
// 使用自定义条件
.condition('CheckHealth', 'IsHealthy', { threshold: 50, operator: 'greater' })
.action('AttackAction', 'Attack')
.end()
.end()
.build();
```
### Builder 方法对照表
| 方法 | 说明 | 使用场景 |
|------|------|----------|
| `.action(type, name?, config?)` | 使用自定义动作执行器 | 自定义 Action 类 |
| `.condition(type, name?, config?)` | 使用自定义条件执行器 | 自定义 Condition 类 |
| `.executeAction(name)` | 调用黑板函数 `action_{name}` | 简单逻辑、快速原型 |
| `.executeCondition(name)` | 调用黑板函数 `condition_{name}` | 简单条件判断 |
### 完整示例
```typescript
import {
BehaviorTreeBuilder,
BehaviorTreeStarter,
NodeExecutorMetadata,
INodeExecutor,
NodeExecutionContext,
TaskStatus,
NodeType,
BindingHelper
} from '@esengine/behavior-tree';
// 1. 定义自定义执行器
@NodeExecutorMetadata({
implementationType: 'AttackAction',
nodeType: NodeType.Action,
displayName: '攻击',
category: 'Combat',
configSchema: {
damage: { type: 'number', default: 10, supportBinding: true }
}
})
class AttackAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
const damage = BindingHelper.getValue<number>(context, 'damage', 10);
console.log(`执行攻击,造成 ${damage} 点伤害!`);
return TaskStatus.Success;
}
}
// 2. 构建行为树
const enemyAI = BehaviorTreeBuilder.create('EnemyAI')
.defineBlackboardVariable('health', 100)
.defineBlackboardVariable('target', null)
.selector('MainBehavior')
.sequence('AttackBranch')
.condition('CheckHealth', 'HasEnoughHealth', { threshold: 20, operator: 'greater' })
.action('AttackAction', 'Attack', { damage: 50 })
.end()
.log('逃跑', 'Flee')
.end()
.build();
// 3. 启动行为树
const entity = scene.createEntity('Enemy');
BehaviorTreeStarter.start(entity, enemyAI);
```
## 注册执行器
### 自动注册

View File

@@ -0,0 +1,136 @@
---
title: "数据库驱动"
description: "MongoDB、Redis 等数据库的连接管理和驱动封装"
---
`@esengine/database-drivers` 是 ESEngine 的数据库连接管理层,提供 MongoDB、Redis 等数据库的统一连接管理。
## 特性
- **连接池管理** - 自动管理连接池,优化资源使用
- **自动重连** - 连接断开时自动重连
- **事件通知** - 连接状态变化事件
- **类型解耦** - 简化接口,不依赖原生驱动类型
- **共享连接** - 单一连接可供多个模块共享
## 安装
```bash
npm install @esengine/database-drivers
```
**对等依赖:**
```bash
npm install mongodb # MongoDB 支持
npm install ioredis # Redis 支持
```
## 架构
```
┌─────────────────────────────────────────────────────────────────┐
│ @esengine/database-drivers (Layer 1) │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ MongoConnection │ │ RedisConnection │ │
│ │ - 连接池管理 │ │ - 自动重连 │ │
│ │ - 自动重连 │ │ - Key 前缀 │ │
│ │ - 事件发射器 │ │ - 事件发射器 │ │
│ └──────────┬──────────┘ └─────────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ IMongoCollection<T> │ ← 类型安全接口 │
│ │ (适配器模式) │ 与 mongodb 类型解耦 │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌───────────────────────┐ ┌───────────────────────┐
│ @esengine/database │ │ @esengine/transaction │
│ (仓库模式) │ │ (分布式事务) │
└───────────────────────┘ └───────────────────────┘
```
## 快速开始
### MongoDB 连接
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
// 创建连接
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game',
pool: {
minSize: 5,
maxSize: 20
},
autoReconnect: true
})
// 监听事件
mongo.on('connected', () => console.log('MongoDB 已连接'))
mongo.on('disconnected', () => console.log('MongoDB 已断开'))
mongo.on('error', (e) => console.error('错误:', e.error))
// 建立连接
await mongo.connect()
// 使用集合
const users = mongo.collection<User>('users')
await users.insertOne({ name: 'John', score: 100 })
const user = await users.findOne({ name: 'John' })
// 完成后断开连接
await mongo.disconnect()
```
### Redis 连接
```typescript
import { createRedisConnection } from '@esengine/database-drivers'
const redis = createRedisConnection({
host: 'localhost',
port: 6379,
keyPrefix: 'game:',
autoReconnect: true
})
await redis.connect()
// 基本操作
await redis.set('session:123', 'data', 3600) // 带 TTL
const value = await redis.get('session:123')
await redis.disconnect()
```
## 服务容器集成
```typescript
import { ServiceContainer } from '@esengine/ecs-framework'
import {
createMongoConnection,
MongoConnectionToken,
RedisConnectionToken
} from '@esengine/database-drivers'
const services = new ServiceContainer()
// 注册连接
const mongo = createMongoConnection({ uri: '...', database: 'game' })
await mongo.connect()
services.register(MongoConnectionToken, mongo)
// 在其他模块中获取
const connection = services.get(MongoConnectionToken)
const users = connection.collection('users')
```
## 文档
- [MongoDB 连接](/modules/database-drivers/mongo/) - MongoDB 连接详细配置
- [Redis 连接](/modules/database-drivers/redis/) - Redis 连接详细配置
- [服务令牌](/modules/database-drivers/tokens/) - 依赖注入集成

View File

@@ -0,0 +1,265 @@
---
title: "MongoDB 连接"
description: "MongoDB 连接管理、连接池、自动重连"
---
## 配置选项
```typescript
interface MongoConnectionConfig {
/** MongoDB 连接 URI */
uri: string
/** 数据库名称 */
database: string
/** 连接池配置 */
pool?: {
minSize?: number // 最小连接数
maxSize?: number // 最大连接数
acquireTimeout?: number // 获取连接超时(毫秒)
maxLifetime?: number // 连接最大生命周期(毫秒)
}
/** 是否自动重连(默认 true */
autoReconnect?: boolean
/** 重连间隔(毫秒,默认 5000 */
reconnectInterval?: number
/** 最大重连次数(默认 10 */
maxReconnectAttempts?: number
}
```
## 完整示例
```typescript
import { createMongoConnection, MongoConnectionToken } from '@esengine/database-drivers'
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game',
pool: {
minSize: 5,
maxSize: 20,
acquireTimeout: 5000,
maxLifetime: 300000
},
autoReconnect: true,
reconnectInterval: 5000,
maxReconnectAttempts: 10
})
// 事件监听
mongo.on('connected', () => {
console.log('MongoDB 已连接')
})
mongo.on('disconnected', () => {
console.log('MongoDB 已断开')
})
mongo.on('reconnecting', () => {
console.log('MongoDB 正在重连...')
})
mongo.on('reconnected', () => {
console.log('MongoDB 重连成功')
})
mongo.on('error', (event) => {
console.error('MongoDB 错误:', event.error)
})
// 连接
await mongo.connect()
// 检查状态
console.log('已连接:', mongo.isConnected())
console.log('Ping:', await mongo.ping())
```
## IMongoConnection 接口
```typescript
interface IMongoConnection {
/** 连接 ID */
readonly id: string
/** 连接状态 */
readonly state: ConnectionState
/** 建立连接 */
connect(): Promise<void>
/** 断开连接 */
disconnect(): Promise<void>
/** 检查是否已连接 */
isConnected(): boolean
/** 测试连接 */
ping(): Promise<boolean>
/** 获取类型化集合 */
collection<T extends object>(name: string): IMongoCollection<T>
/** 获取数据库接口 */
getDatabase(): IMongoDatabase
/** 获取原生客户端(高级用法) */
getNativeClient(): MongoClientType
/** 获取原生数据库(高级用法) */
getNativeDatabase(): Db
}
```
## IMongoCollection 接口
类型安全的集合接口,与原生 MongoDB 类型解耦:
```typescript
interface IMongoCollection<T extends object> {
readonly name: string
// 查询
findOne(filter: object, options?: FindOptions): Promise<T | null>
find(filter: object, options?: FindOptions): Promise<T[]>
countDocuments(filter?: object): Promise<number>
// 插入
insertOne(doc: T): Promise<InsertOneResult>
insertMany(docs: T[]): Promise<InsertManyResult>
// 更新
updateOne(filter: object, update: object): Promise<UpdateResult>
updateMany(filter: object, update: object): Promise<UpdateResult>
findOneAndUpdate(
filter: object,
update: object,
options?: FindOneAndUpdateOptions
): Promise<T | null>
// 删除
deleteOne(filter: object): Promise<DeleteResult>
deleteMany(filter: object): Promise<DeleteResult>
// 索引
createIndex(
spec: Record<string, 1 | -1>,
options?: IndexOptions
): Promise<string>
}
```
## 使用示例
### 基本 CRUD
```typescript
interface User {
id: string
name: string
email: string
score: number
}
const users = mongo.collection<User>('users')
// 插入
await users.insertOne({
id: '1',
name: 'John',
email: 'john@example.com',
score: 100
})
// 查询
const user = await users.findOne({ name: 'John' })
const topUsers = await users.find(
{ score: { $gte: 100 } },
{ sort: { score: -1 }, limit: 10 }
)
// 更新
await users.updateOne(
{ id: '1' },
{ $inc: { score: 10 } }
)
// 删除
await users.deleteOne({ id: '1' })
```
### 批量操作
```typescript
// 批量插入
await users.insertMany([
{ id: '1', name: 'Alice', email: 'alice@example.com', score: 100 },
{ id: '2', name: 'Bob', email: 'bob@example.com', score: 200 },
{ id: '3', name: 'Carol', email: 'carol@example.com', score: 150 }
])
// 批量更新
await users.updateMany(
{ score: { $lt: 100 } },
{ $set: { status: 'inactive' } }
)
// 批量删除
await users.deleteMany({ status: 'inactive' })
```
### 索引管理
```typescript
// 创建索引
await users.createIndex({ email: 1 }, { unique: true })
await users.createIndex({ score: -1 })
await users.createIndex({ name: 1, score: -1 })
```
## 与其他模块集成
### 与 @esengine/database 集成
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
import { UserRepository, createRepository } from '@esengine/database'
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
})
await mongo.connect()
// 使用 UserRepository
const userRepo = new UserRepository(mongo)
await userRepo.register({ username: 'john', password: '123456' })
// 使用通用仓库
const playerRepo = createRepository<Player>(mongo, 'players')
```
### 与 @esengine/transaction 集成
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
import { createMongoStorage, TransactionManager } from '@esengine/transaction'
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
})
await mongo.connect()
// 创建事务存储(共享连接)
const storage = createMongoStorage(mongo)
await storage.ensureIndexes()
const txManager = new TransactionManager({ storage })
```

View File

@@ -0,0 +1,228 @@
---
title: "Redis 连接"
description: "Redis 连接管理、自动重连、键前缀"
---
## 配置选项
```typescript
interface RedisConnectionConfig {
/** Redis 主机 */
host?: string
/** Redis 端口 */
port?: number
/** 认证密码 */
password?: string
/** 数据库编号 */
db?: number
/** 键前缀 */
keyPrefix?: string
/** 是否自动重连(默认 true */
autoReconnect?: boolean
/** 重连间隔(毫秒,默认 5000 */
reconnectInterval?: number
/** 最大重连次数(默认 10 */
maxReconnectAttempts?: number
}
```
## 完整示例
```typescript
import { createRedisConnection, RedisConnectionToken } from '@esengine/database-drivers'
const redis = createRedisConnection({
host: 'localhost',
port: 6379,
password: 'your-password',
db: 0,
keyPrefix: 'game:',
autoReconnect: true,
reconnectInterval: 5000,
maxReconnectAttempts: 10
})
// 事件监听
redis.on('connected', () => {
console.log('Redis 已连接')
})
redis.on('disconnected', () => {
console.log('Redis 已断开')
})
redis.on('error', (event) => {
console.error('Redis 错误:', event.error)
})
// 连接
await redis.connect()
// 检查状态
console.log('已连接:', redis.isConnected())
console.log('Ping:', await redis.ping())
```
## IRedisConnection 接口
```typescript
interface IRedisConnection {
/** 连接 ID */
readonly id: string
/** 连接状态 */
readonly state: ConnectionState
/** 建立连接 */
connect(): Promise<void>
/** 断开连接 */
disconnect(): Promise<void>
/** 检查是否已连接 */
isConnected(): boolean
/** 测试连接 */
ping(): Promise<boolean>
/** 获取值 */
get(key: string): Promise<string | null>
/** 设置值(可选 TTL单位秒 */
set(key: string, value: string, ttl?: number): Promise<void>
/** 删除键 */
del(key: string): Promise<boolean>
/** 检查键是否存在 */
exists(key: string): Promise<boolean>
/** 设置过期时间(秒) */
expire(key: string, seconds: number): Promise<boolean>
/** 获取剩余过期时间(秒) */
ttl(key: string): Promise<number>
/** 获取原生客户端(高级用法) */
getNativeClient(): Redis
}
```
## 使用示例
### 基本操作
```typescript
// 设置值
await redis.set('user:1:name', 'John')
// 设置带过期时间的值1 小时)
await redis.set('session:abc123', 'user-data', 3600)
// 获取值
const name = await redis.get('user:1:name')
// 检查键是否存在
const exists = await redis.exists('user:1:name')
// 删除键
await redis.del('user:1:name')
// 获取剩余过期时间
const ttl = await redis.ttl('session:abc123')
```
### 键前缀
配置 `keyPrefix` 后,所有操作自动添加前缀:
```typescript
const redis = createRedisConnection({
host: 'localhost',
keyPrefix: 'game:'
})
// 实际操作的键是 'game:user:1'
await redis.set('user:1', 'data')
// 实际查询的键是 'game:user:1'
const data = await redis.get('user:1')
```
### 高级操作
使用原生客户端进行高级操作:
```typescript
const client = redis.getNativeClient()
// 使用 Pipeline
const pipeline = client.pipeline()
pipeline.set('key1', 'value1')
pipeline.set('key2', 'value2')
pipeline.set('key3', 'value3')
await pipeline.exec()
// 使用事务
const multi = client.multi()
multi.incr('counter')
multi.get('counter')
const results = await multi.exec()
// 使用 Lua 脚本
const result = await client.eval(
`return redis.call('get', KEYS[1])`,
1,
'mykey'
)
```
## 与事务系统集成
```typescript
import { createRedisConnection } from '@esengine/database-drivers'
import { RedisStorage, TransactionManager } from '@esengine/transaction'
const redis = createRedisConnection({
host: 'localhost',
port: 6379,
keyPrefix: 'tx:'
})
await redis.connect()
// 创建事务存储
const storage = new RedisStorage({
factory: () => redis.getNativeClient(),
prefix: 'tx:'
})
const txManager = new TransactionManager({ storage })
```
## 连接状态
```typescript
type ConnectionState =
| 'disconnected' // 未连接
| 'connecting' // 连接中
| 'connected' // 已连接
| 'disconnecting' // 断开中
| 'error' // 错误状态
```
## 事件
| 事件 | 描述 |
|------|------|
| `connected` | 连接成功 |
| `disconnected` | 连接断开 |
| `reconnecting` | 正在重连 |
| `reconnected` | 重连成功 |
| `error` | 发生错误 |

View File

@@ -0,0 +1,140 @@
---
title: "数据库仓库"
description: "Repository 模式的数据库操作层,支持 CRUD、分页、软删除"
---
`@esengine/database` 是 ESEngine 的数据库操作层,基于 Repository 模式提供类型安全的 CRUD 操作。
## 特性
- **Repository 模式** - 泛型 CRUD 操作,类型安全
- **分页查询** - 内置分页支持
- **软删除** - 可选的软删除与恢复
- **用户管理** - 开箱即用的 UserRepository
- **密码安全** - 使用 scrypt 的密码哈希工具
## 安装
```bash
npm install @esengine/database @esengine/database-drivers
```
## 快速开始
### 基本仓库
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
import { Repository, createRepository } from '@esengine/database'
// 定义实体
interface Player {
id: string
name: string
score: number
createdAt: Date
updatedAt: Date
}
// 创建连接
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
})
await mongo.connect()
// 创建仓库
const playerRepo = createRepository<Player>(mongo, 'players')
// CRUD 操作
const player = await playerRepo.create({
name: 'John',
score: 0
})
const found = await playerRepo.findById(player.id)
await playerRepo.update(player.id, { score: 100 })
await playerRepo.delete(player.id)
```
### 自定义仓库
```typescript
import { Repository, BaseEntity } from '@esengine/database'
import type { IMongoConnection } from '@esengine/database-drivers'
interface Player extends BaseEntity {
name: string
score: number
rank?: string
}
class PlayerRepository extends Repository<Player> {
constructor(connection: IMongoConnection) {
super(connection, 'players')
}
async findTopPlayers(limit: number = 10): Promise<Player[]> {
return this.findMany({
sort: { score: 'desc' },
limit
})
}
async findByRank(rank: string): Promise<Player[]> {
return this.findMany({
where: { rank }
})
}
async incrementScore(playerId: string, amount: number): Promise<Player | null> {
const player = await this.findById(playerId)
if (!player) return null
return this.update(playerId, { score: player.score + amount })
}
}
// 使用
const playerRepo = new PlayerRepository(mongo)
const topPlayers = await playerRepo.findTopPlayers(5)
```
### 用户仓库
```typescript
import { UserRepository } from '@esengine/database'
const userRepo = new UserRepository(mongo)
// 注册新用户
const user = await userRepo.register({
username: 'john',
password: 'securePassword123',
email: 'john@example.com'
})
// 认证
const authenticated = await userRepo.authenticate('john', 'securePassword123')
if (authenticated) {
console.log('登录成功:', authenticated.username)
}
// 修改密码
await userRepo.changePassword(user.id, 'securePassword123', 'newPassword456')
// 角色管理
await userRepo.addRole(user.id, 'admin')
await userRepo.removeRole(user.id, 'admin')
// 查询用户
const admins = await userRepo.findByRole('admin')
const john = await userRepo.findByUsername('john')
```
## 文档
- [仓库 API](/modules/database/repository/) - Repository 详细 API
- [用户管理](/modules/database/user/) - UserRepository 用法
- [查询语法](/modules/database/query/) - 查询条件语法

View File

@@ -0,0 +1,185 @@
---
title: "查询语法"
description: "查询条件操作符和语法"
---
## 基本查询
### 精确匹配
```typescript
await repo.findMany({
where: {
name: 'John',
status: 'active'
}
})
```
### 使用操作符
```typescript
await repo.findMany({
where: {
score: { $gte: 100 },
rank: { $in: ['gold', 'platinum'] }
}
})
```
## 查询操作符
| 操作符 | 描述 | 示例 |
|--------|------|------|
| `$eq` | 等于 | `{ score: { $eq: 100 } }` |
| `$ne` | 不等于 | `{ status: { $ne: 'banned' } }` |
| `$gt` | 大于 | `{ score: { $gt: 50 } }` |
| `$gte` | 大于等于 | `{ level: { $gte: 10 } }` |
| `$lt` | 小于 | `{ age: { $lt: 18 } }` |
| `$lte` | 小于等于 | `{ price: { $lte: 100 } }` |
| `$in` | 在数组中 | `{ rank: { $in: ['gold', 'platinum'] } }` |
| `$nin` | 不在数组中 | `{ status: { $nin: ['banned', 'suspended'] } }` |
| `$like` | 模式匹配 | `{ name: { $like: '%john%' } }` |
| `$regex` | 正则匹配 | `{ email: { $regex: '@gmail.com$' } }` |
## 逻辑操作符
### $or
```typescript
await repo.findMany({
where: {
$or: [
{ score: { $gte: 1000 } },
{ rank: 'legendary' }
]
}
})
```
### $and
```typescript
await repo.findMany({
where: {
$and: [
{ score: { $gte: 100 } },
{ score: { $lte: 500 } }
]
}
})
```
### 组合使用
```typescript
await repo.findMany({
where: {
status: 'active',
$or: [
{ rank: 'gold' },
{ score: { $gte: 1000 } }
]
}
})
```
## 模式匹配
### $like 语法
- `%` - 匹配任意字符序列
- `_` - 匹配单个字符
```typescript
// 以 'John' 开头
{ name: { $like: 'John%' } }
// 以 'son' 结尾
{ name: { $like: '%son' } }
// 包含 'oh'
{ name: { $like: '%oh%' } }
// 第二个字符是 'o'
{ name: { $like: '_o%' } }
```
### $regex 语法
使用标准正则表达式:
```typescript
// 以 'John' 开头(大小写不敏感)
{ name: { $regex: '^john' } }
// Gmail 邮箱
{ email: { $regex: '@gmail\\.com$' } }
// 包含数字
{ username: { $regex: '\\d+' } }
```
## 排序
```typescript
await repo.findMany({
sort: {
score: 'desc', // 降序
name: 'asc' // 升序
}
})
```
## 分页
### 使用 limit/offset
```typescript
// 第一页
await repo.findMany({
limit: 20,
offset: 0
})
// 第二页
await repo.findMany({
limit: 20,
offset: 20
})
```
### 使用 findPaginated
```typescript
const result = await repo.findPaginated(
{ page: 2, pageSize: 20 },
{ sort: { createdAt: 'desc' } }
)
```
## 完整示例
```typescript
// 查找活跃的金牌玩家,分数在 100-1000 之间
// 按分数降序排列,取前 10 个
const players = await repo.findMany({
where: {
status: 'active',
rank: 'gold',
score: { $gte: 100, $lte: 1000 }
},
sort: { score: 'desc' },
limit: 10
})
// 搜索用户名包含 'john' 或邮箱是 gmail 的用户
const users = await repo.findMany({
where: {
$or: [
{ username: { $like: '%john%' } },
{ email: { $regex: '@gmail\\.com$' } }
]
}
})
```

View File

@@ -0,0 +1,244 @@
---
title: "Repository API"
description: "泛型仓库接口CRUD 操作、分页、软删除"
---
## 创建仓库
### 使用工厂函数
```typescript
import { createRepository } from '@esengine/database'
const playerRepo = createRepository<Player>(mongo, 'players')
// 启用软删除
const playerRepo = createRepository<Player>(mongo, 'players', true)
```
### 继承 Repository
```typescript
import { Repository, BaseEntity } from '@esengine/database'
interface Player extends BaseEntity {
name: string
score: number
}
class PlayerRepository extends Repository<Player> {
constructor(connection: IMongoConnection) {
super(connection, 'players', false) // 第三个参数:启用软删除
}
// 添加自定义方法
async findTopPlayers(limit: number): Promise<Player[]> {
return this.findMany({
sort: { score: 'desc' },
limit
})
}
}
```
## BaseEntity 接口
所有实体必须继承 `BaseEntity`
```typescript
interface BaseEntity {
id: string
createdAt: Date
updatedAt: Date
deletedAt?: Date // 软删除时使用
}
```
## 查询方法
### findById
```typescript
const player = await repo.findById('player-123')
```
### findOne
```typescript
const player = await repo.findOne({
where: { name: 'John' }
})
const topPlayer = await repo.findOne({
sort: { score: 'desc' }
})
```
### findMany
```typescript
// 简单查询
const players = await repo.findMany({
where: { rank: 'gold' }
})
// 复杂查询
const players = await repo.findMany({
where: {
score: { $gte: 100 },
rank: { $in: ['gold', 'platinum'] }
},
sort: { score: 'desc', name: 'asc' },
limit: 10,
offset: 0
})
```
### findPaginated
```typescript
const result = await repo.findPaginated(
{ page: 1, pageSize: 20 },
{
where: { rank: 'gold' },
sort: { score: 'desc' }
}
)
console.log(result.data) // Player[]
console.log(result.total) // 总数量
console.log(result.totalPages) // 总页数
console.log(result.hasNext) // 是否有下一页
console.log(result.hasPrev) // 是否有上一页
```
### count
```typescript
const count = await repo.count({
where: { rank: 'gold' }
})
```
### exists
```typescript
const exists = await repo.exists({
where: { email: 'john@example.com' }
})
```
## 创建方法
### create
```typescript
const player = await repo.create({
name: 'John',
score: 0
})
// 自动生成 id, createdAt, updatedAt
```
### createMany
```typescript
const players = await repo.createMany([
{ name: 'Alice', score: 100 },
{ name: 'Bob', score: 200 },
{ name: 'Carol', score: 150 }
])
```
## 更新方法
### update
```typescript
const updated = await repo.update('player-123', {
score: 200,
rank: 'gold'
})
// 自动更新 updatedAt
```
## 删除方法
### delete
```typescript
// 普通删除
await repo.delete('player-123')
// 软删除(如果启用)
// 实际是设置 deletedAt 字段
```
### deleteMany
```typescript
const count = await repo.deleteMany({
where: { score: { $lt: 10 } }
})
```
## 软删除
### 启用软删除
```typescript
const repo = createRepository<Player>(mongo, 'players', true)
```
### 查询行为
```typescript
// 默认排除软删除记录
const players = await repo.findMany()
// 包含软删除记录
const allPlayers = await repo.findMany({
includeSoftDeleted: true
})
```
### 恢复记录
```typescript
await repo.restore('player-123')
```
## QueryOptions
```typescript
interface QueryOptions<T> {
/** 查询条件 */
where?: WhereCondition<T>
/** 排序 */
sort?: Partial<Record<keyof T, 'asc' | 'desc'>>
/** 限制数量 */
limit?: number
/** 偏移量 */
offset?: number
/** 包含软删除记录(仅在启用软删除时有效) */
includeSoftDeleted?: boolean
}
```
## PaginatedResult
```typescript
interface PaginatedResult<T> {
data: T[]
total: number
page: number
pageSize: number
totalPages: number
hasNext: boolean
hasPrev: boolean
}
```

View File

@@ -0,0 +1,277 @@
---
title: "用户管理"
description: "UserRepository 用户注册、认证、角色管理"
---
## 概述
`UserRepository` 提供开箱即用的用户管理功能:
- 用户注册与认证
- 密码哈希(使用 scrypt
- 角色管理
- 账户状态管理
## 快速开始
```typescript
import { createMongoConnection } from '@esengine/database-drivers'
import { UserRepository } from '@esengine/database'
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
})
await mongo.connect()
const userRepo = new UserRepository(mongo)
```
## 用户注册
```typescript
const user = await userRepo.register({
username: 'john',
password: 'securePassword123',
email: 'john@example.com', // 可选
displayName: 'John Doe', // 可选
roles: ['player'] // 可选,默认 []
})
console.log(user)
// {
// id: 'uuid-...',
// username: 'john',
// email: 'john@example.com',
// displayName: 'John Doe',
// roles: ['player'],
// status: 'active',
// createdAt: Date,
// updatedAt: Date
// }
```
**注意**`register` 返回的 `SafeUser` 不包含密码哈希。
## 用户认证
```typescript
const user = await userRepo.authenticate('john', 'securePassword123')
if (user) {
console.log('登录成功:', user.username)
} else {
console.log('用户名或密码错误')
}
```
## 密码管理
### 修改密码
```typescript
const success = await userRepo.changePassword(
userId,
'oldPassword123',
'newPassword456'
)
if (success) {
console.log('密码修改成功')
} else {
console.log('原密码错误')
}
```
### 重置密码
```typescript
// 管理员直接重置密码
const success = await userRepo.resetPassword(userId, 'newPassword123')
```
## 角色管理
### 添加角色
```typescript
await userRepo.addRole(userId, 'admin')
await userRepo.addRole(userId, 'moderator')
```
### 移除角色
```typescript
await userRepo.removeRole(userId, 'moderator')
```
### 查询角色
```typescript
// 查找所有管理员
const admins = await userRepo.findByRole('admin')
// 检查用户是否有某角色
const user = await userRepo.findById(userId)
const isAdmin = user?.roles.includes('admin')
```
## 查询用户
### 按用户名查找
```typescript
const user = await userRepo.findByUsername('john')
```
### 按邮箱查找
```typescript
const user = await userRepo.findByEmail('john@example.com')
```
### 按角色查找
```typescript
const admins = await userRepo.findByRole('admin')
```
### 使用继承的方法
```typescript
// 分页查询
const result = await userRepo.findPaginated(
{ page: 1, pageSize: 20 },
{
where: { status: 'active' },
sort: { createdAt: 'desc' }
}
)
// 复杂查询
const users = await userRepo.findMany({
where: {
status: 'active',
roles: { $in: ['admin', 'moderator'] }
}
})
```
## 账户状态
```typescript
type UserStatus = 'active' | 'inactive' | 'banned' | 'suspended'
```
### 更新状态
```typescript
await userRepo.update(userId, { status: 'banned' })
```
### 查询特定状态
```typescript
const activeUsers = await userRepo.findMany({
where: { status: 'active' }
})
const bannedUsers = await userRepo.findMany({
where: { status: 'banned' }
})
```
## 类型定义
### UserEntity
```typescript
interface UserEntity extends BaseEntity {
username: string
passwordHash: string
email?: string
displayName?: string
roles: string[]
status: UserStatus
lastLoginAt?: Date
}
```
### SafeUser
```typescript
type SafeUser = Omit<UserEntity, 'passwordHash'>
```
### CreateUserParams
```typescript
interface CreateUserParams {
username: string
password: string
email?: string
displayName?: string
roles?: string[]
}
```
## 密码工具
独立的密码工具函数:
```typescript
import { hashPassword, verifyPassword } from '@esengine/database'
// 哈希密码
const hash = await hashPassword('myPassword123')
// 验证密码
const isValid = await verifyPassword('myPassword123', hash)
```
### 安全说明
- 使用 Node.js 内置的 `scrypt` 算法
- 自动生成随机盐值
- 默认使用安全的迭代参数
- 哈希格式:`salt:hash`(均为 hex 编码)
## 扩展 UserRepository
```typescript
import { UserRepository, UserEntity } from '@esengine/database'
interface GameUser extends UserEntity {
level: number
experience: number
coins: number
}
class GameUserRepository extends UserRepository {
// 重写集合名
constructor(connection: IMongoConnection) {
super(connection, 'game_users')
}
// 添加游戏相关方法
async addExperience(userId: string, amount: number): Promise<GameUser | null> {
const user = await this.findById(userId) as GameUser | null
if (!user) return null
const newExp = user.experience + amount
const newLevel = Math.floor(newExp / 1000) + 1
return this.update(userId, {
experience: newExp,
level: newLevel
}) as Promise<GameUser | null>
}
async findTopPlayers(limit: number = 10): Promise<GameUser[]> {
return this.findMany({
sort: { level: 'desc', experience: 'desc' },
limit
}) as Promise<GameUser[]>
}
}
```

View File

@@ -37,6 +37,13 @@ ESEngine 提供了丰富的功能模块,可以按需引入到你的项目中
| [网络同步](/modules/network/) | `@esengine/network` | 多人游戏网络同步 |
| [事务系统](/modules/transaction/) | `@esengine/transaction` | 游戏事务处理,支持分布式事务 |
### 数据库模块
| 模块 | 包名 | 描述 |
|------|------|------|
| [数据库驱动](/modules/database-drivers/) | `@esengine/database-drivers` | MongoDB、Redis 连接管理 |
| [数据库仓库](/modules/database/) | `@esengine/database` | Repository 模式数据操作 |
## 安装
所有模块都可以独立安装:

View File

@@ -92,6 +92,355 @@ const token = jwtProvider.sign({
const payload = jwtProvider.decode(token)
```
### 自定义提供者
你可以通过实现 `IAuthProvider` 接口来创建自定义认证提供者,以集成任何认证系统(如 OAuth、LDAP、自定义数据库认证等
#### IAuthProvider 接口
```typescript
interface IAuthProvider<TUser = unknown, TCredentials = unknown> {
/** 提供者名称 */
readonly name: string;
/** 验证凭证 */
verify(credentials: TCredentials): Promise<AuthResult<TUser>>;
/** 刷新令牌(可选) */
refresh?(token: string): Promise<AuthResult<TUser>>;
/** 撤销令牌(可选) */
revoke?(token: string): Promise<boolean>;
}
interface AuthResult<TUser> {
success: boolean;
user?: TUser;
error?: string;
errorCode?: AuthErrorCode;
token?: string;
expiresAt?: number;
}
type AuthErrorCode =
| 'INVALID_CREDENTIALS'
| 'EXPIRED_TOKEN'
| 'INVALID_TOKEN'
| 'USER_NOT_FOUND'
| 'ACCOUNT_DISABLED'
| 'RATE_LIMITED'
| 'INSUFFICIENT_PERMISSIONS';
```
#### 自定义提供者示例
**示例 1数据库密码认证**
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface User {
id: string
username: string
roles: string[]
}
interface PasswordCredentials {
username: string
password: string
}
class DatabaseAuthProvider implements IAuthProvider<User, PasswordCredentials> {
readonly name = 'database'
async verify(credentials: PasswordCredentials): Promise<AuthResult<User>> {
const { username, password } = credentials
// 从数据库查询用户
const user = await db.users.findByUsername(username)
if (!user) {
return {
success: false,
error: '用户不存在',
errorCode: 'USER_NOT_FOUND'
}
}
// 验证密码(使用 bcrypt 等库)
const isValid = await bcrypt.compare(password, user.passwordHash)
if (!isValid) {
return {
success: false,
error: '密码错误',
errorCode: 'INVALID_CREDENTIALS'
}
}
// 检查账号状态
if (user.disabled) {
return {
success: false,
error: '账号已禁用',
errorCode: 'ACCOUNT_DISABLED'
}
}
return {
success: true,
user: {
id: user.id,
username: user.username,
roles: user.roles
}
}
}
}
```
**示例 2OAuth/第三方认证**
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface OAuthUser {
id: string
email: string
name: string
provider: string
roles: string[]
}
interface OAuthCredentials {
provider: 'google' | 'github' | 'discord'
accessToken: string
}
class OAuthProvider implements IAuthProvider<OAuthUser, OAuthCredentials> {
readonly name = 'oauth'
async verify(credentials: OAuthCredentials): Promise<AuthResult<OAuthUser>> {
const { provider, accessToken } = credentials
try {
// 根据提供商验证 token
const profile = await this.fetchUserProfile(provider, accessToken)
// 查找或创建本地用户
let user = await db.users.findByOAuth(provider, profile.id)
if (!user) {
user = await db.users.create({
oauthProvider: provider,
oauthId: profile.id,
email: profile.email,
name: profile.name,
roles: ['player']
})
}
return {
success: true,
user: {
id: user.id,
email: user.email,
name: user.name,
provider,
roles: user.roles
}
}
} catch (error) {
return {
success: false,
error: 'OAuth 验证失败',
errorCode: 'INVALID_TOKEN'
}
}
}
private async fetchUserProfile(provider: string, token: string) {
switch (provider) {
case 'google':
return fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json())
case 'github':
return fetch('https://api.github.com/user', {
headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json())
// 其他提供商...
default:
throw new Error(`不支持的提供商: ${provider}`)
}
}
}
```
**示例 3API Key 认证**
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface ApiUser {
id: string
name: string
roles: string[]
rateLimit: number
}
class ApiKeyAuthProvider implements IAuthProvider<ApiUser, string> {
readonly name = 'api-key'
private revokedKeys = new Set<string>()
async verify(apiKey: string): Promise<AuthResult<ApiUser>> {
if (!apiKey || !apiKey.startsWith('sk_')) {
return {
success: false,
error: 'API Key 格式无效',
errorCode: 'INVALID_TOKEN'
}
}
if (this.revokedKeys.has(apiKey)) {
return {
success: false,
error: 'API Key 已被撤销',
errorCode: 'INVALID_TOKEN'
}
}
// 从数据库查询 API Key
const keyData = await db.apiKeys.findByKey(apiKey)
if (!keyData) {
return {
success: false,
error: 'API Key 不存在',
errorCode: 'INVALID_CREDENTIALS'
}
}
// 检查过期
if (keyData.expiresAt && keyData.expiresAt < Date.now()) {
return {
success: false,
error: 'API Key 已过期',
errorCode: 'EXPIRED_TOKEN'
}
}
return {
success: true,
user: {
id: keyData.userId,
name: keyData.name,
roles: keyData.roles,
rateLimit: keyData.rateLimit
},
expiresAt: keyData.expiresAt
}
}
async revoke(apiKey: string): Promise<boolean> {
this.revokedKeys.add(apiKey)
await db.apiKeys.revoke(apiKey)
return true
}
}
```
#### 使用自定义提供者
```typescript
import { createServer } from '@esengine/server'
import { withAuth } from '@esengine/server/auth'
// 创建自定义提供者
const dbAuthProvider = new DatabaseAuthProvider()
// 或使用 OAuth 提供者
const oauthProvider = new OAuthProvider()
// 使用自定义提供者
const server = withAuth(await createServer({ port: 3000 }), {
provider: dbAuthProvider, // 或 oauthProvider
// 从 WebSocket 连接请求中提取凭证
extractCredentials: (req) => {
const url = new URL(req.url, 'http://localhost')
// 对于数据库认证:从查询参数获取
const username = url.searchParams.get('username')
const password = url.searchParams.get('password')
if (username && password) {
return { username, password }
}
// 对于 OAuth从 token 参数获取
const provider = url.searchParams.get('provider')
const accessToken = url.searchParams.get('access_token')
if (provider && accessToken) {
return { provider, accessToken }
}
// 对于 API Key从请求头获取
const apiKey = req.headers['x-api-key']
if (apiKey) {
return apiKey as string
}
return null
},
onAuthFailure: (conn, error) => {
console.log(`认证失败: ${error.errorCode} - ${error.error}`)
}
})
await server.start()
```
#### 组合多个提供者
你可以创建一个复合提供者来支持多种认证方式:
```typescript
import type { IAuthProvider, AuthResult } from '@esengine/server/auth'
interface MultiAuthCredentials {
type: 'jwt' | 'oauth' | 'apikey' | 'password'
data: unknown
}
class MultiAuthProvider implements IAuthProvider<User, MultiAuthCredentials> {
readonly name = 'multi'
constructor(
private jwtProvider: JwtAuthProvider<User>,
private oauthProvider: OAuthProvider,
private apiKeyProvider: ApiKeyAuthProvider,
private dbProvider: DatabaseAuthProvider
) {}
async verify(credentials: MultiAuthCredentials): Promise<AuthResult<User>> {
switch (credentials.type) {
case 'jwt':
return this.jwtProvider.verify(credentials.data as string)
case 'oauth':
return this.oauthProvider.verify(credentials.data as OAuthCredentials)
case 'apikey':
return this.apiKeyProvider.verify(credentials.data as string)
case 'password':
return this.dbProvider.verify(credentials.data as PasswordCredentials)
default:
return {
success: false,
error: '不支持的认证类型',
errorCode: 'INVALID_CREDENTIALS'
}
}
}
}
```
### Session 提供者
使用服务端会话实现有状态认证:

View File

@@ -0,0 +1,441 @@
---
title: "分布式房间"
description: "使用 DistributedRoomManager 实现多服务器房间管理"
---
## 概述
分布式房间支持允许多个服务器实例共享房间注册表,实现跨服务器玩家路由和故障转移。
```
┌─────────────────────────────────────────────────────────┐
│ Server A Server B Server C │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Room 1 │ │ Room 3 │ │ Room 5 │ │
│ │ Room 2 │ │ Room 4 │ │ Room 6 │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ IDistributedAdapter │ │
│ │ (Redis / Memory) │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
## 快速开始
### 单机模式(测试用)
```typescript
import {
DistributedRoomManager,
MemoryAdapter,
Room
} from '@esengine/server';
// 定义房间类型
class GameRoom extends Room {
maxPlayers = 4;
}
// 创建适配器和管理器
const adapter = new MemoryAdapter();
const manager = new DistributedRoomManager(adapter, {
serverId: 'server-1',
serverAddress: 'localhost',
serverPort: 3000
}, (conn, type, data) => conn.send(JSON.stringify({ type, data })));
// 注册房间类型
manager.define('game', GameRoom);
// 启动管理器
await manager.start();
// 分布式加入/创建房间
const result = await manager.joinOrCreateDistributed('game', 'player-1', conn);
if ('redirect' in result) {
// 玩家应连接到其他服务器
console.log(`重定向到: ${result.redirect}`);
} else {
// 玩家加入本地房间
const { room, player } = result;
}
// 优雅关闭
await manager.stop(true);
```
### 多服务器模式(生产用)
```typescript
import Redis from 'ioredis';
import { DistributedRoomManager, RedisAdapter } from '@esengine/server';
const adapter = new RedisAdapter({
factory: () => new Redis({
host: 'redis.example.com',
port: 6379
}),
prefix: 'game:',
serverTtl: 30,
snapshotTtl: 86400
});
const manager = new DistributedRoomManager(adapter, {
serverId: process.env.SERVER_ID,
serverAddress: process.env.PUBLIC_IP,
serverPort: 3000,
heartbeatInterval: 5000,
snapshotInterval: 30000,
enableFailover: true,
capacity: 100
}, sendFn);
```
## DistributedRoomManager
### 配置选项
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `serverId` | `string` | 必填 | 服务器唯一标识 |
| `serverAddress` | `string` | 必填 | 客户端连接的公开地址 |
| `serverPort` | `number` | 必填 | 服务器端口 |
| `heartbeatInterval` | `number` | `5000` | 心跳间隔(毫秒) |
| `snapshotInterval` | `number` | `30000` | 状态快照间隔0 禁用 |
| `migrationTimeout` | `number` | `10000` | 房间迁移超时 |
| `enableFailover` | `boolean` | `true` | 启用自动故障转移 |
| `capacity` | `number` | `100` | 本服务器最大房间数 |
### 生命周期方法
#### start()
启动分布式房间管理器。连接适配器、注册服务器、启动心跳。
```typescript
await manager.start();
```
#### stop(graceful?)
停止管理器。如果 `graceful=true`,将服务器标记为 draining 并保存所有房间快照。
```typescript
await manager.stop(true);
```
### 路由方法
#### joinOrCreateDistributed()
分布式感知的加入或创建房间。返回本地房间的 `{ room, player }` 或远程房间的 `{ redirect: string }`
```typescript
const result = await manager.joinOrCreateDistributed('game', 'player-1', conn);
if ('redirect' in result) {
// 客户端应重定向到其他服务器
res.json({ redirect: result.redirect });
} else {
// 玩家加入了本地房间
const { room, player } = result;
}
```
#### route()
将玩家路由到合适的房间/服务器。
```typescript
const result = await manager.route({
roomType: 'game',
playerId: 'p1'
});
switch (result.type) {
case 'local': // 房间在本服务器
break;
case 'redirect': // 房间在其他服务器
// result.serverAddress 包含目标服务器地址
break;
case 'create': // 没有可用房间,需要创建
break;
case 'unavailable': // 无法找到或创建房间
// result.reason 包含错误信息
break;
}
```
### 状态管理
#### saveSnapshot()
手动保存房间状态快照。
```typescript
await manager.saveSnapshot(roomId);
```
#### restoreFromSnapshot()
从保存的快照恢复房间。
```typescript
const success = await manager.restoreFromSnapshot(roomId);
```
### 查询方法
#### getServers()
获取所有在线服务器。
```typescript
const servers = await manager.getServers();
```
#### queryDistributedRooms()
查询所有服务器上的房间。
```typescript
const rooms = await manager.queryDistributedRooms({
roomType: 'game',
hasSpace: true,
notLocked: true
});
```
## IDistributedAdapter
分布式后端的接口。实现此接口以支持 Redis、消息队列等。
### 内置适配器
#### MemoryAdapter
用于测试和单机模式的内存实现。
```typescript
const adapter = new MemoryAdapter({
serverTtl: 15000, // 无心跳后服务器离线时间(毫秒)
enableTtlCheck: true, // 启用自动 TTL 检查
ttlCheckInterval: 5000 // TTL 检查间隔(毫秒)
});
```
#### RedisAdapter
用于生产环境多服务器部署的 Redis 实现。
```typescript
import Redis from 'ioredis';
import { RedisAdapter } from '@esengine/server';
const adapter = new RedisAdapter({
factory: () => new Redis('redis://localhost:6379'),
prefix: 'game:', // 键前缀(默认: 'dist:'
serverTtl: 30, // 服务器 TTL默认: 30
roomTtl: 0, // 房间 TTL0 = 永不过期(默认: 0
snapshotTtl: 86400, // 快照 TTL默认: 24 小时)
channel: 'game:events' // Pub/Sub 频道(默认: 'distributed:events'
});
```
**RedisAdapter 配置:**
| 属性 | 类型 | 默认值 | 描述 |
|------|------|--------|------|
| `factory` | `() => RedisClient` | 必填 | Redis 客户端工厂(惰性连接) |
| `prefix` | `string` | `'dist:'` | 所有 Redis 键的前缀 |
| `serverTtl` | `number` | `30` | 服务器 TTL |
| `roomTtl` | `number` | `0` | 房间 TTL0 = 不过期 |
| `snapshotTtl` | `number` | `86400` | 快照 TTL |
| `channel` | `string` | `'distributed:events'` | Pub/Sub 频道名 |
**功能特性:**
- 带自动心跳 TTL 的服务器注册
- 跨服务器查找的房间注册
- 可配置 TTL 的状态快照
- 跨服务器事件的 Pub/Sub
- 使用 Redis SET NX 的分布式锁
### 自定义适配器
```typescript
import type { IDistributedAdapter } from '@esengine/server';
class MyAdapter implements IDistributedAdapter {
// 生命周期
async connect(): Promise<void> { }
async disconnect(): Promise<void> { }
isConnected(): boolean { return true; }
// 服务器注册
async registerServer(server: ServerRegistration): Promise<void> { }
async unregisterServer(serverId: string): Promise<void> { }
async heartbeat(serverId: string): Promise<void> { }
async getServers(): Promise<ServerRegistration[]> { return []; }
// 房间注册
async registerRoom(room: RoomRegistration): Promise<void> { }
async unregisterRoom(roomId: string): Promise<void> { }
async queryRooms(query: RoomQuery): Promise<RoomRegistration[]> { return []; }
async findAvailableRoom(roomType: string): Promise<RoomRegistration | null> { return null; }
// 状态快照
async saveSnapshot(snapshot: RoomSnapshot): Promise<void> { }
async loadSnapshot(roomId: string): Promise<RoomSnapshot | null> { return null; }
// 发布/订阅
async publish(event: DistributedEvent): Promise<void> { }
async subscribe(pattern: string, handler: Function): Promise<() => void> { return () => {}; }
// 分布式锁
async acquireLock(key: string, ttlMs: number): Promise<boolean> { return true; }
async releaseLock(key: string): Promise<void> { }
}
```
## 玩家路由流程
```
客户端 服务器 A 服务器 B
│ │ │
│─── joinOrCreate ────────►│ │
│ │ │
│ │── findAvailableRoom() ───►│
│ │◄──── 服务器 B 上有房间 ────│
│ │ │
│◄─── redirect: B:3001 ────│ │
│ │ │
│───────────────── 连接到服务器 B ────────────────────►│
│ │ │
│◄─────────────────────────────── 已加入 ─────────────│
```
## 事件类型
分布式系统发布以下事件:
| 事件 | 描述 |
|------|------|
| `server:online` | 服务器上线 |
| `server:offline` | 服务器离线 |
| `server:draining` | 服务器正在排空 |
| `room:created` | 房间已创建 |
| `room:disposed` | 房间已销毁 |
| `room:updated` | 房间信息已更新 |
| `room:message` | 跨服务器房间消息 |
| `room:migrated` | 房间已迁移到其他服务器 |
| `player:joined` | 玩家加入房间 |
| `player:left` | 玩家离开房间 |
## 最佳实践
1. **使用唯一服务器 ID** - 使用主机名、容器 ID 或 UUID
2. **配置合适的心跳** - 在新鲜度和网络开销之间平衡
3. **为有状态房间启用快照** - 确保房间状态在服务器重启后存活
4. **优雅处理重定向** - 客户端应重新连接到目标服务器
```typescript
// 客户端处理重定向
if (response.redirect) {
await client.disconnect();
await client.connect(response.redirect);
await client.joinRoom(roomId);
}
```
5. **使用分布式锁** - 防止 joinOrCreate 中的竞态条件
## 使用 createServer 集成
最简单的使用方式是通过 `createServer` 的 `distributed` 配置:
```typescript
import { createServer } from '@esengine/server';
import { RedisAdapter, Room } from '@esengine/server';
import Redis from 'ioredis';
class GameRoom extends Room {
maxPlayers = 4;
}
const server = await createServer({
port: 3000,
distributed: {
enabled: true,
adapter: new RedisAdapter({ factory: () => new Redis() }),
serverId: 'server-1',
serverAddress: 'ws://192.168.1.100',
serverPort: 3000,
enableFailover: true,
capacity: 100
}
});
server.define('game', GameRoom);
await server.start();
```
当客户端调用 `JoinRoom` API 时,服务器会自动:
1. 查找可用房间(本地或远程)
2. 如果房间在其他服务器,发送 `$redirect` 消息给客户端
3. 客户端收到重定向消息后连接到目标服务器
## 负载均衡
使用 `LoadBalancedRouter` 进行服务器选择:
```typescript
import { LoadBalancedRouter, createLoadBalancedRouter } from '@esengine/server';
// 使用工厂函数
const router = createLoadBalancedRouter('least-players');
// 或直接创建
const router = new LoadBalancedRouter({
strategy: 'least-rooms', // 选择房间数最少的服务器
preferLocal: true // 优先选择本地服务器
});
// 可用策略
// - 'round-robin': 轮询
// - 'least-rooms': 最少房间数
// - 'least-players': 最少玩家数
// - 'random': 随机选择
// - 'weighted': 权重(基于容量使用率)
```
## 故障转移
当服务器离线时,启用 `enableFailover` 后系统会自动:
1. 检测到服务器离线(通过心跳超时)
2. 查询该服务器上的所有房间
3. 使用分布式锁防止多服务器同时恢复
4. 从快照恢复房间状态
5. 发布 `room:migrated` 事件通知其他服务器
```typescript
// 确保定期保存快照
const manager = new DistributedRoomManager(adapter, {
serverId: 'server-1',
serverAddress: 'localhost',
serverPort: 3000,
snapshotInterval: 30000, // 每 30 秒保存快照
enableFailover: true // 启用故障转移
}, sendFn);
```
## 后续版本
- Redis Cluster 支持
- 更多负载均衡策略(地理位置、延迟感知)

View File

@@ -0,0 +1,679 @@
---
title: "HTTP 路由"
description: "HTTP REST API 路由功能,支持与 WebSocket 共用端口"
---
`@esengine/server` 内置了轻量级的 HTTP 路由功能,可以与 WebSocket 服务共用同一端口,方便实现 REST API。
## 快速开始
### 内联路由定义
最简单的方式是在创建服务器时直接定义 HTTP 路由:
```typescript
import { createServer } from '@esengine/server'
const server = await createServer({
port: 3000,
http: {
'/api/health': (req, res) => {
res.json({ status: 'ok', time: Date.now() })
},
'/api/users': {
GET: (req, res) => {
res.json({ users: [] })
},
POST: async (req, res) => {
const body = req.body as { name: string }
res.status(201).json({ id: '1', name: body.name })
}
}
},
cors: true // 启用 CORS
})
await server.start()
```
### 文件路由
对于较大的项目,推荐使用文件路由。创建 `src/http` 目录,每个文件对应一个路由:
```typescript
// src/http/login.ts
import { defineHttp } from '@esengine/server'
interface LoginBody {
username: string
password: string
}
export default defineHttp<LoginBody>({
method: 'POST',
handler(req, res) {
const { username, password } = req.body as LoginBody
// 验证用户...
if (username === 'admin' && password === '123456') {
res.json({ token: 'jwt-token-here', userId: 'user-1' })
} else {
res.error(401, '用户名或密码错误')
}
}
})
```
```typescript
// server.ts
import { createServer } from '@esengine/server'
const server = await createServer({
port: 3000,
httpDir: './src/http', // HTTP 路由目录
httpPrefix: '/api', // 路由前缀
cors: true
})
await server.start()
// 路由: POST /api/login
```
## defineHttp 定义
`defineHttp` 用于定义类型安全的 HTTP 处理器:
```typescript
import { defineHttp } from '@esengine/server'
interface CreateUserBody {
username: string
email: string
password: string
}
export default defineHttp<CreateUserBody>({
// HTTP 方法(默认 POST
method: 'POST',
// 处理函数
handler(req, res) {
const body = req.body as CreateUserBody
// 处理请求...
res.status(201).json({ id: 'new-user-id' })
}
})
```
### 支持的 HTTP 方法
```typescript
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS'
```
## HttpRequest 对象
HTTP 请求对象包含以下属性:
```typescript
interface HttpRequest {
/** 原始 Node.js IncomingMessage */
raw: IncomingMessage
/** HTTP 方法 */
method: string
/** 请求路径 */
path: string
/** 路由参数(从 URL 路径提取,如 /users/:id */
params: Record<string, string>
/** 查询参数 */
query: Record<string, string>
/** 请求头 */
headers: Record<string, string | string[] | undefined>
/** 解析后的请求体 */
body: unknown
/** 客户端 IP */
ip: string
}
```
### 使用示例
```typescript
export default defineHttp({
method: 'GET',
handler(req, res) {
// 获取查询参数
const page = parseInt(req.query.page ?? '1')
const limit = parseInt(req.query.limit ?? '10')
// 获取请求头
const authHeader = req.headers.authorization
// 获取客户端 IP
console.log('Request from:', req.ip)
res.json({ page, limit })
}
})
```
### 请求体解析
请求体会根据 `Content-Type` 自动解析:
- `application/json` - 解析为 JSON 对象
- `application/x-www-form-urlencoded` - 解析为键值对对象
- 其他 - 保持原始字符串
```typescript
export default defineHttp<{ name: string; age: number }>({
method: 'POST',
handler(req, res) {
// body 已自动解析
const { name, age } = req.body as { name: string; age: number }
res.json({ received: { name, age } })
}
})
```
## HttpResponse 对象
HTTP 响应对象提供链式 API
```typescript
interface HttpResponse {
/** 原始 Node.js ServerResponse */
raw: ServerResponse
/** 设置状态码 */
status(code: number): HttpResponse
/** 设置响应头 */
header(name: string, value: string): HttpResponse
/** 发送 JSON 响应 */
json(data: unknown): void
/** 发送文本响应 */
text(data: string): void
/** 发送错误响应 */
error(code: number, message: string): void
}
```
### 使用示例
```typescript
export default defineHttp({
method: 'POST',
handler(req, res) {
// 设置状态码和自定义头
res
.status(201)
.header('X-Custom-Header', 'value')
.json({ created: true })
}
})
```
```typescript
export default defineHttp({
method: 'GET',
handler(req, res) {
// 发送错误响应
res.error(404, '资源不存在')
// 等价于: res.status(404).json({ error: '资源不存在' })
}
})
```
```typescript
export default defineHttp({
method: 'GET',
handler(req, res) {
// 发送纯文本
res.text('Hello, World!')
}
})
```
## 文件路由规范
### 命名转换
文件名会自动转换为路由路径:
| 文件路径 | 路由路径prefix=/api |
|---------|----------------------|
| `login.ts` | `/api/login` |
| `users/profile.ts` | `/api/users/profile` |
| `users/[id].ts` | `/api/users/:id` |
| `game/room/[roomId].ts` | `/api/game/room/:roomId` |
### 动态路由参数
使用 `[param]` 语法定义动态参数:
```typescript
// src/http/users/[id].ts
import { defineHttp } from '@esengine/server'
export default defineHttp({
method: 'GET',
handler(req, res) {
// 直接从 params 获取路由参数
const { id } = req.params
res.json({ userId: id })
}
})
```
多个参数的情况:
```typescript
// src/http/users/[userId]/posts/[postId].ts
import { defineHttp } from '@esengine/server'
export default defineHttp({
method: 'GET',
handler(req, res) {
const { userId, postId } = req.params
res.json({ userId, postId })
}
})
```
### 跳过规则
以下文件会被自动跳过:
-`_` 开头的文件(如 `_helper.ts`
- `index.ts` / `index.js` 文件
-`.ts` / `.js` / `.mts` / `.mjs` 文件
### 目录结构示例
```
src/
└── http/
├── _utils.ts # 跳过(下划线开头)
├── index.ts # 跳过index 文件)
├── health.ts # GET /api/health
├── login.ts # POST /api/login
├── register.ts # POST /api/register
└── users/
├── index.ts # 跳过
├── list.ts # GET /api/users/list
└── [id].ts # GET /api/users/:id
```
## CORS 配置
### 快速启用
```typescript
const server = await createServer({
port: 3000,
cors: true // 使用默认配置
})
```
### 自定义配置
```typescript
const server = await createServer({
port: 3000,
cors: {
// 允许的来源
origin: ['http://localhost:5173', 'https://myapp.com'],
// 或使用通配符
// origin: '*',
// origin: true, // 反射请求来源
// 允许的 HTTP 方法
methods: ['GET', 'POST', 'PUT', 'DELETE'],
// 允许的请求头
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
// 是否允许携带凭证cookies
credentials: true,
// 预检请求缓存时间(秒)
maxAge: 86400
}
})
```
### CorsOptions 类型
```typescript
interface CorsOptions {
/** 允许的来源字符串、字符串数组、true反射或 '*' */
origin?: string | string[] | boolean
/** 允许的 HTTP 方法 */
methods?: string[]
/** 允许的请求头 */
allowedHeaders?: string[]
/** 是否允许携带凭证 */
credentials?: boolean
/** 预检请求缓存时间(秒) */
maxAge?: number
}
```
## 路由合并
文件路由和内联路由可以同时使用,内联路由优先级更高:
```typescript
const server = await createServer({
port: 3000,
httpDir: './src/http',
httpPrefix: '/api',
// 内联路由会与文件路由合并
http: {
'/health': (req, res) => res.json({ status: 'ok' }),
'/api/special': (req, res) => res.json({ special: true })
}
})
```
## 与 WebSocket 共用端口
HTTP 路由与 WebSocket 服务自动共用同一端口:
```typescript
const server = await createServer({
port: 3000,
// WebSocket 相关配置
apiDir: './src/api',
msgDir: './src/msg',
// HTTP 相关配置
httpDir: './src/http',
httpPrefix: '/api',
cors: true
})
await server.start()
// 同一端口 3000
// - WebSocket: ws://localhost:3000
// - HTTP API: http://localhost:3000/api/*
```
## 完整示例
### 游戏服务器登录 API
```typescript
// src/http/auth/login.ts
import { defineHttp } from '@esengine/server'
import { createJwtAuthProvider } from '@esengine/server/auth'
interface LoginRequest {
username: string
password: string
}
interface LoginResponse {
token: string
userId: string
expiresAt: number
}
const jwtProvider = createJwtAuthProvider({
secret: process.env.JWT_SECRET!,
expiresIn: 3600
})
export default defineHttp<LoginRequest>({
method: 'POST',
async handler(req, res) {
const { username, password } = req.body as LoginRequest
// 验证用户
const user = await db.users.findByUsername(username)
if (!user || !await verifyPassword(password, user.passwordHash)) {
res.error(401, '用户名或密码错误')
return
}
// 生成 JWT
const token = jwtProvider.sign({
sub: user.id,
name: user.username,
roles: user.roles
})
const response: LoginResponse = {
token,
userId: user.id,
expiresAt: Date.now() + 3600 * 1000
}
res.json(response)
}
})
```
### 游戏数据查询 API
```typescript
// src/http/game/leaderboard.ts
import { defineHttp } from '@esengine/server'
export default defineHttp({
method: 'GET',
async handler(req, res) {
const limit = parseInt(req.query.limit ?? '10')
const offset = parseInt(req.query.offset ?? '0')
const players = await db.players.findMany({
sort: { score: 'desc' },
limit,
offset
})
res.json({
data: players,
pagination: { limit, offset }
})
}
})
```
## 中间件
### 中间件类型
中间件是在路由处理前后执行的函数:
```typescript
type HttpMiddleware = (
req: HttpRequest,
res: HttpResponse,
next: () => Promise<void>
) => void | Promise<void>
```
### 内置中间件
```typescript
import {
requestLogger,
bodyLimit,
responseTime,
requestId,
securityHeaders
} from '@esengine/server'
const server = await createServer({
port: 3000,
http: { /* ... */ },
// 全局中间件通过 createHttpRouter 配置
})
```
#### requestLogger - 请求日志
```typescript
import { requestLogger } from '@esengine/server'
// 记录请求和响应时间
requestLogger()
// 同时记录请求体
requestLogger({ logBody: true })
```
#### bodyLimit - 请求体大小限制
```typescript
import { bodyLimit } from '@esengine/server'
// 限制请求体为 1MB
bodyLimit(1024 * 1024)
```
#### responseTime - 响应时间头
```typescript
import { responseTime } from '@esengine/server'
// 自动添加 X-Response-Time 响应头
responseTime()
```
#### requestId - 请求 ID
```typescript
import { requestId } from '@esengine/server'
// 自动生成并添加 X-Request-ID 响应头
requestId()
// 自定义头名称
requestId('X-Trace-ID')
```
#### securityHeaders - 安全头
```typescript
import { securityHeaders } from '@esengine/server'
// 添加常用安全响应头
securityHeaders()
// 自定义配置
securityHeaders({
hidePoweredBy: true,
frameOptions: 'DENY',
noSniff: true
})
```
### 自定义中间件
```typescript
import type { HttpMiddleware } from '@esengine/server'
// 认证中间件
const authMiddleware: HttpMiddleware = async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
res.error(401, 'Unauthorized')
return // 不调用 next(),终止请求
}
// 验证 token...
(req as any).userId = 'decoded-user-id'
await next() // 继续执行后续中间件和处理器
}
```
### 使用中间件
#### 使用 createHttpRouter
```typescript
import { createHttpRouter, requestLogger, bodyLimit } from '@esengine/server'
const router = createHttpRouter({
'/api/users': (req, res) => res.json([]),
'/api/admin': {
GET: {
handler: (req, res) => res.json({ admin: true }),
middlewares: [adminAuthMiddleware] // 路由级中间件
}
}
}, {
middlewares: [requestLogger(), bodyLimit(1024 * 1024)], // 全局中间件
timeout: 30000 // 全局超时 30 秒
})
```
## 请求超时
### 全局超时
```typescript
import { createHttpRouter } from '@esengine/server'
const router = createHttpRouter({
'/api/data': async (req, res) => {
// 如果处理超过 30 秒,自动返回 408 Request Timeout
await someSlowOperation()
res.json({ data: 'result' })
}
}, {
timeout: 30000 // 30 秒
})
```
### 路由级超时
```typescript
const router = createHttpRouter({
'/api/quick': (req, res) => res.json({ fast: true }),
'/api/slow': {
POST: {
handler: async (req, res) => {
await verySlowOperation()
res.json({ done: true })
},
timeout: 120000 // 这个路由允许 2 分钟
}
}
}, {
timeout: 10000 // 全局 10 秒(被路由级覆盖)
})
```
## 最佳实践
1. **使用 defineHttp** - 获得更好的类型提示和代码组织
2. **统一错误处理** - 使用 `res.error()` 返回一致的错误格式
3. **启用 CORS** - 前后端分离时必须配置
4. **目录组织** - 按功能模块组织 HTTP 路由文件
5. **验证输入** - 始终验证 `req.body``req.query` 的内容
6. **状态码规范** - 遵循 HTTP 状态码规范200、201、400、401、404、500 等)
7. **使用中间件** - 通过中间件实现认证、日志、限流等横切关注点
8. **设置超时** - 避免慢请求阻塞服务器

View File

@@ -147,6 +147,7 @@ service.on('chat', (data) => {
- [客户端使用](/modules/network/client/) - NetworkPlugin、组件和系统
- [服务器端](/modules/network/server/) - GameServer 和 Room 管理
- [分布式房间](/modules/network/distributed/) - 多服务器房间管理和玩家路由
- [状态同步](/modules/network/sync/) - 插值和快照缓冲
- [客户端预测](/modules/network/prediction/) - 输入预测和服务器校正
- [兴趣区域 (AOI)](/modules/network/aoi/) - 视野过滤和带宽优化

View File

@@ -79,10 +79,47 @@ await server.start()
| `tickRate` | `number` | `20` | 全局 Tick 频率 (Hz) |
| `apiDir` | `string` | `'src/api'` | API 处理器目录 |
| `msgDir` | `string` | `'src/msg'` | 消息处理器目录 |
| `httpDir` | `string` | `'src/http'` | HTTP 路由目录 |
| `httpPrefix` | `string` | `'/api'` | HTTP 路由前缀 |
| `cors` | `boolean \| CorsOptions` | - | CORS 配置 |
| `onStart` | `(port) => void` | - | 启动回调 |
| `onConnect` | `(conn) => void` | - | 连接回调 |
| `onDisconnect` | `(conn) => void` | - | 断开回调 |
## HTTP 路由
支持 HTTP API 与 WebSocket 共用端口,适用于登录、注册等场景。
```typescript
const server = await createServer({
port: 3000,
httpDir: './src/http', // HTTP 路由目录
httpPrefix: '/api', // 路由前缀
cors: true,
// 或内联定义
http: {
'/health': (req, res) => res.json({ status: 'ok' })
}
})
```
```typescript
// src/http/login.ts
import { defineHttp } from '@esengine/server'
export default defineHttp<{ username: string; password: string }>({
method: 'POST',
handler(req, res) {
const { username, password } = req.body
// 验证并返回 token...
res.json({ token: '...' })
}
})
```
> 详细文档请参考 [HTTP 路由](/modules/network/http)
## Room 系统
Room 是游戏房间的基类,管理玩家和游戏状态。
@@ -243,6 +280,122 @@ class GameRoom extends Room {
}
```
## Schema 验证
使用内置的 Schema 验证系统进行运行时类型验证:
### 基础用法
```typescript
import { s, defineApiWithSchema } from '@esengine/server'
// 定义 Schema
const MoveSchema = s.object({
x: s.number(),
y: s.number(),
speed: s.number().optional()
})
// 类型自动推断
type Move = s.infer<typeof MoveSchema> // { x: number; y: number; speed?: number }
// 使用 Schema 定义 API自动验证
export default defineApiWithSchema(MoveSchema, {
handler(req, ctx) {
// req 已验证,类型安全
console.log(req.x, req.y)
}
})
```
### 验证器类型
| 类型 | 示例 | 描述 |
|------|------|------|
| `s.string()` | `s.string().min(1).max(50)` | 字符串,支持长度限制 |
| `s.number()` | `s.number().min(0).int()` | 数字,支持范围和整数限制 |
| `s.boolean()` | `s.boolean()` | 布尔值 |
| `s.literal()` | `s.literal('admin')` | 字面量类型 |
| `s.object()` | `s.object({ name: s.string() })` | 对象 |
| `s.array()` | `s.array(s.number())` | 数组 |
| `s.enum()` | `s.enum(['a', 'b'] as const)` | 枚举 |
| `s.union()` | `s.union([s.string(), s.number()])` | 联合类型 |
| `s.record()` | `s.record(s.any())` | 记录类型 |
### 修饰符
```typescript
// 可选字段
s.string().optional()
// 默认值
s.number().default(0)
// 可为 null
s.string().nullable()
// 字符串验证
s.string().min(1).max(100).email().url().regex(/^[a-z]+$/)
// 数字验证
s.number().min(0).max(100).int().positive()
// 数组验证
s.array(s.string()).min(1).max(10).nonempty()
// 对象验证
s.object({ ... }).strict() // 不允许额外字段
s.object({ ... }).partial() // 所有字段可选
s.object({ ... }).pick('name', 'age') // 选择字段
s.object({ ... }).omit('password') // 排除字段
```
### 消息验证
```typescript
import { s, defineMsgWithSchema } from '@esengine/server'
const InputSchema = s.object({
keys: s.array(s.string()),
timestamp: s.number()
})
export default defineMsgWithSchema(InputSchema, {
handler(msg, ctx) {
// msg 已验证
console.log(msg.keys, msg.timestamp)
}
})
```
### 手动验证
```typescript
import { s, parse, safeParse, createGuard } from '@esengine/server'
const UserSchema = s.object({
name: s.string(),
age: s.number().int().min(0)
})
// 抛出错误
const user = parse(UserSchema, data)
// 返回结果对象
const result = safeParse(UserSchema, data)
if (result.success) {
console.log(result.data)
} else {
console.error(result.error)
}
// 类型守卫
const isUser = createGuard(UserSchema)
if (isUser(data)) {
// data 是 User 类型
}
```
## 协议定义
`src/shared/protocol.ts` 中定义客户端和服务端共享的类型:

View File

@@ -125,23 +125,24 @@ tx:data:{key} - 业务数据
## MongoStorage
MongoDB 存储,适用于需要持久化和复杂查询的场景。使用工厂模式实现惰性连接。
MongoDB 存储,适用于需要持久化和复杂查询的场景。使用 `@esengine/database-drivers` 的共享连接。
```typescript
import { MongoClient } from 'mongodb';
import { MongoStorage } from '@esengine/transaction';
import { createMongoConnection } from '@esengine/database-drivers';
import { createMongoStorage, TransactionManager } from '@esengine/transaction';
// 工厂模式:惰性连接,首次操作时才创建连接
const storage = new MongoStorage({
factory: async () => {
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
return client;
},
database: 'game',
transactionCollection: 'transactions', // 事务日志集合
dataCollection: 'transaction_data', // 业务数据集合
lockCollection: 'transaction_locks', // 锁集合
// 创建共享连接
const mongo = createMongoConnection({
uri: 'mongodb://localhost:27017',
database: 'game'
});
await mongo.connect();
// 使用共享连接创建存储
const storage = createMongoStorage(mongo, {
transactionCollection: 'transactions', // 事务日志集合(可选)
dataCollection: 'transaction_data', // 业务数据集合(可选)
lockCollection: 'transaction_locks', // 锁集合(可选)
});
// 创建索引(首次运行时执行)
@@ -149,11 +150,14 @@ await storage.ensureIndexes();
const manager = new TransactionManager({ storage });
// 使用后关闭连接
// 关闭存储(不会关闭共享连接
await storage.close();
// 或使用 await using 自动关闭 (TypeScript 5.2+)
await using storage = new MongoStorage({ ... });
// 共享连接可继续用于其他模块
const userRepo = new UserRepository(mongo); // @esengine/database
// 最后关闭共享连接
await mongo.disconnect();
```
### 特点

View File

@@ -13,6 +13,7 @@
"packages/network-ext/*",
"packages/editor/*",
"packages/editor/plugins/*",
"packages/devtools/*",
"packages/rust/*",
"packages/tools/*"
],
@@ -74,6 +75,7 @@
"lint:fix": "turbo run lint:fix",
"build:wasm": "cd packages/rust/engine && wasm-pack build --dev --out-dir pkg",
"build:wasm:release": "cd packages/rust/engine && wasm-pack build --release --out-dir pkg",
"build:rapier2d": "node scripts/build-rapier2d.mjs",
"copy-modules": "node scripts/copy-engine-modules.mjs"
},
"author": "yhh",

View File

@@ -0,0 +1,21 @@
# @esengine/node-editor
## 1.2.0
### Minor Changes
- [#430](https://github.com/esengine/esengine/pull/430) [`caf3be7`](https://github.com/esengine/esengine/commit/caf3be72cdcc730492c63abe5f1715893f3579ac) Thanks [@esengine](https://github.com/esengine)! - feat(node-editor): 添加 Shadow DOM 样式注入支持 | Add Shadow DOM style injection support
**@esengine/node-editor**
- 新增 `nodeEditorCssText` 导出,包含所有编辑器样式的 CSS 文本 | Added `nodeEditorCssText` export containing all editor styles as CSS text
- 新增 `injectNodeEditorStyles(root)` 函数,支持将样式注入到 Shadow DOM | Added `injectNodeEditorStyles(root)` function for injecting styles into Shadow DOM
- 支持在 Cocos Creator 等使用 Shadow DOM 的环境中使用 | Support usage in Shadow DOM environments like Cocos Creator
## 1.1.0
### Minor Changes
- [#426](https://github.com/esengine/esengine/pull/426) [`6970394`](https://github.com/esengine/esengine/commit/6970394717ab8f743b0a41e248e3404a3b6fc7dc) Thanks [@esengine](https://github.com/esengine)! - feat: 独立发布节点编辑器 | Standalone node editor release
- 移动到 packages/devtools 目录 | Move to packages/devtools directory
- 清理依赖,使包可独立使用 | Clean dependencies for standalone use
- 可用于 Cocos Creator / LayaAir 插件开发 | Available for Cocos/Laya plugin development

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/node-editor",
"version": "1.0.0",
"version": "1.2.0",
"description": "Universal node-based visual editor for blueprint, shader graph, and state machine",
"main": "dist/index.js",
"module": "dist/index.js",
@@ -9,7 +9,8 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./styles": {
"import": "./dist/styles/index.css"
@@ -30,17 +31,18 @@
"blueprint",
"shader-graph",
"state-machine",
"ecs",
"game-engine"
"react"
],
"author": "yhh",
"author": "ESEngine Team",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0"
},
"devDependencies": {
"react": "^18.3.1",
"zustand": "^5.0.8",
"@types/node": "^20.19.17",
"@types/react": "^18.3.12",
"@vitejs/plugin-react": "^4.7.0",
"react": "^18.3.1",
"rimraf": "^5.0.0",
"typescript": "^5.8.3",
"vite": "^6.0.7",
@@ -56,7 +58,6 @@
"repository": {
"type": "git",
"url": "https://github.com/esengine/esengine.git",
"directory": "packages/node-editor"
},
"private": true
"directory": "packages/devtools/node-editor"
}
}

View File

@@ -1,4 +1,4 @@
import React, { useRef, useCallback, useState, useMemo } from 'react';
import React, { useRef, useCallback, useState, useMemo, useEffect } from 'react';
import { Graph } from '../../domain/models/Graph';
import { GraphNode, NodeTemplate } from '../../domain/models/GraphNode';
import { Connection } from '../../domain/models/Connection';
@@ -127,6 +127,18 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
const [connectionDrag, setConnectionDrag] = useState<ConnectionDragState | null>(null);
const [hoveredPin, setHoveredPin] = useState<Pin | null>(null);
// Force re-render after mount to ensure connections are drawn correctly
// 挂载后强制重渲染以确保连接线正确绘制
const [, forceUpdate] = useState(0);
useEffect(() => {
// Use requestAnimationFrame to wait for DOM to be fully rendered
// 使用 requestAnimationFrame 等待 DOM 完全渲染
const rafId = requestAnimationFrame(() => {
forceUpdate(n => n + 1);
});
return () => cancelAnimationFrame(rafId);
}, [graph.id]);
/**
* Converts screen coordinates to canvas coordinates
*

View File

@@ -10,6 +10,9 @@
// Import styles (导入样式)
import './styles/index.css';
// CSS utilities for Shadow DOM (Shadow DOM 的 CSS 工具)
export { nodeEditorCssText, injectNodeEditorStyles } from './styles/cssText';
// Domain models (领域模型)
export {
// Models

View File

@@ -0,0 +1,55 @@
/**
* @zh 节点编辑器 CSS 样式文本
* @en Node Editor CSS style text
*
* @zh 此文件在构建时由 vite 插件自动生成
* @en This file is auto-generated by vite plugin during build
*/
// Placeholder - will be replaced by vite plugin during build
export const nodeEditorCssText = '__NODE_EDITOR_CSS_PLACEHOLDER__';
/**
* @zh 将 CSS 注入到指定的根节点(支持 Shadow DOM
* @en Inject CSS into specified root node (supports Shadow DOM)
*
* @param root - @zh 目标根节点Document 或 ShadowRoot@en Target root node (Document or ShadowRoot)
* @param styleId - @zh 样式标签的 ID @en ID for the style tag
* @returns @zh 创建的 style 元素 @en The created style element
*
* @example
* ```typescript
* // Inject into Shadow DOM
* const shadowRoot = element.attachShadow({ mode: 'open' });
* injectNodeEditorStyles(shadowRoot);
*
* // Inject into document (with custom ID)
* injectNodeEditorStyles(document, 'my-editor-styles');
* ```
*/
export function injectNodeEditorStyles(
root: Document | ShadowRoot | DocumentFragment,
styleId: string = 'esengine-node-editor-styles'
): HTMLStyleElement | null {
// Check if already injected
const existingStyle = (root as any).getElementById?.(styleId) ||
(root as any).querySelector?.(`#${styleId}`);
if (existingStyle) {
return existingStyle as HTMLStyleElement;
}
// Create and inject style element
const style = document.createElement('style');
style.id = styleId;
style.textContent = nodeEditorCssText;
if ('head' in root) {
// Document
(root as Document).head.appendChild(style);
} else {
// ShadowRoot or DocumentFragment
root.appendChild(style);
}
return style;
}

View File

@@ -4,12 +4,14 @@ import dts from 'vite-plugin-dts';
import react from '@vitejs/plugin-react';
/**
* Custom plugin: Convert CSS to self-executing style injection code
* CSS
* Custom plugin: Handle CSS for node editor
* CSS
*
* This plugin does two things:
* 1. Auto-injects CSS into document.head for normal usage
* 2. Replaces placeholder in cssText.ts with actual CSS for Shadow DOM usage
*/
function injectCSSPlugin(): any {
let cssCounter = 0;
return {
name: 'inject-css-plugin',
enforce: 'post' as const,
@@ -23,19 +25,28 @@ function injectCSSPlugin(): any {
const cssChunk = bundle[cssFile];
if (!cssChunk || !cssChunk.source) continue;
const cssContent = cssChunk.source;
const styleId = `esengine-node-editor-style-${cssCounter++}`;
const cssContent = cssChunk.source as string;
const styleId = 'esengine-node-editor-styles';
// Generate style injection code (生成样式注入代码)
const injectCode = `(function(){if(typeof document!=='undefined'){var s=document.createElement('style');s.id='${styleId}';if(!document.getElementById(s.id)){s.textContent=${JSON.stringify(cssContent)};document.head.appendChild(s);}}})();`;
// Inject into index.js (注入到 index.js)
// Process all JS bundles (处理所有 JS 包)
for (const jsKey of bundleKeys) {
if (!jsKey.endsWith('.js')) continue;
if (!jsKey.endsWith('.js') && !jsKey.endsWith('.cjs')) continue;
const jsChunk = bundle[jsKey];
if (!jsChunk || jsChunk.type !== 'chunk' || !jsChunk.code) continue;
if (jsKey === 'index.js') {
// Replace CSS placeholder with actual CSS content
// 将 CSS 占位符替换为实际的 CSS 内容
// Match both single and double quotes (ESM uses single, CJS uses double)
jsChunk.code = jsChunk.code.replace(
/['"]__NODE_EDITOR_CSS_PLACEHOLDER__['"]/g,
JSON.stringify(cssContent)
);
// Auto-inject CSS for index bundles (为 index 包自动注入 CSS)
if (jsKey === 'index.js' || jsKey === 'index.cjs') {
jsChunk.code = injectCode + '\n' + jsChunk.code;
}
}
@@ -65,8 +76,11 @@ export default defineConfig({
entry: {
index: resolve(__dirname, 'src/index.ts')
},
formats: ['es'],
fileName: (format, entryName) => `${entryName}.js`
formats: ['es', 'cjs'],
fileName: (format, entryName) => {
if (format === 'cjs') return `${entryName}.cjs`;
return `${entryName}.js`;
}
},
rollupOptions: {
external: [

View File

@@ -8,12 +8,23 @@ Before running the editor, ensure you have the following installed:
- **Node.js** >= 18.x
- **pnpm** >= 10.x
- **Rust** >= 1.70 (for Tauri)
- **Rust** >= 1.70 (for Tauri and WASM builds)
- **wasm-pack** (for building Rapier2D physics engine)
- **Platform-specific dependencies**:
- **Windows**: Microsoft Visual Studio C++ Build Tools
- **macOS**: Xcode Command Line Tools (`xcode-select --install`)
- **Linux**: See [Tauri prerequisites](https://tauri.app/v1/guides/getting-started/prerequisites)
### Installing wasm-pack
```bash
# Using cargo
cargo install wasm-pack
# Or using the official installer script (Linux/macOS)
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
```
## Quick Start
### 1. Clone and Install
@@ -24,7 +35,23 @@ cd esengine
pnpm install
```
### 2. Build Dependencies
### 2. Build Rapier2D WASM
The editor depends on Rapier2D physics engine WASM artifacts. First-time setup only requires one command:
```bash
pnpm build:rapier2d
```
This command automatically:
1. Prepares the Rust project
2. Builds WASM
3. Copies artifacts to `packages/physics/rapier2d/pkg`
4. Generates TypeScript source code
> **Note**: Requires Rust and wasm-pack to be installed.
### 3. Build Editor
From the project root:
@@ -32,7 +59,7 @@ From the project root:
pnpm build:editor
```
### 3. Run Editor
### 4. Run Editor
```bash
cd packages/editor/editor-app
@@ -43,6 +70,8 @@ pnpm tauri:dev
| Script | Description |
|--------|-------------|
| `pnpm build:rapier2d` | Build Rapier2D WASM (required for first-time setup) |
| `pnpm build:editor` | Build editor and all dependencies |
| `pnpm tauri:dev` | Run editor in development mode with hot-reload |
| `pnpm tauri:build` | Build production application |
| `pnpm build:sdk` | Build editor-runtime SDK |
@@ -62,6 +91,17 @@ editor-app/
## Troubleshooting
### Rapier2D WASM Build Failed
**Error**: `Could not resolve "../pkg/rapier_wasm2d"`
**Cause**: Missing Rapier2D WASM artifacts.
**Solution**:
1. Ensure `wasm-pack` is installed: `cargo install wasm-pack`
2. Run `pnpm build:rapier2d`
3. Verify `packages/physics/rapier2d/pkg/` directory exists and contains `rapier_wasm2d_bg.wasm` file
### Build Errors
```bash
@@ -76,6 +116,12 @@ pnpm build:editor
rustup update
```
### Windows Users Building WASM
The `pnpm build:rapier2d` script works directly on Windows. If you encounter issues:
1. Use Git Bash or WSL
2. Or download pre-built WASM artifacts from [Releases](https://github.com/esengine/esengine/releases)
## Documentation
- [ESEngine Documentation](https://esengine.cn/)

View File

@@ -8,12 +8,23 @@
- **Node.js** >= 18.x
- **pnpm** >= 10.x
- **Rust** >= 1.70 (Tauri 需要)
- **Rust** >= 1.70 (Tauri 和 WASM 构建需要)
- **wasm-pack** (构建 Rapier2D 物理引擎需要)
- **平台相关依赖**
- **Windows**: Microsoft Visual Studio C++ Build Tools
- **macOS**: Xcode Command Line Tools (`xcode-select --install`)
- **Linux**: 参考 [Tauri 环境配置](https://tauri.app/v1/guides/getting-started/prerequisites)
### 安装 wasm-pack
```bash
# 使用 cargo 安装
cargo install wasm-pack
# 或使用官方安装脚本 (Linux/macOS)
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
```
## 快速开始
### 1. 克隆并安装
@@ -24,7 +35,23 @@ cd esengine
pnpm install
```
### 2. 构建依赖
### 2. 构建 Rapier2D WASM
编辑器依赖 Rapier2D 物理引擎的 WASM 产物。首次构建只需执行一条命令:
```bash
pnpm build:rapier2d
```
该命令会自动完成以下步骤:
1. 准备 Rust 项目
2. 构建 WASM
3. 复制产物到 `packages/physics/rapier2d/pkg`
4. 生成 TypeScript 源码
> **注意**:需要已安装 Rust 和 wasm-pack。
### 3. 构建编辑器
在项目根目录执行:
@@ -32,7 +59,7 @@ pnpm install
pnpm build:editor
```
### 3. 启动编辑器
### 4. 启动编辑器
```bash
cd packages/editor/editor-app
@@ -43,6 +70,8 @@ pnpm tauri:dev
| 脚本 | 说明 |
|------|------|
| `pnpm build:rapier2d` | 构建 Rapier2D WASM首次构建必须执行|
| `pnpm build:editor` | 构建编辑器及所有依赖 |
| `pnpm tauri:dev` | 开发模式运行编辑器(支持热重载)|
| `pnpm tauri:build` | 构建生产版本应用 |
| `pnpm build:sdk` | 构建 editor-runtime SDK |
@@ -62,6 +91,17 @@ editor-app/
## 常见问题
### Rapier2D WASM 构建失败
**错误**: `Could not resolve "../pkg/rapier_wasm2d"`
**原因**: 缺少 Rapier2D 的 WASM 产物。
**解决方案**:
1. 确保已安装 `wasm-pack``cargo install wasm-pack`
2. 执行 `pnpm build:rapier2d`
3. 确认 `packages/physics/rapier2d/pkg/` 目录存在且包含 `rapier_wasm2d_bg.wasm` 文件
### 构建错误
```bash
@@ -76,6 +116,12 @@ pnpm build:editor
rustup update
```
### Windows 用户构建 WASM
`pnpm build:rapier2d` 脚本在 Windows 上可以直接运行。如果遇到问题:
1. 使用 Git Bash 或 WSL
2. 或从 [Releases](https://github.com/esengine/esengine/releases) 下载预编译的 WASM 产物
## 文档
- [ESEngine 文档](https://esengine.cn/)

View File

@@ -8,7 +8,10 @@
"rootDir": "./src",
"jsx": "react-jsx",
"skipLibCheck": true,
"moduleResolution": "bundler"
"moduleResolution": "bundler",
"paths": {
"@esengine/asset-system": ["../../../engine/asset-system/src"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]

View File

@@ -2,8 +2,7 @@ import { defineConfig } from 'tsup';
import { editorOnlyPreset } from '../../../tools/build-config/src/presets/plugin-tsup';
export default defineConfig({
...editorOnlyPreset({
external: ['@esengine/asset-system']
}),
tsconfig: 'tsconfig.build.json'
...editorOnlyPreset({}),
tsconfig: 'tsconfig.build.json',
noExternal: ['@esengine/asset-system']
});

View File

@@ -1,5 +1,86 @@
# @esengine/behavior-tree
## 4.2.0
### Minor Changes
- [#408](https://github.com/esengine/esengine/pull/408) [`b9ea8d1`](https://github.com/esengine/esengine/commit/b9ea8d14cf38e1480f638c229f9ee150b65f0c60) Thanks [@esengine](https://github.com/esengine)! - feat: add action() and condition() methods to BehaviorTreeBuilder
Added new methods to support custom executor types directly in the builder:
- `action(implementationType, name?, config?)` - Use custom action executors registered via `@NodeExecutorMetadata`
- `condition(implementationType, name?, config?)` - Use custom condition executors
This provides a cleaner API for using custom node executors compared to the existing `executeAction()` which only supports blackboard functions.
Example:
```typescript
// Define custom executor
@NodeExecutorMetadata({
implementationType: 'AttackAction',
nodeType: NodeType.Action,
displayName: 'Attack',
category: 'Combat'
})
class AttackAction implements INodeExecutor {
execute(context: NodeExecutionContext): TaskStatus {
return TaskStatus.Success;
}
}
// Use in builder
const tree = BehaviorTreeBuilder.create('AI')
.selector('Root')
.action('AttackAction', 'Attack', { damage: 50 })
.end()
.build();
```
## 4.1.2
### Patch Changes
- [#406](https://github.com/esengine/esengine/pull/406) [`0de4527`](https://github.com/esengine/esengine/commit/0de45279e612c04ae9be7fbd65ce496e4797a43c) Thanks [@esengine](https://github.com/esengine)! - fix(behavior-tree): export NodeExecutorMetadata as value instead of type
Fixed the export of `NodeExecutorMetadata` decorator in `execution/index.ts`.
Previously it was exported as `export type { NodeExecutorMetadata }` which only
exported the type signature, not the actual function. This caused runtime errors
in Cocos Creator: "TypeError: (intermediate value) is not a function".
Changed to `export { NodeExecutorMetadata }` to properly export the decorator function.
## 4.1.1
### Patch Changes
- Updated dependencies [[`3e5b778`](https://github.com/esengine/esengine/commit/3e5b7783beec08e247f7525184935401923ecde8)]:
- @esengine/ecs-framework@2.7.1
## 4.1.0
### Minor Changes
- [#400](https://github.com/esengine/esengine/pull/400) [`d2af9ca`](https://github.com/esengine/esengine/commit/d2af9caae9d5620c5f690272ab80dc246e9b7e10) Thanks [@esengine](https://github.com/esengine)! - feat(behavior-tree): add pure BehaviorTreePlugin class for Cocos/Laya integration
- Added `BehaviorTreePlugin` class that only depends on `@esengine/ecs-framework`
- Implements `IPlugin` interface with `install()`, `uninstall()`, and `setupScene()` methods
- Removed `esengine/` subdirectory that incorrectly depended on `@esengine/engine-core`
- Updated package documentation with correct usage examples
Usage:
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import { BehaviorTreePlugin, BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
Core.create();
const plugin = new BehaviorTreePlugin();
await Core.installPlugin(plugin);
const scene = new Scene();
plugin.setupScene(scene);
Core.setScene(scene);
```
## 4.0.0
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/behavior-tree",
"version": "4.0.0",
"version": "4.2.0",
"description": "ECS-based AI behavior tree system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
"main": "dist/index.js",
"module": "dist/index.js",
@@ -29,7 +29,8 @@
"clean": "rimraf dist tsconfig.tsbuildinfo",
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit"
"type-check": "tsc --noEmit",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
},
"author": "yhh",
"license": "MIT",

View File

@@ -181,12 +181,73 @@ export class BehaviorTreeBuilder {
}
/**
* 添加执行动作
* 添加执行动作(通过黑板函数)
*
* @zh 使用黑板中的 action_{actionName} 函数执行动作
* @en Execute action using action_{actionName} function from blackboard
*
* @example
* ```typescript
* BehaviorTreeBuilder.create("AI")
* .defineBlackboardVariable("action_Attack", (entity) => TaskStatus.Success)
* .selector("Root")
* .executeAction("Attack")
* .end()
* .build();
* ```
*/
executeAction(actionName: string, name?: string): BehaviorTreeBuilder {
return this.addActionNode('ExecuteAction', name || 'ExecuteAction', { actionName });
}
/**
* 添加自定义动作节点
*
* @zh 直接使用注册的执行器类型(通过 @NodeExecutorMetadata 装饰器注册的类)
* @en Use a registered executor type directly (class registered via @NodeExecutorMetadata decorator)
*
* @param implementationType - 执行器类型名称(@NodeExecutorMetadata 中的 implementationType
* @param name - 节点显示名称
* @param config - 节点配置参数
*
* @example
* ```typescript
* // 1. 定义自定义执行器
* @NodeExecutorMetadata({
* implementationType: 'AttackAction',
* nodeType: NodeType.Action,
* displayName: '攻击动作',
* category: 'Action'
* })
* class AttackAction implements INodeExecutor {
* execute(context: NodeExecutionContext): TaskStatus {
* console.log("执行攻击!");
* return TaskStatus.Success;
* }
* }
*
* // 2. 在行为树中使用
* BehaviorTreeBuilder.create("AI")
* .selector("Root")
* .action("AttackAction", "Attack")
* .end()
* .build();
* ```
*/
action(implementationType: string, name?: string, config?: Record<string, any>): BehaviorTreeBuilder {
return this.addActionNode(implementationType, name || implementationType, config || {});
}
/**
* 添加自定义条件节点
*
* @zh 直接使用注册的条件执行器类型
* @en Use a registered condition executor type directly
*/
condition(implementationType: string, name?: string, config?: Record<string, any>): BehaviorTreeBuilder {
return this.addConditionNode(implementationType, name || implementationType, config || {});
}
/**
* 添加黑板比较条件
*/

View File

@@ -0,0 +1,118 @@
import type { Core, ServiceContainer, IPlugin, IScene } from '@esengine/ecs-framework';
import { BehaviorTreeExecutionSystem } from './execution/BehaviorTreeExecutionSystem';
import { BehaviorTreeAssetManager } from './execution/BehaviorTreeAssetManager';
import { GlobalBlackboardService } from './Services/GlobalBlackboardService';
/**
* @zh 行为树插件
* @en Behavior Tree Plugin
*
* @zh 为 ECS 框架提供行为树支持的插件。
* 可与任何基于 @esengine/ecs-framework 的引擎集成Cocos、Laya、Node.js 等)。
*
* @en Plugin that provides behavior tree support for ECS framework.
* Can be integrated with any engine based on @esengine/ecs-framework (Cocos, Laya, Node.js, etc.).
*
* @example
* ```typescript
* import { Core, Scene } from '@esengine/ecs-framework';
* import { BehaviorTreePlugin, BehaviorTreeBuilder, BehaviorTreeStarter } from '@esengine/behavior-tree';
*
* // Initialize
* Core.create();
* const plugin = new BehaviorTreePlugin();
* await Core.installPlugin(plugin);
*
* // Setup scene
* const scene = new Scene();
* plugin.setupScene(scene);
* Core.setScene(scene);
*
* // Create and start behavior tree
* const tree = BehaviorTreeBuilder.create('MyAI')
* .selector('Root')
* .log('Hello from behavior tree!')
* .end()
* .build();
*
* const entity = scene.createEntity('AIEntity');
* BehaviorTreeStarter.start(entity, tree);
* ```
*/
export class BehaviorTreePlugin implements IPlugin {
/**
* @zh 插件名称
* @en Plugin name
*/
readonly name = '@esengine/behavior-tree';
/**
* @zh 插件版本
* @en Plugin version
*/
readonly version = '1.0.0';
/**
* @zh 插件依赖
* @en Plugin dependencies
*/
readonly dependencies: readonly string[] = [];
private _services: ServiceContainer | null = null;
/**
* @zh 安装插件
* @en Install plugin
*
* @param _core - Core 实例
* @param services - 服务容器
*/
install(_core: Core, services: ServiceContainer): void {
this._services = services;
// Register services
if (!services.isRegistered(GlobalBlackboardService)) {
services.registerSingleton(GlobalBlackboardService);
}
if (!services.isRegistered(BehaviorTreeAssetManager)) {
services.registerSingleton(BehaviorTreeAssetManager);
}
}
/**
* @zh 卸载插件
* @en Uninstall plugin
*/
uninstall(): void {
if (this._services) {
const assetManager = this._services.tryResolve(BehaviorTreeAssetManager);
if (assetManager) {
assetManager.dispose();
}
const blackboardService = this._services.tryResolve(GlobalBlackboardService);
if (blackboardService) {
blackboardService.dispose();
}
}
this._services = null;
}
/**
* @zh 设置场景,添加行为树执行系统
* @en Setup scene, add behavior tree execution system
*
* @param scene - 要设置的场景
*
* @example
* ```typescript
* const scene = new Scene();
* plugin.setupScene(scene);
* Core.setScene(scene);
* ```
*/
setupScene(scene: IScene): void {
const system = new BehaviorTreeExecutionSystem(this._services ?? undefined);
scene.addSystem(system);
}
}

View File

@@ -1,82 +0,0 @@
/**
* @zh ESEngine 资产加载器
* @en ESEngine asset loader
*
* @zh 实现 IAssetLoader 接口,用于通过 AssetManager 加载行为树文件。
* 此文件仅在使用 ESEngine 时需要。
*
* @en Implements IAssetLoader interface for loading behavior tree files via AssetManager.
* This file is only needed when using ESEngine.
*/
import type {
IAssetLoader,
IAssetParseContext,
IAssetContent,
AssetContentType
} from '@esengine/asset-system';
import { Core } from '@esengine/ecs-framework';
import { BehaviorTreeData } from '../execution/BehaviorTreeData';
import { BehaviorTreeAssetManager } from '../execution/BehaviorTreeAssetManager';
import { EditorToBehaviorTreeDataConverter } from '../Serialization/EditorToBehaviorTreeDataConverter';
import { BehaviorTreeAssetType } from '../constants';
/**
* @zh 行为树资产接口
* @en Behavior tree asset interface
*/
export interface IBehaviorTreeAsset {
/** @zh 行为树数据 @en Behavior tree data */
data: BehaviorTreeData;
/** @zh 文件路径 @en File path */
path: string;
}
/**
* @zh 行为树加载器
* @en Behavior tree loader implementing IAssetLoader interface
*/
export class BehaviorTreeLoader implements IAssetLoader<IBehaviorTreeAsset> {
readonly supportedType = BehaviorTreeAssetType;
readonly supportedExtensions = ['.btree'];
readonly contentType: AssetContentType = 'text';
/**
* @zh 从内容解析行为树资产
* @en Parse behavior tree asset from content
*/
async parse(content: IAssetContent, context: IAssetParseContext): Promise<IBehaviorTreeAsset> {
if (!content.text) {
throw new Error('Behavior tree content is empty');
}
// Convert to runtime data
const treeData = EditorToBehaviorTreeDataConverter.fromEditorJSON(content.text);
// Use file path as ID
const assetPath = context.metadata.path;
treeData.id = assetPath;
// Also register to BehaviorTreeAssetManager for legacy code
const btAssetManager = Core.services.tryResolve(BehaviorTreeAssetManager);
if (btAssetManager) {
btAssetManager.loadAsset(treeData);
}
return {
data: treeData,
path: assetPath
};
}
/**
* @zh 释放资产
* @en Dispose asset
*/
dispose(asset: IBehaviorTreeAsset): void {
const btAssetManager = Core.services.tryResolve(BehaviorTreeAssetManager);
if (btAssetManager && asset.data) {
btAssetManager.unloadAsset(asset.data.id);
}
}
}

View File

@@ -1,93 +0,0 @@
/**
* @zh ESEngine 集成模块
* @en ESEngine integration module
*
* @zh 此文件包含与 ESEngine 引擎核心集成的代码。
* 使用 Cocos/Laya 等其他引擎时不需要此文件。
*
* @en This file contains code for integrating with ESEngine engine-core.
* Not needed when using other engines like Cocos/Laya.
*/
import type { IScene, ServiceContainer, IComponentRegistry } from '@esengine/ecs-framework';
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
import { AssetManagerToken } from '@esengine/asset-system';
import { BehaviorTreeRuntimeComponent } from '../execution/BehaviorTreeRuntimeComponent';
import { BehaviorTreeExecutionSystem } from '../execution/BehaviorTreeExecutionSystem';
import { BehaviorTreeAssetManager } from '../execution/BehaviorTreeAssetManager';
import { GlobalBlackboardService } from '../Services/GlobalBlackboardService';
import { BehaviorTreeLoader } from './BehaviorTreeLoader';
import { BehaviorTreeAssetType } from '../constants';
import { BehaviorTreeSystemToken } from '../tokens';
// Re-export tokens for ESEngine users
export { BehaviorTreeSystemToken } from '../tokens';
class BehaviorTreeRuntimeModule implements IRuntimeModule {
private _loaderRegistered = false;
registerComponents(registry: IComponentRegistry): void {
registry.register(BehaviorTreeRuntimeComponent);
}
registerServices(services: ServiceContainer): void {
if (!services.isRegistered(GlobalBlackboardService)) {
services.registerSingleton(GlobalBlackboardService);
}
if (!services.isRegistered(BehaviorTreeAssetManager)) {
services.registerSingleton(BehaviorTreeAssetManager);
}
}
createSystems(scene: IScene, context: SystemContext): void {
// Get dependencies from service registry
const assetManager = context.services.get(AssetManagerToken);
if (!this._loaderRegistered && assetManager) {
assetManager.registerLoader(BehaviorTreeAssetType, new BehaviorTreeLoader());
this._loaderRegistered = true;
}
// Use ECS service container from context.services
const ecsServices = (context as { ecsServices?: ServiceContainer }).ecsServices;
const behaviorTreeSystem = new BehaviorTreeExecutionSystem(ecsServices);
if (assetManager) {
behaviorTreeSystem.setAssetManager(assetManager);
}
if (context.isEditor) {
behaviorTreeSystem.enabled = false;
}
scene.addSystem(behaviorTreeSystem);
// Register service to service registry
context.services.register(BehaviorTreeSystemToken, behaviorTreeSystem);
}
}
const manifest: ModuleManifest = {
id: 'behavior-tree',
name: '@esengine/behavior-tree',
displayName: 'Behavior Tree',
version: '1.0.0',
description: 'AI behavior tree system',
category: 'AI',
icon: 'GitBranch',
isCore: false,
defaultEnabled: false,
isEngineModule: true,
canContainContent: true,
dependencies: ['core'],
exports: { components: ['BehaviorTreeComponent'] },
editorPackage: '@esengine/behavior-tree-editor'
};
export const BehaviorTreePlugin: IRuntimePlugin = {
manifest,
runtimeModule: new BehaviorTreeRuntimeModule()
};
export { BehaviorTreeRuntimeModule };

View File

@@ -1,39 +0,0 @@
/**
* @zh ESEngine 集成入口
* @en ESEngine integration entry point
*
* @zh 此模块包含与 ESEngine 引擎核心集成所需的所有代码。
* 使用 Cocos/Laya 等其他引擎时,只需导入主模块即可。
*
* @en This module contains all code required for ESEngine engine-core integration.
* When using other engines like Cocos/Laya, just import the main module.
*
* @example ESEngine 使用方式 / ESEngine usage:
* ```typescript
* import { BehaviorTreePlugin } from '@esengine/behavior-tree/esengine';
*
* // Register with ESEngine plugin system
* engine.registerPlugin(BehaviorTreePlugin);
* ```
*
* @example Cocos/Laya 使用方式 / Cocos/Laya usage:
* ```typescript
* import {
* BehaviorTreeAssetManager,
* BehaviorTreeExecutionSystem
* } from '@esengine/behavior-tree';
*
* // Load behavior tree from JSON
* const assetManager = new BehaviorTreeAssetManager();
* assetManager.loadFromEditorJSON(jsonContent);
*
* // Add system to your ECS world
* world.addSystem(new BehaviorTreeExecutionSystem());
* ```
*/
// Runtime module and plugin
export { BehaviorTreeRuntimeModule, BehaviorTreePlugin, BehaviorTreeSystemToken } from './BehaviorTreeRuntimeModule';
// Asset loader for ESEngine asset-system
export { BehaviorTreeLoader, type IBehaviorTreeAsset } from './BehaviorTreeLoader';

View File

@@ -5,7 +5,7 @@ export { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
export type { INodeExecutor, NodeExecutionContext } from './NodeExecutor';
export { NodeExecutorRegistry, BindingHelper } from './NodeExecutor';
export { BehaviorTreeExecutionSystem } from './BehaviorTreeExecutionSystem';
export type { NodeMetadata, ConfigFieldDefinition, NodeExecutorMetadata } from './NodeMetadata';
export { NodeMetadataRegistry } from './NodeMetadata';
export type { NodeMetadata, ConfigFieldDefinition } from './NodeMetadata';
export { NodeMetadataRegistry, NodeExecutorMetadata } from './NodeMetadata';
export * from './Executors';

View File

@@ -4,32 +4,44 @@
* @zh AI 行为树系统,支持运行时执行和可视化编辑
* @en AI Behavior Tree System with runtime execution and visual editor support
*
* @zh 此包是通用的行为树实现,可以与任何 ECS 框架配合使用。
* 对于 ESEngine 集成,请从 '@esengine/behavior-tree/esengine' 导入插件
* @zh 此包是通用的行为树实现,可以与任何基于 @esengine/ecs-framework 的引擎集成
* Cocos Creator、LayaAir、Node.js 等)
*
* @en This package is a generic behavior tree implementation that works with any ECS framework.
* For ESEngine integration, import the plugin from '@esengine/behavior-tree/esengine'.
* @en This package is a generic behavior tree implementation that works with any engine
* based on @esengine/ecs-framework (Cocos Creator, LayaAir, Node.js, etc.).
*
* @example Cocos/Laya/通用 ECS 使用方式:
* @example
* ```typescript
* import { Core, Scene } from '@esengine/ecs-framework';
* import {
* BehaviorTreeAssetManager,
* BehaviorTreeExecutionSystem,
* BehaviorTreeRuntimeComponent
* BehaviorTreePlugin,
* BehaviorTreeBuilder,
* BehaviorTreeStarter
* } from '@esengine/behavior-tree';
*
* // 1. Register service
* Core.services.registerSingleton(BehaviorTreeAssetManager);
* // 1. Initialize Core and install plugin
* Core.create();
* const plugin = new BehaviorTreePlugin();
* await Core.installPlugin(plugin);
*
* // 2. Load behavior tree from JSON
* const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
* assetManager.loadFromEditorJSON(jsonContent);
* // 2. Create scene and setup behavior tree system
* const scene = new Scene();
* plugin.setupScene(scene);
* Core.setScene(scene);
*
* // 3. Add component to entity
* entity.addComponent(new BehaviorTreeRuntimeComponent());
* // 3. Build behavior tree
* const tree = BehaviorTreeBuilder.create('MyAI')
* .selector('Root')
* .log('Hello!')
* .end()
* .build();
*
* // 4. Add system to scene
* scene.addSystem(new BehaviorTreeExecutionSystem());
* // 4. Start behavior tree on entity
* const entity = scene.createEntity('AIEntity');
* BehaviorTreeStarter.start(entity, tree);
*
* // 5. Run game loop
* setInterval(() => Core.update(0.016), 16);
* ```
*
* @packageDocumentation
@@ -65,3 +77,6 @@ export { BlackboardTypes } from './Blackboard/BlackboardTypes';
// Service tokens (using ecs-framework's createServiceToken, not engine-core)
export { BehaviorTreeSystemToken } from './tokens';
// Plugin
export { BehaviorTreePlugin } from './BehaviorTreePlugin';

View File

@@ -1,5 +1,24 @@
# @esengine/blueprint
## 4.1.0
### Minor Changes
- [#430](https://github.com/esengine/esengine/pull/430) [`caf3be7`](https://github.com/esengine/esengine/commit/caf3be72cdcc730492c63abe5f1715893f3579ac) Thanks [@esengine](https://github.com/esengine)! - feat(blueprint): 重构装饰器系统,移除 Reflect 依赖 | Refactor decorator system, remove Reflect dependency
**@esengine/blueprint**
- 移除 `Reflect.getMetadata` 依赖,装饰器现在要求显式指定类型 | Removed `Reflect.getMetadata` dependency, decorators now require explicit type specification
- 简化 `BlueprintProperty``BlueprintMethod` 装饰器的元数据结构 | Simplified metadata structure for `BlueprintProperty` and `BlueprintMethod` decorators
- 新增 `inferPinType` 工具函数用于类型推断 | Added `inferPinType` utility function for type inference
- 优化组件节点生成器以适配新的元数据结构 | Optimized component node generator for new metadata structure
## 4.0.1
### Patch Changes
- Updated dependencies [[`3e5b778`](https://github.com/esengine/esengine/commit/3e5b7783beec08e247f7525184935401923ecde8)]:
- @esengine/ecs-framework@2.7.1
## 4.0.0
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@esengine/blueprint",
"version": "4.0.0",
"version": "4.1.0",
"description": "Visual scripting system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
"main": "dist/index.js",
"module": "dist/index.js",

View File

@@ -1,67 +0,0 @@
/**
* @zh ESEngine 蓝图插件
* @en ESEngine Blueprint Plugin
*
* @zh 此文件包含与 ESEngine 引擎核心集成的代码。
* 使用 Cocos/Laya 等其他引擎时不需要此文件。
*
* @en This file contains code for integrating with ESEngine engine-core.
* Not needed when using other engines like Cocos/Laya.
*/
import type { IRuntimePlugin, ModuleManifest, IRuntimeModule } from '@esengine/engine-core';
/**
* @zh 蓝图运行时模块
* @en Blueprint Runtime Module
*
* @zh 注意:蓝图使用自定义系统 (IBlueprintSystem) 而非 EntitySystem
* 因此这里不实现 createSystems。蓝图系统应使用 createBlueprintSystem(scene) 手动创建。
*
* @en Note: Blueprint uses a custom system (IBlueprintSystem) instead of EntitySystem,
* so createSystems is not implemented here. Blueprint systems should be created
* manually using createBlueprintSystem(scene).
*/
class BlueprintRuntimeModule implements IRuntimeModule {
async onInitialize(): Promise<void> {
// Blueprint system initialization
}
onDestroy(): void {
// Cleanup
}
}
/**
* @zh 蓝图的插件清单
* @en Plugin manifest for Blueprint
*/
const manifest: ModuleManifest = {
id: 'blueprint',
name: '@esengine/blueprint',
displayName: 'Blueprint',
version: '1.0.0',
description: '可视化脚本系统',
category: 'AI',
icon: 'Workflow',
isCore: false,
defaultEnabled: false,
isEngineModule: true,
dependencies: ['core'],
exports: {
components: ['BlueprintComponent'],
systems: ['BlueprintSystem']
},
requiresWasm: false
};
/**
* @zh 蓝图插件
* @en Blueprint Plugin
*/
export const BlueprintPlugin: IRuntimePlugin = {
manifest,
runtimeModule: new BlueprintRuntimeModule()
};
export { BlueprintRuntimeModule };

View File

@@ -1,37 +0,0 @@
/**
* @zh ESEngine 集成入口
* @en ESEngine integration entry point
*
* @zh 此模块包含与 ESEngine 引擎核心集成所需的所有代码。
* 使用 Cocos/Laya 等其他引擎时,只需导入主模块即可。
*
* @en This module contains all code required for ESEngine engine-core integration.
* When using other engines like Cocos/Laya, just import the main module.
*
* @example ESEngine 使用方式 / ESEngine usage:
* ```typescript
* import { BlueprintPlugin } from '@esengine/blueprint/esengine';
*
* // Register with ESEngine plugin system
* engine.registerPlugin(BlueprintPlugin);
* ```
*
* @example Cocos/Laya 使用方式 / Cocos/Laya usage:
* ```typescript
* import {
* createBlueprintSystem,
* createBlueprintComponentData
* } from '@esengine/blueprint';
*
* // Create blueprint system for your scene
* const blueprintSystem = createBlueprintSystem(scene);
*
* // Add to your game loop
* function update(dt) {
* blueprintSystem.process(blueprintEntities, dt);
* }
* ```
*/
// Runtime module and plugin
export { BlueprintPlugin, BlueprintRuntimeModule } from './BlueprintPlugin';

View File

@@ -1,32 +1,47 @@
/**
* @esengine/blueprint - Visual scripting system for ECS Framework
*
* @zh 蓝图可视化脚本系统 - 可与任何 ECS 框架配合使用
* @en Visual scripting system - works with any ECS framework
* @zh 蓝图可视化脚本系统 - ECS 框架深度集成
* @en Visual scripting system - Deep integration with ECS framework
*
* @zh 此包是通用的可视化脚本实现,可以与任何 ECS 框架配合使用。
* 对于 ESEngine 集成,请从 '@esengine/blueprint/esengine' 导入插件。
* @zh 此包提供完整的可视化脚本功能:
* - 内置 ECS 操作节点Entity、Component、Flow
* - 组件自动节点生成(使用装饰器标记)
* - 运行时蓝图执行
*
* @en This package is a generic visual scripting implementation that works with any ECS framework.
* For ESEngine integration, import the plugin from '@esengine/blueprint/esengine'.
* @en This package provides complete visual scripting features:
* - Built-in ECS operation nodes (Entity, Component, Flow)
* - Auto component node generation (using decorators)
* - Runtime blueprint execution
*
* @example Cocos/Laya/通用 ECS 使用方式:
* @example 基础使用 | Basic usage:
* ```typescript
* import {
* createBlueprintSystem,
* createBlueprintComponentData
* registerAllComponentNodes
* } from '@esengine/blueprint';
*
* // Create blueprint system for your scene
* // 注册所有标记的组件节点 | Register all marked component nodes
* registerAllComponentNodes();
*
* // 创建蓝图系统 | Create blueprint system
* const blueprintSystem = createBlueprintSystem(scene);
* ```
*
* // Create component data
* const componentData = createBlueprintComponentData();
* componentData.blueprintAsset = loadedAsset;
* @example 标记组件 | Mark components:
* ```typescript
* import { BlueprintExpose, BlueprintProperty, BlueprintMethod } from '@esengine/blueprint';
*
* // Add to your game loop
* function update(dt) {
* blueprintSystem.process(blueprintEntities, dt);
* @ECSComponent('Health')
* @BlueprintExpose({ displayName: '生命值' })
* export class HealthComponent extends Component {
* @BlueprintProperty({ displayName: '当前生命值' })
* current: number = 100;
*
* @BlueprintMethod({ displayName: '治疗' })
* heal(amount: number): void {
* this.current += amount;
* }
* }
* ```
*
@@ -45,7 +60,10 @@ export * from './triggers';
// Composition
export * from './composition';
// Nodes (import to register)
// Registry (decorators & auto-generation)
export * from './registry';
// Nodes (import to register built-in nodes)
import './nodes';
// Re-export commonly used items
@@ -65,3 +83,12 @@ export {
triggerCustomBlueprintEvent
} from './runtime/BlueprintSystem';
export { createEmptyBlueprint, validateBlueprintAsset } from './types/blueprint';
// Re-export registry for convenience
export {
BlueprintExpose,
BlueprintProperty,
BlueprintMethod,
registerAllComponentNodes,
registerComponentNodes
} from './registry';

View File

@@ -0,0 +1,354 @@
/**
* @zh ECS 组件操作节点
* @en ECS Component Operation Nodes
*
* @zh 提供蓝图中对 ECS 组件的完整操作支持
* @en Provides complete ECS component operations in blueprint
*/
import type { Entity, Component } from '@esengine/ecs-framework';
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
// ============================================================================
// Has Component | 是否有组件
// ============================================================================
export const HasComponentTemplate: BlueprintNodeTemplate = {
type: 'ECS_HasComponent',
title: 'Has Component',
category: 'component',
color: '#1e8b8b',
isPure: true,
description: 'Checks if an entity has a component of the specified type (检查实体是否拥有指定类型的组件)',
keywords: ['component', 'has', 'check', 'exists', 'contains'],
menuPath: ['ECS', 'Component', 'Has Component'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'componentType', type: 'string', displayName: 'Component Type', defaultValue: '' }
],
outputs: [
{ name: 'hasComponent', type: 'bool', displayName: 'Has Component' }
]
};
@RegisterNode(HasComponentTemplate)
export class HasComponentExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const componentType = context.evaluateInput(node.id, 'componentType', '') as string;
if (!entity || entity.isDestroyed || !componentType) {
return { outputs: { hasComponent: false } };
}
const hasIt = entity.components.some(c =>
c.constructor.name === componentType ||
(c.constructor as any).__componentName__ === componentType
);
return { outputs: { hasComponent: hasIt } };
}
}
// ============================================================================
// Get Component | 获取组件
// ============================================================================
export const GetComponentTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetComponent',
title: 'Get Component',
category: 'component',
color: '#1e8b8b',
isPure: true,
description: 'Gets a component from an entity by type name (按类型名称从实体获取组件)',
keywords: ['component', 'get', 'find', 'access'],
menuPath: ['ECS', 'Component', 'Get Component'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'componentType', type: 'string', displayName: 'Component Type', defaultValue: '' }
],
outputs: [
{ name: 'component', type: 'component', displayName: 'Component' },
{ name: 'found', type: 'bool', displayName: 'Found' }
]
};
@RegisterNode(GetComponentTemplate)
export class GetComponentExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const componentType = context.evaluateInput(node.id, 'componentType', '') as string;
if (!entity || entity.isDestroyed || !componentType) {
return { outputs: { component: null, found: false } };
}
const component = entity.components.find(c =>
c.constructor.name === componentType ||
(c.constructor as any).__componentName__ === componentType
);
return {
outputs: {
component: component ?? null,
found: component != null
}
};
}
}
// ============================================================================
// Get All Components | 获取所有组件
// ============================================================================
export const GetAllComponentsTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetAllComponents',
title: 'Get All Components',
category: 'component',
color: '#1e8b8b',
isPure: true,
description: 'Gets all components from an entity (获取实体的所有组件)',
keywords: ['component', 'get', 'all', 'list'],
menuPath: ['ECS', 'Component', 'Get All Components'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'components', type: 'array', displayName: 'Components', arrayType: 'component' },
{ name: 'count', type: 'int', displayName: 'Count' }
]
};
@RegisterNode(GetAllComponentsTemplate)
export class GetAllComponentsExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
if (!entity || entity.isDestroyed) {
return { outputs: { components: [], count: 0 } };
}
const components = [...entity.components];
return {
outputs: {
components,
count: components.length
}
};
}
}
// ============================================================================
// Remove Component | 移除组件
// ============================================================================
export const RemoveComponentTemplate: BlueprintNodeTemplate = {
type: 'ECS_RemoveComponent',
title: 'Remove Component',
category: 'component',
color: '#8b1e1e',
description: 'Removes a component from an entity (从实体移除组件)',
keywords: ['component', 'remove', 'delete', 'destroy'],
menuPath: ['ECS', 'Component', 'Remove Component'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'componentType', type: 'string', displayName: 'Component Type', defaultValue: '' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'removed', type: 'bool', displayName: 'Removed' }
]
};
@RegisterNode(RemoveComponentTemplate)
export class RemoveComponentExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const componentType = context.evaluateInput(node.id, 'componentType', '') as string;
if (!entity || entity.isDestroyed || !componentType) {
return { outputs: { removed: false }, nextExec: 'exec' };
}
const component = entity.components.find(c =>
c.constructor.name === componentType ||
(c.constructor as any).__componentName__ === componentType
);
if (component) {
entity.removeComponent(component);
return { outputs: { removed: true }, nextExec: 'exec' };
}
return { outputs: { removed: false }, nextExec: 'exec' };
}
}
// ============================================================================
// Get Component Property | 获取组件属性
// ============================================================================
export const GetComponentPropertyTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetComponentProperty',
title: 'Get Component Property',
category: 'component',
color: '#1e8b8b',
isPure: true,
description: 'Gets a property value from a component (从组件获取属性值)',
keywords: ['component', 'property', 'get', 'value', 'field'],
menuPath: ['ECS', 'Component', 'Get Property'],
inputs: [
{ name: 'component', type: 'component', displayName: 'Component' },
{ name: 'propertyName', type: 'string', displayName: 'Property Name', defaultValue: '' }
],
outputs: [
{ name: 'value', type: 'any', displayName: 'Value' },
{ name: 'found', type: 'bool', displayName: 'Found' }
]
};
@RegisterNode(GetComponentPropertyTemplate)
export class GetComponentPropertyExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const component = context.evaluateInput(node.id, 'component', null) as Component | null;
const propertyName = context.evaluateInput(node.id, 'propertyName', '') as string;
if (!component || !propertyName) {
return { outputs: { value: null, found: false } };
}
if (propertyName in component) {
return {
outputs: {
value: (component as any)[propertyName],
found: true
}
};
}
return { outputs: { value: null, found: false } };
}
}
// ============================================================================
// Set Component Property | 设置组件属性
// ============================================================================
export const SetComponentPropertyTemplate: BlueprintNodeTemplate = {
type: 'ECS_SetComponentProperty',
title: 'Set Component Property',
category: 'component',
color: '#1e8b8b',
description: 'Sets a property value on a component (设置组件的属性值)',
keywords: ['component', 'property', 'set', 'value', 'field', 'modify'],
menuPath: ['ECS', 'Component', 'Set Property'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'component', type: 'component', displayName: 'Component' },
{ name: 'propertyName', type: 'string', displayName: 'Property Name', defaultValue: '' },
{ name: 'value', type: 'any', displayName: 'Value' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'success', type: 'bool', displayName: 'Success' }
]
};
@RegisterNode(SetComponentPropertyTemplate)
export class SetComponentPropertyExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const component = context.evaluateInput(node.id, 'component', null) as Component | null;
const propertyName = context.evaluateInput(node.id, 'propertyName', '') as string;
const value = context.evaluateInput(node.id, 'value', null);
if (!component || !propertyName) {
return { outputs: { success: false }, nextExec: 'exec' };
}
if (propertyName in component) {
(component as any)[propertyName] = value;
return { outputs: { success: true }, nextExec: 'exec' };
}
return { outputs: { success: false }, nextExec: 'exec' };
}
}
// ============================================================================
// Get Component Type Name | 获取组件类型名称
// ============================================================================
export const GetComponentTypeNameTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetComponentTypeName',
title: 'Get Component Type',
category: 'component',
color: '#1e8b8b',
isPure: true,
description: 'Gets the type name of a component (获取组件的类型名称)',
keywords: ['component', 'type', 'name', 'class'],
menuPath: ['ECS', 'Component', 'Get Type Name'],
inputs: [
{ name: 'component', type: 'component', displayName: 'Component' }
],
outputs: [
{ name: 'typeName', type: 'string', displayName: 'Type Name' }
]
};
@RegisterNode(GetComponentTypeNameTemplate)
export class GetComponentTypeNameExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const component = context.evaluateInput(node.id, 'component', null) as Component | null;
if (!component) {
return { outputs: { typeName: '' } };
}
const typeName = (component.constructor as any).__componentName__ ?? component.constructor.name;
return { outputs: { typeName } };
}
}
// ============================================================================
// Get Entity From Component | 从组件获取实体
// ============================================================================
export const GetEntityFromComponentTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetEntityFromComponent',
title: 'Get Owner Entity',
category: 'component',
color: '#1e8b8b',
isPure: true,
description: 'Gets the entity that owns a component (获取拥有组件的实体)',
keywords: ['component', 'entity', 'owner', 'parent'],
menuPath: ['ECS', 'Component', 'Get Owner Entity'],
inputs: [
{ name: 'component', type: 'component', displayName: 'Component' }
],
outputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'found', type: 'bool', displayName: 'Found' }
]
};
@RegisterNode(GetEntityFromComponentTemplate)
export class GetEntityFromComponentExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const component = context.evaluateInput(node.id, 'component', null) as Component | null;
if (!component || component.entityId == null) {
return { outputs: { entity: null, found: false } };
}
const entity = context.scene.findEntityById(component.entityId);
return {
outputs: {
entity: entity ?? null,
found: entity != null
}
};
}
}

View File

@@ -0,0 +1,485 @@
/**
* @zh ECS 实体操作节点
* @en ECS Entity Operation Nodes
*
* @zh 提供蓝图中对 ECS 实体的完整操作支持
* @en Provides complete ECS entity operations in blueprint
*/
import type { Entity } from '@esengine/ecs-framework';
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
// ============================================================================
// Self Entity | 自身实体
// ============================================================================
export const GetSelfTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetSelf',
title: 'Get Self',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Gets the entity that owns this blueprint (获取拥有此蓝图的实体)',
keywords: ['self', 'this', 'owner', 'entity', 'me'],
menuPath: ['ECS', 'Entity', 'Get Self'],
inputs: [],
outputs: [
{ name: 'entity', type: 'entity', displayName: 'Self' }
]
};
@RegisterNode(GetSelfTemplate)
export class GetSelfExecutor implements INodeExecutor {
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
return { outputs: { entity: context.entity } };
}
}
// ============================================================================
// Create Entity | 创建实体
// ============================================================================
export const CreateEntityTemplate: BlueprintNodeTemplate = {
type: 'ECS_CreateEntity',
title: 'Create Entity',
category: 'entity',
color: '#1e5a8b',
description: 'Creates a new entity in the scene (在场景中创建新实体)',
keywords: ['entity', 'create', 'spawn', 'new', 'instantiate'],
menuPath: ['ECS', 'Entity', 'Create Entity'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'name', type: 'string', displayName: 'Name', defaultValue: 'NewEntity' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' }
]
};
@RegisterNode(CreateEntityTemplate)
export class CreateEntityExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const name = context.evaluateInput(node.id, 'name', 'NewEntity') as string;
const entity = context.scene.createEntity(name);
return { outputs: { entity }, nextExec: 'exec' };
}
}
// ============================================================================
// Destroy Entity | 销毁实体
// ============================================================================
export const DestroyEntityTemplate: BlueprintNodeTemplate = {
type: 'ECS_DestroyEntity',
title: 'Destroy Entity',
category: 'entity',
color: '#8b1e1e',
description: 'Destroys an entity from the scene (从场景中销毁实体)',
keywords: ['entity', 'destroy', 'remove', 'delete', 'kill'],
menuPath: ['ECS', 'Entity', 'Destroy Entity'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' }
]
};
@RegisterNode(DestroyEntityTemplate)
export class DestroyEntityExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', null) as Entity | null;
if (entity && !entity.isDestroyed) {
entity.destroy();
}
return { nextExec: 'exec' };
}
}
// ============================================================================
// Destroy Self | 销毁自身
// ============================================================================
export const DestroySelfTemplate: BlueprintNodeTemplate = {
type: 'ECS_DestroySelf',
title: 'Destroy Self',
category: 'entity',
color: '#8b1e1e',
description: 'Destroys the entity that owns this blueprint (销毁拥有此蓝图的实体)',
keywords: ['self', 'destroy', 'suicide', 'remove', 'delete'],
menuPath: ['ECS', 'Entity', 'Destroy Self'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' }
],
outputs: []
};
@RegisterNode(DestroySelfTemplate)
export class DestroySelfExecutor implements INodeExecutor {
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
if (!context.entity.isDestroyed) {
context.entity.destroy();
}
return { nextExec: null };
}
}
// ============================================================================
// Is Valid | 是否有效
// ============================================================================
export const IsValidTemplate: BlueprintNodeTemplate = {
type: 'ECS_IsValid',
title: 'Is Valid',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Checks if an entity reference is valid and not destroyed (检查实体引用是否有效且未被销毁)',
keywords: ['entity', 'valid', 'null', 'check', 'exists', 'alive'],
menuPath: ['ECS', 'Entity', 'Is Valid'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'isValid', type: 'bool', displayName: 'Is Valid' }
]
};
@RegisterNode(IsValidTemplate)
export class IsValidExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', null) as Entity | null;
const isValid = entity != null && !entity.isDestroyed;
return { outputs: { isValid } };
}
}
// ============================================================================
// Get Entity Name | 获取实体名称
// ============================================================================
export const GetEntityNameTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetEntityName',
title: 'Get Entity Name',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Gets the name of an entity (获取实体的名称)',
keywords: ['entity', 'name', 'get', 'string'],
menuPath: ['ECS', 'Entity', 'Get Name'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'name', type: 'string', displayName: 'Name' }
]
};
@RegisterNode(GetEntityNameTemplate)
export class GetEntityNameExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
return { outputs: { name: entity?.name ?? '' } };
}
}
// ============================================================================
// Set Entity Name | 设置实体名称
// ============================================================================
export const SetEntityNameTemplate: BlueprintNodeTemplate = {
type: 'ECS_SetEntityName',
title: 'Set Entity Name',
category: 'entity',
color: '#1e5a8b',
description: 'Sets the name of an entity (设置实体的名称)',
keywords: ['entity', 'name', 'set', 'rename'],
menuPath: ['ECS', 'Entity', 'Set Name'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'name', type: 'string', displayName: 'Name', defaultValue: '' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' }
]
};
@RegisterNode(SetEntityNameTemplate)
export class SetEntityNameExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const name = context.evaluateInput(node.id, 'name', '') as string;
if (entity && !entity.isDestroyed) {
entity.name = name;
}
return { nextExec: 'exec' };
}
}
// ============================================================================
// Get Entity Tag | 获取实体标签
// ============================================================================
export const GetEntityTagTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetEntityTag',
title: 'Get Entity Tag',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Gets the tag of an entity (获取实体的标签)',
keywords: ['entity', 'tag', 'get', 'category'],
menuPath: ['ECS', 'Entity', 'Get Tag'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'tag', type: 'int', displayName: 'Tag' }
]
};
@RegisterNode(GetEntityTagTemplate)
export class GetEntityTagExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
return { outputs: { tag: entity?.tag ?? 0 } };
}
}
// ============================================================================
// Set Entity Tag | 设置实体标签
// ============================================================================
export const SetEntityTagTemplate: BlueprintNodeTemplate = {
type: 'ECS_SetEntityTag',
title: 'Set Entity Tag',
category: 'entity',
color: '#1e5a8b',
description: 'Sets the tag of an entity (设置实体的标签)',
keywords: ['entity', 'tag', 'set', 'category'],
menuPath: ['ECS', 'Entity', 'Set Tag'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'tag', type: 'int', displayName: 'Tag', defaultValue: 0 }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' }
]
};
@RegisterNode(SetEntityTagTemplate)
export class SetEntityTagExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const tag = context.evaluateInput(node.id, 'tag', 0) as number;
if (entity && !entity.isDestroyed) {
entity.tag = tag;
}
return { nextExec: 'exec' };
}
}
// ============================================================================
// Set Entity Active | 设置实体激活状态
// ============================================================================
export const SetEntityActiveTemplate: BlueprintNodeTemplate = {
type: 'ECS_SetEntityActive',
title: 'Set Active',
category: 'entity',
color: '#1e5a8b',
description: 'Sets whether an entity is active (设置实体是否激活)',
keywords: ['entity', 'active', 'enable', 'disable', 'visible'],
menuPath: ['ECS', 'Entity', 'Set Active'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'active', type: 'bool', displayName: 'Active', defaultValue: true }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' }
]
};
@RegisterNode(SetEntityActiveTemplate)
export class SetEntityActiveExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
const active = context.evaluateInput(node.id, 'active', true) as boolean;
if (entity && !entity.isDestroyed) {
entity.active = active;
}
return { nextExec: 'exec' };
}
}
// ============================================================================
// Is Entity Active | 实体是否激活
// ============================================================================
export const IsEntityActiveTemplate: BlueprintNodeTemplate = {
type: 'ECS_IsEntityActive',
title: 'Is Active',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Checks if an entity is active (检查实体是否激活)',
keywords: ['entity', 'active', 'enabled', 'check'],
menuPath: ['ECS', 'Entity', 'Is Active'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'isActive', type: 'bool', displayName: 'Is Active' }
]
};
@RegisterNode(IsEntityActiveTemplate)
export class IsEntityActiveExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
return { outputs: { isActive: entity?.active ?? false } };
}
}
// ============================================================================
// Find Entity By Name | 按名称查找实体
// ============================================================================
export const FindEntityByNameTemplate: BlueprintNodeTemplate = {
type: 'ECS_FindEntityByName',
title: 'Find Entity By Name',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Finds an entity by name in the scene (在场景中按名称查找实体)',
keywords: ['entity', 'find', 'name', 'search', 'get', 'lookup'],
menuPath: ['ECS', 'Entity', 'Find By Name'],
inputs: [
{ name: 'name', type: 'string', displayName: 'Name', defaultValue: '' }
],
outputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'found', type: 'bool', displayName: 'Found' }
]
};
@RegisterNode(FindEntityByNameTemplate)
export class FindEntityByNameExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const name = context.evaluateInput(node.id, 'name', '') as string;
const entity = context.scene.findEntity(name);
return {
outputs: {
entity: entity ?? null,
found: entity != null
}
};
}
}
// ============================================================================
// Find Entities By Tag | 按标签查找实体
// ============================================================================
export const FindEntitiesByTagTemplate: BlueprintNodeTemplate = {
type: 'ECS_FindEntitiesByTag',
title: 'Find Entities By Tag',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Finds all entities with a specific tag (查找所有具有特定标签的实体)',
keywords: ['entity', 'find', 'tag', 'search', 'get', 'all'],
menuPath: ['ECS', 'Entity', 'Find By Tag'],
inputs: [
{ name: 'tag', type: 'int', displayName: 'Tag', defaultValue: 0 }
],
outputs: [
{ name: 'entities', type: 'array', displayName: 'Entities', arrayType: 'entity' },
{ name: 'count', type: 'int', displayName: 'Count' }
]
};
@RegisterNode(FindEntitiesByTagTemplate)
export class FindEntitiesByTagExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const tag = context.evaluateInput(node.id, 'tag', 0) as number;
const entities = context.scene.findEntitiesByTag(tag);
return {
outputs: {
entities,
count: entities.length
}
};
}
}
// ============================================================================
// Get Entity ID | 获取实体 ID
// ============================================================================
export const GetEntityIdTemplate: BlueprintNodeTemplate = {
type: 'ECS_GetEntityId',
title: 'Get Entity ID',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Gets the unique ID of an entity (获取实体的唯一ID)',
keywords: ['entity', 'id', 'identifier', 'unique'],
menuPath: ['ECS', 'Entity', 'Get ID'],
inputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' }
],
outputs: [
{ name: 'id', type: 'int', displayName: 'ID' }
]
};
@RegisterNode(GetEntityIdTemplate)
export class GetEntityIdExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const entity = context.evaluateInput(node.id, 'entity', context.entity) as Entity;
return { outputs: { id: entity?.id ?? -1 } };
}
}
// ============================================================================
// Find Entity By ID | 按 ID 查找实体
// ============================================================================
export const FindEntityByIdTemplate: BlueprintNodeTemplate = {
type: 'ECS_FindEntityById',
title: 'Find Entity By ID',
category: 'entity',
color: '#1e5a8b',
isPure: true,
description: 'Finds an entity by its unique ID (通过唯一ID查找实体)',
keywords: ['entity', 'find', 'id', 'identifier'],
menuPath: ['ECS', 'Entity', 'Find By ID'],
inputs: [
{ name: 'id', type: 'int', displayName: 'ID', defaultValue: 0 }
],
outputs: [
{ name: 'entity', type: 'entity', displayName: 'Entity' },
{ name: 'found', type: 'bool', displayName: 'Found' }
]
};
@RegisterNode(FindEntityByIdTemplate)
export class FindEntityByIdExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const id = context.evaluateInput(node.id, 'id', 0) as number;
const entity = context.scene.findEntityById(id);
return {
outputs: {
entity: entity ?? null,
found: entity != null
}
};
}
}

View File

@@ -0,0 +1,301 @@
/**
* @zh 流程控制节点
* @en Flow Control Nodes
*
* @zh 提供蓝图中的流程控制支持(分支、循环等)
* @en Provides flow control in blueprint (branch, loop, etc.)
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
// ============================================================================
// Branch | 分支
// ============================================================================
export const BranchTemplate: BlueprintNodeTemplate = {
type: 'Flow_Branch',
title: 'Branch',
category: 'flow',
color: '#4a4a4a',
description: 'Executes one of two paths based on a condition (根据条件执行两条路径之一)',
keywords: ['if', 'branch', 'condition', 'switch', 'else'],
menuPath: ['Flow', 'Branch'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'condition', type: 'bool', displayName: 'Condition', defaultValue: false }
],
outputs: [
{ name: 'true', type: 'exec', displayName: 'True' },
{ name: 'false', type: 'exec', displayName: 'False' }
]
};
@RegisterNode(BranchTemplate)
export class BranchExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const condition = context.evaluateInput(node.id, 'condition', false) as boolean;
return { nextExec: condition ? 'true' : 'false' };
}
}
// ============================================================================
// Sequence | 序列
// ============================================================================
export const SequenceTemplate: BlueprintNodeTemplate = {
type: 'Flow_Sequence',
title: 'Sequence',
category: 'flow',
color: '#4a4a4a',
description: 'Executes multiple outputs in order (按顺序执行多个输出)',
keywords: ['sequence', 'order', 'serial', 'chain'],
menuPath: ['Flow', 'Sequence'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' }
],
outputs: [
{ name: 'then0', type: 'exec', displayName: 'Then 0' },
{ name: 'then1', type: 'exec', displayName: 'Then 1' },
{ name: 'then2', type: 'exec', displayName: 'Then 2' },
{ name: 'then3', type: 'exec', displayName: 'Then 3' }
]
};
@RegisterNode(SequenceTemplate)
export class SequenceExecutor implements INodeExecutor {
private currentIndex = 0;
execute(_node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
const outputs = ['then0', 'then1', 'then2', 'then3'];
const nextPin = outputs[this.currentIndex];
this.currentIndex = (this.currentIndex + 1) % outputs.length;
if (this.currentIndex === 0) {
return { nextExec: null };
}
return { nextExec: nextPin };
}
}
// ============================================================================
// Do Once | 只执行一次
// ============================================================================
export const DoOnceTemplate: BlueprintNodeTemplate = {
type: 'Flow_DoOnce',
title: 'Do Once',
category: 'flow',
color: '#4a4a4a',
description: 'Executes the output only once, subsequent calls are ignored (只执行一次,后续调用被忽略)',
keywords: ['once', 'single', 'first', 'one'],
menuPath: ['Flow', 'Do Once'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'reset', type: 'exec', displayName: 'Reset' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' }
]
};
@RegisterNode(DoOnceTemplate)
export class DoOnceExecutor implements INodeExecutor {
private executed = false;
execute(node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
const inputPin = node.data._lastInputPin as string | undefined;
if (inputPin === 'reset') {
this.executed = false;
return { nextExec: null };
}
if (this.executed) {
return { nextExec: null };
}
this.executed = true;
return { nextExec: 'exec' };
}
}
// ============================================================================
// Flip Flop | 触发器
// ============================================================================
export const FlipFlopTemplate: BlueprintNodeTemplate = {
type: 'Flow_FlipFlop',
title: 'Flip Flop',
category: 'flow',
color: '#4a4a4a',
description: 'Alternates between two outputs on each execution (每次执行时在两个输出之间交替)',
keywords: ['flip', 'flop', 'toggle', 'alternate', 'switch'],
menuPath: ['Flow', 'Flip Flop'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' }
],
outputs: [
{ name: 'a', type: 'exec', displayName: 'A' },
{ name: 'b', type: 'exec', displayName: 'B' },
{ name: 'isA', type: 'bool', displayName: 'Is A' }
]
};
@RegisterNode(FlipFlopTemplate)
export class FlipFlopExecutor implements INodeExecutor {
private isA = true;
execute(_node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
const currentIsA = this.isA;
this.isA = !this.isA;
return {
outputs: { isA: currentIsA },
nextExec: currentIsA ? 'a' : 'b'
};
}
}
// ============================================================================
// Gate | 门
// ============================================================================
export const GateTemplate: BlueprintNodeTemplate = {
type: 'Flow_Gate',
title: 'Gate',
category: 'flow',
color: '#4a4a4a',
description: 'Controls execution flow with open/close state (通过开/关状态控制执行流)',
keywords: ['gate', 'open', 'close', 'block', 'allow'],
menuPath: ['Flow', 'Gate'],
inputs: [
{ name: 'exec', type: 'exec', displayName: 'Enter' },
{ name: 'open', type: 'exec', displayName: 'Open' },
{ name: 'close', type: 'exec', displayName: 'Close' },
{ name: 'toggle', type: 'exec', displayName: 'Toggle' },
{ name: 'startOpen', type: 'bool', displayName: 'Start Open', defaultValue: true }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: 'Exit' }
]
};
@RegisterNode(GateTemplate)
export class GateExecutor implements INodeExecutor {
private isOpen: boolean | null = null;
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
if (this.isOpen === null) {
this.isOpen = context.evaluateInput(node.id, 'startOpen', true) as boolean;
}
const inputPin = node.data._lastInputPin as string | undefined;
switch (inputPin) {
case 'open':
this.isOpen = true;
return { nextExec: null };
case 'close':
this.isOpen = false;
return { nextExec: null };
case 'toggle':
this.isOpen = !this.isOpen;
return { nextExec: null };
default:
return { nextExec: this.isOpen ? 'exec' : null };
}
}
}
// ============================================================================
// For Loop | For 循环
// ============================================================================
export const ForLoopTemplate: BlueprintNodeTemplate = {
type: 'Flow_ForLoop',
title: 'For Loop',
category: 'flow',
color: '#4a4a4a',
description: 'Executes the loop body for each index in range (对范围内的每个索引执行循环体)',
keywords: ['for', 'loop', 'iterate', 'repeat', 'count'],
menuPath: ['Flow', 'For Loop'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'start', type: 'int', displayName: 'Start', defaultValue: 0 },
{ name: 'end', type: 'int', displayName: 'End', defaultValue: 10 }
],
outputs: [
{ name: 'loopBody', type: 'exec', displayName: 'Loop Body' },
{ name: 'completed', type: 'exec', displayName: 'Completed' },
{ name: 'index', type: 'int', displayName: 'Index' }
]
};
@RegisterNode(ForLoopTemplate)
export class ForLoopExecutor implements INodeExecutor {
private currentIndex = 0;
private endIndex = 0;
private isRunning = false;
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
if (!this.isRunning) {
this.currentIndex = context.evaluateInput(node.id, 'start', 0) as number;
this.endIndex = context.evaluateInput(node.id, 'end', 10) as number;
this.isRunning = true;
}
if (this.currentIndex < this.endIndex) {
const index = this.currentIndex;
this.currentIndex++;
return {
outputs: { index },
nextExec: 'loopBody'
};
}
this.isRunning = false;
return {
outputs: { index: this.endIndex },
nextExec: 'completed'
};
}
}
// ============================================================================
// While Loop | While 循环
// ============================================================================
export const WhileLoopTemplate: BlueprintNodeTemplate = {
type: 'Flow_WhileLoop',
title: 'While Loop',
category: 'flow',
color: '#4a4a4a',
description: 'Executes the loop body while condition is true (当条件为真时执行循环体)',
keywords: ['while', 'loop', 'repeat', 'condition'],
menuPath: ['Flow', 'While Loop'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'condition', type: 'bool', displayName: 'Condition', defaultValue: true }
],
outputs: [
{ name: 'loopBody', type: 'exec', displayName: 'Loop Body' },
{ name: 'completed', type: 'exec', displayName: 'Completed' }
]
};
@RegisterNode(WhileLoopTemplate)
export class WhileLoopExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const condition = context.evaluateInput(node.id, 'condition', true) as boolean;
if (condition) {
return { nextExec: 'loopBody' };
}
return { nextExec: 'completed' };
}
}

View File

@@ -0,0 +1,16 @@
/**
* @zh ECS 核心节点
* @en ECS Core Nodes
*
* @zh 提供与 ECS 框架交互的蓝图节点
* @en Provides blueprint nodes for ECS framework interaction
*/
// Entity operations | 实体操作
export * from './EntityNodes';
// Component operations | 组件操作
export * from './ComponentNodes';
// Flow control | 流程控制
export * from './FlowNodes';

View File

@@ -1,118 +0,0 @@
/**
* @zh 碰撞事件节点 - 碰撞发生时触发
* @en Event Collision Node - Triggered on collision events
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventCollisionEnter 节点模板
* @en EventCollisionEnter node template
*/
export const EventCollisionEnterTemplate: BlueprintNodeTemplate = {
type: 'EventCollisionEnter',
title: 'Event Collision Enter',
category: 'event',
color: '#CC0000',
description: 'Triggered when collision starts / 碰撞开始时触发',
keywords: ['collision', 'enter', 'hit', 'overlap', 'event'],
menuPath: ['Event', 'Collision', 'Enter'],
inputs: [],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'otherEntityId',
type: 'string',
displayName: 'Other Entity'
},
{
name: 'pointX',
type: 'float',
displayName: 'Point X'
},
{
name: 'pointY',
type: 'float',
displayName: 'Point Y'
},
{
name: 'normalX',
type: 'float',
displayName: 'Normal X'
},
{
name: 'normalY',
type: 'float',
displayName: 'Normal Y'
}
]
};
/**
* @zh EventCollisionEnter 节点执行器
* @en EventCollisionEnter node executor
*/
@RegisterNode(EventCollisionEnterTemplate)
export class EventCollisionEnterExecutor implements INodeExecutor {
execute(_node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
otherEntityId: '',
pointX: 0,
pointY: 0,
normalX: 0,
normalY: 0
}
};
}
}
/**
* @zh EventCollisionExit 节点模板
* @en EventCollisionExit node template
*/
export const EventCollisionExitTemplate: BlueprintNodeTemplate = {
type: 'EventCollisionExit',
title: 'Event Collision Exit',
category: 'event',
color: '#CC0000',
description: 'Triggered when collision ends / 碰撞结束时触发',
keywords: ['collision', 'exit', 'end', 'separate', 'event'],
menuPath: ['Event', 'Collision', 'Exit'],
inputs: [],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'otherEntityId',
type: 'string',
displayName: 'Other Entity'
}
]
};
/**
* @zh EventCollisionExit 节点执行器
* @en EventCollisionExit node executor
*/
@RegisterNode(EventCollisionExitTemplate)
export class EventCollisionExitExecutor implements INodeExecutor {
execute(_node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
otherEntityId: ''
}
};
}
}

View File

@@ -1,79 +0,0 @@
/**
* @zh 输入事件节点 - 输入触发时触发
* @en Event Input Node - Triggered on input events
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventInput 节点模板
* @en EventInput node template
*/
export const EventInputTemplate: BlueprintNodeTemplate = {
type: 'EventInput',
title: 'Event Input',
category: 'event',
color: '#CC0000',
description: 'Triggered when input action occurs / 输入动作发生时触发',
keywords: ['input', 'key', 'button', 'action', 'event'],
menuPath: ['Event', 'Input'],
inputs: [
{
name: 'action',
type: 'string',
displayName: 'Action',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'action',
type: 'string',
displayName: 'Action'
},
{
name: 'value',
type: 'float',
displayName: 'Value'
},
{
name: 'pressed',
type: 'bool',
displayName: 'Pressed'
},
{
name: 'released',
type: 'bool',
displayName: 'Released'
}
]
};
/**
* @zh EventInput 节点执行器
* @en EventInput node executor
*
* @zh 注意:事件节点的输出由 VM 在触发时通过 setOutputs 设置
* @en Note: Event node outputs are set by VM via setOutputs when triggered
*/
@RegisterNode(EventInputTemplate)
export class EventInputExecutor implements INodeExecutor {
execute(node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
action: node.data?.action ?? '',
value: 0,
pressed: false,
released: false
}
};
}
}

View File

@@ -1,70 +0,0 @@
/**
* @zh 消息事件节点 - 接收消息时触发
* @en Event Message Node - Triggered when message is received
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventMessage 节点模板
* @en EventMessage node template
*/
export const EventMessageTemplate: BlueprintNodeTemplate = {
type: 'EventMessage',
title: 'Event Message',
category: 'event',
color: '#CC0000',
description: 'Triggered when a message is received / 接收到消息时触发',
keywords: ['message', 'receive', 'broadcast', 'event', 'signal'],
menuPath: ['Event', 'Message'],
inputs: [
{
name: 'messageName',
type: 'string',
displayName: 'Message Name',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'messageName',
type: 'string',
displayName: 'Message'
},
{
name: 'senderId',
type: 'string',
displayName: 'Sender ID'
},
{
name: 'payload',
type: 'any',
displayName: 'Payload'
}
]
};
/**
* @zh EventMessage 节点执行器
* @en EventMessage node executor
*/
@RegisterNode(EventMessageTemplate)
export class EventMessageExecutor implements INodeExecutor {
execute(node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
messageName: node.data?.messageName ?? '',
senderId: '',
payload: null
}
};
}
}

View File

@@ -1,132 +0,0 @@
/**
* @zh 状态事件节点 - 状态机状态变化时触发
* @en Event State Node - Triggered on state machine state changes
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
/**
* @zh EventStateEnter 节点模板
* @en EventStateEnter node template
*/
export const EventStateEnterTemplate: BlueprintNodeTemplate = {
type: 'EventStateEnter',
title: 'Event State Enter',
category: 'event',
color: '#CC0000',
description: 'Triggered when entering a state / 进入状态时触发',
keywords: ['state', 'enter', 'fsm', 'machine', 'event'],
menuPath: ['Event', 'State', 'Enter'],
inputs: [
{
name: 'stateName',
type: 'string',
displayName: 'State Name',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'stateMachineId',
type: 'string',
displayName: 'State Machine'
},
{
name: 'currentState',
type: 'string',
displayName: 'Current State'
},
{
name: 'previousState',
type: 'string',
displayName: 'Previous State'
}
]
};
/**
* @zh EventStateEnter 节点执行器
* @en EventStateEnter node executor
*/
@RegisterNode(EventStateEnterTemplate)
export class EventStateEnterExecutor implements INodeExecutor {
execute(node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
stateMachineId: '',
currentState: node.data?.stateName ?? '',
previousState: ''
}
};
}
}
/**
* @zh EventStateExit 节点模板
* @en EventStateExit node template
*/
export const EventStateExitTemplate: BlueprintNodeTemplate = {
type: 'EventStateExit',
title: 'Event State Exit',
category: 'event',
color: '#CC0000',
description: 'Triggered when exiting a state / 退出状态时触发',
keywords: ['state', 'exit', 'leave', 'fsm', 'machine', 'event'],
menuPath: ['Event', 'State', 'Exit'],
inputs: [
{
name: 'stateName',
type: 'string',
displayName: 'State Name',
defaultValue: ''
}
],
outputs: [
{
name: 'exec',
type: 'exec',
displayName: ''
},
{
name: 'stateMachineId',
type: 'string',
displayName: 'State Machine'
},
{
name: 'currentState',
type: 'string',
displayName: 'Current State'
},
{
name: 'previousState',
type: 'string',
displayName: 'Previous State'
}
]
};
/**
* @zh EventStateExit 节点执行器
* @en EventStateExit node executor
*/
@RegisterNode(EventStateExitTemplate)
export class EventStateExitExecutor implements INodeExecutor {
execute(node: BlueprintNode): ExecutionResult {
return {
nextExec: 'exec',
outputs: {
stateMachineId: '',
currentState: '',
previousState: node.data?.stateName ?? ''
}
};
}
}

Some files were not shown because too many files have changed in this diff Show More