Compare commits
3 Commits
editor-v1.
...
style/code
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c7c3c98af | ||
|
|
be7b3afb4a | ||
|
|
3e037f4ae0 |
73
.eslintrc.json
Normal file
73
.eslintrc.json
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2020,
|
||||||
|
"sourceType": "module",
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"semi": ["error", "always"],
|
||||||
|
"quotes": ["error", "single", { "avoidEscape": true }],
|
||||||
|
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||||
|
"no-trailing-spaces": "error",
|
||||||
|
"eol-last": ["error", "always"],
|
||||||
|
"comma-dangle": ["error", "none"],
|
||||||
|
"object-curly-spacing": ["error", "always"],
|
||||||
|
"array-bracket-spacing": ["error", "never"],
|
||||||
|
"arrow-parens": ["error", "always"],
|
||||||
|
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1 }],
|
||||||
|
"no-console": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-function-type": "error",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off",
|
||||||
|
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||||
|
"@typescript-eslint/naming-convention": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"selector": "memberLike",
|
||||||
|
"modifiers": ["private"],
|
||||||
|
"format": ["camelCase"],
|
||||||
|
"leadingUnderscore": "require"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "memberLike",
|
||||||
|
"modifiers": ["public"],
|
||||||
|
"format": ["camelCase"],
|
||||||
|
"leadingUnderscore": "forbid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "memberLike",
|
||||||
|
"modifiers": ["protected"],
|
||||||
|
"format": ["camelCase"],
|
||||||
|
"leadingUnderscore": "require"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ignorePatterns": [
|
||||||
|
"node_modules/",
|
||||||
|
"dist/",
|
||||||
|
"bin/",
|
||||||
|
"build/",
|
||||||
|
"coverage/",
|
||||||
|
"thirdparty/",
|
||||||
|
"examples/lawn-mower-demo/",
|
||||||
|
"extensions/",
|
||||||
|
"*.min.js",
|
||||||
|
"*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
73
.github/workflows/ci.yml
vendored
73
.github/workflows/ci.yml
vendored
@@ -6,7 +6,7 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'packages/**'
|
- 'packages/**'
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'pnpm-lock.yaml'
|
- 'package-lock.json'
|
||||||
- 'tsconfig.json'
|
- 'tsconfig.json'
|
||||||
- 'jest.config.*'
|
- 'jest.config.*'
|
||||||
- '.github/workflows/ci.yml'
|
- '.github/workflows/ci.yml'
|
||||||
@@ -15,7 +15,7 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'packages/**'
|
- 'packages/**'
|
||||||
- 'package.json'
|
- 'package.json'
|
||||||
- 'pnpm-lock.yaml'
|
- 'package-lock.json'
|
||||||
- 'tsconfig.json'
|
- 'tsconfig.json'
|
||||||
- 'jest.config.*'
|
- 'jest.config.*'
|
||||||
- '.github/workflows/ci.yml'
|
- '.github/workflows/ci.yml'
|
||||||
@@ -28,69 +28,29 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Install Rust stable
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Install wasm-pack
|
|
||||||
run: cargo install wasm-pack
|
|
||||||
|
|
||||||
- name: Build core package first
|
|
||||||
run: pnpm run build:core
|
|
||||||
|
|
||||||
- name: Build engine WASM package
|
|
||||||
run: |
|
|
||||||
cd packages/engine
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
- name: Copy WASM files to ecs-engine-bindgen
|
|
||||||
run: |
|
|
||||||
mkdir -p packages/ecs-engine-bindgen/src/wasm
|
|
||||||
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
|
|
||||||
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
|
|
||||||
cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/
|
|
||||||
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
|
|
||||||
|
|
||||||
- name: Build dependent packages for type declarations
|
|
||||||
run: |
|
|
||||||
cd packages/platform-common && pnpm run build
|
|
||||||
cd ../asset-system && pnpm run build
|
|
||||||
cd ../components && pnpm run build
|
|
||||||
cd ../editor-core && pnpm run build
|
|
||||||
cd ../ui && pnpm run build
|
|
||||||
cd ../editor-runtime && pnpm run build
|
|
||||||
cd ../behavior-tree && pnpm run build
|
|
||||||
cd ../tilemap && pnpm run build
|
|
||||||
|
|
||||||
- name: Build ecs-engine-bindgen
|
|
||||||
run: |
|
|
||||||
cd packages/ecs-engine-bindgen && pnpm run build
|
|
||||||
|
|
||||||
- name: Type check
|
- name: Type check
|
||||||
run: pnpm run type-check
|
run: npm run type-check
|
||||||
|
|
||||||
- name: Lint check
|
- name: Lint check
|
||||||
run: pnpm run lint
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Build core package first
|
||||||
|
run: npm run build:core
|
||||||
|
|
||||||
- name: Run tests with coverage
|
- name: Run tests with coverage
|
||||||
run: pnpm run test:ci
|
run: npm run test:ci
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v4
|
||||||
continue-on-error: true
|
|
||||||
with:
|
with:
|
||||||
file: ./coverage/lcov.info
|
file: ./coverage/lcov.info
|
||||||
flags: unittests
|
flags: unittests
|
||||||
@@ -105,25 +65,20 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: pnpm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Build npm package
|
- name: Build npm package
|
||||||
run: pnpm run build:npm
|
run: npm run build:npm
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
14
.github/workflows/codecov.yml
vendored
14
.github/workflows/codecov.yml
vendored
@@ -14,34 +14,28 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Run tests with coverage
|
- name: Run tests with coverage
|
||||||
run: |
|
run: |
|
||||||
cd packages/core
|
cd packages/core
|
||||||
pnpm run test:coverage
|
npm run test:coverage
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
uses: codecov/codecov-action@v4
|
uses: codecov/codecov-action@v4
|
||||||
continue-on-error: true
|
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
files: ./packages/core/coverage/coverage-final.json
|
files: ./packages/core/coverage/coverage-final.json
|
||||||
flags: core
|
flags: core
|
||||||
name: core-coverage
|
name: core-coverage
|
||||||
fail_ci_if_error: false
|
fail_ci_if_error: true
|
||||||
verbose: true
|
verbose: true
|
||||||
|
|
||||||
- name: Upload coverage artifact
|
- name: Upload coverage artifact
|
||||||
|
|||||||
9
.github/workflows/commitlint.yml
vendored
9
.github/workflows/commitlint.yml
vendored
@@ -17,20 +17,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install commitlint
|
- name: Install commitlint
|
||||||
run: |
|
run: |
|
||||||
pnpm add -D @commitlint/config-conventional @commitlint/cli
|
npm install --save-dev @commitlint/config-conventional @commitlint/cli
|
||||||
|
|
||||||
- name: Validate PR commits
|
- name: Validate PR commits
|
||||||
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
|
||||||
|
|||||||
15
.github/workflows/docs.yml
vendored
15
.github/workflows/docs.yml
vendored
@@ -29,31 +29,26 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v4
|
uses: actions/configure-pages@v4
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Build core package
|
- name: Build core package
|
||||||
run: pnpm run build:core
|
run: npm run build:core
|
||||||
|
|
||||||
- name: Generate API documentation
|
- name: Generate API documentation
|
||||||
run: pnpm run docs:api
|
run: npm run docs:api
|
||||||
|
|
||||||
- name: Build documentation
|
- name: Build documentation
|
||||||
run: pnpm run docs:build
|
run: npm run docs:build
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
|||||||
105
.github/workflows/release-editor.yml
vendored
105
.github/workflows/release-editor.yml
vendored
@@ -33,16 +33,11 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -62,101 +57,39 @@ jobs:
|
|||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
- name: Install frontend dependencies
|
- name: Install frontend dependencies
|
||||||
run: pnpm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Update version in config files (for manual trigger)
|
- name: Update version in config files (for manual trigger)
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |
|
run: |
|
||||||
cd packages/editor-app
|
cd packages/editor-app
|
||||||
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
# 临时更新版本号用于构建(不提交到仓库)
|
||||||
|
npm version ${{ github.event.inputs.version }} --no-git-tag-version
|
||||||
node scripts/sync-version.js
|
node scripts/sync-version.js
|
||||||
|
|
||||||
# ===== 第一层:基础包(无依赖) =====
|
- name: Cache TypeScript build
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
packages/core/bin
|
||||||
|
packages/editor-core/dist
|
||||||
|
packages/behavior-tree/bin
|
||||||
|
key: ${{ runner.os }}-ts-build-${{ hashFiles('packages/core/src/**', 'packages/editor-core/src/**', 'packages/behavior-tree/src/**') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-ts-build-
|
||||||
|
|
||||||
- name: Build core package
|
- name: Build core package
|
||||||
run: pnpm run build:core
|
run: npm run build:core
|
||||||
|
|
||||||
- name: Build math package
|
|
||||||
run: |
|
|
||||||
cd packages/math
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
- name: Build platform-common package
|
|
||||||
run: |
|
|
||||||
cd packages/platform-common
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
# ===== 第二层:依赖 core 的包 =====
|
|
||||||
- name: Build asset-system package
|
|
||||||
run: |
|
|
||||||
cd packages/asset-system
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
- name: Build components package
|
|
||||||
run: |
|
|
||||||
cd packages/components
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
# ===== 第三层:Rust WASM 引擎 =====
|
|
||||||
- name: Install wasm-pack
|
|
||||||
run: cargo install wasm-pack
|
|
||||||
|
|
||||||
- name: Build engine package (Rust WASM)
|
|
||||||
run: |
|
|
||||||
cd packages/engine
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
- name: Copy WASM files to ecs-engine-bindgen
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mkdir -p packages/ecs-engine-bindgen/src/wasm
|
|
||||||
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
|
|
||||||
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
|
|
||||||
cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/
|
|
||||||
cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/
|
|
||||||
|
|
||||||
- name: Build ecs-engine-bindgen package
|
|
||||||
run: |
|
|
||||||
cd packages/ecs-engine-bindgen
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
# ===== 第四层:依赖 ecs-engine-bindgen/asset-system 的包 =====
|
|
||||||
- name: Build editor-core package
|
- name: Build editor-core package
|
||||||
run: |
|
run: |
|
||||||
cd packages/editor-core
|
cd packages/editor-core
|
||||||
pnpm run build
|
npm run build
|
||||||
|
|
||||||
# ===== 第五层:依赖 editor-core 的包 =====
|
|
||||||
- name: Build UI package
|
|
||||||
run: |
|
|
||||||
cd packages/ui
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
- name: Build tilemap package
|
|
||||||
run: |
|
|
||||||
cd packages/tilemap
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
- name: Build editor-runtime package
|
|
||||||
run: |
|
|
||||||
cd packages/editor-runtime
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
# ===== 第六层:依赖 editor-runtime 的包 =====
|
|
||||||
- name: Build behavior-tree package
|
- name: Build behavior-tree package
|
||||||
run: |
|
run: |
|
||||||
cd packages/behavior-tree
|
cd packages/behavior-tree
|
||||||
pnpm run build
|
npm run build
|
||||||
|
|
||||||
# ===== 第七层:平台包(依赖 ui, tilemap) =====
|
|
||||||
- name: Build platform-web package
|
|
||||||
run: |
|
|
||||||
cd packages/platform-web
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
- name: Bundle runtime files for Tauri
|
|
||||||
run: |
|
|
||||||
cd packages/editor-app
|
|
||||||
node scripts/bundle-runtime.mjs
|
|
||||||
|
|
||||||
- name: Build Tauri app
|
- name: Build Tauri app
|
||||||
uses: tauri-apps/tauri-action@v0.5
|
uses: tauri-apps/tauri-action@v0.5
|
||||||
@@ -193,7 +126,7 @@ jobs:
|
|||||||
- name: Update version files
|
- name: Update version files
|
||||||
run: |
|
run: |
|
||||||
cd packages/editor-app
|
cd packages/editor-app
|
||||||
node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
npm version ${{ github.event.inputs.version }} --no-git-tag-version
|
||||||
node scripts/sync-version.js
|
node scripts/sync-version.js
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
|
|||||||
29
.github/workflows/release.yml
vendored
29
.github/workflows/release.yml
vendored
@@ -41,26 +41,21 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Build core package (if needed)
|
- name: Build core package (if needed)
|
||||||
if: ${{ github.event.inputs.package == 'behavior-tree' || github.event.inputs.package == 'editor-core' }}
|
if: ${{ github.event.inputs.package == 'behavior-tree' || github.event.inputs.package == 'editor-core' }}
|
||||||
run: |
|
run: |
|
||||||
cd packages/core
|
cd packages/core
|
||||||
pnpm run build
|
npm run build
|
||||||
|
|
||||||
# - name: Run tests
|
# - name: Run tests
|
||||||
# run: |
|
# run: |
|
||||||
@@ -72,33 +67,25 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd packages/${{ github.event.inputs.package }}
|
cd packages/${{ github.event.inputs.package }}
|
||||||
if [ "${{ github.event.inputs.version_type }}" = "custom" ]; then
|
if [ "${{ github.event.inputs.version_type }}" = "custom" ]; then
|
||||||
NEW_VERSION=${{ github.event.inputs.custom_version }}
|
npm version ${{ github.event.inputs.custom_version }} --no-git-tag-version --allow-same-version
|
||||||
else
|
else
|
||||||
# Get current version and bump it
|
npm version ${{ github.event.inputs.version_type }} --no-git-tag-version
|
||||||
CURRENT=$(node -p "require('./package.json').version")
|
|
||||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
|
|
||||||
case "${{ github.event.inputs.version_type }}" in
|
|
||||||
major) NEW_VERSION="$((MAJOR+1)).0.0" ;;
|
|
||||||
minor) NEW_VERSION="$MAJOR.$((MINOR+1)).0" ;;
|
|
||||||
patch) NEW_VERSION="$MAJOR.$MINOR.$((PATCH+1))" ;;
|
|
||||||
esac
|
|
||||||
fi
|
fi
|
||||||
# Update package.json using node
|
NEW_VERSION=$(node -p "require('./package.json').version")
|
||||||
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
|
|
||||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "发布版本: $NEW_VERSION"
|
echo "发布版本: $NEW_VERSION"
|
||||||
|
|
||||||
- name: Build package
|
- name: Build package
|
||||||
run: |
|
run: |
|
||||||
cd packages/${{ github.event.inputs.package }}
|
cd packages/${{ github.event.inputs.package }}
|
||||||
pnpm run build:npm
|
npm run build:npm
|
||||||
|
|
||||||
- name: Publish to npm
|
- name: Publish to npm
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
cd packages/${{ github.event.inputs.package }}/dist
|
cd packages/${{ github.event.inputs.package }}/dist
|
||||||
pnpm publish --access public --no-git-checks
|
npm publish --access public
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v6
|
uses: peter-evans/create-pull-request@v6
|
||||||
|
|||||||
11
.github/workflows/size-limit.yml
vendored
11
.github/workflows/size-limit.yml
vendored
@@ -22,24 +22,19 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20.x'
|
node-version: '20.x'
|
||||||
cache: 'pnpm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install
|
run: npm ci
|
||||||
|
|
||||||
- name: Build core package
|
- name: Build core package
|
||||||
run: |
|
run: |
|
||||||
cd packages/core
|
cd packages/core
|
||||||
pnpm run build:npm
|
npm run build:npm
|
||||||
|
|
||||||
- name: Check bundle size
|
- name: Check bundle size
|
||||||
uses: andresz1/size-limit-action@v1
|
uses: andresz1/size-limit-action@v1
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -16,7 +16,6 @@ dist/
|
|||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
.cache/
|
.cache/
|
||||||
.build-cache/
|
|
||||||
|
|
||||||
# IDE 配置
|
# IDE 配置
|
||||||
.idea/
|
.idea/
|
||||||
@@ -49,9 +48,9 @@ logs/
|
|||||||
coverage/
|
coverage/
|
||||||
*.lcov
|
*.lcov
|
||||||
|
|
||||||
# 包管理器锁文件(忽略yarn,保留pnpm)
|
# 包管理器锁文件(保留npm的,忽略其他的)
|
||||||
yarn.lock
|
yarn.lock
|
||||||
package-lock.json
|
pnpm-lock.yaml
|
||||||
|
|
||||||
# 文档生成
|
# 文档生成
|
||||||
docs/api/
|
docs/api/
|
||||||
|
|||||||
@@ -33,26 +33,6 @@ class MyService implements IService {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 服务标识符(ServiceIdentifier)
|
|
||||||
|
|
||||||
服务标识符用于在容器中唯一标识一个服务,支持两种类型:
|
|
||||||
|
|
||||||
- **类构造函数**: 直接使用服务类作为标识符,适用于具体实现类
|
|
||||||
- **Symbol**: 使用 Symbol 作为标识符,适用于接口抽象(推荐用于插件和跨包场景)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 方式1: 使用类作为标识符
|
|
||||||
Core.services.registerSingleton(DataService);
|
|
||||||
const data = Core.services.resolve(DataService);
|
|
||||||
|
|
||||||
// 方式2: 使用 Symbol 作为标识符(推荐用于接口)
|
|
||||||
const IFileSystem = Symbol.for('IFileSystem');
|
|
||||||
Core.services.registerInstance(IFileSystem, new TauriFileSystem());
|
|
||||||
const fs = Core.services.resolve<IFileSystem>(IFileSystem);
|
|
||||||
```
|
|
||||||
|
|
||||||
> **提示**: 使用 `Symbol.for()` 而非 `Symbol()` 可确保跨包/跨模块共享同一个标识符。详见[高级用法 - 接口与 Symbol 标识符模式](#接口与-symbol-标识符模式)。
|
|
||||||
|
|
||||||
#### 生命周期
|
#### 生命周期
|
||||||
|
|
||||||
服务容器支持两种生命周期:
|
服务容器支持两种生命周期:
|
||||||
@@ -64,13 +44,7 @@ const fs = Core.services.resolve<IFileSystem>(IFileSystem);
|
|||||||
|
|
||||||
### 访问服务容器
|
### 访问服务容器
|
||||||
|
|
||||||
ECS Framework 提供了三级服务容器:
|
Core 类内置了服务容器,可以通过 `Core.services` 访问:
|
||||||
|
|
||||||
> **版本说明**:World 服务容器功能在 v2.2.13+ 版本中可用
|
|
||||||
|
|
||||||
#### Core 级别服务容器
|
|
||||||
|
|
||||||
应用程序全局服务容器,可以通过 `Core.services` 访问:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Core } from '@esengine/ecs-framework';
|
import { Core } from '@esengine/ecs-framework';
|
||||||
@@ -78,53 +52,10 @@ import { Core } from '@esengine/ecs-framework';
|
|||||||
// 初始化Core
|
// 初始化Core
|
||||||
Core.create({ debug: true });
|
Core.create({ debug: true });
|
||||||
|
|
||||||
// 访问全局服务容器
|
// 访问服务容器
|
||||||
const container = Core.services;
|
const container = Core.services;
|
||||||
```
|
```
|
||||||
|
|
||||||
#### World 级别服务容器
|
|
||||||
|
|
||||||
每个 World 拥有独立的服务容器,用于管理 World 范围内的服务:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { World } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
// 创建 World
|
|
||||||
const world = new World({ name: 'GameWorld' });
|
|
||||||
|
|
||||||
// 访问 World 级别的服务容器
|
|
||||||
const worldContainer = world.services;
|
|
||||||
|
|
||||||
// 注册 World 级别的服务
|
|
||||||
world.services.registerSingleton(RoomManager);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Scene 级别服务容器
|
|
||||||
|
|
||||||
每个 Scene 拥有独立的服务容器,用于管理 Scene 范围内的服务:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 访问 Scene 级别的服务容器
|
|
||||||
const sceneContainer = scene.services;
|
|
||||||
|
|
||||||
// 注册 Scene 级别的服务
|
|
||||||
scene.services.registerSingleton(PhysicsSystem);
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 服务容器层级
|
|
||||||
|
|
||||||
```
|
|
||||||
Core.services (应用程序全局)
|
|
||||||
└─ World.services (World 级别)
|
|
||||||
└─ Scene.services (Scene 级别)
|
|
||||||
```
|
|
||||||
|
|
||||||
不同级别的服务容器是独立的,服务不会自动向上或向下查找。选择合适的容器级别:
|
|
||||||
|
|
||||||
- **Core.services**: 应用程序级别的全局服务(配置、插件管理器等)
|
|
||||||
- **World.services**: World 级别的服务(房间管理器、多人游戏状态等)
|
|
||||||
- **Scene.services**: Scene 级别的服务(ECS 系统、场景特定逻辑等)
|
|
||||||
|
|
||||||
### 注册服务
|
### 注册服务
|
||||||
|
|
||||||
#### 注册单例服务
|
#### 注册单例服务
|
||||||
@@ -353,20 +284,21 @@ class GameService implements IService {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### @InjectProperty 装饰器
|
### @Inject 装饰器
|
||||||
|
|
||||||
通过属性装饰器注入依赖。注入时机是在构造函数执行后、`onInitialize()` 调用前完成:
|
在构造函数中注入依赖:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Injectable, InjectProperty, IService } from '@esengine/ecs-framework';
|
import { Injectable, Inject, IService } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
class PlayerService implements IService {
|
class PlayerService implements IService {
|
||||||
@InjectProperty(DataService)
|
constructor(
|
||||||
private data!: DataService;
|
@Inject(DataService) private data: DataService,
|
||||||
|
@Inject(GameService) private game: GameService
|
||||||
@InjectProperty(GameService)
|
) {
|
||||||
private game!: GameService;
|
// data 和 game 会自动从容器中解析
|
||||||
|
}
|
||||||
|
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
// 清理资源
|
// 清理资源
|
||||||
@@ -374,35 +306,6 @@ class PlayerService implements IService {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
在 EntitySystem 中使用属性注入:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Injectable()
|
|
||||||
class CombatSystem extends EntitySystem {
|
|
||||||
@InjectProperty(TimeService)
|
|
||||||
private timeService!: TimeService;
|
|
||||||
|
|
||||||
@InjectProperty(AudioService)
|
|
||||||
private audio!: AudioService;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(Matcher.all(Health, Attack));
|
|
||||||
}
|
|
||||||
|
|
||||||
onInitialize(): void {
|
|
||||||
// 此时属性已注入完成,可以安全使用
|
|
||||||
console.log('Delta time:', this.timeService.getDeltaTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
processEntity(entity: Entity): void {
|
|
||||||
// 使用注入的服务
|
|
||||||
this.audio.playSound('attack');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
> **注意**: 属性声明时使用 `!` 断言(如 `private data!: DataService`),表示该属性会在使用前被注入。
|
|
||||||
|
|
||||||
### 注册可注入服务
|
### 注册可注入服务
|
||||||
|
|
||||||
使用 `registerInjectable` 自动处理依赖注入:
|
使用 `registerInjectable` 自动处理依赖注入:
|
||||||
@@ -410,10 +313,10 @@ class CombatSystem extends EntitySystem {
|
|||||||
```typescript
|
```typescript
|
||||||
import { registerInjectable } from '@esengine/ecs-framework';
|
import { registerInjectable } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
// 注册服务(会自动解析 @InjectProperty 依赖)
|
// 注册服务(会自动解析@Inject依赖)
|
||||||
registerInjectable(Core.services, PlayerService);
|
registerInjectable(Core.services, PlayerService);
|
||||||
|
|
||||||
// 解析时会自动注入属性依赖
|
// 解析时会自动注入依赖
|
||||||
const player = Core.services.resolve(PlayerService);
|
const player = Core.services.resolve(PlayerService);
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -541,164 +444,22 @@ registerInjectable(Core.services, NetworkService);
|
|||||||
|
|
||||||
## 高级用法
|
## 高级用法
|
||||||
|
|
||||||
### 接口与 Symbol 标识符模式
|
### 服务替换(测试)
|
||||||
|
|
||||||
在大型项目或需要跨平台适配的游戏中,推荐使用"接口 + Symbol.for 标识符"模式。这种模式实现了真正的依赖倒置,让代码依赖于抽象而非具体实现。
|
在测试中替换真实服务为模拟服务:
|
||||||
|
|
||||||
#### 为什么使用 Symbol.for
|
|
||||||
|
|
||||||
- **跨包共享**: `Symbol.for('key')` 在全局 Symbol 注册表中创建/获取 Symbol,确保不同包中使用相同的标识符
|
|
||||||
- **接口解耦**: 消费者只依赖接口定义,不依赖具体实现类
|
|
||||||
- **可替换实现**: 可以在运行时注入不同的实现(如测试 Mock、不同平台适配)
|
|
||||||
|
|
||||||
#### 定义接口和标识符
|
|
||||||
|
|
||||||
以音频服务为例,游戏需要在不同平台(Web、微信小游戏、原生App)使用不同的音频实现:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// IAudioService.ts - 定义接口和标识符
|
// 测试代码
|
||||||
export interface IAudioService {
|
class MockDataService implements IService {
|
||||||
dispose(): void;
|
getData(key: string) {
|
||||||
playSound(id: string): void;
|
return 'mock data';
|
||||||
playMusic(id: string, loop?: boolean): void;
|
}
|
||||||
stopMusic(): void;
|
|
||||||
setVolume(volume: number): void;
|
dispose(): void {}
|
||||||
preload(id: string, url: string): Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 Symbol.for 确保跨包共享同一个 Symbol
|
// 注册模拟服务(用于测试)
|
||||||
export const IAudioService = Symbol.for('IAudioService');
|
Core.services.registerInstance(DataService, new MockDataService());
|
||||||
```
|
|
||||||
|
|
||||||
#### 实现接口
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// WebAudioService.ts - Web 平台实现
|
|
||||||
import { IAudioService } from './IAudioService';
|
|
||||||
|
|
||||||
export class WebAudioService implements IAudioService {
|
|
||||||
private audioContext: AudioContext;
|
|
||||||
private sounds: Map<string, AudioBuffer> = new Map();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.audioContext = new AudioContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
playSound(id: string): void {
|
|
||||||
const buffer = this.sounds.get(id);
|
|
||||||
if (buffer) {
|
|
||||||
const source = this.audioContext.createBufferSource();
|
|
||||||
source.buffer = buffer;
|
|
||||||
source.connect(this.audioContext.destination);
|
|
||||||
source.start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async preload(id: string, url: string): Promise<void> {
|
|
||||||
const response = await fetch(url);
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
||||||
this.sounds.set(id, audioBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... 其他方法实现
|
|
||||||
|
|
||||||
dispose(): void {
|
|
||||||
this.audioContext.close();
|
|
||||||
this.sounds.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// WechatAudioService.ts - 微信小游戏平台实现
|
|
||||||
export class WechatAudioService implements IAudioService {
|
|
||||||
private innerAudioContexts: Map<string, WechatMinigame.InnerAudioContext> = new Map();
|
|
||||||
|
|
||||||
playSound(id: string): void {
|
|
||||||
const ctx = this.innerAudioContexts.get(id);
|
|
||||||
if (ctx) {
|
|
||||||
ctx.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async preload(id: string, url: string): Promise<void> {
|
|
||||||
const ctx = wx.createInnerAudioContext();
|
|
||||||
ctx.src = url;
|
|
||||||
this.innerAudioContexts.set(id, ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... 其他方法实现
|
|
||||||
|
|
||||||
dispose(): void {
|
|
||||||
for (const ctx of this.innerAudioContexts.values()) {
|
|
||||||
ctx.destroy();
|
|
||||||
}
|
|
||||||
this.innerAudioContexts.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 注册和使用
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { IAudioService } from './IAudioService';
|
|
||||||
import { WebAudioService } from './WebAudioService';
|
|
||||||
import { WechatAudioService } from './WechatAudioService';
|
|
||||||
|
|
||||||
// 根据平台注册不同实现
|
|
||||||
if (typeof wx !== 'undefined') {
|
|
||||||
Core.services.registerInstance(IAudioService, new WechatAudioService());
|
|
||||||
} else {
|
|
||||||
Core.services.registerInstance(IAudioService, new WebAudioService());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 业务代码中使用 - 不关心具体实现
|
|
||||||
const audio = Core.services.resolve<IAudioService>(IAudioService);
|
|
||||||
await audio.preload('explosion', '/sounds/explosion.mp3');
|
|
||||||
audio.playSound('explosion');
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 跨模块使用
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 在游戏系统中使用
|
|
||||||
import { IAudioService } from '@mygame/core';
|
|
||||||
|
|
||||||
class CombatSystem extends EntitySystem {
|
|
||||||
private audio: IAudioService;
|
|
||||||
|
|
||||||
initialize(): void {
|
|
||||||
// 获取音频服务,不需要知道具体实现
|
|
||||||
this.audio = this.scene.services.resolve<IAudioService>(IAudioService);
|
|
||||||
}
|
|
||||||
|
|
||||||
onEntityDeath(entity: Entity): void {
|
|
||||||
this.audio.playSound('death');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Symbol vs Symbol.for
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Symbol() - 每次创建唯一的 Symbol
|
|
||||||
const sym1 = Symbol('test');
|
|
||||||
const sym2 = Symbol('test');
|
|
||||||
console.log(sym1 === sym2); // false - 不同的 Symbol
|
|
||||||
|
|
||||||
// Symbol.for() - 在全局注册表中共享
|
|
||||||
const sym3 = Symbol.for('test');
|
|
||||||
const sym4 = Symbol.for('test');
|
|
||||||
console.log(sym3 === sym4); // true - 同一个 Symbol
|
|
||||||
|
|
||||||
// 跨包场景
|
|
||||||
// package-a/index.ts
|
|
||||||
export const IMyService = Symbol.for('IMyService');
|
|
||||||
|
|
||||||
// package-b/index.ts (不同的包)
|
|
||||||
const IMyService = Symbol.for('IMyService');
|
|
||||||
// 与 package-a 中的是同一个 Symbol!
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 循环依赖检测
|
### 循环依赖检测
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ const worldManager = Core.services.resolve(WorldManager);
|
|||||||
// {
|
// {
|
||||||
// maxWorlds: 50,
|
// maxWorlds: 50,
|
||||||
// autoCleanup: true,
|
// autoCleanup: true,
|
||||||
// cleanupFrameInterval: 1800 // 间隔多少帧清理闲置 World
|
// cleanupInterval: 30000 // 30 秒
|
||||||
// }
|
// }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -10,43 +10,38 @@ export default [
|
|||||||
parser: tseslint.parser,
|
parser: tseslint.parser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
sourceType: 'module',
|
sourceType: 'module'
|
||||||
project: true,
|
|
||||||
tsconfigRootDir: import.meta.dirname
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'semi': ['warn', 'always'],
|
'semi': 'warn',
|
||||||
'quotes': ['warn', 'single', { avoidEscape: true }],
|
'quotes': 'warn',
|
||||||
'indent': ['warn', 4, {
|
'indent': 'off',
|
||||||
SwitchCase: 1,
|
|
||||||
ignoredNodes: [
|
|
||||||
'PropertyDefinition[decorators.length > 0]',
|
|
||||||
'TSTypeParameterInstantiation'
|
|
||||||
]
|
|
||||||
}],
|
|
||||||
'no-trailing-spaces': 'warn',
|
'no-trailing-spaces': 'warn',
|
||||||
'eol-last': ['warn', 'always'],
|
'eol-last': 'warn',
|
||||||
'comma-dangle': ['warn', 'never'],
|
'comma-dangle': 'warn',
|
||||||
'object-curly-spacing': ['warn', 'always'],
|
'object-curly-spacing': 'warn',
|
||||||
'array-bracket-spacing': ['warn', 'never'],
|
'array-bracket-spacing': 'warn',
|
||||||
'arrow-parens': ['warn', 'always'],
|
'arrow-parens': 'warn',
|
||||||
'no-multiple-empty-lines': ['warn', { max: 2, maxEOF: 1 }],
|
'prefer-const': 'warn',
|
||||||
|
'no-multiple-empty-lines': 'warn',
|
||||||
'no-console': 'off',
|
'no-console': 'off',
|
||||||
'no-empty': 'warn',
|
|
||||||
'no-case-declarations': 'warn',
|
|
||||||
'no-useless-catch': 'warn',
|
|
||||||
'no-prototype-builtins': 'warn',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'warn',
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
|
||||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
|
||||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
|
||||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
|
||||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
|
||||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-call': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-return': 'off',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off'
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-require-imports': 'warn',
|
||||||
|
'@typescript-eslint/no-this-alias': 'warn',
|
||||||
|
'no-case-declarations': 'warn',
|
||||||
|
'no-prototype-builtins': 'warn',
|
||||||
|
'no-empty': 'warn',
|
||||||
|
'no-useless-catch': 'warn'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -65,8 +60,7 @@ export default [
|
|||||||
'examples/lawn-mower-demo/**',
|
'examples/lawn-mower-demo/**',
|
||||||
'extensions/**',
|
'extensions/**',
|
||||||
'**/*.min.js',
|
'**/*.min.js',
|
||||||
'**/*.d.ts',
|
'**/*.d.ts'
|
||||||
'**/wasm/**'
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
352
examples/core-demos/pnpm-lock.yaml
generated
352
examples/core-demos/pnpm-lock.yaml
generated
@@ -1,352 +0,0 @@
|
|||||||
lockfileVersion: '9.0'
|
|
||||||
|
|
||||||
settings:
|
|
||||||
autoInstallPeers: true
|
|
||||||
excludeLinksFromLockfile: false
|
|
||||||
|
|
||||||
importers:
|
|
||||||
|
|
||||||
.:
|
|
||||||
dependencies:
|
|
||||||
'@esengine/ecs-framework':
|
|
||||||
specifier: file:../../packages/core
|
|
||||||
version: file:../../packages/core
|
|
||||||
devDependencies:
|
|
||||||
typescript:
|
|
||||||
specifier: ^5.0.0
|
|
||||||
version: 5.9.3
|
|
||||||
vite:
|
|
||||||
specifier: ^4.0.0
|
|
||||||
version: 4.5.14
|
|
||||||
|
|
||||||
packages:
|
|
||||||
|
|
||||||
'@esbuild/android-arm64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [android]
|
|
||||||
|
|
||||||
'@esbuild/android-arm@0.18.20':
|
|
||||||
resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm]
|
|
||||||
os: [android]
|
|
||||||
|
|
||||||
'@esbuild/android-x64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [android]
|
|
||||||
|
|
||||||
'@esbuild/darwin-arm64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
'@esbuild/darwin-x64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
'@esbuild/freebsd-arm64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [freebsd]
|
|
||||||
|
|
||||||
'@esbuild/freebsd-x64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [freebsd]
|
|
||||||
|
|
||||||
'@esbuild/linux-arm64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@esbuild/linux-arm@0.18.20':
|
|
||||||
resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@esbuild/linux-ia32@0.18.20':
|
|
||||||
resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [ia32]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@esbuild/linux-loong64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [loong64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@esbuild/linux-mips64el@0.18.20':
|
|
||||||
resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [mips64el]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@esbuild/linux-ppc64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [ppc64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@esbuild/linux-riscv64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [riscv64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@esbuild/linux-s390x@0.18.20':
|
|
||||||
resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [s390x]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@esbuild/linux-x64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@esbuild/netbsd-x64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [netbsd]
|
|
||||||
|
|
||||||
'@esbuild/openbsd-x64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [openbsd]
|
|
||||||
|
|
||||||
'@esbuild/sunos-x64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [sunos]
|
|
||||||
|
|
||||||
'@esbuild/win32-arm64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@esbuild/win32-ia32@0.18.20':
|
|
||||||
resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [ia32]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@esbuild/win32-x64@0.18.20':
|
|
||||||
resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@esengine/ecs-framework@file:../../packages/core':
|
|
||||||
resolution: {directory: ../../packages/core, type: directory}
|
|
||||||
|
|
||||||
esbuild@0.18.20:
|
|
||||||
resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
|
|
||||||
engines: {node: '>=12'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
fsevents@2.3.3:
|
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
nanoid@3.3.11:
|
|
||||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
picocolors@1.1.1:
|
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
|
||||||
|
|
||||||
postcss@8.5.6:
|
|
||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
|
||||||
|
|
||||||
rollup@3.29.5:
|
|
||||||
resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==}
|
|
||||||
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
source-map-js@1.2.1:
|
|
||||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
tslib@2.8.1:
|
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
|
||||||
|
|
||||||
typescript@5.9.3:
|
|
||||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
|
||||||
engines: {node: '>=14.17'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
vite@4.5.14:
|
|
||||||
resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==}
|
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
'@types/node': '>= 14'
|
|
||||||
less: '*'
|
|
||||||
lightningcss: ^1.21.0
|
|
||||||
sass: '*'
|
|
||||||
stylus: '*'
|
|
||||||
sugarss: '*'
|
|
||||||
terser: ^5.4.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/node':
|
|
||||||
optional: true
|
|
||||||
less:
|
|
||||||
optional: true
|
|
||||||
lightningcss:
|
|
||||||
optional: true
|
|
||||||
sass:
|
|
||||||
optional: true
|
|
||||||
stylus:
|
|
||||||
optional: true
|
|
||||||
sugarss:
|
|
||||||
optional: true
|
|
||||||
terser:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
snapshots:
|
|
||||||
|
|
||||||
'@esbuild/android-arm64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/android-arm@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/android-x64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/darwin-arm64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/darwin-x64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/freebsd-arm64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/freebsd-x64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/linux-arm64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/linux-arm@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/linux-ia32@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/linux-loong64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/linux-mips64el@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/linux-ppc64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/linux-riscv64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/linux-s390x@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/linux-x64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/netbsd-x64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/openbsd-x64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/sunos-x64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/win32-arm64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/win32-ia32@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esbuild/win32-x64@0.18.20':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@esengine/ecs-framework@file:../../packages/core':
|
|
||||||
dependencies:
|
|
||||||
tslib: 2.8.1
|
|
||||||
|
|
||||||
esbuild@0.18.20:
|
|
||||||
optionalDependencies:
|
|
||||||
'@esbuild/android-arm': 0.18.20
|
|
||||||
'@esbuild/android-arm64': 0.18.20
|
|
||||||
'@esbuild/android-x64': 0.18.20
|
|
||||||
'@esbuild/darwin-arm64': 0.18.20
|
|
||||||
'@esbuild/darwin-x64': 0.18.20
|
|
||||||
'@esbuild/freebsd-arm64': 0.18.20
|
|
||||||
'@esbuild/freebsd-x64': 0.18.20
|
|
||||||
'@esbuild/linux-arm': 0.18.20
|
|
||||||
'@esbuild/linux-arm64': 0.18.20
|
|
||||||
'@esbuild/linux-ia32': 0.18.20
|
|
||||||
'@esbuild/linux-loong64': 0.18.20
|
|
||||||
'@esbuild/linux-mips64el': 0.18.20
|
|
||||||
'@esbuild/linux-ppc64': 0.18.20
|
|
||||||
'@esbuild/linux-riscv64': 0.18.20
|
|
||||||
'@esbuild/linux-s390x': 0.18.20
|
|
||||||
'@esbuild/linux-x64': 0.18.20
|
|
||||||
'@esbuild/netbsd-x64': 0.18.20
|
|
||||||
'@esbuild/openbsd-x64': 0.18.20
|
|
||||||
'@esbuild/sunos-x64': 0.18.20
|
|
||||||
'@esbuild/win32-arm64': 0.18.20
|
|
||||||
'@esbuild/win32-ia32': 0.18.20
|
|
||||||
'@esbuild/win32-x64': 0.18.20
|
|
||||||
|
|
||||||
fsevents@2.3.3:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
|
||||||
|
|
||||||
postcss@8.5.6:
|
|
||||||
dependencies:
|
|
||||||
nanoid: 3.3.11
|
|
||||||
picocolors: 1.1.1
|
|
||||||
source-map-js: 1.2.1
|
|
||||||
|
|
||||||
rollup@3.29.5:
|
|
||||||
optionalDependencies:
|
|
||||||
fsevents: 2.3.3
|
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
|
||||||
|
|
||||||
tslib@2.8.1: {}
|
|
||||||
|
|
||||||
typescript@5.9.3: {}
|
|
||||||
|
|
||||||
vite@4.5.14:
|
|
||||||
dependencies:
|
|
||||||
esbuild: 0.18.20
|
|
||||||
postcss: 8.5.6
|
|
||||||
rollup: 3.29.5
|
|
||||||
optionalDependencies:
|
|
||||||
fsevents: 2.3.3
|
|
||||||
27061
package-lock.json
generated
Normal file
27061
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -18,23 +18,35 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"bootstrap": "lerna bootstrap",
|
"bootstrap": "lerna bootstrap",
|
||||||
"clean": "lerna run clean",
|
"clean": "lerna run clean",
|
||||||
"build": "npm run build:core && npm run build:math",
|
"build": "npm run build:core && npm run build:math && npm run build:network-shared && npm run build:network-client && npm run build:network-server",
|
||||||
"build:core": "cd packages/core && npm run build",
|
"build:core": "cd packages/core && npm run build",
|
||||||
"build:math": "cd packages/math && npm run build",
|
"build:math": "cd packages/math && npm run build",
|
||||||
"build:npm": "npm run build:npm:core && npm run build:npm:math",
|
"build:network-shared": "cd packages/network-shared && npm run build",
|
||||||
|
"build:network-client": "cd packages/network-client && npm run build",
|
||||||
|
"build:network-server": "cd packages/network-server && npm run build",
|
||||||
|
"build:npm": "npm run build:npm:core && npm run build:npm:math && npm run build:npm:network-shared && npm run build:npm:network-client && npm run build:npm:network-server",
|
||||||
"build:npm:core": "cd packages/core && npm run build:npm",
|
"build:npm:core": "cd packages/core && npm run build:npm",
|
||||||
"build:npm:math": "cd packages/math && npm run build:npm",
|
"build:npm:math": "cd packages/math && npm run build:npm",
|
||||||
|
"build:npm:network-shared": "cd packages/network-shared && npm run build:npm",
|
||||||
|
"build:npm:network-client": "cd packages/network-client && npm run build:npm",
|
||||||
|
"build:npm:network-server": "cd packages/network-server && npm run build:npm",
|
||||||
"test": "lerna run test",
|
"test": "lerna run test",
|
||||||
"test:coverage": "lerna run test:coverage",
|
"test:coverage": "lerna run test:coverage",
|
||||||
"test:ci": "lerna run test:ci",
|
"test:ci": "lerna run test:ci",
|
||||||
"prepare:publish": "npm run build:npm && node scripts/pre-publish-check.cjs",
|
"prepare:publish": "npm run build:npm && node scripts/pre-publish-check.cjs",
|
||||||
"sync:versions": "node scripts/sync-versions.cjs",
|
"sync:versions": "node scripts/sync-versions.cjs",
|
||||||
"publish:all": "npm run prepare:publish && npm run publish:all:dist",
|
"publish:all": "npm run prepare:publish && npm run publish:all:dist",
|
||||||
"publish:all:dist": "npm run publish:core && npm run publish:math",
|
"publish:all:dist": "npm run publish:core && npm run publish:math && npm run publish:network-shared && npm run publish:network-client && npm run publish:network-server",
|
||||||
"publish:core": "cd packages/core && npm run publish:npm",
|
"publish:core": "cd packages/core && npm run publish:npm",
|
||||||
"publish:core:patch": "cd packages/core && npm run publish:patch",
|
"publish:core:patch": "cd packages/core && npm run publish:patch",
|
||||||
"publish:math": "cd packages/math && npm run publish:npm",
|
"publish:math": "cd packages/math && npm run publish:npm",
|
||||||
"publish:math:patch": "cd packages/math && npm run publish:patch",
|
"publish:math:patch": "cd packages/math && npm run publish:patch",
|
||||||
|
"publish:network-shared": "cd packages/network-shared && npm run publish:npm",
|
||||||
|
"publish:network-shared:patch": "cd packages/network-shared && npm run publish:patch",
|
||||||
|
"publish:network-client": "cd packages/network-client && npm run publish:npm",
|
||||||
|
"publish:network-client:patch": "cd packages/network-client && npm run publish:patch",
|
||||||
|
"publish:network-server": "cd packages/network-server && npm run publish:npm",
|
||||||
|
"publish:network-server:patch": "cd packages/network-server && npm run publish:patch",
|
||||||
"publish": "lerna publish",
|
"publish": "lerna publish",
|
||||||
"version": "lerna version",
|
"version": "lerna version",
|
||||||
"release": "semantic-release",
|
"release": "semantic-release",
|
||||||
@@ -53,16 +65,13 @@
|
|||||||
"format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
"format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||||
"type-check": "lerna run type-check",
|
"type-check": "lerna run type-check",
|
||||||
"lint": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
"lint": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
|
||||||
"lint:fix": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\" --fix",
|
"lint:fix": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\" --fix"
|
||||||
"build:wasm": "cd packages/engine && wasm-pack build --dev --out-dir pkg",
|
|
||||||
"build:wasm:release": "cd packages/engine && wasm-pack build --release --out-dir pkg"
|
|
||||||
},
|
},
|
||||||
"author": "yhh",
|
"author": "yhh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^18.6.0",
|
"@commitlint/cli": "^18.6.0",
|
||||||
"@commitlint/config-conventional": "^18.6.0",
|
"@commitlint/config-conventional": "^18.6.0",
|
||||||
"@eslint/js": "^9.39.1",
|
|
||||||
"@iconify/json": "^2.2.388",
|
"@iconify/json": "^2.2.388",
|
||||||
"@rollup/plugin-commonjs": "^28.0.3",
|
"@rollup/plugin-commonjs": "^28.0.3",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||||
@@ -95,7 +104,6 @@
|
|||||||
"typedoc": "^0.28.13",
|
"typedoc": "^0.28.13",
|
||||||
"typedoc-plugin-markdown": "^4.9.0",
|
"typedoc-plugin-markdown": "^4.9.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"typescript-eslint": "^8.47.0",
|
|
||||||
"unplugin-icons": "^22.3.0",
|
"unplugin-icons": "^22.3.0",
|
||||||
"vitepress": "^1.6.4"
|
"vitepress": "^1.6.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@esengine/asset-system",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Asset management system for ES Engine",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"module": "dist/index.mjs",
|
|
||||||
"types": "dist/index.d.ts",
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"import": "./dist/index.mjs",
|
|
||||||
"require": "./dist/index.js",
|
|
||||||
"types": "./dist/index.d.ts"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"scripts": {
|
|
||||||
"build": "rollup -c",
|
|
||||||
"build:npm": "npm run build",
|
|
||||||
"clean": "rimraf dist",
|
|
||||||
"type-check": "npx tsc --noEmit"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"ecs",
|
|
||||||
"asset",
|
|
||||||
"resource",
|
|
||||||
"bundle"
|
|
||||||
],
|
|
||||||
"author": "yhh",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@esengine/ecs-framework": "^2.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@rollup/plugin-commonjs": "^28.0.3",
|
|
||||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
||||||
"@rollup/plugin-typescript": "^11.1.6",
|
|
||||||
"rimraf": "^5.0.0",
|
|
||||||
"rollup": "^4.42.0",
|
|
||||||
"rollup-plugin-dts": "^6.2.1",
|
|
||||||
"typescript": "^5.8.3"
|
|
||||||
},
|
|
||||||
"publishConfig": {
|
|
||||||
"access": "public"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/esengine/ecs-framework.git",
|
|
||||||
"directory": "packages/asset-system"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import typescript from '@rollup/plugin-typescript';
|
|
||||||
import resolve from '@rollup/plugin-node-resolve';
|
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
|
||||||
import dts from 'rollup-plugin-dts';
|
|
||||||
|
|
||||||
const external = ['@esengine/ecs-framework'];
|
|
||||||
|
|
||||||
export default [
|
|
||||||
// ESM and CJS builds
|
|
||||||
{
|
|
||||||
input: 'src/index.ts',
|
|
||||||
output: [
|
|
||||||
{
|
|
||||||
file: 'dist/index.js',
|
|
||||||
format: 'cjs',
|
|
||||||
sourcemap: true,
|
|
||||||
exports: 'named'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
file: 'dist/index.mjs',
|
|
||||||
format: 'es',
|
|
||||||
sourcemap: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
external,
|
|
||||||
plugins: [
|
|
||||||
resolve({
|
|
||||||
preferBuiltins: false,
|
|
||||||
browser: true
|
|
||||||
}),
|
|
||||||
commonjs(),
|
|
||||||
typescript({
|
|
||||||
tsconfig: './tsconfig.json',
|
|
||||||
declaration: false,
|
|
||||||
declarationMap: false
|
|
||||||
})
|
|
||||||
]
|
|
||||||
},
|
|
||||||
// Type definitions
|
|
||||||
{
|
|
||||||
input: 'src/index.ts',
|
|
||||||
output: {
|
|
||||||
file: 'dist/index.d.ts',
|
|
||||||
format: 'es'
|
|
||||||
},
|
|
||||||
external,
|
|
||||||
plugins: [dts()]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
/**
|
|
||||||
* Asset cache implementation
|
|
||||||
* 资产缓存实现
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { AssetGUID } from '../types/AssetTypes';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cache entry
|
|
||||||
* 缓存条目
|
|
||||||
*/
|
|
||||||
interface CacheEntry {
|
|
||||||
guid: AssetGUID;
|
|
||||||
asset: unknown;
|
|
||||||
lastAccessTime: number;
|
|
||||||
accessCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset cache implementation
|
|
||||||
* 资产缓存实现
|
|
||||||
*/
|
|
||||||
export class AssetCache {
|
|
||||||
private readonly _cache = new Map<AssetGUID, CacheEntry>();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
// 无配置,无限制缓存 / No config, unlimited cache
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cached asset
|
|
||||||
* 获取缓存的资产
|
|
||||||
*/
|
|
||||||
get<T = unknown>(guid: AssetGUID): T | null {
|
|
||||||
const entry = this._cache.get(guid);
|
|
||||||
if (!entry) return null;
|
|
||||||
|
|
||||||
// 更新访问信息 / Update access info
|
|
||||||
entry.lastAccessTime = Date.now();
|
|
||||||
entry.accessCount++;
|
|
||||||
|
|
||||||
return entry.asset as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set cached asset
|
|
||||||
* 设置缓存的资产
|
|
||||||
*/
|
|
||||||
set<T = unknown>(guid: AssetGUID, asset: T): void {
|
|
||||||
const now = Date.now();
|
|
||||||
const entry: CacheEntry = {
|
|
||||||
guid,
|
|
||||||
asset,
|
|
||||||
lastAccessTime: now,
|
|
||||||
accessCount: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
// 如果已存在,更新 / Update if exists
|
|
||||||
const oldEntry = this._cache.get(guid);
|
|
||||||
if (oldEntry) {
|
|
||||||
entry.accessCount = oldEntry.accessCount + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._cache.set(guid, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if asset is cached
|
|
||||||
* 检查资产是否缓存
|
|
||||||
*/
|
|
||||||
has(guid: AssetGUID): boolean {
|
|
||||||
return this._cache.has(guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove asset from cache
|
|
||||||
* 从缓存移除资产
|
|
||||||
*/
|
|
||||||
remove(guid: AssetGUID): void {
|
|
||||||
this._cache.delete(guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all cache
|
|
||||||
* 清空所有缓存
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
this._cache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cache size
|
|
||||||
* 获取缓存大小
|
|
||||||
*/
|
|
||||||
getSize(): number {
|
|
||||||
return this._cache.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all cached GUIDs
|
|
||||||
* 获取所有缓存的GUID
|
|
||||||
*/
|
|
||||||
getAllGuids(): AssetGUID[] {
|
|
||||||
return Array.from(this._cache.keys());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cache statistics
|
|
||||||
* 获取缓存统计
|
|
||||||
*/
|
|
||||||
getStatistics(): {
|
|
||||||
count: number;
|
|
||||||
entries: Array<{
|
|
||||||
guid: AssetGUID;
|
|
||||||
accessCount: number;
|
|
||||||
lastAccessTime: number;
|
|
||||||
}>;
|
|
||||||
} {
|
|
||||||
const entries = Array.from(this._cache.values()).map((entry) => ({
|
|
||||||
guid: entry.guid,
|
|
||||||
accessCount: entry.accessCount,
|
|
||||||
lastAccessTime: entry.lastAccessTime
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
count: this._cache.size,
|
|
||||||
entries
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,431 +0,0 @@
|
|||||||
/**
|
|
||||||
* Asset database for managing asset metadata
|
|
||||||
* 用于管理资产元数据的资产数据库
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
AssetGUID,
|
|
||||||
AssetType,
|
|
||||||
IAssetMetadata,
|
|
||||||
IAssetCatalogEntry
|
|
||||||
} from '../types/AssetTypes';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset database implementation
|
|
||||||
* 资产数据库实现
|
|
||||||
*/
|
|
||||||
export class AssetDatabase {
|
|
||||||
private readonly _metadata = new Map<AssetGUID, IAssetMetadata>();
|
|
||||||
private readonly _pathToGuid = new Map<string, AssetGUID>();
|
|
||||||
private readonly _typeToGuids = new Map<AssetType, Set<AssetGUID>>();
|
|
||||||
private readonly _labelToGuids = new Map<string, Set<AssetGUID>>();
|
|
||||||
private readonly _dependencies = new Map<AssetGUID, Set<AssetGUID>>();
|
|
||||||
private readonly _dependents = new Map<AssetGUID, Set<AssetGUID>>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add asset to database
|
|
||||||
* 添加资产到数据库
|
|
||||||
*/
|
|
||||||
addAsset(metadata: IAssetMetadata): void {
|
|
||||||
const { guid, path, type, labels, dependencies } = metadata;
|
|
||||||
|
|
||||||
// 存储元数据 / Store metadata
|
|
||||||
this._metadata.set(guid, metadata);
|
|
||||||
this._pathToGuid.set(path, guid);
|
|
||||||
|
|
||||||
// 按类型索引 / Index by type
|
|
||||||
if (!this._typeToGuids.has(type)) {
|
|
||||||
this._typeToGuids.set(type, new Set());
|
|
||||||
}
|
|
||||||
this._typeToGuids.get(type)!.add(guid);
|
|
||||||
|
|
||||||
// 按标签索引 / Index by labels
|
|
||||||
labels.forEach((label) => {
|
|
||||||
if (!this._labelToGuids.has(label)) {
|
|
||||||
this._labelToGuids.set(label, new Set());
|
|
||||||
}
|
|
||||||
this._labelToGuids.get(label)!.add(guid);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 建立依赖关系 / Establish dependencies
|
|
||||||
this.updateDependencies(guid, dependencies);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove asset from database
|
|
||||||
* 从数据库移除资产
|
|
||||||
*/
|
|
||||||
removeAsset(guid: AssetGUID): void {
|
|
||||||
const metadata = this._metadata.get(guid);
|
|
||||||
if (!metadata) return;
|
|
||||||
|
|
||||||
// 清理元数据 / Clean up metadata
|
|
||||||
this._metadata.delete(guid);
|
|
||||||
this._pathToGuid.delete(metadata.path);
|
|
||||||
|
|
||||||
// 清理类型索引 / Clean up type index
|
|
||||||
const typeSet = this._typeToGuids.get(metadata.type);
|
|
||||||
if (typeSet) {
|
|
||||||
typeSet.delete(guid);
|
|
||||||
if (typeSet.size === 0) {
|
|
||||||
this._typeToGuids.delete(metadata.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理标签索引 / Clean up label indices
|
|
||||||
metadata.labels.forEach((label) => {
|
|
||||||
const labelSet = this._labelToGuids.get(label);
|
|
||||||
if (labelSet) {
|
|
||||||
labelSet.delete(guid);
|
|
||||||
if (labelSet.size === 0) {
|
|
||||||
this._labelToGuids.delete(label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 清理依赖关系 / Clean up dependencies
|
|
||||||
this.clearDependencies(guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update asset metadata
|
|
||||||
* 更新资产元数据
|
|
||||||
*/
|
|
||||||
updateAsset(guid: AssetGUID, updates: Partial<IAssetMetadata>): void {
|
|
||||||
const metadata = this._metadata.get(guid);
|
|
||||||
if (!metadata) return;
|
|
||||||
|
|
||||||
// 如果路径改变,更新索引 / Update index if path changed
|
|
||||||
if (updates.path && updates.path !== metadata.path) {
|
|
||||||
this._pathToGuid.delete(metadata.path);
|
|
||||||
this._pathToGuid.set(updates.path, guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果类型改变,更新索引 / Update index if type changed
|
|
||||||
if (updates.type && updates.type !== metadata.type) {
|
|
||||||
const oldTypeSet = this._typeToGuids.get(metadata.type);
|
|
||||||
if (oldTypeSet) {
|
|
||||||
oldTypeSet.delete(guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._typeToGuids.has(updates.type)) {
|
|
||||||
this._typeToGuids.set(updates.type, new Set());
|
|
||||||
}
|
|
||||||
this._typeToGuids.get(updates.type)!.add(guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果依赖改变,更新关系 / Update relations if dependencies changed
|
|
||||||
if (updates.dependencies) {
|
|
||||||
this.updateDependencies(guid, updates.dependencies);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合并更新 / Merge updates
|
|
||||||
Object.assign(metadata, updates);
|
|
||||||
metadata.lastModified = Date.now();
|
|
||||||
metadata.version++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset metadata
|
|
||||||
* 获取资产元数据
|
|
||||||
*/
|
|
||||||
getMetadata(guid: AssetGUID): IAssetMetadata | undefined {
|
|
||||||
return this._metadata.get(guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get metadata by path
|
|
||||||
* 通过路径获取元数据
|
|
||||||
*/
|
|
||||||
getMetadataByPath(path: string): IAssetMetadata | undefined {
|
|
||||||
const guid = this._pathToGuid.get(path);
|
|
||||||
return guid ? this._metadata.get(guid) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find assets by type
|
|
||||||
* 按类型查找资产
|
|
||||||
*/
|
|
||||||
findAssetsByType(type: AssetType): AssetGUID[] {
|
|
||||||
const guids = this._typeToGuids.get(type);
|
|
||||||
return guids ? Array.from(guids) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find assets by label
|
|
||||||
* 按标签查找资产
|
|
||||||
*/
|
|
||||||
findAssetsByLabel(label: string): AssetGUID[] {
|
|
||||||
const guids = this._labelToGuids.get(label);
|
|
||||||
return guids ? Array.from(guids) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find assets by multiple labels (AND operation)
|
|
||||||
* 按多个标签查找资产(AND操作)
|
|
||||||
*/
|
|
||||||
findAssetsByLabels(labels: string[]): AssetGUID[] {
|
|
||||||
if (labels.length === 0) return [];
|
|
||||||
|
|
||||||
let result: Set<AssetGUID> | null = null;
|
|
||||||
|
|
||||||
for (const label of labels) {
|
|
||||||
const labelGuids = this._labelToGuids.get(label);
|
|
||||||
if (!labelGuids || labelGuids.size === 0) return [];
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
result = new Set(labelGuids);
|
|
||||||
} else {
|
|
||||||
// 交集 / Intersection
|
|
||||||
const intersection = new Set<AssetGUID>();
|
|
||||||
labelGuids.forEach((guid) => {
|
|
||||||
if (result!.has(guid)) {
|
|
||||||
intersection.add(guid);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
result = intersection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result ? Array.from(result) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search assets by query
|
|
||||||
* 通过查询搜索资产
|
|
||||||
*/
|
|
||||||
searchAssets(query: {
|
|
||||||
name?: string;
|
|
||||||
type?: AssetType;
|
|
||||||
labels?: string[];
|
|
||||||
path?: string;
|
|
||||||
}): AssetGUID[] {
|
|
||||||
let results = Array.from(this._metadata.keys());
|
|
||||||
|
|
||||||
// 按名称过滤 / Filter by name
|
|
||||||
if (query.name) {
|
|
||||||
const nameLower = query.name.toLowerCase();
|
|
||||||
results = results.filter((guid) => {
|
|
||||||
const metadata = this._metadata.get(guid)!;
|
|
||||||
return metadata.name.toLowerCase().includes(nameLower);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按类型过滤 / Filter by type
|
|
||||||
if (query.type) {
|
|
||||||
const typeGuids = this._typeToGuids.get(query.type);
|
|
||||||
if (!typeGuids) return [];
|
|
||||||
results = results.filter((guid) => typeGuids.has(guid));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按标签过滤 / Filter by labels
|
|
||||||
if (query.labels && query.labels.length > 0) {
|
|
||||||
const labelResults = this.findAssetsByLabels(query.labels);
|
|
||||||
const labelSet = new Set(labelResults);
|
|
||||||
results = results.filter((guid) => labelSet.has(guid));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按路径过滤 / Filter by path
|
|
||||||
if (query.path) {
|
|
||||||
const pathLower = query.path.toLowerCase();
|
|
||||||
results = results.filter((guid) => {
|
|
||||||
const metadata = this._metadata.get(guid)!;
|
|
||||||
return metadata.path.toLowerCase().includes(pathLower);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset dependencies
|
|
||||||
* 获取资产依赖
|
|
||||||
*/
|
|
||||||
getDependencies(guid: AssetGUID): AssetGUID[] {
|
|
||||||
const deps = this._dependencies.get(guid);
|
|
||||||
return deps ? Array.from(deps) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset dependents (assets that depend on this one)
|
|
||||||
* 获取资产的依赖者(依赖此资产的其他资产)
|
|
||||||
*/
|
|
||||||
getDependents(guid: AssetGUID): AssetGUID[] {
|
|
||||||
const deps = this._dependents.get(guid);
|
|
||||||
return deps ? Array.from(deps) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all dependencies recursively
|
|
||||||
* 递归获取所有依赖
|
|
||||||
*/
|
|
||||||
getAllDependencies(guid: AssetGUID, visited = new Set<AssetGUID>()): AssetGUID[] {
|
|
||||||
if (visited.has(guid)) return [];
|
|
||||||
visited.add(guid);
|
|
||||||
|
|
||||||
const result: AssetGUID[] = [];
|
|
||||||
const directDeps = this.getDependencies(guid);
|
|
||||||
|
|
||||||
for (const dep of directDeps) {
|
|
||||||
result.push(dep);
|
|
||||||
const transitiveDeps = this.getAllDependencies(dep, visited);
|
|
||||||
result.push(...transitiveDeps);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check for circular dependencies
|
|
||||||
* 检查循环依赖
|
|
||||||
*/
|
|
||||||
hasCircularDependency(guid: AssetGUID): boolean {
|
|
||||||
const visited = new Set<AssetGUID>();
|
|
||||||
const recursionStack = new Set<AssetGUID>();
|
|
||||||
|
|
||||||
const checkCycle = (current: AssetGUID): boolean => {
|
|
||||||
visited.add(current);
|
|
||||||
recursionStack.add(current);
|
|
||||||
|
|
||||||
const deps = this.getDependencies(current);
|
|
||||||
for (const dep of deps) {
|
|
||||||
if (!visited.has(dep)) {
|
|
||||||
if (checkCycle(dep)) return true;
|
|
||||||
} else if (recursionStack.has(dep)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recursionStack.delete(current);
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
return checkCycle(guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update dependencies
|
|
||||||
* 更新依赖关系
|
|
||||||
*/
|
|
||||||
private updateDependencies(guid: AssetGUID, newDependencies: AssetGUID[]): void {
|
|
||||||
// 清除旧的依赖关系 / Clear old dependencies
|
|
||||||
this.clearDependencies(guid);
|
|
||||||
|
|
||||||
// 建立新的依赖关系 / Establish new dependencies
|
|
||||||
if (newDependencies.length > 0) {
|
|
||||||
this._dependencies.set(guid, new Set(newDependencies));
|
|
||||||
|
|
||||||
// 更新被依赖关系 / Update dependent relations
|
|
||||||
newDependencies.forEach((dep) => {
|
|
||||||
if (!this._dependents.has(dep)) {
|
|
||||||
this._dependents.set(dep, new Set());
|
|
||||||
}
|
|
||||||
this._dependents.get(dep)!.add(guid);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear dependencies
|
|
||||||
* 清除依赖关系
|
|
||||||
*/
|
|
||||||
private clearDependencies(guid: AssetGUID): void {
|
|
||||||
// 清除依赖 / Clear dependencies
|
|
||||||
const deps = this._dependencies.get(guid);
|
|
||||||
if (deps) {
|
|
||||||
deps.forEach((dep) => {
|
|
||||||
const dependents = this._dependents.get(dep);
|
|
||||||
if (dependents) {
|
|
||||||
dependents.delete(guid);
|
|
||||||
if (dependents.size === 0) {
|
|
||||||
this._dependents.delete(dep);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this._dependencies.delete(guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清除被依赖 / Clear dependents
|
|
||||||
const dependents = this._dependents.get(guid);
|
|
||||||
if (dependents) {
|
|
||||||
dependents.forEach((dependent) => {
|
|
||||||
const dependencies = this._dependencies.get(dependent);
|
|
||||||
if (dependencies) {
|
|
||||||
dependencies.delete(guid);
|
|
||||||
if (dependencies.size === 0) {
|
|
||||||
this._dependencies.delete(dependent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this._dependents.delete(guid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get database statistics
|
|
||||||
* 获取数据库统计
|
|
||||||
*/
|
|
||||||
getStatistics(): {
|
|
||||||
totalAssets: number;
|
|
||||||
assetsByType: Map<AssetType, number>;
|
|
||||||
totalDependencies: number;
|
|
||||||
assetsWithDependencies: number;
|
|
||||||
circularDependencies: number;
|
|
||||||
} {
|
|
||||||
const assetsByType = new Map<AssetType, number>();
|
|
||||||
this._typeToGuids.forEach((guids, type) => {
|
|
||||||
assetsByType.set(type, guids.size);
|
|
||||||
});
|
|
||||||
|
|
||||||
let circularDependencies = 0;
|
|
||||||
this._metadata.forEach((_, guid) => {
|
|
||||||
if (this.hasCircularDependency(guid)) {
|
|
||||||
circularDependencies++;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
totalAssets: this._metadata.size,
|
|
||||||
assetsByType,
|
|
||||||
totalDependencies: Array.from(this._dependencies.values()).reduce(
|
|
||||||
(sum, deps) => sum + deps.size,
|
|
||||||
0
|
|
||||||
),
|
|
||||||
assetsWithDependencies: this._dependencies.size,
|
|
||||||
circularDependencies
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Export to catalog entries
|
|
||||||
* 导出为目录条目
|
|
||||||
*/
|
|
||||||
exportToCatalog(): IAssetCatalogEntry[] {
|
|
||||||
const entries: IAssetCatalogEntry[] = [];
|
|
||||||
|
|
||||||
this._metadata.forEach((metadata) => {
|
|
||||||
entries.push({
|
|
||||||
guid: metadata.guid,
|
|
||||||
path: metadata.path,
|
|
||||||
type: metadata.type,
|
|
||||||
size: metadata.size,
|
|
||||||
hash: metadata.hash
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return entries;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear database
|
|
||||||
* 清空数据库
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
this._metadata.clear();
|
|
||||||
this._pathToGuid.clear();
|
|
||||||
this._typeToGuids.clear();
|
|
||||||
this._labelToGuids.clear();
|
|
||||||
this._dependencies.clear();
|
|
||||||
this._dependents.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
/**
|
|
||||||
* Priority-based asset loading queue
|
|
||||||
* 基于优先级的资产加载队列
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { AssetGUID, IAssetLoadOptions } from '../types/AssetTypes';
|
|
||||||
import { IAssetLoadQueue } from '../interfaces/IAssetManager';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queue item
|
|
||||||
* 队列项
|
|
||||||
*/
|
|
||||||
interface QueueItem {
|
|
||||||
guid: AssetGUID;
|
|
||||||
priority: number;
|
|
||||||
options?: IAssetLoadOptions;
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset load queue implementation
|
|
||||||
* 资产加载队列实现
|
|
||||||
*/
|
|
||||||
export class AssetLoadQueue implements IAssetLoadQueue {
|
|
||||||
private readonly _queue: QueueItem[] = [];
|
|
||||||
private readonly _guidToIndex = new Map<AssetGUID, number>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add to queue
|
|
||||||
* 添加到队列
|
|
||||||
*/
|
|
||||||
enqueue(guid: AssetGUID, priority: number, options?: IAssetLoadOptions): void {
|
|
||||||
// 检查是否已在队列中 / Check if already in queue
|
|
||||||
if (this._guidToIndex.has(guid)) {
|
|
||||||
this.reprioritize(guid, priority);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const item: QueueItem = {
|
|
||||||
guid,
|
|
||||||
priority,
|
|
||||||
options,
|
|
||||||
timestamp: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
// 二分查找插入位置 / Binary search for insertion position
|
|
||||||
const index = this.findInsertIndex(priority);
|
|
||||||
this._queue.splice(index, 0, item);
|
|
||||||
|
|
||||||
// 更新索引映射 / Update index mapping
|
|
||||||
this.updateIndices(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove from queue
|
|
||||||
* 从队列移除
|
|
||||||
*/
|
|
||||||
dequeue(): { guid: AssetGUID; options?: IAssetLoadOptions } | null {
|
|
||||||
if (this._queue.length === 0) return null;
|
|
||||||
|
|
||||||
const item = this._queue.shift();
|
|
||||||
if (!item) return null;
|
|
||||||
|
|
||||||
// 更新索引映射 / Update index mapping
|
|
||||||
this._guidToIndex.delete(item.guid);
|
|
||||||
this.updateIndices(0);
|
|
||||||
|
|
||||||
return {
|
|
||||||
guid: item.guid,
|
|
||||||
options: item.options
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if queue is empty
|
|
||||||
* 检查队列是否为空
|
|
||||||
*/
|
|
||||||
isEmpty(): boolean {
|
|
||||||
return this._queue.length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get queue size
|
|
||||||
* 获取队列大小
|
|
||||||
*/
|
|
||||||
getSize(): number {
|
|
||||||
return this._queue.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear queue
|
|
||||||
* 清空队列
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
this._queue.length = 0;
|
|
||||||
this._guidToIndex.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reprioritize item
|
|
||||||
* 重新设置优先级
|
|
||||||
*/
|
|
||||||
reprioritize(guid: AssetGUID, newPriority: number): void {
|
|
||||||
const index = this._guidToIndex.get(guid);
|
|
||||||
if (index === undefined) return;
|
|
||||||
|
|
||||||
const item = this._queue[index];
|
|
||||||
if (!item || item.priority === newPriority) return;
|
|
||||||
|
|
||||||
// 移除旧项 / Remove old item
|
|
||||||
this._queue.splice(index, 1);
|
|
||||||
this._guidToIndex.delete(guid);
|
|
||||||
|
|
||||||
// 重新插入 / Reinsert with new priority
|
|
||||||
item.priority = newPriority;
|
|
||||||
const newIndex = this.findInsertIndex(newPriority);
|
|
||||||
this._queue.splice(newIndex, 0, item);
|
|
||||||
|
|
||||||
// 更新索引 / Update indices
|
|
||||||
this.updateIndices(Math.min(index, newIndex));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find insertion index for priority
|
|
||||||
* 查找优先级的插入索引
|
|
||||||
*/
|
|
||||||
private findInsertIndex(priority: number): number {
|
|
||||||
let left = 0;
|
|
||||||
let right = this._queue.length;
|
|
||||||
|
|
||||||
while (left < right) {
|
|
||||||
const mid = Math.floor((left + right) / 2);
|
|
||||||
// 高优先级在前 / Higher priority first
|
|
||||||
if (this._queue[mid].priority >= priority) {
|
|
||||||
left = mid + 1;
|
|
||||||
} else {
|
|
||||||
right = mid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return left;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update indices after modification
|
|
||||||
* 修改后更新索引
|
|
||||||
*/
|
|
||||||
private updateIndices(startIndex: number): void {
|
|
||||||
for (let i = startIndex; i < this._queue.length; i++) {
|
|
||||||
this._guidToIndex.set(this._queue[i].guid, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get queue items (for debugging)
|
|
||||||
* 获取队列项(用于调试)
|
|
||||||
*/
|
|
||||||
getItems(): ReadonlyArray<{
|
|
||||||
guid: AssetGUID;
|
|
||||||
priority: number;
|
|
||||||
waitTime: number;
|
|
||||||
}> {
|
|
||||||
const now = Date.now();
|
|
||||||
return this._queue.map((item) => ({
|
|
||||||
guid: item.guid,
|
|
||||||
priority: item.priority,
|
|
||||||
waitTime: now - item.timestamp
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove specific item from queue
|
|
||||||
* 从队列中移除特定项
|
|
||||||
*/
|
|
||||||
remove(guid: AssetGUID): boolean {
|
|
||||||
const index = this._guidToIndex.get(guid);
|
|
||||||
if (index === undefined) return false;
|
|
||||||
|
|
||||||
this._queue.splice(index, 1);
|
|
||||||
this._guidToIndex.delete(guid);
|
|
||||||
this.updateIndices(index);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if guid is in queue
|
|
||||||
* 检查guid是否在队列中
|
|
||||||
*/
|
|
||||||
contains(guid: AssetGUID): boolean {
|
|
||||||
return this._guidToIndex.has(guid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,541 +0,0 @@
|
|||||||
/**
|
|
||||||
* Asset manager implementation
|
|
||||||
* 资产管理器实现
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
AssetGUID,
|
|
||||||
AssetHandle,
|
|
||||||
AssetType,
|
|
||||||
AssetState,
|
|
||||||
IAssetLoadOptions,
|
|
||||||
IAssetLoadResult,
|
|
||||||
IAssetReferenceInfo,
|
|
||||||
IAssetPreloadGroup,
|
|
||||||
IAssetLoadProgress,
|
|
||||||
IAssetMetadata,
|
|
||||||
AssetLoadError,
|
|
||||||
IAssetCatalog
|
|
||||||
} from '../types/AssetTypes';
|
|
||||||
import {
|
|
||||||
IAssetManager,
|
|
||||||
IAssetLoadQueue
|
|
||||||
} from '../interfaces/IAssetManager';
|
|
||||||
import { IAssetLoader, IAssetLoaderFactory } from '../interfaces/IAssetLoader';
|
|
||||||
import { AssetCache } from './AssetCache';
|
|
||||||
import { AssetLoadQueue } from './AssetLoadQueue';
|
|
||||||
import { AssetLoaderFactory } from '../loaders/AssetLoaderFactory';
|
|
||||||
import { AssetDatabase } from './AssetDatabase';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset entry in the manager
|
|
||||||
* 管理器中的资产条目
|
|
||||||
*/
|
|
||||||
interface AssetEntry {
|
|
||||||
guid: AssetGUID;
|
|
||||||
handle: AssetHandle;
|
|
||||||
asset: unknown;
|
|
||||||
metadata: IAssetMetadata;
|
|
||||||
state: AssetState;
|
|
||||||
referenceCount: number;
|
|
||||||
lastAccessTime: number;
|
|
||||||
loadPromise?: Promise<IAssetLoadResult>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset manager implementation
|
|
||||||
* 资产管理器实现
|
|
||||||
*/
|
|
||||||
export class AssetManager implements IAssetManager {
|
|
||||||
private readonly _assets = new Map<AssetGUID, AssetEntry>();
|
|
||||||
private readonly _handleToGuid = new Map<AssetHandle, AssetGUID>();
|
|
||||||
private readonly _pathToGuid = new Map<string, AssetGUID>();
|
|
||||||
private readonly _cache: AssetCache;
|
|
||||||
private readonly _loadQueue: IAssetLoadQueue;
|
|
||||||
private readonly _loaderFactory: IAssetLoaderFactory;
|
|
||||||
private readonly _database: AssetDatabase;
|
|
||||||
|
|
||||||
private _nextHandle: AssetHandle = 1;
|
|
||||||
|
|
||||||
private _statistics = {
|
|
||||||
loadedCount: 0,
|
|
||||||
failedCount: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
private _isDisposed = false;
|
|
||||||
private _loadingCount = 0;
|
|
||||||
|
|
||||||
constructor(catalog?: IAssetCatalog) {
|
|
||||||
this._cache = new AssetCache();
|
|
||||||
this._loadQueue = new AssetLoadQueue();
|
|
||||||
this._loaderFactory = new AssetLoaderFactory();
|
|
||||||
this._database = new AssetDatabase();
|
|
||||||
|
|
||||||
// 如果提供了目录,初始化数据库 / Initialize database if catalog provided
|
|
||||||
if (catalog) {
|
|
||||||
this.initializeFromCatalog(catalog);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize from catalog
|
|
||||||
* 从目录初始化
|
|
||||||
*/
|
|
||||||
private initializeFromCatalog(catalog: IAssetCatalog): void {
|
|
||||||
catalog.entries.forEach((entry, guid) => {
|
|
||||||
const metadata: IAssetMetadata = {
|
|
||||||
guid,
|
|
||||||
path: entry.path,
|
|
||||||
type: entry.type,
|
|
||||||
name: entry.path.split('/').pop() || '',
|
|
||||||
size: entry.size,
|
|
||||||
hash: entry.hash,
|
|
||||||
dependencies: [],
|
|
||||||
labels: [],
|
|
||||||
tags: new Map(),
|
|
||||||
lastModified: Date.now(),
|
|
||||||
version: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
this._database.addAsset(metadata);
|
|
||||||
this._pathToGuid.set(entry.path, guid);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load asset by GUID
|
|
||||||
* 通过GUID加载资产
|
|
||||||
*/
|
|
||||||
async loadAsset<T = unknown>(
|
|
||||||
guid: AssetGUID,
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<T>> {
|
|
||||||
// 检查是否已加载 / Check if already loaded
|
|
||||||
const entry = this._assets.get(guid);
|
|
||||||
if (entry) {
|
|
||||||
if (entry.state === AssetState.Loaded && !options?.forceReload) {
|
|
||||||
entry.lastAccessTime = Date.now();
|
|
||||||
return {
|
|
||||||
asset: entry.asset as T,
|
|
||||||
handle: entry.handle,
|
|
||||||
metadata: entry.metadata,
|
|
||||||
loadTime: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.state === AssetState.Loading && entry.loadPromise) {
|
|
||||||
return entry.loadPromise as Promise<IAssetLoadResult<T>>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取元数据 / Get metadata
|
|
||||||
const metadata = this._database.getMetadata(guid);
|
|
||||||
if (!metadata) {
|
|
||||||
throw AssetLoadError.fileNotFound(guid, 'Unknown');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建加载器 / Create loader
|
|
||||||
const loader = this._loaderFactory.createLoader(metadata.type);
|
|
||||||
if (!loader) {
|
|
||||||
throw AssetLoadError.unsupportedType(guid, metadata.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始加载 / Start loading
|
|
||||||
const loadStartTime = performance.now();
|
|
||||||
const newEntry: AssetEntry = {
|
|
||||||
guid,
|
|
||||||
handle: this._nextHandle++,
|
|
||||||
asset: null,
|
|
||||||
metadata,
|
|
||||||
state: AssetState.Loading,
|
|
||||||
referenceCount: 0,
|
|
||||||
lastAccessTime: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
this._assets.set(guid, newEntry);
|
|
||||||
this._handleToGuid.set(newEntry.handle, guid);
|
|
||||||
this._loadingCount++;
|
|
||||||
|
|
||||||
// 创建加载Promise / Create loading promise
|
|
||||||
const loadPromise = this.performLoad<T>(loader, metadata, options, loadStartTime, newEntry);
|
|
||||||
newEntry.loadPromise = loadPromise;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await loadPromise;
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
this._statistics.failedCount++;
|
|
||||||
newEntry.state = AssetState.Failed;
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
this._loadingCount--;
|
|
||||||
delete newEntry.loadPromise;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform asset loading
|
|
||||||
* 执行资产加载
|
|
||||||
*/
|
|
||||||
private async performLoad<T>(
|
|
||||||
loader: IAssetLoader,
|
|
||||||
metadata: IAssetMetadata,
|
|
||||||
options: IAssetLoadOptions | undefined,
|
|
||||||
startTime: number,
|
|
||||||
entry: AssetEntry
|
|
||||||
): Promise<IAssetLoadResult<T>> {
|
|
||||||
// 加载依赖 / Load dependencies
|
|
||||||
if (metadata.dependencies.length > 0) {
|
|
||||||
await this.loadDependencies(metadata.dependencies, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行加载 / Execute loading
|
|
||||||
const result = await loader.load(metadata.path, metadata, options);
|
|
||||||
|
|
||||||
// 更新条目 / Update entry
|
|
||||||
entry.asset = result.asset;
|
|
||||||
entry.state = AssetState.Loaded;
|
|
||||||
|
|
||||||
// 缓存资产 / Cache asset
|
|
||||||
this._cache.set(metadata.guid, result.asset);
|
|
||||||
|
|
||||||
// 更新统计 / Update statistics
|
|
||||||
this._statistics.loadedCount++;
|
|
||||||
|
|
||||||
const loadResult: IAssetLoadResult<T> = {
|
|
||||||
asset: result.asset as T,
|
|
||||||
handle: entry.handle,
|
|
||||||
metadata,
|
|
||||||
loadTime: performance.now() - startTime
|
|
||||||
};
|
|
||||||
|
|
||||||
return loadResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load dependencies
|
|
||||||
* 加载依赖
|
|
||||||
*/
|
|
||||||
private async loadDependencies(
|
|
||||||
dependencies: AssetGUID[],
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<void> {
|
|
||||||
const promises = dependencies.map((dep) => this.loadAsset(dep, options));
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load asset by path
|
|
||||||
* 通过路径加载资产
|
|
||||||
*/
|
|
||||||
async loadAssetByPath<T = unknown>(
|
|
||||||
path: string,
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<T>> {
|
|
||||||
const guid = this._pathToGuid.get(path);
|
|
||||||
if (!guid) {
|
|
||||||
// 尝试从数据库查找 / Try to find from database
|
|
||||||
let metadata = this._database.getMetadataByPath(path);
|
|
||||||
if (!metadata) {
|
|
||||||
// 动态创建元数据 / Create metadata dynamically
|
|
||||||
const fileExt = path.substring(path.lastIndexOf('.')).toLowerCase();
|
|
||||||
let assetType = AssetType.Custom;
|
|
||||||
|
|
||||||
// 根据文件扩展名确定资产类型 / Determine asset type by file extension
|
|
||||||
if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'].includes(fileExt)) {
|
|
||||||
assetType = AssetType.Texture;
|
|
||||||
} else if (['.json'].includes(fileExt)) {
|
|
||||||
assetType = AssetType.Json;
|
|
||||||
} else if (['.txt', '.md', '.xml', '.yaml'].includes(fileExt)) {
|
|
||||||
assetType = AssetType.Text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成唯一GUID / Generate unique GUID
|
|
||||||
const dynamicGuid = `dynamic_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
||||||
|
|
||||||
metadata = {
|
|
||||||
guid: dynamicGuid,
|
|
||||||
path: path,
|
|
||||||
type: assetType,
|
|
||||||
name: path.split('/').pop() || path.split('\\').pop() || 'unnamed',
|
|
||||||
size: 0, // 动态加载时未知大小 / Unknown size for dynamic loading
|
|
||||||
hash: '',
|
|
||||||
dependencies: [],
|
|
||||||
labels: [],
|
|
||||||
tags: new Map(),
|
|
||||||
lastModified: Date.now(),
|
|
||||||
version: 1
|
|
||||||
};
|
|
||||||
|
|
||||||
// 注册到数据库 / Register to database
|
|
||||||
this._database.addAsset(metadata);
|
|
||||||
this._pathToGuid.set(path, metadata.guid);
|
|
||||||
} else {
|
|
||||||
this._pathToGuid.set(path, metadata.guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.loadAsset<T>(metadata.guid, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.loadAsset<T>(guid, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load multiple assets
|
|
||||||
* 批量加载资产
|
|
||||||
*/
|
|
||||||
async loadAssets(
|
|
||||||
guids: AssetGUID[],
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<Map<AssetGUID, IAssetLoadResult>> {
|
|
||||||
const results = new Map<AssetGUID, IAssetLoadResult>();
|
|
||||||
|
|
||||||
// 并行加载所有资产 / Load all assets in parallel
|
|
||||||
const promises = guids.map(async (guid) => {
|
|
||||||
try {
|
|
||||||
const result = await this.loadAsset(guid, options);
|
|
||||||
results.set(guid, result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to load asset ${guid}:`, error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(promises);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preload asset group
|
|
||||||
* 预加载资产组
|
|
||||||
*/
|
|
||||||
async preloadGroup(
|
|
||||||
group: IAssetPreloadGroup,
|
|
||||||
onProgress?: (progress: IAssetLoadProgress) => void
|
|
||||||
): Promise<void> {
|
|
||||||
const totalCount = group.assets.length;
|
|
||||||
let loadedCount = 0;
|
|
||||||
let loadedBytes = 0;
|
|
||||||
let totalBytes = 0;
|
|
||||||
|
|
||||||
// 计算总大小 / Calculate total size
|
|
||||||
for (const guid of group.assets) {
|
|
||||||
const metadata = this._database.getMetadata(guid);
|
|
||||||
if (metadata) {
|
|
||||||
totalBytes += metadata.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载每个资产 / Load each asset
|
|
||||||
for (const guid of group.assets) {
|
|
||||||
const metadata = this._database.getMetadata(guid);
|
|
||||||
if (!metadata) continue;
|
|
||||||
|
|
||||||
if (onProgress) {
|
|
||||||
onProgress({
|
|
||||||
currentAsset: metadata.name,
|
|
||||||
loadedCount,
|
|
||||||
totalCount,
|
|
||||||
loadedBytes,
|
|
||||||
totalBytes,
|
|
||||||
progress: loadedCount / totalCount
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.loadAsset(guid, { priority: group.priority });
|
|
||||||
|
|
||||||
loadedCount++;
|
|
||||||
loadedBytes += metadata.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 最终进度 / Final progress
|
|
||||||
if (onProgress) {
|
|
||||||
onProgress({
|
|
||||||
currentAsset: '',
|
|
||||||
loadedCount: totalCount,
|
|
||||||
totalCount,
|
|
||||||
loadedBytes: totalBytes,
|
|
||||||
totalBytes,
|
|
||||||
progress: 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get loaded asset
|
|
||||||
* 获取已加载的资产
|
|
||||||
*/
|
|
||||||
getAsset<T = unknown>(guid: AssetGUID): T | null {
|
|
||||||
const entry = this._assets.get(guid);
|
|
||||||
if (entry && entry.state === AssetState.Loaded) {
|
|
||||||
entry.lastAccessTime = Date.now();
|
|
||||||
return entry.asset as T;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset by handle
|
|
||||||
* 通过句柄获取资产
|
|
||||||
*/
|
|
||||||
getAssetByHandle<T = unknown>(handle: AssetHandle): T | null {
|
|
||||||
const guid = this._handleToGuid.get(handle);
|
|
||||||
if (!guid) return null;
|
|
||||||
return this.getAsset<T>(guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if asset is loaded
|
|
||||||
* 检查资产是否已加载
|
|
||||||
*/
|
|
||||||
isLoaded(guid: AssetGUID): boolean {
|
|
||||||
const entry = this._assets.get(guid);
|
|
||||||
return entry?.state === AssetState.Loaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset state
|
|
||||||
* 获取资产状态
|
|
||||||
*/
|
|
||||||
getAssetState(guid: AssetGUID): AssetState {
|
|
||||||
const entry = this._assets.get(guid);
|
|
||||||
return entry?.state || AssetState.Unloaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unload asset
|
|
||||||
* 卸载资产
|
|
||||||
*/
|
|
||||||
unloadAsset(guid: AssetGUID): void {
|
|
||||||
const entry = this._assets.get(guid);
|
|
||||||
if (!entry) return;
|
|
||||||
|
|
||||||
// 检查引用计数 / Check reference count
|
|
||||||
if (entry.referenceCount > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取加载器以释放资源 / Get loader to dispose resources
|
|
||||||
const loader = this._loaderFactory.createLoader(entry.metadata.type);
|
|
||||||
if (loader) {
|
|
||||||
loader.dispose(entry.asset);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理条目 / Clean up entry
|
|
||||||
this._handleToGuid.delete(entry.handle);
|
|
||||||
this._assets.delete(guid);
|
|
||||||
this._cache.remove(guid);
|
|
||||||
|
|
||||||
// 更新统计 / Update statistics
|
|
||||||
this._statistics.loadedCount--;
|
|
||||||
|
|
||||||
entry.state = AssetState.Unloaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unload all assets
|
|
||||||
* 卸载所有资产
|
|
||||||
*/
|
|
||||||
unloadAllAssets(): void {
|
|
||||||
const guids = Array.from(this._assets.keys());
|
|
||||||
guids.forEach((guid) => this.unloadAsset(guid));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unload unused assets
|
|
||||||
* 卸载未使用的资产
|
|
||||||
*/
|
|
||||||
unloadUnusedAssets(): void {
|
|
||||||
const guids = Array.from(this._assets.keys());
|
|
||||||
guids.forEach((guid) => {
|
|
||||||
const entry = this._assets.get(guid);
|
|
||||||
if (entry && entry.referenceCount === 0) {
|
|
||||||
this.unloadAsset(guid);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add reference to asset
|
|
||||||
* 增加资产引用
|
|
||||||
*/
|
|
||||||
addReference(guid: AssetGUID): void {
|
|
||||||
const entry = this._assets.get(guid);
|
|
||||||
if (entry) {
|
|
||||||
entry.referenceCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove reference from asset
|
|
||||||
* 移除资产引用
|
|
||||||
*/
|
|
||||||
removeReference(guid: AssetGUID): void {
|
|
||||||
const entry = this._assets.get(guid);
|
|
||||||
if (entry && entry.referenceCount > 0) {
|
|
||||||
entry.referenceCount--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get reference info
|
|
||||||
* 获取引用信息
|
|
||||||
*/
|
|
||||||
getReferenceInfo(guid: AssetGUID): IAssetReferenceInfo | null {
|
|
||||||
const entry = this._assets.get(guid);
|
|
||||||
if (!entry) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
guid,
|
|
||||||
handle: entry.handle,
|
|
||||||
referenceCount: entry.referenceCount,
|
|
||||||
lastAccessTime: entry.lastAccessTime,
|
|
||||||
state: entry.state
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register custom loader
|
|
||||||
* 注册自定义加载器
|
|
||||||
*/
|
|
||||||
registerLoader(type: AssetType, loader: IAssetLoader): void {
|
|
||||||
this._loaderFactory.registerLoader(type, loader);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset statistics
|
|
||||||
* 获取资产统计信息
|
|
||||||
*/
|
|
||||||
getStatistics(): { loadedCount: number; loadQueue: number; failedCount: number } {
|
|
||||||
return {
|
|
||||||
loadedCount: this._statistics.loadedCount,
|
|
||||||
loadQueue: this._loadQueue.getSize(),
|
|
||||||
failedCount: this._statistics.failedCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear cache
|
|
||||||
* 清空缓存
|
|
||||||
*/
|
|
||||||
clearCache(): void {
|
|
||||||
this._cache.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose manager
|
|
||||||
* 释放管理器
|
|
||||||
*/
|
|
||||||
dispose(): void {
|
|
||||||
if (this._isDisposed) return;
|
|
||||||
|
|
||||||
this.unloadAllAssets();
|
|
||||||
this._cache.clear();
|
|
||||||
this._loadQueue.clear();
|
|
||||||
this._assets.clear();
|
|
||||||
this._handleToGuid.clear();
|
|
||||||
this._pathToGuid.clear();
|
|
||||||
|
|
||||||
this._isDisposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
/**
|
|
||||||
* Asset path resolver for different platforms and protocols
|
|
||||||
* 不同平台和协议的资产路径解析器
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { AssetPlatform } from '../types/AssetTypes';
|
|
||||||
import { PathValidator } from '../utils/PathValidator';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset path resolver configuration
|
|
||||||
* 资产路径解析器配置
|
|
||||||
*/
|
|
||||||
export interface IAssetPathConfig {
|
|
||||||
/** Base URL for web assets | Web资产的基础URL */
|
|
||||||
baseUrl?: string;
|
|
||||||
|
|
||||||
/** Asset directory path | 资产目录路径 */
|
|
||||||
assetDir?: string;
|
|
||||||
|
|
||||||
/** Asset host for asset:// protocol | 资产协议的主机名 */
|
|
||||||
assetHost?: string;
|
|
||||||
|
|
||||||
/** Current platform | 当前平台 */
|
|
||||||
platform?: AssetPlatform;
|
|
||||||
|
|
||||||
/** Custom path transformer | 自定义路径转换器 */
|
|
||||||
pathTransformer?: (path: string) => string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset path resolver
|
|
||||||
* 资产路径解析器
|
|
||||||
*/
|
|
||||||
export class AssetPathResolver {
|
|
||||||
private config: IAssetPathConfig;
|
|
||||||
|
|
||||||
constructor(config: IAssetPathConfig = {}) {
|
|
||||||
this.config = {
|
|
||||||
baseUrl: '',
|
|
||||||
assetDir: 'assets',
|
|
||||||
platform: AssetPlatform.H5,
|
|
||||||
...config
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update configuration
|
|
||||||
* 更新配置
|
|
||||||
*/
|
|
||||||
updateConfig(config: Partial<IAssetPathConfig>): void {
|
|
||||||
this.config = { ...this.config, ...config };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve asset path to full URL
|
|
||||||
* 解析资产路径为完整URL
|
|
||||||
*/
|
|
||||||
resolve(path: string): string {
|
|
||||||
// Validate input path
|
|
||||||
const validation = PathValidator.validate(path);
|
|
||||||
if (!validation.valid) {
|
|
||||||
console.warn(`Invalid asset path: ${path} - ${validation.reason}`);
|
|
||||||
// Sanitize the path instead of throwing
|
|
||||||
path = PathValidator.sanitize(path);
|
|
||||||
if (!path) {
|
|
||||||
throw new Error(`Cannot resolve invalid path: ${validation.reason}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Already a full URL
|
|
||||||
// 已经是完整URL
|
|
||||||
if (this.isAbsoluteUrl(path)) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Data URL
|
|
||||||
// 数据URL
|
|
||||||
if (path.startsWith('data:')) {
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize the path
|
|
||||||
path = PathValidator.normalize(path);
|
|
||||||
|
|
||||||
// Apply custom transformer if provided
|
|
||||||
// 应用自定义转换器(如果提供)
|
|
||||||
if (this.config.pathTransformer) {
|
|
||||||
path = this.config.pathTransformer(path);
|
|
||||||
// Transformer output is trusted (may be absolute path or asset:// URL)
|
|
||||||
// 转换器输出是可信的(可能是绝对路径或 asset:// URL)
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Platform-specific resolution
|
|
||||||
// 平台特定解析
|
|
||||||
switch (this.config.platform) {
|
|
||||||
case AssetPlatform.H5:
|
|
||||||
return this.resolveH5Path(path);
|
|
||||||
|
|
||||||
case AssetPlatform.WeChat:
|
|
||||||
return this.resolveWeChatPath(path);
|
|
||||||
|
|
||||||
case AssetPlatform.Playable:
|
|
||||||
return this.resolvePlayablePath(path);
|
|
||||||
|
|
||||||
case AssetPlatform.Android:
|
|
||||||
case AssetPlatform.iOS:
|
|
||||||
return this.resolveMobilePath(path);
|
|
||||||
|
|
||||||
case AssetPlatform.Editor:
|
|
||||||
return this.resolveEditorPath(path);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return this.resolveH5Path(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve path for H5 platform
|
|
||||||
* 解析H5平台路径
|
|
||||||
*/
|
|
||||||
private resolveH5Path(path: string): string {
|
|
||||||
// Remove leading slash if present
|
|
||||||
// 移除开头的斜杠(如果存在)
|
|
||||||
path = path.replace(/^\//, '');
|
|
||||||
|
|
||||||
// Combine with base URL and asset directory
|
|
||||||
// 与基础URL和资产目录结合
|
|
||||||
const base = this.config.baseUrl || (typeof window !== 'undefined' ? window.location.origin : '');
|
|
||||||
const assetDir = this.config.assetDir || 'assets';
|
|
||||||
|
|
||||||
return `${base}/${assetDir}/${path}`.replace(/\/+/g, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve path for WeChat Mini Game
|
|
||||||
* 解析微信小游戏路径
|
|
||||||
*/
|
|
||||||
private resolveWeChatPath(path: string): string {
|
|
||||||
// WeChat mini games use relative paths
|
|
||||||
// 微信小游戏使用相对路径
|
|
||||||
return `${this.config.assetDir}/${path}`.replace(/\/+/g, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve path for Playable Ads platform
|
|
||||||
* 解析试玩广告平台路径
|
|
||||||
*/
|
|
||||||
private resolvePlayablePath(path: string): string {
|
|
||||||
// Playable ads typically use base64 embedded resources or relative paths
|
|
||||||
// 试玩广告通常使用base64内嵌资源或相对路径
|
|
||||||
|
|
||||||
// If custom transformer is provided (e.g., for base64 encoding)
|
|
||||||
// 如果提供了自定义转换器(例如用于base64编码)
|
|
||||||
if (this.config.pathTransformer) {
|
|
||||||
return this.config.pathTransformer(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to relative path without directory prefix
|
|
||||||
// 默认使用不带目录前缀的相对路径
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve path for mobile platform (Android/iOS)
|
|
||||||
* 解析移动平台路径(Android/iOS)
|
|
||||||
*/
|
|
||||||
private resolveMobilePath(path: string): string {
|
|
||||||
// Mobile platforms use relative paths or file:// protocol
|
|
||||||
// 移动平台使用相对路径或file://协议
|
|
||||||
return `./${this.config.assetDir}/${path}`.replace(/\/+/g, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve path for Editor platform (Tauri)
|
|
||||||
* 解析编辑器平台路径(Tauri)
|
|
||||||
*/
|
|
||||||
private resolveEditorPath(path: string): string {
|
|
||||||
// For Tauri editor, use pathTransformer if provided
|
|
||||||
// 对于Tauri编辑器,使用pathTransformer(如果提供)
|
|
||||||
if (this.config.pathTransformer) {
|
|
||||||
return this.config.pathTransformer(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use configurable asset host or default to 'localhost'
|
|
||||||
// 使用可配置的资产主机或默认为 'localhost'
|
|
||||||
const host = this.config.assetHost || 'localhost';
|
|
||||||
const sanitizedPath = PathValidator.sanitize(path);
|
|
||||||
return `asset://${host}/${sanitizedPath}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if path is absolute URL
|
|
||||||
* 检查路径是否为绝对URL
|
|
||||||
*/
|
|
||||||
private isAbsoluteUrl(path: string): boolean {
|
|
||||||
return /^(https?:\/\/|file:\/\/|asset:\/\/)/.test(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset directory from path
|
|
||||||
* 从路径获取资产目录
|
|
||||||
*/
|
|
||||||
getAssetDirectory(path: string): string {
|
|
||||||
const resolved = this.resolve(path);
|
|
||||||
const lastSlash = resolved.lastIndexOf('/');
|
|
||||||
return lastSlash >= 0 ? resolved.substring(0, lastSlash) : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset filename from path
|
|
||||||
* 从路径获取资产文件名
|
|
||||||
*/
|
|
||||||
getAssetFilename(path: string): string {
|
|
||||||
const resolved = this.resolve(path);
|
|
||||||
const lastSlash = resolved.lastIndexOf('/');
|
|
||||||
return lastSlash >= 0 ? resolved.substring(lastSlash + 1) : resolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Join paths
|
|
||||||
* 连接路径
|
|
||||||
*/
|
|
||||||
join(...paths: string[]): string {
|
|
||||||
return paths.join('/').replace(/\/+/g, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize path
|
|
||||||
* 规范化路径
|
|
||||||
*/
|
|
||||||
normalize(path: string): string {
|
|
||||||
return path.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Global asset path resolver instance
|
|
||||||
* 全局资产路径解析器实例
|
|
||||||
*/
|
|
||||||
export const globalPathResolver = new AssetPathResolver();
|
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
/**
|
|
||||||
* Asset reference for lazy loading
|
|
||||||
* 用于懒加载的资产引用
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { AssetGUID, IAssetLoadOptions, AssetState } from '../types/AssetTypes';
|
|
||||||
import { IAssetManager } from '../interfaces/IAssetManager';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset reference class for lazy loading
|
|
||||||
* 懒加载资产引用类
|
|
||||||
*/
|
|
||||||
export class AssetReference<T = unknown> {
|
|
||||||
private _guid: AssetGUID;
|
|
||||||
private _asset?: T;
|
|
||||||
private _loadPromise?: Promise<T>;
|
|
||||||
private _manager?: IAssetManager;
|
|
||||||
private _isReleased = false;
|
|
||||||
private _autoRelease = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
* 构造函数
|
|
||||||
*/
|
|
||||||
constructor(guid: AssetGUID, manager?: IAssetManager) {
|
|
||||||
this._guid = guid;
|
|
||||||
this._manager = manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset GUID
|
|
||||||
* 获取资产GUID
|
|
||||||
*/
|
|
||||||
get guid(): AssetGUID {
|
|
||||||
return this._guid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if asset is loaded
|
|
||||||
* 检查资产是否已加载
|
|
||||||
*/
|
|
||||||
get isLoaded(): boolean {
|
|
||||||
return this._asset !== undefined && !this._isReleased;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset synchronously (returns null if not loaded)
|
|
||||||
* 同步获取资产(如果未加载则返回null)
|
|
||||||
*/
|
|
||||||
get asset(): T | null {
|
|
||||||
return this._asset ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set asset manager
|
|
||||||
* 设置资产管理器
|
|
||||||
*/
|
|
||||||
setManager(manager: IAssetManager): void {
|
|
||||||
this._manager = manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load asset asynchronously
|
|
||||||
* 异步加载资产
|
|
||||||
*/
|
|
||||||
async loadAsync(options?: IAssetLoadOptions): Promise<T> {
|
|
||||||
if (this._isReleased) {
|
|
||||||
throw new Error(`Asset reference ${this._guid} has been released`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果已经加载,直接返回 / Return if already loaded
|
|
||||||
if (this._asset !== undefined) {
|
|
||||||
return this._asset;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果正在加载,返回加载Promise / Return loading promise if loading
|
|
||||||
if (this._loadPromise) {
|
|
||||||
return this._loadPromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._manager) {
|
|
||||||
throw new Error('Asset manager not set for AssetReference');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始加载 / Start loading
|
|
||||||
this._loadPromise = this.performLoad(options);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const asset = await this._loadPromise;
|
|
||||||
return asset;
|
|
||||||
} finally {
|
|
||||||
this._loadPromise = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform asset loading
|
|
||||||
* 执行资产加载
|
|
||||||
*/
|
|
||||||
private async performLoad(options?: IAssetLoadOptions): Promise<T> {
|
|
||||||
if (!this._manager) {
|
|
||||||
throw new Error('Asset manager not set');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this._manager.loadAsset<T>(this._guid, options);
|
|
||||||
this._asset = result.asset;
|
|
||||||
|
|
||||||
// 增加引用计数 / Increase reference count
|
|
||||||
this._manager.addReference(this._guid);
|
|
||||||
|
|
||||||
return this._asset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release asset reference
|
|
||||||
* 释放资产引用
|
|
||||||
*/
|
|
||||||
release(): void {
|
|
||||||
if (this._isReleased) return;
|
|
||||||
|
|
||||||
if (this._manager && this._asset !== undefined) {
|
|
||||||
// 减少引用计数 / Decrease reference count
|
|
||||||
this._manager.removeReference(this._guid);
|
|
||||||
|
|
||||||
// 如果引用计数为0,可以考虑卸载 / Consider unloading if reference count is 0
|
|
||||||
const refInfo = this._manager.getReferenceInfo(this._guid);
|
|
||||||
if (refInfo && refInfo.referenceCount === 0 && this._autoRelease) {
|
|
||||||
this._manager.unloadAsset(this._guid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._asset = undefined;
|
|
||||||
this._isReleased = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set auto-release mode
|
|
||||||
* 设置自动释放模式
|
|
||||||
*/
|
|
||||||
setAutoRelease(autoRelease: boolean): void {
|
|
||||||
this._autoRelease = autoRelease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate reference
|
|
||||||
* 验证引用
|
|
||||||
*/
|
|
||||||
validate(): boolean {
|
|
||||||
if (!this._manager) return false;
|
|
||||||
|
|
||||||
const state = this._manager.getAssetState(this._guid);
|
|
||||||
return state !== AssetState.Failed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset state
|
|
||||||
* 获取资产状态
|
|
||||||
*/
|
|
||||||
getState(): AssetState {
|
|
||||||
if (this._isReleased) return AssetState.Unloaded;
|
|
||||||
if (!this._manager) return AssetState.Unloaded;
|
|
||||||
|
|
||||||
return this._manager.getAssetState(this._guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clone reference
|
|
||||||
* 克隆引用
|
|
||||||
*/
|
|
||||||
clone(): AssetReference<T> {
|
|
||||||
const newRef = new AssetReference<T>(this._guid, this._manager);
|
|
||||||
newRef.setAutoRelease(this._autoRelease);
|
|
||||||
return newRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert to JSON
|
|
||||||
* 转换为JSON
|
|
||||||
*/
|
|
||||||
toJSON(): { guid: AssetGUID } {
|
|
||||||
return { guid: this._guid };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create from JSON
|
|
||||||
* 从JSON创建
|
|
||||||
*/
|
|
||||||
static fromJSON<T = unknown>(
|
|
||||||
json: { guid: AssetGUID },
|
|
||||||
manager?: IAssetManager
|
|
||||||
): AssetReference<T> {
|
|
||||||
return new AssetReference<T>(json.guid, manager);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Weak asset reference that doesn't prevent unloading
|
|
||||||
* 不阻止卸载的弱资产引用
|
|
||||||
*/
|
|
||||||
export class WeakAssetReference<T = unknown> {
|
|
||||||
private _guid: AssetGUID;
|
|
||||||
private _manager?: IAssetManager;
|
|
||||||
|
|
||||||
constructor(guid: AssetGUID, manager?: IAssetManager) {
|
|
||||||
this._guid = guid;
|
|
||||||
this._manager = manager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset GUID
|
|
||||||
* 获取资产GUID
|
|
||||||
*/
|
|
||||||
get guid(): AssetGUID {
|
|
||||||
return this._guid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try get asset without loading
|
|
||||||
* 尝试获取资产而不加载
|
|
||||||
*/
|
|
||||||
tryGet(): T | null {
|
|
||||||
if (!this._manager) return null;
|
|
||||||
return this._manager.getAsset<T>(this._guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load asset if not loaded
|
|
||||||
* 如果未加载则加载资产
|
|
||||||
*/
|
|
||||||
async loadAsync(options?: IAssetLoadOptions): Promise<T> {
|
|
||||||
if (!this._manager) {
|
|
||||||
throw new Error('Asset manager not set');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this._manager.loadAsset<T>(this._guid, options);
|
|
||||||
// 不增加引用计数 / Don't increase reference count for weak reference
|
|
||||||
return result.asset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if asset is loaded
|
|
||||||
* 检查资产是否已加载
|
|
||||||
*/
|
|
||||||
isLoaded(): boolean {
|
|
||||||
if (!this._manager) return false;
|
|
||||||
return this._manager.isLoaded(this._guid);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set asset manager
|
|
||||||
* 设置资产管理器
|
|
||||||
*/
|
|
||||||
setManager(manager: IAssetManager): void {
|
|
||||||
this._manager = manager;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset reference array for managing multiple references
|
|
||||||
* 用于管理多个引用的资产引用数组
|
|
||||||
*/
|
|
||||||
export class AssetReferenceArray<T = unknown> {
|
|
||||||
private _references: AssetReference<T>[] = [];
|
|
||||||
private _manager?: IAssetManager;
|
|
||||||
|
|
||||||
constructor(guids: AssetGUID[] = [], manager?: IAssetManager) {
|
|
||||||
this._manager = manager;
|
|
||||||
this._references = guids.map((guid) => new AssetReference<T>(guid, manager));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add reference
|
|
||||||
* 添加引用
|
|
||||||
*/
|
|
||||||
add(guid: AssetGUID): void {
|
|
||||||
this._references.push(new AssetReference<T>(guid, this._manager));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove reference
|
|
||||||
* 移除引用
|
|
||||||
*/
|
|
||||||
remove(guid: AssetGUID): boolean {
|
|
||||||
const index = this._references.findIndex((ref) => ref.guid === guid);
|
|
||||||
if (index >= 0) {
|
|
||||||
this._references[index].release();
|
|
||||||
this._references.splice(index, 1);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load all assets
|
|
||||||
* 加载所有资产
|
|
||||||
*/
|
|
||||||
async loadAllAsync(options?: IAssetLoadOptions): Promise<T[]> {
|
|
||||||
const promises = this._references.map((ref) => ref.loadAsync(options));
|
|
||||||
return Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Release all references
|
|
||||||
* 释放所有引用
|
|
||||||
*/
|
|
||||||
releaseAll(): void {
|
|
||||||
this._references.forEach((ref) => ref.release());
|
|
||||||
this._references = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all loaded assets
|
|
||||||
* 获取所有已加载的资产
|
|
||||||
*/
|
|
||||||
getLoadedAssets(): T[] {
|
|
||||||
return this._references
|
|
||||||
.filter((ref) => ref.isLoaded)
|
|
||||||
.map((ref) => ref.asset!)
|
|
||||||
.filter((asset) => asset !== null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get reference count
|
|
||||||
* 获取引用数量
|
|
||||||
*/
|
|
||||||
get count(): number {
|
|
||||||
return this._references.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set asset manager
|
|
||||||
* 设置资产管理器
|
|
||||||
*/
|
|
||||||
setManager(manager: IAssetManager): void {
|
|
||||||
this._manager = manager;
|
|
||||||
this._references.forEach((ref) => ref.setManager(manager));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
/**
|
|
||||||
* Asset System for ECS Framework
|
|
||||||
* ECS框架的资产系统
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Types
|
|
||||||
export * from './types/AssetTypes';
|
|
||||||
|
|
||||||
// Interfaces
|
|
||||||
export * from './interfaces/IAssetLoader';
|
|
||||||
export * from './interfaces/IAssetManager';
|
|
||||||
export * from './interfaces/IResourceComponent';
|
|
||||||
|
|
||||||
// Core
|
|
||||||
export { AssetManager } from './core/AssetManager';
|
|
||||||
export { AssetCache } from './core/AssetCache';
|
|
||||||
export { AssetDatabase } from './core/AssetDatabase';
|
|
||||||
export { AssetLoadQueue } from './core/AssetLoadQueue';
|
|
||||||
export { AssetReference, WeakAssetReference, AssetReferenceArray } from './core/AssetReference';
|
|
||||||
export { AssetPathResolver, globalPathResolver } from './core/AssetPathResolver';
|
|
||||||
export type { IAssetPathConfig } from './core/AssetPathResolver';
|
|
||||||
|
|
||||||
// Loaders
|
|
||||||
export { AssetLoaderFactory } from './loaders/AssetLoaderFactory';
|
|
||||||
export { TextureLoader } from './loaders/TextureLoader';
|
|
||||||
export { JsonLoader } from './loaders/JsonLoader';
|
|
||||||
export { TextLoader } from './loaders/TextLoader';
|
|
||||||
export { BinaryLoader } from './loaders/BinaryLoader';
|
|
||||||
|
|
||||||
// Integration
|
|
||||||
export { EngineIntegration } from './integration/EngineIntegration';
|
|
||||||
export type { IEngineBridge } from './integration/EngineIntegration';
|
|
||||||
|
|
||||||
// Services
|
|
||||||
export { SceneResourceManager } from './services/SceneResourceManager';
|
|
||||||
export type { IResourceLoader } from './services/SceneResourceManager';
|
|
||||||
|
|
||||||
// Utils
|
|
||||||
export { UVHelper } from './utils/UVHelper';
|
|
||||||
|
|
||||||
// Default instance
|
|
||||||
import { AssetManager } from './core/AssetManager';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default asset manager instance
|
|
||||||
* 默认资产管理器实例
|
|
||||||
*/
|
|
||||||
export const assetManager = new AssetManager();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize asset system with catalog
|
|
||||||
* 使用目录初始化资产系统
|
|
||||||
*/
|
|
||||||
export function initializeAssetSystem(catalog?: any): AssetManager {
|
|
||||||
if (catalog) {
|
|
||||||
return new AssetManager(catalog);
|
|
||||||
}
|
|
||||||
return assetManager;
|
|
||||||
}
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
/**
|
|
||||||
* Engine integration for asset system
|
|
||||||
* 资产系统的引擎集成
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { AssetManager } from '../core/AssetManager';
|
|
||||||
import { AssetGUID } from '../types/AssetTypes';
|
|
||||||
import { ITextureAsset } from '../interfaces/IAssetLoader';
|
|
||||||
import { globalPathResolver } from '../core/AssetPathResolver';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Engine bridge interface
|
|
||||||
* 引擎桥接接口
|
|
||||||
*/
|
|
||||||
export interface IEngineBridge {
|
|
||||||
/**
|
|
||||||
* Load texture to GPU
|
|
||||||
* 加载纹理到GPU
|
|
||||||
*/
|
|
||||||
loadTexture(id: number, url: string): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load multiple textures
|
|
||||||
* 批量加载纹理
|
|
||||||
*/
|
|
||||||
loadTextures(requests: Array<{ id: number; url: string }>): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unload texture from GPU
|
|
||||||
* 从GPU卸载纹理
|
|
||||||
*/
|
|
||||||
unloadTexture(id: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get texture info
|
|
||||||
* 获取纹理信息
|
|
||||||
*/
|
|
||||||
getTextureInfo(id: number): { width: number; height: number } | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset system engine integration
|
|
||||||
* 资产系统引擎集成
|
|
||||||
*/
|
|
||||||
export class EngineIntegration {
|
|
||||||
private _assetManager: AssetManager;
|
|
||||||
private _engineBridge?: IEngineBridge;
|
|
||||||
private _textureIdMap = new Map<AssetGUID, number>();
|
|
||||||
private _pathToTextureId = new Map<string, number>();
|
|
||||||
|
|
||||||
constructor(assetManager: AssetManager, engineBridge?: IEngineBridge) {
|
|
||||||
this._assetManager = assetManager;
|
|
||||||
this._engineBridge = engineBridge;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set engine bridge
|
|
||||||
* 设置引擎桥接
|
|
||||||
*/
|
|
||||||
setEngineBridge(bridge: IEngineBridge): void {
|
|
||||||
this._engineBridge = bridge;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load texture for component
|
|
||||||
* 为组件加载纹理
|
|
||||||
*
|
|
||||||
* 统一的路径解析入口:相对路径会被转换为 Tauri 可用的 asset:// URL
|
|
||||||
* Unified path resolution entry: relative paths will be converted to Tauri-compatible asset:// URLs
|
|
||||||
*/
|
|
||||||
async loadTextureForComponent(texturePath: string): Promise<number> {
|
|
||||||
// 检查缓存(使用原始路径作为键)
|
|
||||||
// Check cache (using original path as key)
|
|
||||||
const existingId = this._pathToTextureId.get(texturePath);
|
|
||||||
if (existingId) {
|
|
||||||
return existingId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用 globalPathResolver 转换路径
|
|
||||||
// Use globalPathResolver to transform the path
|
|
||||||
const resolvedPath = globalPathResolver.resolve(texturePath);
|
|
||||||
|
|
||||||
// 通过资产系统加载(使用解析后的路径)
|
|
||||||
// Load through asset system (using resolved path)
|
|
||||||
const result = await this._assetManager.loadAssetByPath<ITextureAsset>(resolvedPath);
|
|
||||||
const textureAsset = result.asset;
|
|
||||||
|
|
||||||
// 如果有引擎桥接,上传到GPU(使用解析后的路径)
|
|
||||||
// Upload to GPU if bridge exists (using resolved path)
|
|
||||||
if (this._engineBridge && textureAsset.data) {
|
|
||||||
await this._engineBridge.loadTexture(textureAsset.textureId, resolvedPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存映射(使用原始路径作为键,避免重复解析)
|
|
||||||
// Cache mapping (using original path as key to avoid re-resolving)
|
|
||||||
this._pathToTextureId.set(texturePath, textureAsset.textureId);
|
|
||||||
|
|
||||||
return textureAsset.textureId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load texture by GUID
|
|
||||||
* 通过GUID加载纹理
|
|
||||||
*/
|
|
||||||
async loadTextureByGuid(guid: AssetGUID): Promise<number> {
|
|
||||||
// 检查是否已有纹理ID / Check if texture ID exists
|
|
||||||
const existingId = this._textureIdMap.get(guid);
|
|
||||||
if (existingId) {
|
|
||||||
return existingId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过资产系统加载 / Load through asset system
|
|
||||||
const result = await this._assetManager.loadAsset<ITextureAsset>(guid);
|
|
||||||
const textureAsset = result.asset;
|
|
||||||
|
|
||||||
// 如果有引擎桥接,上传到GPU / Upload to GPU if bridge exists
|
|
||||||
if (this._engineBridge && textureAsset.data) {
|
|
||||||
const metadata = result.metadata;
|
|
||||||
await this._engineBridge.loadTexture(textureAsset.textureId, metadata.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 缓存映射 / Cache mapping
|
|
||||||
this._textureIdMap.set(guid, textureAsset.textureId);
|
|
||||||
|
|
||||||
return textureAsset.textureId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Batch load textures
|
|
||||||
* 批量加载纹理
|
|
||||||
*/
|
|
||||||
async loadTexturesBatch(paths: string[]): Promise<Map<string, number>> {
|
|
||||||
const results = new Map<string, number>();
|
|
||||||
|
|
||||||
// 收集需要加载的纹理 / Collect textures to load
|
|
||||||
const toLoad: string[] = [];
|
|
||||||
for (const path of paths) {
|
|
||||||
const existingId = this._pathToTextureId.get(path);
|
|
||||||
if (existingId) {
|
|
||||||
results.set(path, existingId);
|
|
||||||
} else {
|
|
||||||
toLoad.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (toLoad.length === 0) {
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 并行加载所有纹理 / Load all textures in parallel
|
|
||||||
const loadPromises = toLoad.map(async (path) => {
|
|
||||||
try {
|
|
||||||
const id = await this.loadTextureForComponent(path);
|
|
||||||
results.set(path, id);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to load texture: ${path}`, error);
|
|
||||||
results.set(path, 0); // 使用默认纹理ID / Use default texture ID
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all(loadPromises);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量加载资源(通用方法,支持 IResourceLoader 接口)
|
|
||||||
* Load resources in batch (generic method for IResourceLoader interface)
|
|
||||||
*
|
|
||||||
* @param paths 资源路径数组 / Array of resource paths
|
|
||||||
* @param type 资源类型 / Resource type
|
|
||||||
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
|
||||||
*/
|
|
||||||
async loadResourcesBatch(paths: string[], type: 'texture' | 'audio' | 'font' | 'data'): Promise<Map<string, number>> {
|
|
||||||
// 目前只支持纹理 / Currently only supports textures
|
|
||||||
if (type === 'texture') {
|
|
||||||
return this.loadTexturesBatch(paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 其他资源类型暂未实现 / Other resource types not yet implemented
|
|
||||||
console.warn(`[EngineIntegration] Resource type '${type}' not yet supported`);
|
|
||||||
return new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unload texture
|
|
||||||
* 卸载纹理
|
|
||||||
*/
|
|
||||||
unloadTexture(textureId: number): void {
|
|
||||||
// 从引擎卸载 / Unload from engine
|
|
||||||
if (this._engineBridge) {
|
|
||||||
this._engineBridge.unloadTexture(textureId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理映射 / Clean up mappings
|
|
||||||
for (const [path, id] of this._pathToTextureId.entries()) {
|
|
||||||
if (id === textureId) {
|
|
||||||
this._pathToTextureId.delete(path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [guid, id] of this._textureIdMap.entries()) {
|
|
||||||
if (id === textureId) {
|
|
||||||
this._textureIdMap.delete(guid);
|
|
||||||
// 也从资产管理器卸载 / Also unload from asset manager
|
|
||||||
this._assetManager.unloadAsset(guid);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get texture ID for path
|
|
||||||
* 获取路径的纹理ID
|
|
||||||
*/
|
|
||||||
getTextureId(path: string): number | null {
|
|
||||||
return this._pathToTextureId.get(path) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preload textures for scene
|
|
||||||
* 为场景预加载纹理
|
|
||||||
*/
|
|
||||||
async preloadSceneTextures(texturePaths: string[]): Promise<void> {
|
|
||||||
await this.loadTexturesBatch(texturePaths);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all texture mappings
|
|
||||||
* 清空所有纹理映射
|
|
||||||
*/
|
|
||||||
clearTextureMappings(): void {
|
|
||||||
this._textureIdMap.clear();
|
|
||||||
this._pathToTextureId.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get statistics
|
|
||||||
* 获取统计信息
|
|
||||||
*/
|
|
||||||
getStatistics(): {
|
|
||||||
loadedTextures: number;
|
|
||||||
} {
|
|
||||||
return {
|
|
||||||
loadedTextures: this._pathToTextureId.size
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
/**
|
|
||||||
* Asset loader interfaces
|
|
||||||
* 资产加载器接口
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
AssetType,
|
|
||||||
AssetGUID,
|
|
||||||
IAssetLoadOptions,
|
|
||||||
IAssetMetadata,
|
|
||||||
IAssetLoadResult
|
|
||||||
} from '../types/AssetTypes';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base asset loader interface
|
|
||||||
* 基础资产加载器接口
|
|
||||||
*/
|
|
||||||
export interface IAssetLoader<T = unknown> {
|
|
||||||
/** 支持的资产类型 / Supported asset type */
|
|
||||||
readonly supportedType: AssetType;
|
|
||||||
|
|
||||||
/** 支持的文件扩展名 / Supported file extensions */
|
|
||||||
readonly supportedExtensions: string[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load an asset from the given path
|
|
||||||
* 从指定路径加载资产
|
|
||||||
*/
|
|
||||||
load(
|
|
||||||
path: string,
|
|
||||||
metadata: IAssetMetadata,
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<T>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate if the loader can handle this asset
|
|
||||||
* 验证加载器是否可以处理此资产
|
|
||||||
*/
|
|
||||||
canLoad(path: string, metadata: IAssetMetadata): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose loaded asset and free resources
|
|
||||||
* 释放已加载的资产并释放资源
|
|
||||||
*/
|
|
||||||
dispose(asset: T): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset loader factory interface
|
|
||||||
* 资产加载器工厂接口
|
|
||||||
*/
|
|
||||||
export interface IAssetLoaderFactory {
|
|
||||||
/**
|
|
||||||
* Create loader for specific asset type
|
|
||||||
* 为特定资产类型创建加载器
|
|
||||||
*/
|
|
||||||
createLoader(type: AssetType): IAssetLoader | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register custom loader
|
|
||||||
* 注册自定义加载器
|
|
||||||
*/
|
|
||||||
registerLoader(type: AssetType, loader: IAssetLoader): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregister loader
|
|
||||||
* 注销加载器
|
|
||||||
*/
|
|
||||||
unregisterLoader(type: AssetType): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if loader exists for type
|
|
||||||
* 检查类型是否有加载器
|
|
||||||
*/
|
|
||||||
hasLoader(type: AssetType): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Texture asset interface
|
|
||||||
* 纹理资产接口
|
|
||||||
*/
|
|
||||||
export interface ITextureAsset {
|
|
||||||
/** WebGL纹理ID / WebGL texture ID */
|
|
||||||
textureId: number;
|
|
||||||
/** 宽度 / Width */
|
|
||||||
width: number;
|
|
||||||
/** 高度 / Height */
|
|
||||||
height: number;
|
|
||||||
/** 格式 / Format */
|
|
||||||
format: 'rgba' | 'rgb' | 'alpha';
|
|
||||||
/** 是否有Mipmap / Has mipmaps */
|
|
||||||
hasMipmaps: boolean;
|
|
||||||
/** 原始数据(如果可用) / Raw image data if available */
|
|
||||||
data?: ImageData | HTMLImageElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mesh asset interface
|
|
||||||
* 网格资产接口
|
|
||||||
*/
|
|
||||||
export interface IMeshAsset {
|
|
||||||
/** 顶点数据 / Vertex data */
|
|
||||||
vertices: Float32Array;
|
|
||||||
/** 索引数据 / Index data */
|
|
||||||
indices: Uint16Array | Uint32Array;
|
|
||||||
/** 法线数据 / Normal data */
|
|
||||||
normals?: Float32Array;
|
|
||||||
/** UV坐标 / UV coordinates */
|
|
||||||
uvs?: Float32Array;
|
|
||||||
/** 切线数据 / Tangent data */
|
|
||||||
tangents?: Float32Array;
|
|
||||||
/** 边界盒 / Axis-aligned bounding box */
|
|
||||||
bounds: {
|
|
||||||
min: [number, number, number];
|
|
||||||
max: [number, number, number];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Audio asset interface
|
|
||||||
* 音频资产接口
|
|
||||||
*/
|
|
||||||
export interface IAudioAsset {
|
|
||||||
/** 音频缓冲区 / Audio buffer */
|
|
||||||
buffer: AudioBuffer;
|
|
||||||
/** 时长(秒) / Duration in seconds */
|
|
||||||
duration: number;
|
|
||||||
/** 采样率 / Sample rate */
|
|
||||||
sampleRate: number;
|
|
||||||
/** 声道数 / Number of channels */
|
|
||||||
channels: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Material asset interface
|
|
||||||
* 材质资产接口
|
|
||||||
*/
|
|
||||||
export interface IMaterialAsset {
|
|
||||||
/** 着色器名称 / Shader name */
|
|
||||||
shader: string;
|
|
||||||
/** 材质属性 / Material properties */
|
|
||||||
properties: Map<string, unknown>;
|
|
||||||
/** 纹理映射 / Texture slot mappings */
|
|
||||||
textures: Map<string, AssetGUID>;
|
|
||||||
/** 渲染状态 / Render states */
|
|
||||||
renderStates: {
|
|
||||||
cullMode?: 'none' | 'front' | 'back';
|
|
||||||
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply';
|
|
||||||
depthTest?: boolean;
|
|
||||||
depthWrite?: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prefab asset interface
|
|
||||||
* 预制体资产接口
|
|
||||||
*/
|
|
||||||
export interface IPrefabAsset {
|
|
||||||
/** 根实体数据 / Serialized entity hierarchy */
|
|
||||||
root: unknown;
|
|
||||||
/** 包含的组件类型 / Component types used in prefab */
|
|
||||||
componentTypes: string[];
|
|
||||||
/** 引用的资产 / All referenced assets */
|
|
||||||
referencedAssets: AssetGUID[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scene asset interface
|
|
||||||
* 场景资产接口
|
|
||||||
*/
|
|
||||||
export interface ISceneAsset {
|
|
||||||
/** 场景名称 / Scene name */
|
|
||||||
name: string;
|
|
||||||
/** 实体列表 / Serialized entity list */
|
|
||||||
entities: unknown[];
|
|
||||||
/** 场景设置 / Scene settings */
|
|
||||||
settings: {
|
|
||||||
/** 环境光 / Ambient light */
|
|
||||||
ambientLight?: [number, number, number];
|
|
||||||
/** 雾效 / Fog settings */
|
|
||||||
fog?: {
|
|
||||||
enabled: boolean;
|
|
||||||
color: [number, number, number];
|
|
||||||
density: number;
|
|
||||||
};
|
|
||||||
/** 天空盒 / Skybox asset */
|
|
||||||
skybox?: AssetGUID;
|
|
||||||
};
|
|
||||||
/** 引用的资产 / All referenced assets */
|
|
||||||
referencedAssets: AssetGUID[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON asset interface
|
|
||||||
* JSON资产接口
|
|
||||||
*/
|
|
||||||
export interface IJsonAsset {
|
|
||||||
/** JSON数据 / JSON data */
|
|
||||||
data: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Text asset interface
|
|
||||||
* 文本资产接口
|
|
||||||
*/
|
|
||||||
export interface ITextAsset {
|
|
||||||
/** 文本内容 / Text content */
|
|
||||||
content: string;
|
|
||||||
/** 编码格式 / Encoding */
|
|
||||||
encoding: 'utf8' | 'utf16' | 'ascii';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binary asset interface
|
|
||||||
* 二进制资产接口
|
|
||||||
*/
|
|
||||||
export interface IBinaryAsset {
|
|
||||||
/** 二进制数据 / Binary data */
|
|
||||||
data: ArrayBuffer;
|
|
||||||
/** MIME类型 / MIME type */
|
|
||||||
mimeType?: string;
|
|
||||||
}
|
|
||||||
@@ -1,328 +0,0 @@
|
|||||||
/**
|
|
||||||
* Asset manager interfaces
|
|
||||||
* 资产管理器接口
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
AssetGUID,
|
|
||||||
AssetHandle,
|
|
||||||
AssetType,
|
|
||||||
AssetState,
|
|
||||||
IAssetLoadOptions,
|
|
||||||
IAssetLoadResult,
|
|
||||||
IAssetReferenceInfo,
|
|
||||||
IAssetPreloadGroup,
|
|
||||||
IAssetLoadProgress
|
|
||||||
} from '../types/AssetTypes';
|
|
||||||
import { IAssetLoader } from './IAssetLoader';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset manager interface
|
|
||||||
* 资产管理器接口
|
|
||||||
*/
|
|
||||||
export interface IAssetManager {
|
|
||||||
/**
|
|
||||||
* Load asset by GUID
|
|
||||||
* 通过GUID加载资产
|
|
||||||
*/
|
|
||||||
loadAsset<T = unknown>(
|
|
||||||
guid: AssetGUID,
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<T>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load asset by path
|
|
||||||
* 通过路径加载资产
|
|
||||||
*/
|
|
||||||
loadAssetByPath<T = unknown>(
|
|
||||||
path: string,
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<T>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load multiple assets
|
|
||||||
* 批量加载资产
|
|
||||||
*/
|
|
||||||
loadAssets(
|
|
||||||
guids: AssetGUID[],
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<Map<AssetGUID, IAssetLoadResult>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preload asset group
|
|
||||||
* 预加载资产组
|
|
||||||
*/
|
|
||||||
preloadGroup(
|
|
||||||
group: IAssetPreloadGroup,
|
|
||||||
onProgress?: (progress: IAssetLoadProgress) => void
|
|
||||||
): Promise<void>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get loaded asset
|
|
||||||
* 获取已加载的资产
|
|
||||||
*/
|
|
||||||
getAsset<T = unknown>(guid: AssetGUID): T | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset by handle
|
|
||||||
* 通过句柄获取资产
|
|
||||||
*/
|
|
||||||
getAssetByHandle<T = unknown>(handle: AssetHandle): T | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if asset is loaded
|
|
||||||
* 检查资产是否已加载
|
|
||||||
*/
|
|
||||||
isLoaded(guid: AssetGUID): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset state
|
|
||||||
* 获取资产状态
|
|
||||||
*/
|
|
||||||
getAssetState(guid: AssetGUID): AssetState;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unload asset
|
|
||||||
* 卸载资产
|
|
||||||
*/
|
|
||||||
unloadAsset(guid: AssetGUID): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unload all assets
|
|
||||||
* 卸载所有资产
|
|
||||||
*/
|
|
||||||
unloadAllAssets(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unload unused assets
|
|
||||||
* 卸载未使用的资产
|
|
||||||
*/
|
|
||||||
unloadUnusedAssets(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add reference to asset
|
|
||||||
* 增加资产引用
|
|
||||||
*/
|
|
||||||
addReference(guid: AssetGUID): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove reference from asset
|
|
||||||
* 移除资产引用
|
|
||||||
*/
|
|
||||||
removeReference(guid: AssetGUID): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get reference info
|
|
||||||
* 获取引用信息
|
|
||||||
*/
|
|
||||||
getReferenceInfo(guid: AssetGUID): IAssetReferenceInfo | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register custom loader
|
|
||||||
* 注册自定义加载器
|
|
||||||
*/
|
|
||||||
registerLoader(type: AssetType, loader: IAssetLoader): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset statistics
|
|
||||||
* 获取资产统计信息
|
|
||||||
*/
|
|
||||||
getStatistics(): {
|
|
||||||
loadedCount: number;
|
|
||||||
loadQueue: number;
|
|
||||||
failedCount: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear cache
|
|
||||||
* 清空缓存
|
|
||||||
*/
|
|
||||||
clearCache(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose manager
|
|
||||||
* 释放管理器
|
|
||||||
*/
|
|
||||||
dispose(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset cache interface
|
|
||||||
* 资产缓存接口
|
|
||||||
*/
|
|
||||||
export interface IAssetCache {
|
|
||||||
/**
|
|
||||||
* Get cached asset
|
|
||||||
* 获取缓存的资产
|
|
||||||
*/
|
|
||||||
get<T = unknown>(guid: AssetGUID): T | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set cached asset
|
|
||||||
* 设置缓存的资产
|
|
||||||
*/
|
|
||||||
set<T = unknown>(guid: AssetGUID, asset: T, size: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if asset is cached
|
|
||||||
* 检查资产是否已缓存
|
|
||||||
*/
|
|
||||||
has(guid: AssetGUID): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove from cache
|
|
||||||
* 从缓存中移除
|
|
||||||
*/
|
|
||||||
remove(guid: AssetGUID): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all cache
|
|
||||||
* 清空所有缓存
|
|
||||||
*/
|
|
||||||
clear(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cache size
|
|
||||||
* 获取缓存大小
|
|
||||||
*/
|
|
||||||
getSize(): number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get cached asset count
|
|
||||||
* 获取缓存资产数量
|
|
||||||
*/
|
|
||||||
getCount(): number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evict assets based on policy
|
|
||||||
* 根据策略驱逐资产
|
|
||||||
*/
|
|
||||||
evict(targetSize: number): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset loading queue interface
|
|
||||||
* 资产加载队列接口
|
|
||||||
*/
|
|
||||||
export interface IAssetLoadQueue {
|
|
||||||
/**
|
|
||||||
* Add to queue
|
|
||||||
* 添加到队列
|
|
||||||
*/
|
|
||||||
enqueue(
|
|
||||||
guid: AssetGUID,
|
|
||||||
priority: number,
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove from queue
|
|
||||||
* 从队列移除
|
|
||||||
*/
|
|
||||||
dequeue(): {
|
|
||||||
guid: AssetGUID;
|
|
||||||
options?: IAssetLoadOptions;
|
|
||||||
} | null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if queue is empty
|
|
||||||
* 检查队列是否为空
|
|
||||||
*/
|
|
||||||
isEmpty(): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get queue size
|
|
||||||
* 获取队列大小
|
|
||||||
*/
|
|
||||||
getSize(): number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear queue
|
|
||||||
* 清空队列
|
|
||||||
*/
|
|
||||||
clear(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reprioritize item
|
|
||||||
* 重新设置优先级
|
|
||||||
*/
|
|
||||||
reprioritize(guid: AssetGUID, newPriority: number): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset dependency resolver interface
|
|
||||||
* 资产依赖解析器接口
|
|
||||||
*/
|
|
||||||
export interface IAssetDependencyResolver {
|
|
||||||
/**
|
|
||||||
* Resolve dependencies for asset
|
|
||||||
* 解析资产的依赖
|
|
||||||
*/
|
|
||||||
resolveDependencies(guid: AssetGUID): Promise<AssetGUID[]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get direct dependencies
|
|
||||||
* 获取直接依赖
|
|
||||||
*/
|
|
||||||
getDirectDependencies(guid: AssetGUID): AssetGUID[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all dependencies recursively
|
|
||||||
* 递归获取所有依赖
|
|
||||||
*/
|
|
||||||
getAllDependencies(guid: AssetGUID): AssetGUID[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check for circular dependencies
|
|
||||||
* 检查循环依赖
|
|
||||||
*/
|
|
||||||
hasCircularDependency(guid: AssetGUID): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build dependency graph
|
|
||||||
* 构建依赖图
|
|
||||||
*/
|
|
||||||
buildDependencyGraph(guids: AssetGUID[]): Map<AssetGUID, AssetGUID[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset streaming interface
|
|
||||||
* 资产流式加载接口
|
|
||||||
*/
|
|
||||||
export interface IAssetStreaming {
|
|
||||||
/**
|
|
||||||
* Start streaming assets
|
|
||||||
* 开始流式加载资产
|
|
||||||
*/
|
|
||||||
startStreaming(guids: AssetGUID[]): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop streaming
|
|
||||||
* 停止流式加载
|
|
||||||
*/
|
|
||||||
stopStreaming(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pause streaming
|
|
||||||
* 暂停流式加载
|
|
||||||
*/
|
|
||||||
pauseStreaming(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resume streaming
|
|
||||||
* 恢复流式加载
|
|
||||||
*/
|
|
||||||
resumeStreaming(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set streaming budget per frame
|
|
||||||
* 设置每帧流式加载预算
|
|
||||||
*/
|
|
||||||
setFrameBudget(milliseconds: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get streaming progress
|
|
||||||
* 获取流式加载进度
|
|
||||||
*/
|
|
||||||
getProgress(): IAssetLoadProgress;
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
/**
|
|
||||||
* 资源组件接口 - 用于依赖运行时资源的组件(纹理、音频等)
|
|
||||||
* Interface for components that depend on runtime resources (textures, audio, etc.)
|
|
||||||
*
|
|
||||||
* 实现此接口的组件可以参与 SceneResourceManager 管理的集中式资源加载
|
|
||||||
* Components implementing this interface can participate in centralized resource loading managed by SceneResourceManager
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 资源引用 - 包含路径和运行时 ID
|
|
||||||
* Resource reference with path and runtime ID
|
|
||||||
*/
|
|
||||||
export interface ResourceReference {
|
|
||||||
/** 资源路径(例如 "assets/sprites/player.png")/ Asset path (e.g., "assets/sprites/player.png") */
|
|
||||||
path: string;
|
|
||||||
/** 引擎分配的运行时资源 ID(例如 GPU 上的纹理 ID)/ Runtime resource ID assigned by engine (e.g., texture ID on GPU) */
|
|
||||||
runtimeId?: number;
|
|
||||||
/** 资源类型标识符 / Resource type identifier */
|
|
||||||
type: 'texture' | 'audio' | 'font' | 'data';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 资源组件接口
|
|
||||||
* Resource component interface
|
|
||||||
*
|
|
||||||
* 实现此接口的组件可以在场景启动前由 SceneResourceManager 集中加载资源
|
|
||||||
* Components implementing this interface can have their resources loaded centrally by SceneResourceManager before the scene starts
|
|
||||||
*/
|
|
||||||
export interface IResourceComponent {
|
|
||||||
/**
|
|
||||||
* 获取此组件需要的所有资源引用
|
|
||||||
* Get all resource references needed by this component
|
|
||||||
*
|
|
||||||
* 在场景加载期间调用以收集资源路径
|
|
||||||
* Called during scene loading to collect resource paths
|
|
||||||
*/
|
|
||||||
getResourceReferences(): ResourceReference[];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置已加载资源的运行时 ID
|
|
||||||
* Set runtime IDs for loaded resources
|
|
||||||
*
|
|
||||||
* 在 SceneResourceManager 加载资源后调用
|
|
||||||
* Called after resources are loaded by SceneResourceManager
|
|
||||||
*
|
|
||||||
* @param pathToId 资源路径到运行时 ID 的映射 / Map of resource paths to runtime IDs
|
|
||||||
*/
|
|
||||||
setResourceIds(pathToId: Map<string, number>): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 类型守卫 - 检查组件是否实现了 IResourceComponent
|
|
||||||
* Type guard to check if a component implements IResourceComponent
|
|
||||||
*/
|
|
||||||
export function isResourceComponent(component: any): component is IResourceComponent {
|
|
||||||
return (
|
|
||||||
component !== null &&
|
|
||||||
typeof component === 'object' &&
|
|
||||||
typeof component.getResourceReferences === 'function' &&
|
|
||||||
typeof component.setResourceIds === 'function'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
/**
|
|
||||||
* Asset loader factory implementation
|
|
||||||
* 资产加载器工厂实现
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { AssetType } from '../types/AssetTypes';
|
|
||||||
import { IAssetLoader, IAssetLoaderFactory } from '../interfaces/IAssetLoader';
|
|
||||||
import { TextureLoader } from './TextureLoader';
|
|
||||||
import { JsonLoader } from './JsonLoader';
|
|
||||||
import { TextLoader } from './TextLoader';
|
|
||||||
import { BinaryLoader } from './BinaryLoader';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset loader factory
|
|
||||||
* 资产加载器工厂
|
|
||||||
*/
|
|
||||||
export class AssetLoaderFactory implements IAssetLoaderFactory {
|
|
||||||
private readonly _loaders = new Map<AssetType, IAssetLoader>();
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
// 注册默认加载器 / Register default loaders
|
|
||||||
this.registerDefaultLoaders();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register default loaders
|
|
||||||
* 注册默认加载器
|
|
||||||
*/
|
|
||||||
private registerDefaultLoaders(): void {
|
|
||||||
// 纹理加载器 / Texture loader
|
|
||||||
this._loaders.set(AssetType.Texture, new TextureLoader());
|
|
||||||
|
|
||||||
// JSON加载器 / JSON loader
|
|
||||||
this._loaders.set(AssetType.Json, new JsonLoader());
|
|
||||||
|
|
||||||
// 文本加载器 / Text loader
|
|
||||||
this._loaders.set(AssetType.Text, new TextLoader());
|
|
||||||
|
|
||||||
// 二进制加载器 / Binary loader
|
|
||||||
this._loaders.set(AssetType.Binary, new BinaryLoader());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create loader for specific asset type
|
|
||||||
* 为特定资产类型创建加载器
|
|
||||||
*/
|
|
||||||
createLoader(type: AssetType): IAssetLoader | null {
|
|
||||||
return this._loaders.get(type) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register custom loader
|
|
||||||
* 注册自定义加载器
|
|
||||||
*/
|
|
||||||
registerLoader(type: AssetType, loader: IAssetLoader): void {
|
|
||||||
this._loaders.set(type, loader);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unregister loader
|
|
||||||
* 注销加载器
|
|
||||||
*/
|
|
||||||
unregisterLoader(type: AssetType): void {
|
|
||||||
this._loaders.delete(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if loader exists for type
|
|
||||||
* 检查类型是否有加载器
|
|
||||||
*/
|
|
||||||
hasLoader(type: AssetType): boolean {
|
|
||||||
return this._loaders.has(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all registered loaders
|
|
||||||
* 获取所有注册的加载器
|
|
||||||
*/
|
|
||||||
getRegisteredTypes(): AssetType[] {
|
|
||||||
return Array.from(this._loaders.keys());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all loaders
|
|
||||||
* 清空所有加载器
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
this._loaders.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
/**
|
|
||||||
* Binary asset loader
|
|
||||||
* 二进制资产加载器
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
AssetType,
|
|
||||||
IAssetLoadOptions,
|
|
||||||
IAssetMetadata,
|
|
||||||
IAssetLoadResult,
|
|
||||||
AssetLoadError
|
|
||||||
} from '../types/AssetTypes';
|
|
||||||
import { IAssetLoader, IBinaryAsset } from '../interfaces/IAssetLoader';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Binary loader implementation
|
|
||||||
* 二进制加载器实现
|
|
||||||
*/
|
|
||||||
export class BinaryLoader implements IAssetLoader<IBinaryAsset> {
|
|
||||||
readonly supportedType = AssetType.Binary;
|
|
||||||
readonly supportedExtensions = [
|
|
||||||
'.bin', '.dat', '.raw', '.bytes',
|
|
||||||
'.wasm', '.so', '.dll', '.dylib'
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load binary asset
|
|
||||||
* 加载二进制资产
|
|
||||||
*/
|
|
||||||
async load(
|
|
||||||
path: string,
|
|
||||||
metadata: IAssetMetadata,
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<IBinaryAsset>> {
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.fetchWithTimeout(path, options?.timeout);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取MIME类型 / Get MIME type
|
|
||||||
const mimeType = response.headers.get('content-type') || undefined;
|
|
||||||
|
|
||||||
// 获取总大小用于进度回调 / Get total size for progress callback
|
|
||||||
const contentLength = response.headers.get('content-length');
|
|
||||||
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
||||||
|
|
||||||
// 读取响应 / Read response
|
|
||||||
let data: ArrayBuffer;
|
|
||||||
if (options?.onProgress && total > 0) {
|
|
||||||
data = await this.readResponseWithProgress(response, total, options.onProgress);
|
|
||||||
} else {
|
|
||||||
data = await response.arrayBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
const asset: IBinaryAsset = {
|
|
||||||
data,
|
|
||||||
mimeType
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
asset,
|
|
||||||
handle: 0,
|
|
||||||
metadata,
|
|
||||||
loadTime: performance.now() - startTime
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw new AssetLoadError(
|
|
||||||
`Failed to load binary: ${error.message}`,
|
|
||||||
metadata.guid,
|
|
||||||
AssetType.Binary,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch with timeout
|
|
||||||
* 带超时的fetch
|
|
||||||
*/
|
|
||||||
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
signal: controller.signal,
|
|
||||||
mode: 'cors',
|
|
||||||
credentials: 'same-origin'
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read response with progress
|
|
||||||
* 带进度读取响应
|
|
||||||
*/
|
|
||||||
private async readResponseWithProgress(
|
|
||||||
response: Response,
|
|
||||||
total: number,
|
|
||||||
onProgress: (progress: number) => void
|
|
||||||
): Promise<ArrayBuffer> {
|
|
||||||
const reader = response.body?.getReader();
|
|
||||||
if (!reader) {
|
|
||||||
return response.arrayBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunks: Uint8Array[] = [];
|
|
||||||
let receivedLength = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
chunks.push(value);
|
|
||||||
receivedLength += value.length;
|
|
||||||
|
|
||||||
// 报告进度 / Report progress
|
|
||||||
onProgress(receivedLength / total);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合并chunks到ArrayBuffer / Merge chunks into ArrayBuffer
|
|
||||||
const result = new Uint8Array(receivedLength);
|
|
||||||
let position = 0;
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
result.set(chunk, position);
|
|
||||||
position += chunk.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate if the loader can handle this asset
|
|
||||||
* 验证加载器是否可以处理此资产
|
|
||||||
*/
|
|
||||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
|
||||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
|
||||||
return this.supportedExtensions.includes(ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Estimate memory usage for the asset
|
|
||||||
* 估算资产的内存使用量
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose loaded asset
|
|
||||||
* 释放已加载的资产
|
|
||||||
*/
|
|
||||||
dispose(asset: IBinaryAsset): void {
|
|
||||||
// ArrayBuffer无法直接释放,但可以清空引用 / Can't directly release ArrayBuffer, but clear reference
|
|
||||||
(asset as any).data = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
/**
|
|
||||||
* JSON asset loader
|
|
||||||
* JSON资产加载器
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
AssetType,
|
|
||||||
IAssetLoadOptions,
|
|
||||||
IAssetMetadata,
|
|
||||||
IAssetLoadResult,
|
|
||||||
AssetLoadError
|
|
||||||
} from '../types/AssetTypes';
|
|
||||||
import { IAssetLoader, IJsonAsset } from '../interfaces/IAssetLoader';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON loader implementation
|
|
||||||
* JSON加载器实现
|
|
||||||
*/
|
|
||||||
export class JsonLoader implements IAssetLoader<IJsonAsset> {
|
|
||||||
readonly supportedType = AssetType.Json;
|
|
||||||
readonly supportedExtensions = ['.json', '.jsonc'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load JSON asset
|
|
||||||
* 加载JSON资产
|
|
||||||
*/
|
|
||||||
async load(
|
|
||||||
path: string,
|
|
||||||
metadata: IAssetMetadata,
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<IJsonAsset>> {
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.fetchWithTimeout(path, options?.timeout);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取总大小用于进度回调 / Get total size for progress callback
|
|
||||||
const contentLength = response.headers.get('content-length');
|
|
||||||
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
||||||
|
|
||||||
// 读取响应 / Read response
|
|
||||||
let jsonData: unknown;
|
|
||||||
if (options?.onProgress && total > 0) {
|
|
||||||
jsonData = await this.readResponseWithProgress(response, total, options.onProgress);
|
|
||||||
} else {
|
|
||||||
jsonData = await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
const asset: IJsonAsset = {
|
|
||||||
data: jsonData
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
asset,
|
|
||||||
handle: 0,
|
|
||||||
metadata,
|
|
||||||
loadTime: performance.now() - startTime
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw new AssetLoadError(
|
|
||||||
`Failed to load JSON: ${error.message}`,
|
|
||||||
metadata.guid,
|
|
||||||
AssetType.Json,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch with timeout
|
|
||||||
* 带超时的fetch
|
|
||||||
*/
|
|
||||||
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
signal: controller.signal,
|
|
||||||
mode: 'cors',
|
|
||||||
credentials: 'same-origin'
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read response with progress
|
|
||||||
* 带进度读取响应
|
|
||||||
*/
|
|
||||||
private async readResponseWithProgress(
|
|
||||||
response: Response,
|
|
||||||
total: number,
|
|
||||||
onProgress: (progress: number) => void
|
|
||||||
): Promise<unknown> {
|
|
||||||
const reader = response.body?.getReader();
|
|
||||||
if (!reader) {
|
|
||||||
return response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunks: Uint8Array[] = [];
|
|
||||||
let receivedLength = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
chunks.push(value);
|
|
||||||
receivedLength += value.length;
|
|
||||||
|
|
||||||
// 报告进度 / Report progress
|
|
||||||
onProgress(receivedLength / total);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合并chunks / Merge chunks
|
|
||||||
const allChunks = new Uint8Array(receivedLength);
|
|
||||||
let position = 0;
|
|
||||||
for (const chunk of chunks) {
|
|
||||||
allChunks.set(chunk, position);
|
|
||||||
position += chunk.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解码为字符串并解析JSON / Decode to string and parse JSON
|
|
||||||
const decoder = new TextDecoder('utf-8');
|
|
||||||
const jsonString = decoder.decode(allChunks);
|
|
||||||
return JSON.parse(jsonString);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate if the loader can handle this asset
|
|
||||||
* 验证加载器是否可以处理此资产
|
|
||||||
*/
|
|
||||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
|
||||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
|
||||||
return this.supportedExtensions.includes(ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Estimate memory usage for the asset
|
|
||||||
* 估算资产的内存使用量
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose loaded asset
|
|
||||||
* 释放已加载的资产
|
|
||||||
*/
|
|
||||||
dispose(asset: IJsonAsset): void {
|
|
||||||
// JSON资产通常不需要特殊清理 / JSON assets usually don't need special cleanup
|
|
||||||
// 但可以清空引用以帮助GC / But can clear references to help GC
|
|
||||||
(asset as any).data = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
/**
|
|
||||||
* Text asset loader
|
|
||||||
* 文本资产加载器
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
AssetType,
|
|
||||||
IAssetLoadOptions,
|
|
||||||
IAssetMetadata,
|
|
||||||
IAssetLoadResult,
|
|
||||||
AssetLoadError
|
|
||||||
} from '../types/AssetTypes';
|
|
||||||
import { IAssetLoader, ITextAsset } from '../interfaces/IAssetLoader';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Text loader implementation
|
|
||||||
* 文本加载器实现
|
|
||||||
*/
|
|
||||||
export class TextLoader implements IAssetLoader<ITextAsset> {
|
|
||||||
readonly supportedType = AssetType.Text;
|
|
||||||
readonly supportedExtensions = ['.txt', '.text', '.md', '.csv', '.xml', '.html', '.css', '.js', '.ts'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load text asset
|
|
||||||
* 加载文本资产
|
|
||||||
*/
|
|
||||||
async load(
|
|
||||||
path: string,
|
|
||||||
metadata: IAssetMetadata,
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<ITextAsset>> {
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.fetchWithTimeout(path, options?.timeout);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取总大小用于进度回调 / Get total size for progress callback
|
|
||||||
const contentLength = response.headers.get('content-length');
|
|
||||||
const total = contentLength ? parseInt(contentLength, 10) : 0;
|
|
||||||
|
|
||||||
// 读取响应 / Read response
|
|
||||||
let content: string;
|
|
||||||
if (options?.onProgress && total > 0) {
|
|
||||||
content = await this.readResponseWithProgress(response, total, options.onProgress);
|
|
||||||
} else {
|
|
||||||
content = await response.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检测编码 / Detect encoding
|
|
||||||
const encoding = this.detectEncoding(content);
|
|
||||||
|
|
||||||
const asset: ITextAsset = {
|
|
||||||
content,
|
|
||||||
encoding
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
asset,
|
|
||||||
handle: 0,
|
|
||||||
metadata,
|
|
||||||
loadTime: performance.now() - startTime
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw new AssetLoadError(
|
|
||||||
`Failed to load text: ${error.message}`,
|
|
||||||
metadata.guid,
|
|
||||||
AssetType.Text,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch with timeout
|
|
||||||
* 带超时的fetch
|
|
||||||
*/
|
|
||||||
private async fetchWithTimeout(url: string, timeout = 30000): Promise<Response> {
|
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
signal: controller.signal,
|
|
||||||
mode: 'cors',
|
|
||||||
credentials: 'same-origin'
|
|
||||||
});
|
|
||||||
return response;
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read response with progress
|
|
||||||
* 带进度读取响应
|
|
||||||
*/
|
|
||||||
private async readResponseWithProgress(
|
|
||||||
response: Response,
|
|
||||||
total: number,
|
|
||||||
onProgress: (progress: number) => void
|
|
||||||
): Promise<string> {
|
|
||||||
const reader = response.body?.getReader();
|
|
||||||
if (!reader) {
|
|
||||||
return response.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
const decoder = new TextDecoder('utf-8');
|
|
||||||
let result = '';
|
|
||||||
let receivedLength = 0;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
|
|
||||||
if (done) break;
|
|
||||||
|
|
||||||
receivedLength += value.length;
|
|
||||||
result += decoder.decode(value, { stream: true });
|
|
||||||
|
|
||||||
// 报告进度 / Report progress
|
|
||||||
onProgress(receivedLength / total);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect text encoding
|
|
||||||
* 检测文本编码
|
|
||||||
*/
|
|
||||||
private detectEncoding(content: string): 'utf8' | 'utf16' | 'ascii' {
|
|
||||||
// 简单的编码检测 / Simple encoding detection
|
|
||||||
// 检查是否包含非ASCII字符 / Check for non-ASCII characters
|
|
||||||
for (let i = 0; i < content.length; i++) {
|
|
||||||
const charCode = content.charCodeAt(i);
|
|
||||||
if (charCode > 127) {
|
|
||||||
// 包含非ASCII字符,可能是UTF-8或UTF-16 / Contains non-ASCII, likely UTF-8 or UTF-16
|
|
||||||
return charCode > 255 ? 'utf16' : 'utf8';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 'ascii';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate if the loader can handle this asset
|
|
||||||
* 验证加载器是否可以处理此资产
|
|
||||||
*/
|
|
||||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
|
||||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
|
||||||
return this.supportedExtensions.includes(ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Estimate memory usage for the asset
|
|
||||||
* 估算资产的内存使用量
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose loaded asset
|
|
||||||
* 释放已加载的资产
|
|
||||||
*/
|
|
||||||
dispose(asset: ITextAsset): void {
|
|
||||||
// 清空内容以帮助GC / Clear content to help GC
|
|
||||||
(asset as any).content = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
/**
|
|
||||||
* Texture asset loader
|
|
||||||
* 纹理资产加载器
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
AssetType,
|
|
||||||
IAssetLoadOptions,
|
|
||||||
IAssetMetadata,
|
|
||||||
IAssetLoadResult,
|
|
||||||
AssetLoadError
|
|
||||||
} from '../types/AssetTypes';
|
|
||||||
import { IAssetLoader, ITextureAsset } from '../interfaces/IAssetLoader';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Texture loader implementation
|
|
||||||
* 纹理加载器实现
|
|
||||||
*/
|
|
||||||
export class TextureLoader implements IAssetLoader<ITextureAsset> {
|
|
||||||
readonly supportedType = AssetType.Texture;
|
|
||||||
readonly supportedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp'];
|
|
||||||
|
|
||||||
private static _nextTextureId = 1;
|
|
||||||
private readonly _loadedTextures = new Map<string, ITextureAsset>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load texture asset
|
|
||||||
* 加载纹理资产
|
|
||||||
*/
|
|
||||||
async load(
|
|
||||||
path: string,
|
|
||||||
metadata: IAssetMetadata,
|
|
||||||
options?: IAssetLoadOptions
|
|
||||||
): Promise<IAssetLoadResult<ITextureAsset>> {
|
|
||||||
const startTime = performance.now();
|
|
||||||
|
|
||||||
// 检查缓存 / Check cache
|
|
||||||
if (!options?.forceReload && this._loadedTextures.has(path)) {
|
|
||||||
const cached = this._loadedTextures.get(path)!;
|
|
||||||
return {
|
|
||||||
asset: cached,
|
|
||||||
handle: cached.textureId,
|
|
||||||
metadata,
|
|
||||||
loadTime: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建图像元素 / Create image element
|
|
||||||
const image = await this.loadImage(path, options);
|
|
||||||
|
|
||||||
// 创建纹理资产 / Create texture asset
|
|
||||||
const textureAsset: ITextureAsset = {
|
|
||||||
textureId: TextureLoader._nextTextureId++,
|
|
||||||
width: image.width,
|
|
||||||
height: image.height,
|
|
||||||
format: 'rgba', // 默认格式 / Default format
|
|
||||||
hasMipmaps: false,
|
|
||||||
data: image
|
|
||||||
};
|
|
||||||
|
|
||||||
// 缓存纹理 / Cache texture
|
|
||||||
this._loadedTextures.set(path, textureAsset);
|
|
||||||
|
|
||||||
// 触发引擎纹理加载(如果有引擎桥接) / Trigger engine texture loading if bridge exists
|
|
||||||
if (typeof window !== 'undefined' && (window as any).engineBridge) {
|
|
||||||
await this.uploadToGPU(textureAsset, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
asset: textureAsset,
|
|
||||||
handle: textureAsset.textureId,
|
|
||||||
metadata,
|
|
||||||
loadTime: performance.now() - startTime
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
throw AssetLoadError.fileNotFound(metadata.guid, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load image from URL
|
|
||||||
* 从URL加载图像
|
|
||||||
*/
|
|
||||||
private async loadImage(url: string, options?: IAssetLoadOptions): Promise<HTMLImageElement> {
|
|
||||||
// For Tauri asset URLs, use fetch to load the image
|
|
||||||
// 对于Tauri资产URL,使用fetch加载图像
|
|
||||||
if (url.startsWith('http://asset.localhost/') || url.startsWith('asset://')) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to fetch image: ${response.statusText}`);
|
|
||||||
}
|
|
||||||
const blob = await response.blob();
|
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const image = new Image();
|
|
||||||
image.onload = () => {
|
|
||||||
// Clean up blob URL after loading
|
|
||||||
// 加载后清理blob URL
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
resolve(image);
|
|
||||||
};
|
|
||||||
image.onerror = () => {
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
reject(new Error(`Failed to load image from blob: ${url}`));
|
|
||||||
};
|
|
||||||
image.src = blobUrl;
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to load Tauri asset: ${url} - ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For regular URLs, use standard Image loading
|
|
||||||
// 对于常规URL,使用标准Image加载
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const image = new Image();
|
|
||||||
image.crossOrigin = 'anonymous';
|
|
||||||
|
|
||||||
// 超时处理 / Timeout handling
|
|
||||||
const timeout = options?.timeout || 30000;
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
reject(new Error(`Image load timeout: ${url}`));
|
|
||||||
}, timeout);
|
|
||||||
|
|
||||||
// 进度回调 / Progress callback
|
|
||||||
if (options?.onProgress) {
|
|
||||||
// 图像加载没有真正的进度事件,模拟进度 / Images don't have real progress events, simulate
|
|
||||||
let progress = 0;
|
|
||||||
const progressInterval = setInterval(() => {
|
|
||||||
progress = Math.min(progress + 0.1, 0.9);
|
|
||||||
options.onProgress!(progress);
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
image.onload = () => {
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
options.onProgress!(1);
|
|
||||||
resolve(image);
|
|
||||||
};
|
|
||||||
|
|
||||||
image.onerror = () => {
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
reject(new Error(`Failed to load image: ${url}`));
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
image.onload = () => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
resolve(image);
|
|
||||||
};
|
|
||||||
|
|
||||||
image.onerror = () => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
reject(new Error(`Failed to load image: ${url}`));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
image.src = url;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload texture to GPU
|
|
||||||
* 上传纹理到GPU
|
|
||||||
*/
|
|
||||||
private async uploadToGPU(textureAsset: ITextureAsset, path: string): Promise<void> {
|
|
||||||
const bridge = (window as any).engineBridge;
|
|
||||||
if (bridge && bridge.loadTexture) {
|
|
||||||
await bridge.loadTexture(textureAsset.textureId, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate if the loader can handle this asset
|
|
||||||
* 验证加载器是否可以处理此资产
|
|
||||||
*/
|
|
||||||
canLoad(path: string, _metadata: IAssetMetadata): boolean {
|
|
||||||
const ext = path.toLowerCase().substring(path.lastIndexOf('.'));
|
|
||||||
return this.supportedExtensions.includes(ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Estimate memory usage for the asset
|
|
||||||
* 估算资产的内存使用量
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispose loaded asset
|
|
||||||
* 释放已加载的资产
|
|
||||||
*/
|
|
||||||
dispose(asset: ITextureAsset): void {
|
|
||||||
// 从缓存中移除 / Remove from cache
|
|
||||||
for (const [path, cached] of this._loadedTextures.entries()) {
|
|
||||||
if (cached === asset) {
|
|
||||||
this._loadedTextures.delete(path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 释放GPU资源 / Release GPU resources
|
|
||||||
if (typeof window !== 'undefined' && (window as any).engineBridge) {
|
|
||||||
const bridge = (window as any).engineBridge;
|
|
||||||
if (bridge.unloadTexture) {
|
|
||||||
bridge.unloadTexture(asset.textureId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理图像数据 / Clean up image data
|
|
||||||
if (asset.data instanceof HTMLImageElement) {
|
|
||||||
asset.data.src = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
/**
|
|
||||||
* 场景资源管理器 - 集中式场景资源加载
|
|
||||||
* SceneResourceManager - Centralized resource loading for scenes
|
|
||||||
*
|
|
||||||
* 扫描场景中所有组件,收集资源引用,批量加载资源,并将运行时 ID 分配回组件
|
|
||||||
* Scans all components in a scene, collects resource references, batch-loads them, and assigns runtime IDs back to components
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Scene } from '@esengine/ecs-framework';
|
|
||||||
import { isResourceComponent, type ResourceReference } from '../interfaces/IResourceComponent';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 资源加载器接口
|
|
||||||
* Resource loader interface
|
|
||||||
*/
|
|
||||||
export interface IResourceLoader {
|
|
||||||
/**
|
|
||||||
* 批量加载资源并返回路径到 ID 的映射
|
|
||||||
* Load a batch of resources and return path-to-ID mapping
|
|
||||||
* @param paths 资源路径数组 / Array of resource paths
|
|
||||||
* @param type 资源类型 / Resource type
|
|
||||||
* @returns 路径到运行时 ID 的映射 / Map of paths to runtime IDs
|
|
||||||
*/
|
|
||||||
loadResourcesBatch(paths: string[], type: ResourceReference['type']): Promise<Map<string, number>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SceneResourceManager {
|
|
||||||
private resourceLoader: IResourceLoader | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置资源加载器实现
|
|
||||||
* Set the resource loader implementation
|
|
||||||
*
|
|
||||||
* 应由引擎集成层调用
|
|
||||||
* This should be called by the engine integration layer
|
|
||||||
*
|
|
||||||
* @param loader 资源加载器实例 / Resource loader instance
|
|
||||||
*/
|
|
||||||
setResourceLoader(loader: IResourceLoader): void {
|
|
||||||
this.resourceLoader = loader;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 加载场景所需的所有资源
|
|
||||||
* Load all resources required by a scene
|
|
||||||
*
|
|
||||||
* 流程 / Process:
|
|
||||||
* 1. 扫描所有实体并从 IResourceComponent 实现中收集资源引用
|
|
||||||
* Scan all entities and collect resource references from IResourceComponent implementations
|
|
||||||
* 2. 按类型分组资源(纹理、音频等)
|
|
||||||
* Group resources by type (texture, audio, etc.)
|
|
||||||
* 3. 批量加载每种资源类型
|
|
||||||
* Batch load each resource type
|
|
||||||
* 4. 将运行时 ID 分配回组件
|
|
||||||
* Assign runtime IDs back to components
|
|
||||||
*
|
|
||||||
* @param scene 要加载资源的场景 / The scene to load resources for
|
|
||||||
* @returns 当所有资源加载完成时解析的 Promise / Promise that resolves when all resources are loaded
|
|
||||||
*/
|
|
||||||
async loadSceneResources(scene: Scene): Promise<void> {
|
|
||||||
if (!this.resourceLoader) {
|
|
||||||
console.warn('[SceneResourceManager] No resource loader set, skipping resource loading');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从组件收集所有资源引用 / Collect all resource references from components
|
|
||||||
const resourceRefs = this.collectResourceReferences(scene);
|
|
||||||
|
|
||||||
if (resourceRefs.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 按资源类型分组 / Group by resource type
|
|
||||||
const resourcesByType = new Map<ResourceReference['type'], Set<string>>();
|
|
||||||
for (const ref of resourceRefs) {
|
|
||||||
if (!resourcesByType.has(ref.type)) {
|
|
||||||
resourcesByType.set(ref.type, new Set());
|
|
||||||
}
|
|
||||||
resourcesByType.get(ref.type)!.add(ref.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量加载每种资源类型 / Load each resource type in batch
|
|
||||||
const allResourceIds = new Map<string, number>();
|
|
||||||
|
|
||||||
for (const [type, paths] of resourcesByType) {
|
|
||||||
const pathsArray = Array.from(paths);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resourceIds = await this.resourceLoader.loadResourcesBatch(pathsArray, type);
|
|
||||||
|
|
||||||
// 合并到总映射表 / Merge into combined map
|
|
||||||
for (const [path, id] of resourceIds) {
|
|
||||||
allResourceIds.set(path, id);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[SceneResourceManager] Failed to load ${type} resources:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将资源 ID 分配回组件 / Assign resource IDs back to components
|
|
||||||
this.assignResourceIds(scene, allResourceIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从场景实体收集所有资源引用
|
|
||||||
* Collect all resource references from scene entities
|
|
||||||
*/
|
|
||||||
private collectResourceReferences(scene: Scene): ResourceReference[] {
|
|
||||||
const refs: ResourceReference[] = [];
|
|
||||||
|
|
||||||
for (const entity of scene.entities.buffer) {
|
|
||||||
for (const component of entity.components) {
|
|
||||||
if (isResourceComponent(component)) {
|
|
||||||
const componentRefs = component.getResourceReferences();
|
|
||||||
refs.push(...componentRefs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return refs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将已加载的资源 ID 分配回组件
|
|
||||||
* Assign loaded resource IDs back to components
|
|
||||||
*
|
|
||||||
* @param scene 场景 / Scene
|
|
||||||
* @param pathToId 路径到 ID 的映射 / Path to ID mapping
|
|
||||||
*/
|
|
||||||
private assignResourceIds(scene: Scene, pathToId: Map<string, number>): void {
|
|
||||||
for (const entity of scene.entities.buffer) {
|
|
||||||
for (const component of entity.components) {
|
|
||||||
if (isResourceComponent(component)) {
|
|
||||||
component.setResourceIds(pathToId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 卸载场景使用的所有资源
|
|
||||||
* Unload all resources used by a scene
|
|
||||||
*
|
|
||||||
* 在场景销毁时调用
|
|
||||||
* Called when a scene is being destroyed
|
|
||||||
*
|
|
||||||
* @param scene 要卸载资源的场景 / The scene to unload resources for
|
|
||||||
*/
|
|
||||||
async unloadSceneResources(_scene: Scene): Promise<void> {
|
|
||||||
// TODO: 实现资源卸载 / Implement resource unloading
|
|
||||||
// 需要跟踪资源引用计数,仅在不再使用时卸载
|
|
||||||
// Need to track resource reference counts and only unload when no longer used
|
|
||||||
console.log('[SceneResourceManager] Scene resource unloading not yet implemented');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,408 +0,0 @@
|
|||||||
/**
|
|
||||||
* Core asset system types and enums
|
|
||||||
* 核心资产系统类型和枚举
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unique identifier for assets across the project
|
|
||||||
* 项目中资产的唯一标识符
|
|
||||||
*/
|
|
||||||
export type AssetGUID = string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runtime asset handle for efficient access
|
|
||||||
* 运行时资产句柄,用于高效访问
|
|
||||||
*/
|
|
||||||
export type AssetHandle = number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset loading state
|
|
||||||
* 资产加载状态
|
|
||||||
*/
|
|
||||||
export enum AssetState {
|
|
||||||
/** 未加载 */
|
|
||||||
Unloaded = 'unloaded',
|
|
||||||
/** 加载中 */
|
|
||||||
Loading = 'loading',
|
|
||||||
/** 已加载 */
|
|
||||||
Loaded = 'loaded',
|
|
||||||
/** 加载失败 */
|
|
||||||
Failed = 'failed',
|
|
||||||
/** 释放中 */
|
|
||||||
Disposing = 'disposing'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset types supported by the system
|
|
||||||
* 系统支持的资产类型
|
|
||||||
*/
|
|
||||||
export enum AssetType {
|
|
||||||
/** 纹理 */
|
|
||||||
Texture = 'texture',
|
|
||||||
/** 网格 */
|
|
||||||
Mesh = 'mesh',
|
|
||||||
/** 材质 */
|
|
||||||
Material = 'material',
|
|
||||||
/** 着色器 */
|
|
||||||
Shader = 'shader',
|
|
||||||
/** 音频 */
|
|
||||||
Audio = 'audio',
|
|
||||||
/** 字体 */
|
|
||||||
Font = 'font',
|
|
||||||
/** 预制体 */
|
|
||||||
Prefab = 'prefab',
|
|
||||||
/** 场景 */
|
|
||||||
Scene = 'scene',
|
|
||||||
/** 脚本 */
|
|
||||||
Script = 'script',
|
|
||||||
/** 动画片段 */
|
|
||||||
AnimationClip = 'animation',
|
|
||||||
/** 行为树 */
|
|
||||||
BehaviorTree = 'behaviortree',
|
|
||||||
/** 瓦片地图 */
|
|
||||||
Tilemap = 'tilemap',
|
|
||||||
/** 瓦片集 */
|
|
||||||
Tileset = 'tileset',
|
|
||||||
/** JSON数据 */
|
|
||||||
Json = 'json',
|
|
||||||
/** 文本 */
|
|
||||||
Text = 'text',
|
|
||||||
/** 二进制 */
|
|
||||||
Binary = 'binary',
|
|
||||||
/** 自定义 */
|
|
||||||
Custom = 'custom'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Platform variants for assets
|
|
||||||
* 资产的平台变体
|
|
||||||
*/
|
|
||||||
export enum AssetPlatform {
|
|
||||||
/** H5平台(浏览器) */
|
|
||||||
H5 = 'h5',
|
|
||||||
/** 微信小游戏 */
|
|
||||||
WeChat = 'wechat',
|
|
||||||
/** 试玩广告(Playable Ads) */
|
|
||||||
Playable = 'playable',
|
|
||||||
/** Android平台 */
|
|
||||||
Android = 'android',
|
|
||||||
/** iOS平台 */
|
|
||||||
iOS = 'ios',
|
|
||||||
/** 编辑器(Tauri桌面) */
|
|
||||||
Editor = 'editor'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Quality levels for asset variants
|
|
||||||
* 资产变体的质量级别
|
|
||||||
*/
|
|
||||||
export enum AssetQuality {
|
|
||||||
/** 低质量 */
|
|
||||||
Low = 'low',
|
|
||||||
/** 中等质量 */
|
|
||||||
Medium = 'medium',
|
|
||||||
/** 高质量 */
|
|
||||||
High = 'high',
|
|
||||||
/** 超高质量 */
|
|
||||||
Ultra = 'ultra'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset metadata stored in the database
|
|
||||||
* 存储在数据库中的资产元数据
|
|
||||||
*/
|
|
||||||
export interface IAssetMetadata {
|
|
||||||
/** 全局唯一标识符 */
|
|
||||||
guid: AssetGUID;
|
|
||||||
/** 资产路径 */
|
|
||||||
path: string;
|
|
||||||
/** 资产类型 */
|
|
||||||
type: AssetType;
|
|
||||||
/** 资产名称 */
|
|
||||||
name: string;
|
|
||||||
/** 文件大小(字节) / File size in bytes */
|
|
||||||
size: number;
|
|
||||||
/** 内容哈希值 / Content hash for versioning */
|
|
||||||
hash: string;
|
|
||||||
/** 依赖的其他资产 / Dependencies on other assets */
|
|
||||||
dependencies: AssetGUID[];
|
|
||||||
/** 资产标签 / User-defined labels for categorization */
|
|
||||||
labels: string[];
|
|
||||||
/** 自定义标签 / Custom metadata tags */
|
|
||||||
tags: Map<string, string>;
|
|
||||||
/** 导入设置 / Import-time settings */
|
|
||||||
importSettings?: Record<string, unknown>;
|
|
||||||
/** 最后修改时间 / Unix timestamp of last modification */
|
|
||||||
lastModified: number;
|
|
||||||
/** 版本号 / Asset version number */
|
|
||||||
version: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset variant descriptor
|
|
||||||
* 资产变体描述符
|
|
||||||
*/
|
|
||||||
export interface IAssetVariant {
|
|
||||||
/** 目标平台 */
|
|
||||||
platform: AssetPlatform;
|
|
||||||
/** 质量级别 */
|
|
||||||
quality: AssetQuality;
|
|
||||||
/** 本地化语言 / Language code for localized assets */
|
|
||||||
locale?: string;
|
|
||||||
/** 主题变体 / Theme identifier (e.g., 'dark', 'light') */
|
|
||||||
theme?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset load options
|
|
||||||
* 资产加载选项
|
|
||||||
*/
|
|
||||||
export interface IAssetLoadOptions {
|
|
||||||
/** 加载优先级(0-100,越高越优先) / Priority level 0-100, higher loads first */
|
|
||||||
priority?: number;
|
|
||||||
/** 是否异步加载 / Use async loading */
|
|
||||||
async?: boolean;
|
|
||||||
/** 指定加载的变体 / Specific variant to load */
|
|
||||||
variant?: IAssetVariant;
|
|
||||||
/** 强制重新加载 / Force reload even if cached */
|
|
||||||
forceReload?: boolean;
|
|
||||||
/** 超时时间(毫秒) / Timeout in milliseconds */
|
|
||||||
timeout?: number;
|
|
||||||
/** 进度回调 / Progress callback (0-1) */
|
|
||||||
onProgress?: (progress: number) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset bundle manifest
|
|
||||||
* 资产包清单
|
|
||||||
*/
|
|
||||||
export interface IAssetBundleManifest {
|
|
||||||
/** 包名称 */
|
|
||||||
name: string;
|
|
||||||
/** 版本号 */
|
|
||||||
version: string;
|
|
||||||
/** 内容哈希 / Content hash for integrity check */
|
|
||||||
hash: string;
|
|
||||||
/** 压缩类型 */
|
|
||||||
compression?: 'none' | 'gzip' | 'brotli';
|
|
||||||
/** 包含的资产列表 / Assets contained in this bundle */
|
|
||||||
assets: AssetGUID[];
|
|
||||||
/** 依赖的其他包 / Other bundles this depends on */
|
|
||||||
dependencies: string[];
|
|
||||||
/** 包大小(字节) / Bundle size in bytes */
|
|
||||||
size: number;
|
|
||||||
/** 创建时间戳 / Creation timestamp */
|
|
||||||
createdAt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset loading result
|
|
||||||
* 资产加载结果
|
|
||||||
*/
|
|
||||||
export interface IAssetLoadResult<T = unknown> {
|
|
||||||
/** 加载的资产实例 */
|
|
||||||
asset: T;
|
|
||||||
/** 资产句柄 */
|
|
||||||
handle: AssetHandle;
|
|
||||||
/** 资产元数据 */
|
|
||||||
metadata: IAssetMetadata;
|
|
||||||
/** 加载耗时(毫秒) / Load time in milliseconds */
|
|
||||||
loadTime: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset loading error
|
|
||||||
* 资产加载错误
|
|
||||||
*/
|
|
||||||
export class AssetLoadError extends Error {
|
|
||||||
constructor(
|
|
||||||
message: string,
|
|
||||||
public readonly guid: AssetGUID,
|
|
||||||
public readonly type: AssetType,
|
|
||||||
public readonly cause?: Error
|
|
||||||
) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'AssetLoadError';
|
|
||||||
Object.setPrototypeOf(this, new.target.prototype);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory method for file not found error
|
|
||||||
* 文件未找到错误的工厂方法
|
|
||||||
*/
|
|
||||||
static fileNotFound(guid: AssetGUID, path: string): AssetLoadError {
|
|
||||||
return new AssetLoadError(`Asset file not found: ${path}`, guid, AssetType.Custom);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory method for unsupported type error
|
|
||||||
* 不支持的类型错误的工厂方法
|
|
||||||
*/
|
|
||||||
static unsupportedType(guid: AssetGUID, type: AssetType): AssetLoadError {
|
|
||||||
return new AssetLoadError(`Unsupported asset type: ${type}`, guid, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory method for load timeout error
|
|
||||||
* 加载超时错误的工厂方法
|
|
||||||
*/
|
|
||||||
static loadTimeout(guid: AssetGUID, type: AssetType, timeout: number): AssetLoadError {
|
|
||||||
return new AssetLoadError(`Asset load timeout after ${timeout}ms`, guid, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory method for dependency failed error
|
|
||||||
* 依赖加载失败错误的工厂方法
|
|
||||||
*/
|
|
||||||
static dependencyFailed(guid: AssetGUID, type: AssetType, depGuid: AssetGUID): AssetLoadError {
|
|
||||||
return new AssetLoadError(`Dependency failed to load: ${depGuid}`, guid, type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset reference counting info
|
|
||||||
* 资产引用计数信息
|
|
||||||
*/
|
|
||||||
export interface IAssetReferenceInfo {
|
|
||||||
/** 资产GUID */
|
|
||||||
guid: AssetGUID;
|
|
||||||
/** 资产句柄 */
|
|
||||||
handle: AssetHandle;
|
|
||||||
/** 引用计数 */
|
|
||||||
referenceCount: number;
|
|
||||||
/** 最后访问时间 / Unix timestamp of last access */
|
|
||||||
lastAccessTime: number;
|
|
||||||
/** 当前状态 */
|
|
||||||
state: AssetState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset import options
|
|
||||||
* 资产导入选项
|
|
||||||
*/
|
|
||||||
export interface IAssetImportOptions {
|
|
||||||
/** 资产类型 */
|
|
||||||
type: AssetType;
|
|
||||||
/** 生成Mipmap / Generate mipmaps for textures */
|
|
||||||
generateMipmaps?: boolean;
|
|
||||||
/** 纹理压缩格式 / Texture compression format */
|
|
||||||
compression?: 'none' | 'dxt' | 'etc2' | 'astc';
|
|
||||||
/** 最大纹理尺寸 / Maximum texture dimension */
|
|
||||||
maxTextureSize?: number;
|
|
||||||
/** 生成LOD / Generate LODs for meshes */
|
|
||||||
generateLODs?: boolean;
|
|
||||||
/** 优化网格 / Optimize mesh geometry */
|
|
||||||
optimizeMesh?: boolean;
|
|
||||||
/** 音频格式 / Audio encoding format */
|
|
||||||
audioFormat?: 'mp3' | 'ogg' | 'wav';
|
|
||||||
/** 自定义处理器 / Custom processor plugin name */
|
|
||||||
customProcessor?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset usage statistics
|
|
||||||
* 资产使用统计
|
|
||||||
*/
|
|
||||||
export interface IAssetUsageStats {
|
|
||||||
/** 资产GUID */
|
|
||||||
guid: AssetGUID;
|
|
||||||
/** 加载次数 */
|
|
||||||
loadCount: number;
|
|
||||||
/** 总加载时间(毫秒) / Total time spent loading in ms */
|
|
||||||
totalLoadTime: number;
|
|
||||||
/** 平均加载时间(毫秒) / Average load time in ms */
|
|
||||||
averageLoadTime: number;
|
|
||||||
/** 最后使用时间 / Unix timestamp of last use */
|
|
||||||
lastUsedTime: number;
|
|
||||||
/** 被引用的资产列表 / Assets that reference this one */
|
|
||||||
referencedBy: AssetGUID[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset preload group
|
|
||||||
* 资产预加载组
|
|
||||||
*/
|
|
||||||
export interface IAssetPreloadGroup {
|
|
||||||
/** 组名称 */
|
|
||||||
name: string;
|
|
||||||
/** 包含的资产 */
|
|
||||||
assets: AssetGUID[];
|
|
||||||
/** 加载优先级 / Load priority 0-100 */
|
|
||||||
priority: number;
|
|
||||||
/** 是否必需 / Must be loaded before scene start */
|
|
||||||
required: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset loading progress info
|
|
||||||
* 资产加载进度信息
|
|
||||||
*/
|
|
||||||
export interface IAssetLoadProgress {
|
|
||||||
/** 当前加载的资产 */
|
|
||||||
currentAsset: string;
|
|
||||||
/** 已加载数量 */
|
|
||||||
loadedCount: number;
|
|
||||||
/** 总数量 */
|
|
||||||
totalCount: number;
|
|
||||||
/** 已加载字节数 */
|
|
||||||
loadedBytes: number;
|
|
||||||
/** 总字节数 */
|
|
||||||
totalBytes: number;
|
|
||||||
/** 进度百分比(0-1) / Progress value 0-1 */
|
|
||||||
progress: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset catalog entry for runtime lookups
|
|
||||||
* 运行时查找的资产目录条目
|
|
||||||
*/
|
|
||||||
export interface IAssetCatalogEntry {
|
|
||||||
/** 资产GUID */
|
|
||||||
guid: AssetGUID;
|
|
||||||
/** 资产路径 */
|
|
||||||
path: string;
|
|
||||||
/** 资产类型 */
|
|
||||||
type: AssetType;
|
|
||||||
/** 所在包名称 / Bundle containing this asset */
|
|
||||||
bundleName?: string;
|
|
||||||
/** 可用变体 / Available variants */
|
|
||||||
variants?: IAssetVariant[];
|
|
||||||
/** 大小(字节) / Size in bytes */
|
|
||||||
size: number;
|
|
||||||
/** 内容哈希 / Content hash */
|
|
||||||
hash: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runtime asset catalog
|
|
||||||
* 运行时资产目录
|
|
||||||
*/
|
|
||||||
export interface IAssetCatalog {
|
|
||||||
/** 版本号 */
|
|
||||||
version: string;
|
|
||||||
/** 创建时间戳 / Creation timestamp */
|
|
||||||
createdAt: number;
|
|
||||||
/** 所有目录条目 / All catalog entries */
|
|
||||||
entries: Map<AssetGUID, IAssetCatalogEntry>;
|
|
||||||
/** 此目录中的包 / Bundles in this catalog */
|
|
||||||
bundles: Map<string, IAssetBundleManifest>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asset hot-reload event
|
|
||||||
* 资产热重载事件
|
|
||||||
*/
|
|
||||||
export interface IAssetHotReloadEvent {
|
|
||||||
/** 资产GUID */
|
|
||||||
guid: AssetGUID;
|
|
||||||
/** 资产路径 */
|
|
||||||
path: string;
|
|
||||||
/** 资产类型 */
|
|
||||||
type: AssetType;
|
|
||||||
/** 旧版本哈希 / Previous version hash */
|
|
||||||
oldHash: string;
|
|
||||||
/** 新版本哈希 / New version hash */
|
|
||||||
newHash: string;
|
|
||||||
/** 时间戳 */
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
/**
|
|
||||||
* Path Validator
|
|
||||||
* 路径验证器
|
|
||||||
*
|
|
||||||
* Validates and sanitizes asset paths for security
|
|
||||||
* 验证并清理资产路径以确保安全
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class PathValidator {
|
|
||||||
// Dangerous path patterns
|
|
||||||
private static readonly DANGEROUS_PATTERNS = [
|
|
||||||
/\.\.[/\\]/g, // Path traversal attempts (..)
|
|
||||||
/^[/\\]/, // Absolute paths on Unix
|
|
||||||
/^[a-zA-Z]:[/\\]/, // Absolute paths on Windows
|
|
||||||
/[<>:"|?*]/, // Invalid characters for Windows paths
|
|
||||||
/\0/, // Null bytes
|
|
||||||
/%00/, // URL encoded null bytes
|
|
||||||
/\.\.%2[fF]/ // URL encoded path traversal
|
|
||||||
];
|
|
||||||
|
|
||||||
// Valid path characters (alphanumeric, dash, underscore, dot, slash)
|
|
||||||
private static readonly VALID_PATH_REGEX = /^[a-zA-Z0-9\-_./\\@]+$/;
|
|
||||||
|
|
||||||
// Maximum path length
|
|
||||||
private static readonly MAX_PATH_LENGTH = 260;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate if a path is safe
|
|
||||||
* 验证路径是否安全
|
|
||||||
*/
|
|
||||||
static validate(path: string): { valid: boolean; reason?: string } {
|
|
||||||
// Check for null/undefined/empty
|
|
||||||
if (!path || typeof path !== 'string') {
|
|
||||||
return { valid: false, reason: 'Path is empty or invalid' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check length
|
|
||||||
if (path.length > this.MAX_PATH_LENGTH) {
|
|
||||||
return { valid: false, reason: `Path exceeds maximum length of ${this.MAX_PATH_LENGTH} characters` };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for dangerous patterns
|
|
||||||
for (const pattern of this.DANGEROUS_PATTERNS) {
|
|
||||||
if (pattern.test(path)) {
|
|
||||||
return { valid: false, reason: 'Path contains dangerous pattern' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for valid characters
|
|
||||||
if (!this.VALID_PATH_REGEX.test(path)) {
|
|
||||||
return { valid: false, reason: 'Path contains invalid characters' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { valid: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitize a path
|
|
||||||
* 清理路径
|
|
||||||
*/
|
|
||||||
static sanitize(path: string): string {
|
|
||||||
if (!path || typeof path !== 'string') {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove dangerous patterns
|
|
||||||
let sanitized = path;
|
|
||||||
|
|
||||||
// Remove path traversal (apply repeatedly until fully removed)
|
|
||||||
let prev;
|
|
||||||
do {
|
|
||||||
prev = sanitized;
|
|
||||||
sanitized = sanitized.replace(/\.\.[/\\]/g, '');
|
|
||||||
} while (sanitized !== prev);
|
|
||||||
|
|
||||||
// Remove leading slashes
|
|
||||||
sanitized = sanitized.replace(/^[/\\]+/, '');
|
|
||||||
|
|
||||||
// Remove null bytes
|
|
||||||
sanitized = sanitized.replace(/\0/g, '');
|
|
||||||
sanitized = sanitized.replace(/%00/g, '');
|
|
||||||
|
|
||||||
// Remove invalid Windows characters
|
|
||||||
sanitized = sanitized.replace(/[<>:"|?*]/g, '_');
|
|
||||||
|
|
||||||
// Normalize slashes
|
|
||||||
sanitized = sanitized.replace(/\\/g, '/');
|
|
||||||
|
|
||||||
// Remove double slashes
|
|
||||||
sanitized = sanitized.replace(/\/+/g, '/');
|
|
||||||
|
|
||||||
// Trim whitespace
|
|
||||||
sanitized = sanitized.trim();
|
|
||||||
|
|
||||||
// Truncate if too long
|
|
||||||
if (sanitized.length > this.MAX_PATH_LENGTH) {
|
|
||||||
sanitized = sanitized.substring(0, this.MAX_PATH_LENGTH);
|
|
||||||
}
|
|
||||||
|
|
||||||
return sanitized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if path is trying to escape the base directory
|
|
||||||
* 检查路径是否试图逃离基础目录
|
|
||||||
*/
|
|
||||||
static isPathTraversal(path: string): boolean {
|
|
||||||
const normalized = path.replace(/\\/g, '/');
|
|
||||||
return normalized.includes('../') || normalized.includes('..\\');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize a path for consistent handling
|
|
||||||
* 规范化路径以便一致处理
|
|
||||||
*/
|
|
||||||
static normalize(path: string): string {
|
|
||||||
if (!path) return '';
|
|
||||||
|
|
||||||
// Sanitize first
|
|
||||||
let normalized = this.sanitize(path);
|
|
||||||
|
|
||||||
// Convert backslashes to forward slashes
|
|
||||||
normalized = normalized.replace(/\\/g, '/');
|
|
||||||
|
|
||||||
// Remove trailing slash (except for root)
|
|
||||||
if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
||||||
normalized = normalized.slice(0, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Join path segments safely
|
|
||||||
* 安全地连接路径段
|
|
||||||
*/
|
|
||||||
static join(...segments: string[]): string {
|
|
||||||
const validSegments = segments
|
|
||||||
.filter((s) => s && typeof s === 'string')
|
|
||||||
.map((s) => this.sanitize(s))
|
|
||||||
.filter((s) => s.length > 0);
|
|
||||||
|
|
||||||
if (validSegments.length === 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.normalize(validSegments.join('/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get file extension safely
|
|
||||||
* 安全地获取文件扩展名
|
|
||||||
*/
|
|
||||||
static getExtension(path: string): string {
|
|
||||||
const sanitized = this.sanitize(path);
|
|
||||||
const lastDot = sanitized.lastIndexOf('.');
|
|
||||||
const lastSlash = sanitized.lastIndexOf('/');
|
|
||||||
|
|
||||||
if (lastDot > lastSlash && lastDot > 0) {
|
|
||||||
return sanitized.substring(lastDot + 1).toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
/**
|
|
||||||
* UV Coordinate Helper
|
|
||||||
* UV 坐标辅助工具
|
|
||||||
*
|
|
||||||
* 引擎使用图像坐标系:
|
|
||||||
* Engine uses image coordinate system:
|
|
||||||
* - 原点 (0, 0) 在左上角 | Origin at top-left
|
|
||||||
* - V 轴向下增长 | V-axis increases downward
|
|
||||||
* - UV 格式:[u0, v0, u1, v1] 其中 v0 < v1
|
|
||||||
*/
|
|
||||||
export class UVHelper {
|
|
||||||
/**
|
|
||||||
* Calculate UV coordinates for a texture region
|
|
||||||
* 计算纹理区域的 UV 坐标
|
|
||||||
*/
|
|
||||||
static calculateUV(
|
|
||||||
imageRect: { x: number; y: number; width: number; height: number },
|
|
||||||
textureSize: { width: number; height: number }
|
|
||||||
): [number, number, number, number] {
|
|
||||||
const { x, y, width, height } = imageRect;
|
|
||||||
const { width: tw, height: th } = textureSize;
|
|
||||||
|
|
||||||
return [
|
|
||||||
x / tw, // u0
|
|
||||||
y / th, // v0
|
|
||||||
(x + width) / tw, // u1
|
|
||||||
(y + height) / th // v1
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate UV coordinates for a tile in a tileset
|
|
||||||
* 计算 tileset 中某个 tile 的 UV 坐标
|
|
||||||
*/
|
|
||||||
static calculateTileUV(
|
|
||||||
tileIndex: number,
|
|
||||||
tilesetInfo: {
|
|
||||||
columns: number;
|
|
||||||
tileWidth: number;
|
|
||||||
tileHeight: number;
|
|
||||||
imageWidth: number;
|
|
||||||
imageHeight: number;
|
|
||||||
margin?: number;
|
|
||||||
spacing?: number;
|
|
||||||
}
|
|
||||||
): [number, number, number, number] | null {
|
|
||||||
if (tileIndex < 0) return null;
|
|
||||||
|
|
||||||
const {
|
|
||||||
columns,
|
|
||||||
tileWidth,
|
|
||||||
tileHeight,
|
|
||||||
imageWidth,
|
|
||||||
imageHeight,
|
|
||||||
margin = 0,
|
|
||||||
spacing = 0
|
|
||||||
} = tilesetInfo;
|
|
||||||
|
|
||||||
const col = tileIndex % columns;
|
|
||||||
const row = Math.floor(tileIndex / columns);
|
|
||||||
const x = margin + col * (tileWidth + spacing);
|
|
||||||
const y = margin + row * (tileHeight + spacing);
|
|
||||||
|
|
||||||
return this.calculateUV(
|
|
||||||
{ x, y, width: tileWidth, height: tileHeight },
|
|
||||||
{ width: imageWidth, height: imageHeight }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static validateUV(uv: [number, number, number, number]): boolean {
|
|
||||||
const [u0, v0, u1, v1] = uv;
|
|
||||||
return u0 >= 0 && u0 <= 1 && u1 >= 0 && u1 <= 1 &&
|
|
||||||
v0 >= 0 && v0 <= 1 && v1 >= 0 && v1 <= 1 &&
|
|
||||||
u0 < u1 && v0 < v1;
|
|
||||||
}
|
|
||||||
|
|
||||||
static debugPrint(uv: [number, number, number, number], label?: string): void {
|
|
||||||
const prefix = label ? `[${label}] ` : '';
|
|
||||||
console.log(`${prefix}UV: [${uv.map(n => n.toFixed(4)).join(', ')}]`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"module": "ESNext",
|
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": false,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"noImplicitAny": true,
|
|
||||||
"strictNullChecks": true,
|
|
||||||
"strictFunctionTypes": true,
|
|
||||||
"strictBindCallApply": true,
|
|
||||||
"strictPropertyInitialization": true,
|
|
||||||
"noImplicitThis": true,
|
|
||||||
"alwaysStrict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"allowSyntheticDefaultImports": true
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,21 @@
|
|||||||
{
|
{
|
||||||
"name": "@esengine/behavior-tree",
|
"name": "@esengine/behavior-tree",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "ECS-based AI behavior tree system with visual editor and runtime execution",
|
"description": "完全ECS化的行为树系统,基于组件和实体的行为树实现",
|
||||||
"main": "dist/index.js",
|
"main": "bin/index.js",
|
||||||
"module": "dist/index.js",
|
"types": "bin/index.d.ts",
|
||||||
"types": "dist/index.d.ts",
|
|
||||||
"type": "module",
|
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./bin/index.d.ts",
|
||||||
"import": "./dist/index.js"
|
"import": "./bin/index.js",
|
||||||
},
|
"development": {
|
||||||
"./runtime": {
|
"types": "./src/index.ts",
|
||||||
"types": "./dist/runtime.d.ts",
|
"import": "./src/index.ts"
|
||||||
"import": "./dist/runtime.js"
|
}
|
||||||
},
|
}
|
||||||
"./editor": {
|
|
||||||
"types": "./dist/editor/index.d.ts",
|
|
||||||
"import": "./dist/editor/index.js"
|
|
||||||
},
|
|
||||||
"./plugin.json": "./plugin.json"
|
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"bin/**/*"
|
||||||
"plugin.json"
|
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ecs",
|
"ecs",
|
||||||
@@ -33,52 +25,38 @@
|
|||||||
"entity-component-system"
|
"entity-component-system"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||||
"build": "vite build",
|
"build:ts": "tsc",
|
||||||
"build:watch": "vite build --watch",
|
"prebuild": "npm run clean",
|
||||||
"type-check": "tsc --noEmit",
|
"build": "npm run build:ts",
|
||||||
|
"build:watch": "tsc --watch",
|
||||||
|
"rebuild": "npm run clean && npm run build",
|
||||||
|
"build:npm": "npm run build && node build-rollup.cjs",
|
||||||
"test": "jest --config jest.config.cjs",
|
"test": "jest --config jest.config.cjs",
|
||||||
"test:watch": "jest --watch --config jest.config.cjs"
|
"test:watch": "jest --watch --config jest.config.cjs"
|
||||||
},
|
},
|
||||||
"author": "yhh",
|
"author": "yhh",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@esengine/ecs-framework": ">=2.0.0",
|
"@esengine/ecs-framework": "^2.2.8"
|
||||||
"@esengine/ecs-components": "workspace:*",
|
|
||||||
"@esengine/editor-runtime": "workspace:*",
|
|
||||||
"lucide-react": "^0.545.0",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"zustand": "^4.5.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@esengine/ecs-components": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@esengine/editor-runtime": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"lucide-react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"zustand": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/plugin-fs": "^2.4.2",
|
"@babel/core": "^7.28.3",
|
||||||
|
"@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1",
|
||||||
|
"@babel/plugin-transform-optional-chaining": "^7.27.1",
|
||||||
|
"@babel/preset-env": "^7.28.3",
|
||||||
|
"@rollup/plugin-babel": "^6.0.4",
|
||||||
|
"@rollup/plugin-commonjs": "^28.0.3",
|
||||||
|
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||||
|
"@rollup/plugin-terser": "^0.4.4",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^20.19.17",
|
"@types/node": "^20.19.17",
|
||||||
"@types/react": "^18.3.12",
|
|
||||||
"@vitejs/plugin-react": "^4.7.0",
|
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"rimraf": "^5.0.0",
|
"rimraf": "^5.0.0",
|
||||||
|
"rollup": "^4.42.0",
|
||||||
|
"rollup-plugin-dts": "^6.2.1",
|
||||||
"ts-jest": "^29.4.0",
|
"ts-jest": "^29.4.0",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3"
|
||||||
"vite": "^6.0.7",
|
|
||||||
"vite-plugin-dts": "^3.7.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.8.1"
|
"tslib": "^2.8.1"
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "@esengine/behavior-tree",
|
|
||||||
"name": "Behavior Tree System",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "AI behavior tree system with visual editor and runtime execution",
|
|
||||||
"category": "ai",
|
|
||||||
"loadingPhase": "default",
|
|
||||||
"enabledByDefault": true,
|
|
||||||
"canContainContent": false,
|
|
||||||
"isEnginePlugin": false,
|
|
||||||
"modules": [
|
|
||||||
{
|
|
||||||
"name": "BehaviorTreeRuntime",
|
|
||||||
"type": "runtime",
|
|
||||||
"entry": "./src/index.ts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "BehaviorTreeEditor",
|
|
||||||
"type": "editor",
|
|
||||||
"entry": "./src/editor/index.ts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"dependencies": [
|
|
||||||
{
|
|
||||||
"id": "@esengine/core",
|
|
||||||
"version": ">=1.0.0"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"icon": "GitBranch"
|
|
||||||
}
|
|
||||||
4312
packages/behavior-tree/pnpm-lock.yaml
generated
4312
packages/behavior-tree/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
import { BehaviorTreeData, BehaviorNodeData } from './execution/BehaviorTreeData';
|
import { BehaviorTreeData, BehaviorNodeData } from './Runtime/BehaviorTreeData';
|
||||||
import { NodeType } from './Types/TaskStatus';
|
import { NodeType } from './Types/TaskStatus';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
89
packages/behavior-tree/src/BehaviorTreePlugin.ts
Normal file
89
packages/behavior-tree/src/BehaviorTreePlugin.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import type { Core } from '@esengine/ecs-framework';
|
||||||
|
import type { ServiceContainer, IPlugin, IScene } from '@esengine/ecs-framework';
|
||||||
|
import { WorldManager } from '@esengine/ecs-framework';
|
||||||
|
import { BehaviorTreeExecutionSystem } from './Runtime/BehaviorTreeExecutionSystem';
|
||||||
|
import { GlobalBlackboardService } from './Services/GlobalBlackboardService';
|
||||||
|
import { BehaviorTreeAssetManager } from './Runtime/BehaviorTreeAssetManager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 行为树插件
|
||||||
|
*
|
||||||
|
* 提供便捷方法向场景添加行为树系统
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const core = Core.create();
|
||||||
|
* const plugin = new BehaviorTreePlugin();
|
||||||
|
* await core.pluginManager.install(plugin);
|
||||||
|
*
|
||||||
|
* // 为场景添加行为树系统
|
||||||
|
* const scene = new Scene();
|
||||||
|
* plugin.setupScene(scene);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class BehaviorTreePlugin implements IPlugin {
|
||||||
|
readonly name = '@esengine/behavior-tree';
|
||||||
|
readonly version = '1.0.0';
|
||||||
|
|
||||||
|
private worldManager: WorldManager | null = null;
|
||||||
|
private services: ServiceContainer | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装插件
|
||||||
|
*/
|
||||||
|
async install(_core: Core, services: ServiceContainer): Promise<void> {
|
||||||
|
this.services = services;
|
||||||
|
|
||||||
|
// 注册全局服务
|
||||||
|
services.registerSingleton(GlobalBlackboardService);
|
||||||
|
services.registerSingleton(BehaviorTreeAssetManager);
|
||||||
|
|
||||||
|
this.worldManager = services.resolve(WorldManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 卸载插件
|
||||||
|
*/
|
||||||
|
async uninstall(): Promise<void> {
|
||||||
|
if (this.services) {
|
||||||
|
this.services.unregister(GlobalBlackboardService);
|
||||||
|
this.services.unregister(BehaviorTreeAssetManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.worldManager = null;
|
||||||
|
this.services = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为场景设置行为树系统
|
||||||
|
*
|
||||||
|
* 向场景添加行为树执行系统
|
||||||
|
*
|
||||||
|
* @param scene 目标场景
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const scene = new Scene();
|
||||||
|
* behaviorTreePlugin.setupScene(scene);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public setupScene(scene: IScene): void {
|
||||||
|
scene.addSystem(new BehaviorTreeExecutionSystem());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为所有现有场景设置行为树系统
|
||||||
|
*/
|
||||||
|
public setupAllScenes(): void {
|
||||||
|
if (!this.worldManager) {
|
||||||
|
throw new Error('Plugin not installed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const worlds = this.worldManager.getAllWorlds();
|
||||||
|
for (const world of worlds) {
|
||||||
|
for (const scene of world.getAllScenes()) {
|
||||||
|
this.setupScene(scene);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* Behavior Tree Runtime Module (Pure runtime, no editor dependencies)
|
|
||||||
* 行为树运行时模块(纯运行时,无编辑器依赖)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { IScene, ServiceContainer } from '@esengine/ecs-framework';
|
|
||||||
import { ComponentRegistry, Core } from '@esengine/ecs-framework';
|
|
||||||
import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components';
|
|
||||||
|
|
||||||
import { BehaviorTreeRuntimeComponent } from './execution/BehaviorTreeRuntimeComponent';
|
|
||||||
import { BehaviorTreeExecutionSystem } from './execution/BehaviorTreeExecutionSystem';
|
|
||||||
import { BehaviorTreeAssetManager } from './execution/BehaviorTreeAssetManager';
|
|
||||||
import { GlobalBlackboardService } from './Services/GlobalBlackboardService';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Behavior Tree Runtime Module
|
|
||||||
* 行为树运行时模块
|
|
||||||
*/
|
|
||||||
export class BehaviorTreeRuntimeModule implements IRuntimeModuleLoader {
|
|
||||||
registerComponents(registry: typeof ComponentRegistry): 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 {
|
|
||||||
const behaviorTreeSystem = new BehaviorTreeExecutionSystem(Core);
|
|
||||||
|
|
||||||
if (context.isEditor) {
|
|
||||||
behaviorTreeSystem.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.addSystem(behaviorTreeSystem);
|
|
||||||
context.behaviorTreeSystem = behaviorTreeSystem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Entity, Core } from '@esengine/ecs-framework';
|
import { Entity, Core } from '@esengine/ecs-framework';
|
||||||
import { BehaviorTreeData } from './execution/BehaviorTreeData';
|
import { BehaviorTreeData } from './Runtime/BehaviorTreeData';
|
||||||
import { BehaviorTreeRuntimeComponent } from './execution/BehaviorTreeRuntimeComponent';
|
import { BehaviorTreeRuntimeComponent } from './Runtime/BehaviorTreeRuntimeComponent';
|
||||||
import { BehaviorTreeAssetManager } from './execution/BehaviorTreeAssetManager';
|
import { BehaviorTreeAssetManager } from './Runtime/BehaviorTreeAssetManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 行为树启动辅助类
|
* 行为树启动辅助类
|
||||||
|
|||||||
@@ -1,248 +0,0 @@
|
|||||||
export enum BlackboardValueType {
|
|
||||||
// 基础类型
|
|
||||||
String = 'string',
|
|
||||||
Number = 'number',
|
|
||||||
Boolean = 'boolean',
|
|
||||||
|
|
||||||
// 数学类型
|
|
||||||
Vector2 = 'vector2',
|
|
||||||
Vector3 = 'vector3',
|
|
||||||
Vector4 = 'vector4',
|
|
||||||
Quaternion = 'quaternion',
|
|
||||||
Color = 'color',
|
|
||||||
|
|
||||||
// 引用类型
|
|
||||||
GameObject = 'gameObject',
|
|
||||||
Transform = 'transform',
|
|
||||||
Component = 'component',
|
|
||||||
AssetReference = 'assetReference',
|
|
||||||
|
|
||||||
// 集合类型
|
|
||||||
Array = 'array',
|
|
||||||
Map = 'map',
|
|
||||||
|
|
||||||
// 高级类型
|
|
||||||
Enum = 'enum',
|
|
||||||
Struct = 'struct',
|
|
||||||
Function = 'function',
|
|
||||||
|
|
||||||
// 游戏特定类型
|
|
||||||
EntityId = 'entityId',
|
|
||||||
NodePath = 'nodePath',
|
|
||||||
ResourcePath = 'resourcePath',
|
|
||||||
AnimationState = 'animationState',
|
|
||||||
AudioClip = 'audioClip',
|
|
||||||
Material = 'material',
|
|
||||||
Texture = 'texture'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Vector2 {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Vector3 extends Vector2 {
|
|
||||||
z: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Vector4 extends Vector3 {
|
|
||||||
w: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Quaternion {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
z: number;
|
|
||||||
w: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Color {
|
|
||||||
r: number;
|
|
||||||
g: number;
|
|
||||||
b: number;
|
|
||||||
a: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BlackboardTypeDefinition {
|
|
||||||
type: BlackboardValueType;
|
|
||||||
displayName: string;
|
|
||||||
category: 'basic' | 'math' | 'reference' | 'collection' | 'advanced' | 'game';
|
|
||||||
defaultValue: any;
|
|
||||||
editorComponent?: string; // 自定义编辑器组件
|
|
||||||
validator?: (value: any) => boolean;
|
|
||||||
converter?: (value: any) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BlackboardTypes: Record<BlackboardValueType, BlackboardTypeDefinition> = {
|
|
||||||
[BlackboardValueType.String]: {
|
|
||||||
type: BlackboardValueType.String,
|
|
||||||
displayName: '字符串',
|
|
||||||
category: 'basic',
|
|
||||||
defaultValue: '',
|
|
||||||
validator: (v) => typeof v === 'string'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Number]: {
|
|
||||||
type: BlackboardValueType.Number,
|
|
||||||
displayName: '数字',
|
|
||||||
category: 'basic',
|
|
||||||
defaultValue: 0,
|
|
||||||
validator: (v) => typeof v === 'number'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Boolean]: {
|
|
||||||
type: BlackboardValueType.Boolean,
|
|
||||||
displayName: '布尔值',
|
|
||||||
category: 'basic',
|
|
||||||
defaultValue: false,
|
|
||||||
validator: (v) => typeof v === 'boolean'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Vector2]: {
|
|
||||||
type: BlackboardValueType.Vector2,
|
|
||||||
displayName: '二维向量',
|
|
||||||
category: 'math',
|
|
||||||
defaultValue: { x: 0, y: 0 },
|
|
||||||
editorComponent: 'Vector2Editor',
|
|
||||||
validator: (v) => v && typeof v.x === 'number' && typeof v.y === 'number'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Vector3]: {
|
|
||||||
type: BlackboardValueType.Vector3,
|
|
||||||
displayName: '三维向量',
|
|
||||||
category: 'math',
|
|
||||||
defaultValue: { x: 0, y: 0, z: 0 },
|
|
||||||
editorComponent: 'Vector3Editor',
|
|
||||||
validator: (v) => v && typeof v.x === 'number' && typeof v.y === 'number' && typeof v.z === 'number'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Color]: {
|
|
||||||
type: BlackboardValueType.Color,
|
|
||||||
displayName: '颜色',
|
|
||||||
category: 'math',
|
|
||||||
defaultValue: { r: 1, g: 1, b: 1, a: 1 },
|
|
||||||
editorComponent: 'ColorEditor',
|
|
||||||
validator: (v) => v && typeof v.r === 'number' && typeof v.g === 'number' && typeof v.b === 'number' && typeof v.a === 'number'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.GameObject]: {
|
|
||||||
type: BlackboardValueType.GameObject,
|
|
||||||
displayName: '游戏对象',
|
|
||||||
category: 'reference',
|
|
||||||
defaultValue: null,
|
|
||||||
editorComponent: 'GameObjectPicker'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Transform]: {
|
|
||||||
type: BlackboardValueType.Transform,
|
|
||||||
displayName: '变换组件',
|
|
||||||
category: 'reference',
|
|
||||||
defaultValue: null,
|
|
||||||
editorComponent: 'ComponentPicker'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.AssetReference]: {
|
|
||||||
type: BlackboardValueType.AssetReference,
|
|
||||||
displayName: '资源引用',
|
|
||||||
category: 'reference',
|
|
||||||
defaultValue: null,
|
|
||||||
editorComponent: 'AssetPicker'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.EntityId]: {
|
|
||||||
type: BlackboardValueType.EntityId,
|
|
||||||
displayName: '实体ID',
|
|
||||||
category: 'game',
|
|
||||||
defaultValue: -1,
|
|
||||||
validator: (v) => typeof v === 'number' && v >= -1
|
|
||||||
},
|
|
||||||
[BlackboardValueType.ResourcePath]: {
|
|
||||||
type: BlackboardValueType.ResourcePath,
|
|
||||||
displayName: '资源路径',
|
|
||||||
category: 'game',
|
|
||||||
defaultValue: '',
|
|
||||||
editorComponent: 'AssetPathPicker'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Array]: {
|
|
||||||
type: BlackboardValueType.Array,
|
|
||||||
displayName: '数组',
|
|
||||||
category: 'collection',
|
|
||||||
defaultValue: [],
|
|
||||||
editorComponent: 'ArrayEditor'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Map]: {
|
|
||||||
type: BlackboardValueType.Map,
|
|
||||||
displayName: '映射表',
|
|
||||||
category: 'collection',
|
|
||||||
defaultValue: {},
|
|
||||||
editorComponent: 'MapEditor'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Enum]: {
|
|
||||||
type: BlackboardValueType.Enum,
|
|
||||||
displayName: '枚举',
|
|
||||||
category: 'advanced',
|
|
||||||
defaultValue: '',
|
|
||||||
editorComponent: 'EnumPicker'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.AnimationState]: {
|
|
||||||
type: BlackboardValueType.AnimationState,
|
|
||||||
displayName: '动画状态',
|
|
||||||
category: 'game',
|
|
||||||
defaultValue: '',
|
|
||||||
editorComponent: 'AnimationStatePicker'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.AudioClip]: {
|
|
||||||
type: BlackboardValueType.AudioClip,
|
|
||||||
displayName: '音频片段',
|
|
||||||
category: 'game',
|
|
||||||
defaultValue: null,
|
|
||||||
editorComponent: 'AudioClipPicker'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Material]: {
|
|
||||||
type: BlackboardValueType.Material,
|
|
||||||
displayName: '材质',
|
|
||||||
category: 'game',
|
|
||||||
defaultValue: null,
|
|
||||||
editorComponent: 'MaterialPicker'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Texture]: {
|
|
||||||
type: BlackboardValueType.Texture,
|
|
||||||
displayName: '纹理',
|
|
||||||
category: 'game',
|
|
||||||
defaultValue: null,
|
|
||||||
editorComponent: 'TexturePicker'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Vector4]: {
|
|
||||||
type: BlackboardValueType.Vector4,
|
|
||||||
displayName: '四维向量',
|
|
||||||
category: 'math',
|
|
||||||
defaultValue: { x: 0, y: 0, z: 0, w: 0 },
|
|
||||||
editorComponent: 'Vector4Editor'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Quaternion]: {
|
|
||||||
type: BlackboardValueType.Quaternion,
|
|
||||||
displayName: '四元数',
|
|
||||||
category: 'math',
|
|
||||||
defaultValue: { x: 0, y: 0, z: 0, w: 1 },
|
|
||||||
editorComponent: 'QuaternionEditor'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Component]: {
|
|
||||||
type: BlackboardValueType.Component,
|
|
||||||
displayName: '组件',
|
|
||||||
category: 'reference',
|
|
||||||
defaultValue: null,
|
|
||||||
editorComponent: 'ComponentPicker'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Struct]: {
|
|
||||||
type: BlackboardValueType.Struct,
|
|
||||||
displayName: '结构体',
|
|
||||||
category: 'advanced',
|
|
||||||
defaultValue: {},
|
|
||||||
editorComponent: 'StructEditor'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.Function]: {
|
|
||||||
type: BlackboardValueType.Function,
|
|
||||||
displayName: '函数',
|
|
||||||
category: 'advanced',
|
|
||||||
defaultValue: null,
|
|
||||||
editorComponent: 'FunctionPicker'
|
|
||||||
},
|
|
||||||
[BlackboardValueType.NodePath]: {
|
|
||||||
type: BlackboardValueType.NodePath,
|
|
||||||
displayName: '节点路径',
|
|
||||||
category: 'game',
|
|
||||||
defaultValue: '',
|
|
||||||
editorComponent: 'NodePathPicker'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { BehaviorTreeData } from './BehaviorTreeData';
|
import { BehaviorTreeData } from './BehaviorTreeData';
|
||||||
import { createLogger, IService } from '@esengine/ecs-framework';
|
import { createLogger, IService } from '@esengine/ecs-framework';
|
||||||
import { EditorToBehaviorTreeDataConverter } from '../Serialization/EditorToBehaviorTreeDataConverter';
|
|
||||||
|
|
||||||
const logger = createLogger('BehaviorTreeAssetManager');
|
const logger = createLogger('BehaviorTreeAssetManager');
|
||||||
|
|
||||||
@@ -36,50 +35,6 @@ export class BehaviorTreeAssetManager implements IService {
|
|||||||
logger.info(`行为树资产已加载: ${asset.name} (${asset.nodes.size}个节点)`);
|
logger.info(`行为树资产已加载: ${asset.name} (${asset.nodes.size}个节点)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 从编辑器 JSON 格式加载行为树资产
|
|
||||||
*
|
|
||||||
* @param json 编辑器导出的 JSON 字符串
|
|
||||||
* @returns 加载的行为树数据
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* const assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
|
||||||
* const jsonContent = await readFile('path/to/tree.btree');
|
|
||||||
* const treeData = assetManager.loadFromEditorJSON(jsonContent);
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
loadFromEditorJSON(json: string): BehaviorTreeData {
|
|
||||||
try {
|
|
||||||
const treeData = EditorToBehaviorTreeDataConverter.fromEditorJSON(json);
|
|
||||||
this.loadAsset(treeData);
|
|
||||||
return treeData;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('从编辑器JSON加载失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量加载多个行为树资产(从编辑器JSON)
|
|
||||||
*
|
|
||||||
* @param jsonDataList JSON字符串列表
|
|
||||||
* @returns 成功加载的资产数量
|
|
||||||
*/
|
|
||||||
loadMultipleFromEditorJSON(jsonDataList: string[]): number {
|
|
||||||
let successCount = 0;
|
|
||||||
for (const json of jsonDataList) {
|
|
||||||
try {
|
|
||||||
this.loadFromEditorJSON(json);
|
|
||||||
successCount++;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('批量加载时出错:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.info(`批量加载完成: ${successCount}/${jsonDataList.length} 个资产`);
|
|
||||||
return successCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取行为树资产
|
* 获取行为树资产
|
||||||
*/
|
*/
|
||||||
@@ -63,9 +63,6 @@ export interface NodeRuntimeState {
|
|||||||
/** 当前执行的子节点索引(复合节点使用) */
|
/** 当前执行的子节点索引(复合节点使用) */
|
||||||
currentChildIndex: number;
|
currentChildIndex: number;
|
||||||
|
|
||||||
/** 执行顺序号(用于调试和可视化) */
|
|
||||||
executionOrder?: number;
|
|
||||||
|
|
||||||
/** 开始执行时间(某些节点需要) */
|
/** 开始执行时间(某些节点需要) */
|
||||||
startTime?: number;
|
startTime?: number;
|
||||||
|
|
||||||
@@ -14,25 +14,16 @@ import './Executors';
|
|||||||
*/
|
*/
|
||||||
@ECSSystem('BehaviorTreeExecution')
|
@ECSSystem('BehaviorTreeExecution')
|
||||||
export class BehaviorTreeExecutionSystem extends EntitySystem {
|
export class BehaviorTreeExecutionSystem extends EntitySystem {
|
||||||
private assetManager: BehaviorTreeAssetManager | null = null;
|
private assetManager: BehaviorTreeAssetManager;
|
||||||
private executorRegistry: NodeExecutorRegistry;
|
private executorRegistry: NodeExecutorRegistry;
|
||||||
private coreInstance: typeof Core | null = null;
|
|
||||||
|
|
||||||
constructor(coreInstance?: typeof Core) {
|
constructor() {
|
||||||
super(Matcher.empty().all(BehaviorTreeRuntimeComponent));
|
super(Matcher.empty().all(BehaviorTreeRuntimeComponent));
|
||||||
this.coreInstance = coreInstance || null;
|
this.assetManager = Core.services.resolve(BehaviorTreeAssetManager);
|
||||||
this.executorRegistry = new NodeExecutorRegistry();
|
this.executorRegistry = new NodeExecutorRegistry();
|
||||||
this.registerBuiltInExecutors();
|
this.registerBuiltInExecutors();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAssetManager(): BehaviorTreeAssetManager {
|
|
||||||
if (!this.assetManager) {
|
|
||||||
const core = this.coreInstance || Core;
|
|
||||||
this.assetManager = core.services.resolve(BehaviorTreeAssetManager);
|
|
||||||
}
|
|
||||||
return this.assetManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册所有执行器(包括内置和插件提供的)
|
* 注册所有执行器(包括内置和插件提供的)
|
||||||
*/
|
*/
|
||||||
@@ -64,7 +55,7 @@ export class BehaviorTreeExecutionSystem extends EntitySystem {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const treeData = this.getAssetManager().getAsset(runtime.treeAssetId);
|
const treeData = this.assetManager.getAsset(runtime.treeAssetId);
|
||||||
if (!treeData) {
|
if (!treeData) {
|
||||||
this.logger.warn(`未找到行为树资产: ${runtime.treeAssetId}`);
|
this.logger.warn(`未找到行为树资产: ${runtime.treeAssetId}`);
|
||||||
continue;
|
continue;
|
||||||
@@ -134,11 +125,6 @@ export class BehaviorTreeExecutionSystem extends EntitySystem {
|
|||||||
runtime.activeNodeIds.add(nodeData.id);
|
runtime.activeNodeIds.add(nodeData.id);
|
||||||
state.isAborted = false;
|
state.isAborted = false;
|
||||||
|
|
||||||
if (state.executionOrder === undefined) {
|
|
||||||
runtime.executionOrderCounter++;
|
|
||||||
state.executionOrder = runtime.executionOrderCounter;
|
|
||||||
}
|
|
||||||
|
|
||||||
const executor = this.executorRegistry.get(nodeData.implementationType);
|
const executor = this.executorRegistry.get(nodeData.implementationType);
|
||||||
if (!executor) {
|
if (!executor) {
|
||||||
this.logger.error(`未找到执行器: ${nodeData.implementationType}`);
|
this.logger.error(`未找到执行器: ${nodeData.implementationType}`);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, ECSComponent, Property } from '@esengine/ecs-framework';
|
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||||
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework';
|
||||||
import { NodeRuntimeState, createDefaultRuntimeState } from './BehaviorTreeData';
|
import { NodeRuntimeState, createDefaultRuntimeState } from './BehaviorTreeData';
|
||||||
import { TaskStatus } from '../Types/TaskStatus';
|
import { TaskStatus } from '../Types/TaskStatus';
|
||||||
@@ -30,14 +30,12 @@ export class BehaviorTreeRuntimeComponent extends Component {
|
|||||||
* 引用的行为树资产ID(可序列化)
|
* 引用的行为树资产ID(可序列化)
|
||||||
*/
|
*/
|
||||||
@Serialize()
|
@Serialize()
|
||||||
@Property({ type: 'asset', label: 'Behavior Tree', extensions: ['.btree'] })
|
|
||||||
treeAssetId: string = '';
|
treeAssetId: string = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否自动启动
|
* 是否自动启动
|
||||||
*/
|
*/
|
||||||
@Serialize()
|
@Serialize()
|
||||||
@Property({ type: 'boolean', label: 'Auto Start' })
|
|
||||||
autoStart: boolean = true;
|
autoStart: boolean = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,12 +82,6 @@ export class BehaviorTreeRuntimeComponent extends Component {
|
|||||||
@IgnoreSerialization()
|
@IgnoreSerialization()
|
||||||
nodesToAbort: Set<string> = new Set();
|
nodesToAbort: Set<string> = new Set();
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行顺序计数器(用于调试和可视化)
|
|
||||||
*/
|
|
||||||
@IgnoreSerialization()
|
|
||||||
executionOrderCounter: number = 0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取节点运行时状态
|
* 获取节点运行时状态
|
||||||
*/
|
*/
|
||||||
@@ -123,7 +115,6 @@ export class BehaviorTreeRuntimeComponent extends Component {
|
|||||||
resetAllStates(): void {
|
resetAllStates(): void {
|
||||||
this.nodeStates.clear();
|
this.nodeStates.clear();
|
||||||
this.activeNodeIds.clear();
|
this.activeNodeIds.clear();
|
||||||
this.executionOrderCounter = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -225,7 +216,7 @@ export class BehaviorTreeRuntimeComponent extends Component {
|
|||||||
*/
|
*/
|
||||||
unobserveBlackboard(nodeId: string): void {
|
unobserveBlackboard(nodeId: string): void {
|
||||||
for (const observers of this.blackboardObservers.values()) {
|
for (const observers of this.blackboardObservers.values()) {
|
||||||
const index = observers.findIndex((o) => o.nodeId === nodeId);
|
const index = observers.findIndex(o => o.nodeId === nodeId);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
observers.splice(index, 1);
|
observers.splice(index, 1);
|
||||||
}
|
}
|
||||||
@@ -12,11 +12,7 @@ import { NodeExecutorMetadata } from '../NodeMetadata';
|
|||||||
nodeType: NodeType.Decorator,
|
nodeType: NodeType.Decorator,
|
||||||
displayName: '反转',
|
displayName: '反转',
|
||||||
description: '反转子节点的执行结果',
|
description: '反转子节点的执行结果',
|
||||||
category: 'Decorator',
|
category: 'Decorator'
|
||||||
childrenConstraints: {
|
|
||||||
min: 1,
|
|
||||||
max: 1
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
export class InverterExecutor implements INodeExecutor {
|
export class InverterExecutor implements INodeExecutor {
|
||||||
execute(context: NodeExecutionContext): TaskStatus {
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
@@ -26,9 +26,6 @@ import { NodeExecutorMetadata } from '../NodeMetadata';
|
|||||||
description: '失败策略',
|
description: '失败策略',
|
||||||
options: ['all', 'one']
|
options: ['all', 'one']
|
||||||
}
|
}
|
||||||
},
|
|
||||||
childrenConstraints: {
|
|
||||||
min: 2
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export class ParallelExecutor implements INodeExecutor {
|
export class ParallelExecutor implements INodeExecutor {
|
||||||
@@ -12,10 +12,7 @@ import { NodeExecutorMetadata } from '../NodeMetadata';
|
|||||||
nodeType: NodeType.Composite,
|
nodeType: NodeType.Composite,
|
||||||
displayName: '随机序列',
|
displayName: '随机序列',
|
||||||
description: '随机顺序执行子节点,全部成功才成功',
|
description: '随机顺序执行子节点,全部成功才成功',
|
||||||
category: 'Composite',
|
category: 'Composite'
|
||||||
childrenConstraints: {
|
|
||||||
min: 1
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
export class RandomSequenceExecutor implements INodeExecutor {
|
export class RandomSequenceExecutor implements INodeExecutor {
|
||||||
execute(context: NodeExecutionContext): TaskStatus {
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
@@ -25,10 +25,6 @@ import { NodeExecutorMetadata } from '../NodeMetadata';
|
|||||||
default: false,
|
default: false,
|
||||||
description: '子节点失败时是否结束'
|
description: '子节点失败时是否结束'
|
||||||
}
|
}
|
||||||
},
|
|
||||||
childrenConstraints: {
|
|
||||||
min: 1,
|
|
||||||
max: 1
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export class RepeaterExecutor implements INodeExecutor {
|
export class RepeaterExecutor implements INodeExecutor {
|
||||||
@@ -12,10 +12,7 @@ import { NodeExecutorMetadata } from '../NodeMetadata';
|
|||||||
nodeType: NodeType.Composite,
|
nodeType: NodeType.Composite,
|
||||||
displayName: '选择器',
|
displayName: '选择器',
|
||||||
description: '按顺序执行子节点,任一成功则成功',
|
description: '按顺序执行子节点,任一成功则成功',
|
||||||
category: 'Composite',
|
category: 'Composite'
|
||||||
childrenConstraints: {
|
|
||||||
min: 1
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
export class SelectorExecutor implements INodeExecutor {
|
export class SelectorExecutor implements INodeExecutor {
|
||||||
execute(context: NodeExecutionContext): TaskStatus {
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
@@ -12,10 +12,7 @@ import { NodeExecutorMetadata } from '../NodeMetadata';
|
|||||||
nodeType: NodeType.Composite,
|
nodeType: NodeType.Composite,
|
||||||
displayName: '序列',
|
displayName: '序列',
|
||||||
description: '按顺序执行子节点,全部成功才成功',
|
description: '按顺序执行子节点,全部成功才成功',
|
||||||
category: 'Composite',
|
category: 'Composite'
|
||||||
childrenConstraints: {
|
|
||||||
min: 1
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
export class SequenceExecutor implements INodeExecutor {
|
export class SequenceExecutor implements INodeExecutor {
|
||||||
execute(context: NodeExecutionContext): TaskStatus {
|
execute(context: NodeExecutionContext): TaskStatus {
|
||||||
@@ -14,15 +14,6 @@ export interface ConfigFieldDefinition {
|
|||||||
allowMultipleConnections?: boolean;
|
allowMultipleConnections?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 子节点约束配置
|
|
||||||
*/
|
|
||||||
export interface ChildrenConstraints {
|
|
||||||
min?: number;
|
|
||||||
max?: number;
|
|
||||||
required?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点元数据
|
* 节点元数据
|
||||||
*/
|
*/
|
||||||
@@ -33,26 +24,6 @@ export interface NodeMetadata {
|
|||||||
description?: string;
|
description?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
configSchema?: Record<string, ConfigFieldDefinition>;
|
configSchema?: Record<string, ConfigFieldDefinition>;
|
||||||
childrenConstraints?: ChildrenConstraints;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 节点元数据默认值
|
|
||||||
*/
|
|
||||||
export class NodeMetadataDefaults {
|
|
||||||
static getDefaultConstraints(nodeType: NodeType): ChildrenConstraints | undefined {
|
|
||||||
switch (nodeType) {
|
|
||||||
case NodeType.Composite:
|
|
||||||
return { min: 1 };
|
|
||||||
case NodeType.Decorator:
|
|
||||||
return { min: 1, max: 1 };
|
|
||||||
case NodeType.Action:
|
|
||||||
case NodeType.Condition:
|
|
||||||
return { max: 0 };
|
|
||||||
default:
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,11 +49,11 @@ export class NodeMetadataRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getByCategory(category: string): NodeMetadata[] {
|
static getByCategory(category: string): NodeMetadata[] {
|
||||||
return this.getAllMetadata().filter((m) => m.category === category);
|
return this.getAllMetadata().filter(m => m.category === category);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getByNodeType(nodeType: NodeType): NodeMetadata[] {
|
static getByNodeType(nodeType: NodeType): NodeMetadata[] {
|
||||||
return this.getAllMetadata().filter((m) => m.nodeType === nodeType);
|
return this.getAllMetadata().filter(m => m.nodeType === nodeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getImplementationType(executorClass: Function): string | undefined {
|
static getImplementationType(executorClass: Function): string | undefined {
|
||||||
8
packages/behavior-tree/src/Runtime/index.ts
Normal file
8
packages/behavior-tree/src/Runtime/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export { BehaviorTreeData, BehaviorNodeData, NodeRuntimeState, createDefaultRuntimeState } from './BehaviorTreeData';
|
||||||
|
export { BehaviorTreeRuntimeComponent } from './BehaviorTreeRuntimeComponent';
|
||||||
|
export { BehaviorTreeAssetManager } from './BehaviorTreeAssetManager';
|
||||||
|
export { INodeExecutor, NodeExecutionContext, NodeExecutorRegistry, BindingHelper } from './NodeExecutor';
|
||||||
|
export { BehaviorTreeExecutionSystem } from './BehaviorTreeExecutionSystem';
|
||||||
|
export { NodeMetadata, ConfigFieldDefinition, NodeMetadataRegistry, NodeExecutorMetadata } from './NodeMetadata';
|
||||||
|
|
||||||
|
export * from './Executors';
|
||||||
@@ -131,7 +131,7 @@ export class BehaviorTreeAssetValidator {
|
|||||||
errors.push('Missing or invalid nodes array');
|
errors.push('Missing or invalid nodes array');
|
||||||
} else {
|
} else {
|
||||||
const nodeIds = new Set<string>();
|
const nodeIds = new Set<string>();
|
||||||
const rootNode = asset.nodes.find((n) => n.id === asset.rootNodeId);
|
const rootNode = asset.nodes.find(n => n.id === asset.rootNodeId);
|
||||||
|
|
||||||
if (!rootNode) {
|
if (!rootNode) {
|
||||||
errors.push(`Root node '${asset.rootNodeId}' not found in nodes array`);
|
errors.push(`Root node '${asset.rootNodeId}' not found in nodes array`);
|
||||||
@@ -157,7 +157,7 @@ export class BehaviorTreeAssetValidator {
|
|||||||
// 检查子节点引用
|
// 检查子节点引用
|
||||||
if (node.children) {
|
if (node.children) {
|
||||||
for (const childId of node.children) {
|
for (const childId of node.children) {
|
||||||
if (!asset.nodes.find((n) => n.id === childId)) {
|
if (!asset.nodes.find(n => n.id === childId)) {
|
||||||
errors.push(`Node ${node.id} references non-existent child: ${childId}`);
|
errors.push(`Node ${node.id} references non-existent child: ${childId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +167,7 @@ export class BehaviorTreeAssetValidator {
|
|||||||
// 检查是否有孤立节点
|
// 检查是否有孤立节点
|
||||||
const referencedNodes = new Set<string>([asset.rootNodeId]);
|
const referencedNodes = new Set<string>([asset.rootNodeId]);
|
||||||
const collectReferencedNodes = (nodeId: string) => {
|
const collectReferencedNodes = (nodeId: string) => {
|
||||||
const node = asset.nodes.find((n) => n.id === nodeId);
|
const node = asset.nodes.find(n => n.id === nodeId);
|
||||||
if (node && node.children) {
|
if (node && node.children) {
|
||||||
for (const childId of node.children) {
|
for (const childId of node.children) {
|
||||||
referencedNodes.add(childId);
|
referencedNodes.add(childId);
|
||||||
@@ -206,8 +206,8 @@ export class BehaviorTreeAssetValidator {
|
|||||||
|
|
||||||
// 检查属性绑定
|
// 检查属性绑定
|
||||||
if (asset.propertyBindings && Array.isArray(asset.propertyBindings)) {
|
if (asset.propertyBindings && Array.isArray(asset.propertyBindings)) {
|
||||||
const nodeIds = new Set(asset.nodes.map((n) => n.id));
|
const nodeIds = new Set(asset.nodes.map(n => n.id));
|
||||||
const varNames = new Set(asset.blackboard?.map((v) => v.name) || []);
|
const varNames = new Set(asset.blackboard?.map(v => v.name) || []);
|
||||||
|
|
||||||
for (const binding of asset.propertyBindings) {
|
for (const binding of asset.propertyBindings) {
|
||||||
if (!nodeIds.has(binding.nodeId)) {
|
if (!nodeIds.has(binding.nodeId)) {
|
||||||
@@ -276,7 +276,7 @@ export class BehaviorTreeAssetValidator {
|
|||||||
|
|
||||||
// 计算最大深度
|
// 计算最大深度
|
||||||
const getDepth = (nodeId: string, currentDepth: number = 0): number => {
|
const getDepth = (nodeId: string, currentDepth: number = 0): number => {
|
||||||
const node = asset.nodes.find((n) => n.id === nodeId);
|
const node = asset.nodes.find(n => n.id === nodeId);
|
||||||
if (!node || !node.children || node.children.length === 0) {
|
if (!node || !node.children || node.children.length === 0) {
|
||||||
return currentDepth;
|
return currentDepth;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export class EditorFormatConverter {
|
|||||||
* 查找根节点
|
* 查找根节点
|
||||||
*/
|
*/
|
||||||
private static findRootNode(nodes: EditorNode[]): EditorNode | null {
|
private static findRootNode(nodes: EditorNode[]): EditorNode | null {
|
||||||
return nodes.find((node) =>
|
return nodes.find(node =>
|
||||||
node.template.category === '根节点' ||
|
node.template.category === '根节点' ||
|
||||||
node.data.nodeType === 'root'
|
node.data.nodeType === 'root'
|
||||||
) || null;
|
) || null;
|
||||||
@@ -144,7 +144,7 @@ export class EditorFormatConverter {
|
|||||||
* 转换节点列表
|
* 转换节点列表
|
||||||
*/
|
*/
|
||||||
private static convertNodes(editorNodes: EditorNode[]): BehaviorTreeNodeData[] {
|
private static convertNodes(editorNodes: EditorNode[]): BehaviorTreeNodeData[] {
|
||||||
return editorNodes.map((node) => this.convertNode(node));
|
return editorNodes.map(node => this.convertNode(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -211,13 +211,13 @@ export class EditorFormatConverter {
|
|||||||
blackboard: BlackboardVariableDefinition[]
|
blackboard: BlackboardVariableDefinition[]
|
||||||
): PropertyBinding[] {
|
): PropertyBinding[] {
|
||||||
const bindings: PropertyBinding[] = [];
|
const bindings: PropertyBinding[] = [];
|
||||||
const blackboardVarNames = new Set(blackboard.map((v) => v.name));
|
const blackboardVarNames = new Set(blackboard.map(v => v.name));
|
||||||
|
|
||||||
const propertyConnections = connections.filter((conn) => conn.connectionType === 'property');
|
const propertyConnections = connections.filter(conn => conn.connectionType === 'property');
|
||||||
|
|
||||||
for (const conn of propertyConnections) {
|
for (const conn of propertyConnections) {
|
||||||
const fromNode = nodes.find((n) => n.id === conn.from);
|
const fromNode = nodes.find(n => n.id === conn.from);
|
||||||
const toNode = nodes.find((n) => n.id === conn.to);
|
const toNode = nodes.find(n => n.id === conn.to);
|
||||||
|
|
||||||
if (!fromNode || !toNode || !conn.toProperty) {
|
if (!fromNode || !toNode || !conn.toProperty) {
|
||||||
logger.warn(`跳过无效的属性连接: from=${conn.from}, to=${conn.to}`);
|
logger.warn(`跳过无效的属性连接: from=${conn.from}, to=${conn.to}`);
|
||||||
|
|||||||
@@ -1,294 +0,0 @@
|
|||||||
import { BehaviorTreeData, BehaviorNodeData } from '../execution/BehaviorTreeData';
|
|
||||||
import { NodeType, AbortType } from '../Types/TaskStatus';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 编辑器节点数据接口
|
|
||||||
*/
|
|
||||||
interface EditorNode {
|
|
||||||
id: string;
|
|
||||||
template: {
|
|
||||||
type: string;
|
|
||||||
className: string;
|
|
||||||
displayName?: string;
|
|
||||||
};
|
|
||||||
data: Record<string, any>;
|
|
||||||
children?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 编辑器行为树数据接口
|
|
||||||
*/
|
|
||||||
interface EditorBehaviorTreeData {
|
|
||||||
version?: string;
|
|
||||||
metadata?: {
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
createdAt?: string;
|
|
||||||
modifiedAt?: string;
|
|
||||||
};
|
|
||||||
nodes: EditorNode[];
|
|
||||||
blackboard?: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 编辑器格式到运行时格式的转换器
|
|
||||||
*
|
|
||||||
* 负责将编辑器的 JSON 格式(包含UI信息)转换为运行时的 BehaviorTreeData 格式
|
|
||||||
*/
|
|
||||||
export class EditorToBehaviorTreeDataConverter {
|
|
||||||
/**
|
|
||||||
* 将编辑器 JSON 字符串转换为运行时 BehaviorTreeData
|
|
||||||
*/
|
|
||||||
static fromEditorJSON(json: string): BehaviorTreeData {
|
|
||||||
const editorData: EditorBehaviorTreeData = JSON.parse(json);
|
|
||||||
return this.convert(editorData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将编辑器数据对象转换为运行时 BehaviorTreeData
|
|
||||||
*/
|
|
||||||
static convert(editorData: EditorBehaviorTreeData): BehaviorTreeData {
|
|
||||||
// 查找根节点
|
|
||||||
const rootNode = editorData.nodes.find((n) =>
|
|
||||||
n.template.type === 'root' || n.data['nodeType'] === 'root'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!rootNode) {
|
|
||||||
throw new Error('Behavior tree must have a root node');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换所有节点
|
|
||||||
const nodesMap = new Map<string, BehaviorNodeData>();
|
|
||||||
for (const editorNode of editorData.nodes) {
|
|
||||||
const behaviorNodeData = this.convertNode(editorNode);
|
|
||||||
nodesMap.set(behaviorNodeData.id, behaviorNodeData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换黑板变量
|
|
||||||
const blackboardVariables = editorData.blackboard
|
|
||||||
? new Map(Object.entries(editorData.blackboard))
|
|
||||||
: new Map();
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: this.generateTreeId(editorData),
|
|
||||||
name: editorData.metadata?.name || 'Untitled',
|
|
||||||
rootNodeId: rootNode.id,
|
|
||||||
nodes: nodesMap,
|
|
||||||
blackboardVariables
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 转换单个节点
|
|
||||||
*/
|
|
||||||
private static convertNode(editorNode: EditorNode): BehaviorNodeData {
|
|
||||||
const nodeType = this.mapNodeType(editorNode.template.type);
|
|
||||||
const config = this.extractConfig(editorNode.data);
|
|
||||||
const bindings = this.extractBindings(editorNode.data);
|
|
||||||
const abortType = this.extractAbortType(editorNode.data);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: editorNode.id,
|
|
||||||
name: editorNode.template.displayName || editorNode.template.className,
|
|
||||||
nodeType,
|
|
||||||
implementationType: editorNode.template.className,
|
|
||||||
children: editorNode.children || [],
|
|
||||||
config,
|
|
||||||
...(Object.keys(bindings).length > 0 && { bindings }),
|
|
||||||
...(abortType && { abortType })
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 映射节点类型
|
|
||||||
*/
|
|
||||||
private static mapNodeType(type: string): NodeType {
|
|
||||||
switch (type.toLowerCase()) {
|
|
||||||
case 'root':
|
|
||||||
return NodeType.Root;
|
|
||||||
case 'composite':
|
|
||||||
return NodeType.Composite;
|
|
||||||
case 'decorator':
|
|
||||||
return NodeType.Decorator;
|
|
||||||
case 'action':
|
|
||||||
return NodeType.Action;
|
|
||||||
case 'condition':
|
|
||||||
return NodeType.Condition;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown node type: ${type}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提取节点配置(过滤掉内部字段和绑定字段)
|
|
||||||
*/
|
|
||||||
private static extractConfig(data: Record<string, any>): Record<string, any> {
|
|
||||||
const config: Record<string, any> = {};
|
|
||||||
const internalFields = new Set(['nodeType', 'abortType']);
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
|
||||||
// 跳过内部字段
|
|
||||||
if (internalFields.has(key)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 跳过黑板绑定字段(它们会被提取到 bindings 中)
|
|
||||||
if (this.isBinding(value)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
config[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提取黑板变量绑定
|
|
||||||
*/
|
|
||||||
private static extractBindings(data: Record<string, any>): Record<string, string> {
|
|
||||||
const bindings: Record<string, string> = {};
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(data)) {
|
|
||||||
if (this.isBinding(value)) {
|
|
||||||
bindings[key] = this.extractBindingKey(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return bindings;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断是否为黑板绑定
|
|
||||||
*/
|
|
||||||
private static isBinding(value: any): boolean {
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
|
||||||
return value._isBlackboardBinding === true ||
|
|
||||||
value.type === 'blackboard' ||
|
|
||||||
(value.blackboardKey !== undefined);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提取黑板绑定的键名
|
|
||||||
*/
|
|
||||||
private static extractBindingKey(binding: any): string {
|
|
||||||
return binding.blackboardKey || binding.key || binding.value || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提取中止类型(条件装饰器使用)
|
|
||||||
*/
|
|
||||||
private static extractAbortType(data: Record<string, any>): AbortType | undefined {
|
|
||||||
if (!data['abortType']) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const abortTypeStr = String(data['abortType']).toLowerCase();
|
|
||||||
switch (abortTypeStr) {
|
|
||||||
case 'none':
|
|
||||||
return AbortType.None;
|
|
||||||
case 'self':
|
|
||||||
return AbortType.Self;
|
|
||||||
case 'lowerpriority':
|
|
||||||
case 'lower_priority':
|
|
||||||
return AbortType.LowerPriority;
|
|
||||||
case 'both':
|
|
||||||
return AbortType.Both;
|
|
||||||
default:
|
|
||||||
return AbortType.None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成行为树ID
|
|
||||||
*/
|
|
||||||
private static generateTreeId(editorData: EditorBehaviorTreeData): string {
|
|
||||||
if (editorData.metadata?.name) {
|
|
||||||
// 将名称转换为合法ID(移除特殊字符)
|
|
||||||
return editorData.metadata.name.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
||||||
}
|
|
||||||
return `tree_${Date.now()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将运行时格式转换回编辑器格式(用于双向转换)
|
|
||||||
*/
|
|
||||||
static toEditorJSON(treeData: BehaviorTreeData): string {
|
|
||||||
const editorData = this.convertToEditor(treeData);
|
|
||||||
return JSON.stringify(editorData, null, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将运行时 BehaviorTreeData 转换为编辑器格式
|
|
||||||
*/
|
|
||||||
static convertToEditor(treeData: BehaviorTreeData): EditorBehaviorTreeData {
|
|
||||||
const nodes: EditorNode[] = [];
|
|
||||||
|
|
||||||
for (const [_id, nodeData] of treeData.nodes) {
|
|
||||||
nodes.push(this.convertNodeToEditor(nodeData));
|
|
||||||
}
|
|
||||||
|
|
||||||
const blackboard = treeData.blackboardVariables
|
|
||||||
? Object.fromEntries(treeData.blackboardVariables)
|
|
||||||
: {};
|
|
||||||
|
|
||||||
return {
|
|
||||||
version: '1.0.0',
|
|
||||||
metadata: {
|
|
||||||
name: treeData.name,
|
|
||||||
description: '',
|
|
||||||
modifiedAt: new Date().toISOString()
|
|
||||||
},
|
|
||||||
nodes,
|
|
||||||
blackboard
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 将运行时节点转换为编辑器节点
|
|
||||||
*/
|
|
||||||
private static convertNodeToEditor(nodeData: BehaviorNodeData): EditorNode {
|
|
||||||
const data: Record<string, any> = { ...nodeData.config };
|
|
||||||
|
|
||||||
// 添加绑定回数据对象
|
|
||||||
if (nodeData.bindings) {
|
|
||||||
for (const [key, blackboardKey] of Object.entries(nodeData.bindings)) {
|
|
||||||
data[key] = {
|
|
||||||
_isBlackboardBinding: true,
|
|
||||||
blackboardKey
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加中止类型
|
|
||||||
if (nodeData.abortType !== undefined) {
|
|
||||||
data['abortType'] = nodeData.abortType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取节点类型字符串
|
|
||||||
let typeStr: string;
|
|
||||||
if (typeof nodeData.nodeType === 'string') {
|
|
||||||
typeStr = nodeData.nodeType;
|
|
||||||
} else {
|
|
||||||
typeStr = 'action'; // 默认值
|
|
||||||
}
|
|
||||||
|
|
||||||
const result: EditorNode = {
|
|
||||||
id: nodeData.id,
|
|
||||||
template: {
|
|
||||||
type: typeStr,
|
|
||||||
className: nodeData.implementationType,
|
|
||||||
displayName: nodeData.name
|
|
||||||
},
|
|
||||||
data
|
|
||||||
};
|
|
||||||
|
|
||||||
if (nodeData.children && nodeData.children.length > 0) {
|
|
||||||
result.children = nodeData.children;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NodeType } from '../Types/TaskStatus';
|
import { NodeType } from '../Types/TaskStatus';
|
||||||
import { NodeMetadataRegistry, ConfigFieldDefinition, NodeMetadata } from '../execution/NodeMetadata';
|
import { NodeMetadataRegistry, ConfigFieldDefinition } from '../Runtime/NodeMetadata';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 节点数据JSON格式
|
* 节点数据JSON格式
|
||||||
@@ -48,7 +48,7 @@ export const PropertyType = {
|
|||||||
* type: 'curve-editor'
|
* type: 'curve-editor'
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export type PropertyType = (typeof PropertyType)[keyof typeof PropertyType] | string;
|
export type PropertyType = typeof PropertyType[keyof typeof PropertyType] | string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 属性定义(用于编辑器)
|
* 属性定义(用于编辑器)
|
||||||
@@ -65,24 +65,6 @@ export interface PropertyDefinition {
|
|||||||
step?: number;
|
step?: number;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
|
||||||
/**
|
|
||||||
* 字段编辑器配置
|
|
||||||
*
|
|
||||||
* 指定使用哪个字段编辑器以及相关选项
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* fieldEditor: {
|
|
||||||
* type: 'asset',
|
|
||||||
* options: { fileExtension: '.btree' }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
fieldEditor?: {
|
|
||||||
type: string;
|
|
||||||
options?: Record<string, any>;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自定义渲染配置
|
* 自定义渲染配置
|
||||||
*
|
*
|
||||||
@@ -132,7 +114,7 @@ export interface PropertyDefinition {
|
|||||||
/** 验证失败的提示信息 */
|
/** 验证失败的提示信息 */
|
||||||
message?: string;
|
message?: string;
|
||||||
/** 自定义验证函数 */
|
/** 自定义验证函数 */
|
||||||
validator?: string; // 函数字符串,编辑器会解析
|
validator?: string; // 函数字符串,编辑器会解析
|
||||||
/** 最小长度(字符串) */
|
/** 最小长度(字符串) */
|
||||||
minLength?: number;
|
minLength?: number;
|
||||||
/** 最大长度(字符串) */
|
/** 最大长度(字符串) */
|
||||||
@@ -159,8 +141,6 @@ export interface NodeTemplate {
|
|||||||
className?: string;
|
className?: string;
|
||||||
componentClass?: Function;
|
componentClass?: Function;
|
||||||
requiresChildren?: boolean;
|
requiresChildren?: boolean;
|
||||||
minChildren?: number;
|
|
||||||
maxChildren?: number;
|
|
||||||
defaultConfig: Partial<NodeDataJSON>;
|
defaultConfig: Partial<NodeDataJSON>;
|
||||||
properties: PropertyDefinition[];
|
properties: PropertyDefinition[];
|
||||||
}
|
}
|
||||||
@@ -174,14 +154,14 @@ export class NodeTemplates {
|
|||||||
*/
|
*/
|
||||||
static getAllTemplates(): NodeTemplate[] {
|
static getAllTemplates(): NodeTemplate[] {
|
||||||
const allMetadata = NodeMetadataRegistry.getAllMetadata();
|
const allMetadata = NodeMetadataRegistry.getAllMetadata();
|
||||||
return allMetadata.map((metadata) => this.convertMetadataToTemplate(metadata));
|
return allMetadata.map(metadata => this.convertMetadataToTemplate(metadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据类型和子类型获取模板
|
* 根据类型和子类型获取模板
|
||||||
*/
|
*/
|
||||||
static getTemplate(type: NodeType, subType: string): NodeTemplate | undefined {
|
static getTemplate(type: NodeType, subType: string): NodeTemplate | undefined {
|
||||||
return this.getAllTemplates().find((t) => {
|
return this.getAllTemplates().find(t => {
|
||||||
if (t.type !== type) return false;
|
if (t.type !== type) return false;
|
||||||
const config: any = t.defaultConfig;
|
const config: any = t.defaultConfig;
|
||||||
|
|
||||||
@@ -203,7 +183,7 @@ export class NodeTemplates {
|
|||||||
/**
|
/**
|
||||||
* 将NodeMetadata转换为NodeTemplate
|
* 将NodeMetadata转换为NodeTemplate
|
||||||
*/
|
*/
|
||||||
private static convertMetadataToTemplate(metadata: NodeMetadata): NodeTemplate {
|
private static convertMetadataToTemplate(metadata: any): NodeTemplate {
|
||||||
const properties = this.convertConfigSchemaToProperties(metadata.configSchema || {});
|
const properties = this.convertConfigSchemaToProperties(metadata.configSchema || {});
|
||||||
|
|
||||||
const defaultConfig: Partial<NodeDataJSON> = {
|
const defaultConfig: Partial<NodeDataJSON> = {
|
||||||
@@ -237,10 +217,7 @@ export class NodeTemplates {
|
|||||||
// 根据节点类型生成默认颜色和图标
|
// 根据节点类型生成默认颜色和图标
|
||||||
const { icon, color } = this.getIconAndColorByType(metadata.nodeType, metadata.category || '');
|
const { icon, color } = this.getIconAndColorByType(metadata.nodeType, metadata.category || '');
|
||||||
|
|
||||||
// 应用子节点约束
|
return {
|
||||||
const constraints = metadata.childrenConstraints || this.getDefaultConstraintsByNodeType(metadata.nodeType);
|
|
||||||
|
|
||||||
const template: NodeTemplate = {
|
|
||||||
type: metadata.nodeType,
|
type: metadata.nodeType,
|
||||||
displayName: metadata.displayName,
|
displayName: metadata.displayName,
|
||||||
category: metadata.category || this.getCategoryByNodeType(metadata.nodeType),
|
category: metadata.category || this.getCategoryByNodeType(metadata.nodeType),
|
||||||
@@ -251,35 +228,6 @@ export class NodeTemplates {
|
|||||||
defaultConfig,
|
defaultConfig,
|
||||||
properties
|
properties
|
||||||
};
|
};
|
||||||
|
|
||||||
if (constraints) {
|
|
||||||
if (constraints.min !== undefined) {
|
|
||||||
template.minChildren = constraints.min;
|
|
||||||
template.requiresChildren = constraints.min > 0;
|
|
||||||
}
|
|
||||||
if (constraints.max !== undefined) {
|
|
||||||
template.maxChildren = constraints.max;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return template;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取节点类型的默认约束
|
|
||||||
*/
|
|
||||||
private static getDefaultConstraintsByNodeType(nodeType: NodeType): { min?: number; max?: number } | undefined {
|
|
||||||
switch (nodeType) {
|
|
||||||
case NodeType.Composite:
|
|
||||||
return { min: 1 };
|
|
||||||
case NodeType.Decorator:
|
|
||||||
return { min: 1, max: 1 };
|
|
||||||
case NodeType.Action:
|
|
||||||
case NodeType.Condition:
|
|
||||||
return { max: 0 };
|
|
||||||
default:
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -318,7 +266,7 @@ export class NodeTemplates {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (field.options) {
|
if (field.options) {
|
||||||
property.options = field.options.map((opt) => ({
|
property.options = field.options.map(opt => ({
|
||||||
label: opt,
|
label: opt,
|
||||||
value: opt
|
value: opt
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ export enum TaskStatus {
|
|||||||
* 内置节点类型常量
|
* 内置节点类型常量
|
||||||
*/
|
*/
|
||||||
export const NodeType = {
|
export const NodeType = {
|
||||||
/** 根节点 - 行为树的起始节点 */
|
|
||||||
Root: 'root',
|
|
||||||
/** 复合节点 - 有多个子节点 */
|
/** 复合节点 - 有多个子节点 */
|
||||||
Composite: 'composite',
|
Composite: 'composite',
|
||||||
/** 装饰器节点 - 有一个子节点 */
|
/** 装饰器节点 - 有一个子节点 */
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
/**
|
|
||||||
* Behavior Tree Unified Plugin
|
|
||||||
* 行为树统一插件
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { IScene, ServiceContainer } from '@esengine/ecs-framework';
|
|
||||||
import { ComponentRegistry, Core } from '@esengine/ecs-framework';
|
|
||||||
import type {
|
|
||||||
IPluginLoader,
|
|
||||||
IRuntimeModuleLoader,
|
|
||||||
PluginDescriptor,
|
|
||||||
SystemContext
|
|
||||||
} from '@esengine/editor-runtime';
|
|
||||||
|
|
||||||
// Runtime imports
|
|
||||||
import { BehaviorTreeRuntimeComponent } from '../execution/BehaviorTreeRuntimeComponent';
|
|
||||||
import { BehaviorTreeExecutionSystem } from '../execution/BehaviorTreeExecutionSystem';
|
|
||||||
import { BehaviorTreeAssetManager } from '../execution/BehaviorTreeAssetManager';
|
|
||||||
import { GlobalBlackboardService } from '../Services/GlobalBlackboardService';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插件描述符
|
|
||||||
*/
|
|
||||||
export const descriptor: PluginDescriptor = {
|
|
||||||
id: '@esengine/behavior-tree',
|
|
||||||
name: 'Behavior Tree System',
|
|
||||||
version: '1.0.0',
|
|
||||||
description: 'AI 行为树系统,支持可视化编辑和运行时执行',
|
|
||||||
category: 'ai',
|
|
||||||
enabledByDefault: true,
|
|
||||||
canContainContent: false,
|
|
||||||
isEnginePlugin: false,
|
|
||||||
modules: [
|
|
||||||
{
|
|
||||||
name: 'BehaviorTreeRuntime',
|
|
||||||
type: 'runtime',
|
|
||||||
loadingPhase: 'default',
|
|
||||||
entry: './src/index.ts'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'BehaviorTreeEditor',
|
|
||||||
type: 'editor',
|
|
||||||
loadingPhase: 'default',
|
|
||||||
entry: './src/editor/index.ts'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
dependencies: [
|
|
||||||
{ id: '@esengine/core', version: '>=1.0.0' }
|
|
||||||
],
|
|
||||||
icon: 'GitBranch'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Behavior Tree Runtime Module
|
|
||||||
* 行为树运行时模块
|
|
||||||
*/
|
|
||||||
export class BehaviorTreeRuntimeModule implements IRuntimeModuleLoader {
|
|
||||||
registerComponents(registry: typeof ComponentRegistry): 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 {
|
|
||||||
const behaviorTreeSystem = new BehaviorTreeExecutionSystem(Core);
|
|
||||||
|
|
||||||
// 编辑器模式下默认禁用
|
|
||||||
if (context.isEditor) {
|
|
||||||
behaviorTreeSystem.enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
scene.addSystem(behaviorTreeSystem);
|
|
||||||
|
|
||||||
// 保存引用
|
|
||||||
context.behaviorTreeSystem = behaviorTreeSystem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Behavior Tree Plugin Loader
|
|
||||||
* 行为树插件加载器
|
|
||||||
*
|
|
||||||
* 注意:editorModule 在 ./index.ts 中通过 createBehaviorTreePlugin() 设置
|
|
||||||
*/
|
|
||||||
export const BehaviorTreePlugin: IPluginLoader = {
|
|
||||||
descriptor,
|
|
||||||
runtimeModule: new BehaviorTreeRuntimeModule(),
|
|
||||||
// editorModule 将在 index.ts 中设置
|
|
||||||
};
|
|
||||||
|
|
||||||
export default BehaviorTreePlugin;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import type { ServiceContainer } from '@esengine/editor-runtime';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插件上下文
|
|
||||||
* 存储插件安装时传入的服务容器引用
|
|
||||||
*/
|
|
||||||
class PluginContextClass {
|
|
||||||
private _services: ServiceContainer | null = null;
|
|
||||||
|
|
||||||
setServices(services: ServiceContainer): void {
|
|
||||||
this._services = services;
|
|
||||||
}
|
|
||||||
|
|
||||||
getServices(): ServiceContainer {
|
|
||||||
if (!this._services) {
|
|
||||||
throw new Error('PluginContext not initialized. Make sure the plugin is properly installed.');
|
|
||||||
}
|
|
||||||
return this._services;
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(): void {
|
|
||||||
this._services = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PluginContext = new PluginContextClass();
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
import { ICommand } from './ICommand';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 命令历史记录配置
|
|
||||||
*/
|
|
||||||
export interface CommandManagerConfig {
|
|
||||||
/**
|
|
||||||
* 最大历史记录数量
|
|
||||||
*/
|
|
||||||
maxHistorySize?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 是否自动合并相似命令
|
|
||||||
*/
|
|
||||||
autoMerge?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 命令管理器
|
|
||||||
* 管理命令的执行、撤销、重做以及历史记录
|
|
||||||
*/
|
|
||||||
export class CommandManager {
|
|
||||||
private undoStack: ICommand[] = [];
|
|
||||||
private redoStack: ICommand[] = [];
|
|
||||||
private readonly config: Required<CommandManagerConfig>;
|
|
||||||
private isExecuting = false;
|
|
||||||
|
|
||||||
constructor(config: CommandManagerConfig = {}) {
|
|
||||||
this.config = {
|
|
||||||
maxHistorySize: config.maxHistorySize ?? 100,
|
|
||||||
autoMerge: config.autoMerge ?? true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行命令
|
|
||||||
*/
|
|
||||||
execute(command: ICommand): void {
|
|
||||||
if (this.isExecuting) {
|
|
||||||
throw new Error('不能在命令执行过程中执行新命令');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isExecuting = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
command.execute();
|
|
||||||
|
|
||||||
if (this.config.autoMerge && this.undoStack.length > 0) {
|
|
||||||
const lastCommand = this.undoStack[this.undoStack.length - 1];
|
|
||||||
if (lastCommand && lastCommand.canMergeWith(command)) {
|
|
||||||
const mergedCommand = lastCommand.mergeWith(command);
|
|
||||||
this.undoStack[this.undoStack.length - 1] = mergedCommand;
|
|
||||||
this.redoStack = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.undoStack.push(command);
|
|
||||||
this.redoStack = [];
|
|
||||||
|
|
||||||
if (this.undoStack.length > this.config.maxHistorySize) {
|
|
||||||
this.undoStack.shift();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.isExecuting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 撤销上一个命令
|
|
||||||
*/
|
|
||||||
undo(): void {
|
|
||||||
if (this.isExecuting) {
|
|
||||||
throw new Error('不能在命令执行过程中撤销');
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = this.undoStack.pop();
|
|
||||||
if (!command) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isExecuting = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
command.undo();
|
|
||||||
this.redoStack.push(command);
|
|
||||||
} catch (error) {
|
|
||||||
this.undoStack.push(command);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
this.isExecuting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重做上一个被撤销的命令
|
|
||||||
*/
|
|
||||||
redo(): void {
|
|
||||||
if (this.isExecuting) {
|
|
||||||
throw new Error('不能在命令执行过程中重做');
|
|
||||||
}
|
|
||||||
|
|
||||||
const command = this.redoStack.pop();
|
|
||||||
if (!command) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isExecuting = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
command.execute();
|
|
||||||
this.undoStack.push(command);
|
|
||||||
} catch (error) {
|
|
||||||
this.redoStack.push(command);
|
|
||||||
throw error;
|
|
||||||
} finally {
|
|
||||||
this.isExecuting = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否可以撤销
|
|
||||||
*/
|
|
||||||
canUndo(): boolean {
|
|
||||||
return this.undoStack.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查是否可以重做
|
|
||||||
*/
|
|
||||||
canRedo(): boolean {
|
|
||||||
return this.redoStack.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取撤销栈的描述列表
|
|
||||||
*/
|
|
||||||
getUndoHistory(): string[] {
|
|
||||||
return this.undoStack.map((cmd) => cmd.getDescription());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取重做栈的描述列表
|
|
||||||
*/
|
|
||||||
getRedoHistory(): string[] {
|
|
||||||
return this.redoStack.map((cmd) => cmd.getDescription());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清空所有历史记录
|
|
||||||
*/
|
|
||||||
clear(): void {
|
|
||||||
this.undoStack = [];
|
|
||||||
this.redoStack = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量执行命令(作为单一操作,可以一次撤销)
|
|
||||||
*/
|
|
||||||
executeBatch(commands: ICommand[]): void {
|
|
||||||
if (commands.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const batchCommand = new BatchCommand(commands);
|
|
||||||
this.execute(batchCommand);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量命令
|
|
||||||
* 将多个命令组合为一个命令
|
|
||||||
*/
|
|
||||||
class BatchCommand implements ICommand {
|
|
||||||
constructor(private readonly commands: ICommand[]) {}
|
|
||||||
|
|
||||||
execute(): void {
|
|
||||||
for (const command of this.commands) {
|
|
||||||
command.execute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
undo(): void {
|
|
||||||
for (let i = this.commands.length - 1; i >= 0; i--) {
|
|
||||||
const command = this.commands[i];
|
|
||||||
if (command) {
|
|
||||||
command.undo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getDescription(): string {
|
|
||||||
return `批量操作 (${this.commands.length} 个命令)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
canMergeWith(): boolean {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mergeWith(): ICommand {
|
|
||||||
throw new Error('批量命令不支持合并');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* 命令接口
|
|
||||||
* 实现命令模式,支持撤销/重做功能
|
|
||||||
*/
|
|
||||||
export interface ICommand {
|
|
||||||
/**
|
|
||||||
* 执行命令
|
|
||||||
*/
|
|
||||||
execute(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 撤销命令
|
|
||||||
*/
|
|
||||||
undo(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取命令描述(用于显示历史记录)
|
|
||||||
*/
|
|
||||||
getDescription(): string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查命令是否可以合并
|
|
||||||
* 用于优化撤销/重做历史,例如连续的移动操作可以合并为一个
|
|
||||||
*/
|
|
||||||
canMergeWith(other: ICommand): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 与另一个命令合并
|
|
||||||
*/
|
|
||||||
mergeWith(other: ICommand): ICommand;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 行为树状态接口
|
|
||||||
* 命令通过此接口操作状态
|
|
||||||
*/
|
|
||||||
export interface ITreeState {
|
|
||||||
/**
|
|
||||||
* 获取当前行为树
|
|
||||||
*/
|
|
||||||
getTree(): BehaviorTree;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置行为树
|
|
||||||
*/
|
|
||||||
setTree(tree: BehaviorTree): void;
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Connection } from '../../../domain/models/Connection';
|
|
||||||
import { BaseCommand } from '@esengine/editor-runtime';
|
|
||||||
import { ITreeState } from '../ITreeState';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加连接命令
|
|
||||||
*/
|
|
||||||
export class AddConnectionCommand extends BaseCommand {
|
|
||||||
constructor(
|
|
||||||
private readonly state: ITreeState,
|
|
||||||
private readonly connection: Connection
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(): void {
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
const newTree = tree.addConnection(this.connection);
|
|
||||||
this.state.setTree(newTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
undo(): void {
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
const newTree = tree.removeConnection(
|
|
||||||
this.connection.from,
|
|
||||||
this.connection.to,
|
|
||||||
this.connection.fromProperty,
|
|
||||||
this.connection.toProperty
|
|
||||||
);
|
|
||||||
this.state.setTree(newTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDescription(): string {
|
|
||||||
return `添加连接: ${this.connection.from} -> ${this.connection.to}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Node } from '../../../domain/models/Node';
|
|
||||||
import { BaseCommand } from '@esengine/editor-runtime';
|
|
||||||
import { ITreeState } from '../ITreeState';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建节点命令
|
|
||||||
*/
|
|
||||||
export class CreateNodeCommand extends BaseCommand {
|
|
||||||
private createdNodeId: string;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly state: ITreeState,
|
|
||||||
private readonly node: Node
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
this.createdNodeId = node.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(): void {
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
const newTree = tree.addNode(this.node);
|
|
||||||
this.state.setTree(newTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
undo(): void {
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
const newTree = tree.removeNode(this.createdNodeId);
|
|
||||||
this.state.setTree(newTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDescription(): string {
|
|
||||||
return `创建节点: ${this.node.template.displayName}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Node } from '../../../domain/models/Node';
|
|
||||||
import { BaseCommand } from '@esengine/editor-runtime';
|
|
||||||
import { ITreeState } from '../ITreeState';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除节点命令
|
|
||||||
*/
|
|
||||||
export class DeleteNodeCommand extends BaseCommand {
|
|
||||||
private deletedNode: Node | null = null;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly state: ITreeState,
|
|
||||||
private readonly nodeId: string
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(): void {
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
this.deletedNode = tree.getNode(this.nodeId);
|
|
||||||
const newTree = tree.removeNode(this.nodeId);
|
|
||||||
this.state.setTree(newTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
undo(): void {
|
|
||||||
if (!this.deletedNode) {
|
|
||||||
throw new Error('无法撤销:未保存已删除的节点');
|
|
||||||
}
|
|
||||||
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
const newTree = tree.addNode(this.deletedNode);
|
|
||||||
this.state.setTree(newTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDescription(): string {
|
|
||||||
return `删除节点: ${this.deletedNode?.template.displayName ?? this.nodeId}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { Position } from '../../../domain/value-objects/Position';
|
|
||||||
import { BaseCommand, ICommand } from '@esengine/editor-runtime';
|
|
||||||
import { ITreeState } from '../ITreeState';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移动节点命令
|
|
||||||
* 支持合并连续的移动操作
|
|
||||||
*/
|
|
||||||
export class MoveNodeCommand extends BaseCommand {
|
|
||||||
private oldPosition: Position;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly state: ITreeState,
|
|
||||||
private readonly nodeId: string,
|
|
||||||
private readonly newPosition: Position
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
const node = tree.getNode(nodeId);
|
|
||||||
this.oldPosition = node.position;
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(): void {
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
const newTree = tree.updateNode(this.nodeId, (node) =>
|
|
||||||
node.moveToPosition(this.newPosition)
|
|
||||||
);
|
|
||||||
this.state.setTree(newTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
undo(): void {
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
const newTree = tree.updateNode(this.nodeId, (node) =>
|
|
||||||
node.moveToPosition(this.oldPosition)
|
|
||||||
);
|
|
||||||
this.state.setTree(newTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDescription(): string {
|
|
||||||
return `移动节点: ${this.nodeId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移动命令可以合并
|
|
||||||
*/
|
|
||||||
canMergeWith(other: ICommand): boolean {
|
|
||||||
if (!(other instanceof MoveNodeCommand)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.nodeId === other.nodeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 合并移动命令
|
|
||||||
* 保留初始位置,更新最终位置
|
|
||||||
*/
|
|
||||||
mergeWith(other: ICommand): ICommand {
|
|
||||||
if (!(other instanceof MoveNodeCommand)) {
|
|
||||||
throw new Error('只能与 MoveNodeCommand 合并');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.nodeId !== other.nodeId) {
|
|
||||||
throw new Error('只能合并同一节点的移动命令');
|
|
||||||
}
|
|
||||||
|
|
||||||
const merged = new MoveNodeCommand(
|
|
||||||
this.state,
|
|
||||||
this.nodeId,
|
|
||||||
other.newPosition
|
|
||||||
);
|
|
||||||
merged.oldPosition = this.oldPosition;
|
|
||||||
return merged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { Connection } from '../../../domain/models/Connection';
|
|
||||||
import { BaseCommand } from '@esengine/editor-runtime';
|
|
||||||
import { ITreeState } from '../ITreeState';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除连接命令
|
|
||||||
*/
|
|
||||||
export class RemoveConnectionCommand extends BaseCommand {
|
|
||||||
private removedConnection: Connection | null = null;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly state: ITreeState,
|
|
||||||
private readonly from: string,
|
|
||||||
private readonly to: string,
|
|
||||||
private readonly fromProperty?: string,
|
|
||||||
private readonly toProperty?: string
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(): void {
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
|
|
||||||
const connection = tree.connections.find((c) =>
|
|
||||||
c.matches(this.from, this.to, this.fromProperty, this.toProperty)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!connection) {
|
|
||||||
throw new Error(`连接不存在: ${this.from} -> ${this.to}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.removedConnection = connection;
|
|
||||||
const newTree = tree.removeConnection(this.from, this.to, this.fromProperty, this.toProperty);
|
|
||||||
this.state.setTree(newTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
undo(): void {
|
|
||||||
if (!this.removedConnection) {
|
|
||||||
throw new Error('无法撤销:未保存已删除的连接');
|
|
||||||
}
|
|
||||||
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
const newTree = tree.addConnection(this.removedConnection);
|
|
||||||
this.state.setTree(newTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDescription(): string {
|
|
||||||
return `移除连接: ${this.from} -> ${this.to}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { BaseCommand } from '@esengine/editor-runtime';
|
|
||||||
import { ITreeState } from '../ITreeState';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新节点数据命令
|
|
||||||
*/
|
|
||||||
export class UpdateNodeDataCommand extends BaseCommand {
|
|
||||||
private oldData: Record<string, unknown>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly state: ITreeState,
|
|
||||||
private readonly nodeId: string,
|
|
||||||
private readonly newData: Record<string, unknown>
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
const node = tree.getNode(nodeId);
|
|
||||||
this.oldData = node.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(): void {
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
const newTree = tree.updateNode(this.nodeId, (node) =>
|
|
||||||
node.updateData(this.newData)
|
|
||||||
);
|
|
||||||
this.state.setTree(newTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
undo(): void {
|
|
||||||
const tree = this.state.getTree();
|
|
||||||
const newTree = tree.updateNode(this.nodeId, (node) =>
|
|
||||||
node.updateData(this.oldData)
|
|
||||||
);
|
|
||||||
this.state.setTree(newTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
getDescription(): string {
|
|
||||||
return `更新节点数据: ${this.nodeId}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export { CreateNodeCommand } from './CreateNodeCommand';
|
|
||||||
export { DeleteNodeCommand } from './DeleteNodeCommand';
|
|
||||||
export { AddConnectionCommand } from './AddConnectionCommand';
|
|
||||||
export { RemoveConnectionCommand } from './RemoveConnectionCommand';
|
|
||||||
export { MoveNodeCommand } from './MoveNodeCommand';
|
|
||||||
export { UpdateNodeDataCommand } from './UpdateNodeDataCommand';
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
import { Node as BehaviorTreeNode } from '../../domain/models/Node';
|
|
||||||
import { Connection } from '../../domain/models/Connection';
|
|
||||||
import { ExecutionLog } from '../../utils/BehaviorTreeExecutor';
|
|
||||||
import { BlackboardValue } from '../../domain/models/Blackboard';
|
|
||||||
import { createLogger } from '@esengine/ecs-framework';
|
|
||||||
|
|
||||||
const logger = createLogger('ExecutionHooks');
|
|
||||||
|
|
||||||
type BlackboardVariables = Record<string, BlackboardValue>;
|
|
||||||
type NodeExecutionStatus = 'idle' | 'running' | 'success' | 'failure';
|
|
||||||
|
|
||||||
export interface ExecutionContext {
|
|
||||||
nodes: BehaviorTreeNode[];
|
|
||||||
connections: Connection[];
|
|
||||||
blackboardVariables: BlackboardVariables;
|
|
||||||
rootNodeId: string;
|
|
||||||
tickCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NodeStatusChangeEvent {
|
|
||||||
nodeId: string;
|
|
||||||
status: NodeExecutionStatus;
|
|
||||||
previousStatus?: NodeExecutionStatus;
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IExecutionHooks {
|
|
||||||
beforePlay?(context: ExecutionContext): void | Promise<void>;
|
|
||||||
|
|
||||||
afterPlay?(context: ExecutionContext): void | Promise<void>;
|
|
||||||
|
|
||||||
beforePause?(): void | Promise<void>;
|
|
||||||
|
|
||||||
afterPause?(): void | Promise<void>;
|
|
||||||
|
|
||||||
beforeResume?(): void | Promise<void>;
|
|
||||||
|
|
||||||
afterResume?(): void | Promise<void>;
|
|
||||||
|
|
||||||
beforeStop?(): void | Promise<void>;
|
|
||||||
|
|
||||||
afterStop?(): void | Promise<void>;
|
|
||||||
|
|
||||||
beforeStep?(deltaTime: number): void | Promise<void>;
|
|
||||||
|
|
||||||
afterStep?(deltaTime: number): void | Promise<void>;
|
|
||||||
|
|
||||||
onTick?(tickCount: number, deltaTime: number): void | Promise<void>;
|
|
||||||
|
|
||||||
onNodeStatusChange?(event: NodeStatusChangeEvent): void | Promise<void>;
|
|
||||||
|
|
||||||
onExecutionComplete?(logs: ExecutionLog[]): void | Promise<void>;
|
|
||||||
|
|
||||||
onBlackboardUpdate?(variables: BlackboardVariables): void | Promise<void>;
|
|
||||||
|
|
||||||
onError?(error: Error, context?: string): void | Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ExecutionHooksManager {
|
|
||||||
private hooks: Set<IExecutionHooks> = new Set();
|
|
||||||
|
|
||||||
register(hook: IExecutionHooks): void {
|
|
||||||
this.hooks.add(hook);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregister(hook: IExecutionHooks): void {
|
|
||||||
this.hooks.delete(hook);
|
|
||||||
}
|
|
||||||
|
|
||||||
clear(): void {
|
|
||||||
this.hooks.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerBeforePlay(context: ExecutionContext): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.beforePlay) {
|
|
||||||
try {
|
|
||||||
await hook.beforePlay(context);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in beforePlay hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerAfterPlay(context: ExecutionContext): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.afterPlay) {
|
|
||||||
try {
|
|
||||||
await hook.afterPlay(context);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in afterPlay hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerBeforePause(): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.beforePause) {
|
|
||||||
try {
|
|
||||||
await hook.beforePause();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in beforePause hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerAfterPause(): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.afterPause) {
|
|
||||||
try {
|
|
||||||
await hook.afterPause();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in afterPause hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerBeforeResume(): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.beforeResume) {
|
|
||||||
try {
|
|
||||||
await hook.beforeResume();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in beforeResume hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerAfterResume(): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.afterResume) {
|
|
||||||
try {
|
|
||||||
await hook.afterResume();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in afterResume hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerBeforeStop(): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.beforeStop) {
|
|
||||||
try {
|
|
||||||
await hook.beforeStop();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in beforeStop hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerAfterStop(): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.afterStop) {
|
|
||||||
try {
|
|
||||||
await hook.afterStop();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in afterStop hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerBeforeStep(deltaTime: number): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.beforeStep) {
|
|
||||||
try {
|
|
||||||
await hook.beforeStep(deltaTime);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in beforeStep hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerAfterStep(deltaTime: number): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.afterStep) {
|
|
||||||
try {
|
|
||||||
await hook.afterStep(deltaTime);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in afterStep hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerOnTick(tickCount: number, deltaTime: number): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.onTick) {
|
|
||||||
try {
|
|
||||||
await hook.onTick(tickCount, deltaTime);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in onTick hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerOnNodeStatusChange(event: NodeStatusChangeEvent): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.onNodeStatusChange) {
|
|
||||||
try {
|
|
||||||
await hook.onNodeStatusChange(event);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in onNodeStatusChange hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerOnExecutionComplete(logs: ExecutionLog[]): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.onExecutionComplete) {
|
|
||||||
try {
|
|
||||||
await hook.onExecutionComplete(logs);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in onExecutionComplete hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerOnBlackboardUpdate(variables: BlackboardVariables): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.onBlackboardUpdate) {
|
|
||||||
try {
|
|
||||||
await hook.onBlackboardUpdate(variables);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('Error in onBlackboardUpdate hook:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async triggerOnError(error: Error, context?: string): Promise<void> {
|
|
||||||
for (const hook of this.hooks) {
|
|
||||||
if (hook.onError) {
|
|
||||||
try {
|
|
||||||
await hook.onError(error, context);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Error in onError hook:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user