refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,192 @@
/**
* @zh 种子随机数生成器
* @en Seeded Random Number Generator
*
* @zh 基于 xorshift128+ 算法的确定性伪随机数生成器
* @en Deterministic PRNG based on xorshift128+ algorithm
*/
/**
* @zh 种子随机数生成器
* @en Seeded random number generator
*/
export class SeededRandom {
private _s0: number;
private _s1: number;
private readonly _initialS0: number;
private readonly _initialS1: number;
/**
* @zh 创建种子随机数生成器
* @en Create seeded random number generator
*
* @param seed - @zh 随机种子 @en Random seed
*/
constructor(seed: number = Date.now()) {
// Initialize with MurmurHash3 mixing
let h = seed | 0;
h = Math.imul(h ^ (h >>> 16), 0x85ebca6b);
h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35);
h ^= h >>> 16;
this._s0 = h >>> 0;
this._s1 = (h * 0x9e3779b9) >>> 0;
// Ensure non-zero state
if (this._s0 === 0) this._s0 = 1;
if (this._s1 === 0) this._s1 = 1;
this._initialS0 = this._s0;
this._initialS1 = this._s1;
// Warm up
for (let i = 0; i < 10; i++) {
this.next();
}
}
/**
* @zh 重置到初始状态
* @en Reset to initial state
*/
reset(): void {
this._s0 = this._initialS0;
this._s1 = this._initialS1;
for (let i = 0; i < 10; i++) {
this.next();
}
}
/**
* @zh 生成下一个随机数 [0, 1)
* @en Generate next random number [0, 1)
*/
next(): number {
let s1 = this._s0;
const s0 = this._s1;
this._s0 = s0;
s1 ^= s1 << 23;
s1 ^= s1 >>> 17;
s1 ^= s0;
s1 ^= s0 >>> 26;
this._s1 = s1;
return ((this._s0 + this._s1) >>> 0) / 4294967296;
}
/**
* @zh 生成整数 [min, max]
* @en Generate integer [min, max]
*/
nextInt(min: number, max: number): number {
return Math.floor(this.next() * (max - min + 1)) + min;
}
/**
* @zh 生成浮点数 [min, max)
* @en Generate float [min, max)
*/
nextFloat(min: number, max: number): number {
return this.next() * (max - min) + min;
}
/**
* @zh 生成布尔值
* @en Generate boolean
*
* @param probability - @zh 为 true 的概率 [0, 1] @en Probability of true [0, 1]
*/
nextBool(probability: number = 0.5): boolean {
return this.next() < probability;
}
/**
* @zh 生成正态分布随机数 (Box-Muller 变换)
* @en Generate normally distributed random number (Box-Muller transform)
*
* @param mean - @zh 均值 @en Mean
* @param stdDev - @zh 标准差 @en Standard deviation
*/
nextGaussian(mean: number = 0, stdDev: number = 1): number {
const u1 = this.next();
const u2 = this.next();
const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
return z0 * stdDev + mean;
}
/**
* @zh 生成指数分布随机数
* @en Generate exponentially distributed random number
*
* @param lambda - @zh 率参数 @en Rate parameter
*/
nextExponential(lambda: number = 1): number {
return -Math.log(1 - this.next()) / lambda;
}
/**
* @zh 在圆内生成均匀分布的随机点
* @en Generate uniformly distributed random point in circle
*
* @param radius - @zh 半径 @en Radius
*/
nextPointInCircle(radius: number = 1): { x: number; y: number } {
const r = Math.sqrt(this.next()) * radius;
const theta = this.next() * 2 * Math.PI;
return {
x: r * Math.cos(theta),
y: r * Math.sin(theta)
};
}
/**
* @zh 在圆环上生成随机点
* @en Generate random point on circle
*
* @param radius - @zh 半径 @en Radius
*/
nextPointOnCircle(radius: number = 1): { x: number; y: number } {
const theta = this.next() * 2 * Math.PI;
return {
x: radius * Math.cos(theta),
y: radius * Math.sin(theta)
};
}
/**
* @zh 在球内生成均匀分布的随机点
* @en Generate uniformly distributed random point in sphere
*
* @param radius - @zh 半径 @en Radius
*/
nextPointInSphere(radius: number = 1): { x: number; y: number; z: number } {
const r = Math.cbrt(this.next()) * radius;
const theta = this.next() * 2 * Math.PI;
const phi = Math.acos(2 * this.next() - 1);
return {
x: r * Math.sin(phi) * Math.cos(theta),
y: r * Math.sin(phi) * Math.sin(theta),
z: r * Math.cos(phi)
};
}
/**
* @zh 生成随机方向向量
* @en Generate random direction vector
*/
nextDirection2D(): { x: number; y: number } {
const theta = this.next() * 2 * Math.PI;
return {
x: Math.cos(theta),
y: Math.sin(theta)
};
}
}
/**
* @zh 创建种子随机数生成器
* @en Create seeded random number generator
*/
export function createSeededRandom(seed?: number): SeededRandom {
return new SeededRandom(seed);
}

View File

@@ -0,0 +1,193 @@
/**
* @zh 洗牌和采样工具
* @en Shuffle and Sampling Utilities
*/
import type { SeededRandom } from './SeededRandom';
/**
* @zh Fisher-Yates 洗牌算法(原地修改)
* @en Fisher-Yates shuffle algorithm (in-place)
*
* @param array - @zh 要洗牌的数组 @en Array to shuffle
* @param rng - @zh 随机数生成器 @en Random number generator
* @returns @zh 洗牌后的数组(同一数组)@en Shuffled array (same array)
*/
export function shuffle<T>(array: T[], rng: SeededRandom | { next(): number }): T[] {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(rng.next() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
/**
* @zh 创建洗牌后的副本(不修改原数组)
* @en Create shuffled copy (does not modify original)
*
* @param array - @zh 原数组 @en Original array
* @param rng - @zh 随机数生成器 @en Random number generator
* @returns @zh 洗牌后的新数组 @en New shuffled array
*/
export function shuffleCopy<T>(array: readonly T[], rng: SeededRandom | { next(): number }): T[] {
return shuffle([...array], rng);
}
/**
* @zh 从数组中随机选择一个元素
* @en Pick a random element from array
*
* @param array - @zh 数组 @en Array
* @param rng - @zh 随机数生成器 @en Random number generator
* @returns @zh 随机元素 @en Random element
*/
export function pickOne<T>(array: readonly T[], rng: SeededRandom | { next(): number }): T {
if (array.length === 0) {
throw new Error('Cannot pick from empty array');
}
return array[Math.floor(rng.next() * array.length)];
}
/**
* @zh 从数组中随机采样 N 个不重复元素
* @en Sample N unique elements from array
*
* @param array - @zh 数组 @en Array
* @param count - @zh 采样数量 @en Sample count
* @param rng - @zh 随机数生成器 @en Random number generator
* @returns @zh 采样结果 @en Sample result
*/
export function sample<T>(array: readonly T[], count: number, rng: SeededRandom | { next(): number }): T[] {
if (count > array.length) {
throw new Error('Sample count exceeds array length');
}
if (count === array.length) {
return shuffleCopy(array, rng);
}
// For small sample sizes relative to array, use selection
if (count < array.length / 2) {
const result: T[] = [];
const indices = new Set<number>();
while (result.length < count) {
const index = Math.floor(rng.next() * array.length);
if (!indices.has(index)) {
indices.add(index);
result.push(array[index]);
}
}
return result;
}
// For large sample sizes, shuffle and take first N
return shuffleCopy(array, rng).slice(0, count);
}
/**
* @zh 从数组中随机采样 N 个元素(可重复)
* @en Sample N elements from array (with replacement)
*
* @param array - @zh 数组 @en Array
* @param count - @zh 采样数量 @en Sample count
* @param rng - @zh 随机数生成器 @en Random number generator
* @returns @zh 采样结果 @en Sample result
*/
export function sampleWithReplacement<T>(
array: readonly T[],
count: number,
rng: SeededRandom | { next(): number }
): T[] {
if (array.length === 0) {
throw new Error('Cannot sample from empty array');
}
const result: T[] = [];
for (let i = 0; i < count; i++) {
result.push(pickOne(array, rng));
}
return result;
}
/**
* @zh 生成范围内的随机整数数组(不重复)
* @en Generate array of random unique integers in range
*
* @param min - @zh 最小值(包含)@en Minimum (inclusive)
* @param max - @zh 最大值(包含)@en Maximum (inclusive)
* @param count - @zh 数量 @en Count
* @param rng - @zh 随机数生成器 @en Random number generator
* @returns @zh 随机整数数组 @en Array of random integers
*/
export function randomIntegers(
min: number,
max: number,
count: number,
rng: SeededRandom | { next(): number }
): number[] {
const range = max - min + 1;
if (count > range) {
throw new Error('Count exceeds range');
}
// Generate all numbers and sample
const numbers: number[] = [];
for (let i = min; i <= max; i++) {
numbers.push(i);
}
return sample(numbers, count, rng);
}
/**
* @zh 按权重从数组中采样(不重复)
* @en Sample from array by weight (without replacement)
*
* @param items - @zh 项目数组 @en Item array
* @param weights - @zh 权重数组 @en Weight array
* @param count - @zh 采样数量 @en Sample count
* @param rng - @zh 随机数生成器 @en Random number generator
*/
export function weightedSample<T>(
items: readonly T[],
weights: readonly number[],
count: number,
rng: SeededRandom | { next(): number }
): T[] {
if (items.length !== weights.length) {
throw new Error('Items and weights must have same length');
}
if (count > items.length) {
throw new Error('Sample count exceeds array length');
}
const result: T[] = [];
const remaining = [...items];
const remainingWeights = [...weights];
for (let i = 0; i < count; i++) {
let totalWeight = 0;
for (const w of remainingWeights) {
totalWeight += w;
}
let r = rng.next() * totalWeight;
let selectedIndex = 0;
for (let j = 0; j < remainingWeights.length; j++) {
r -= remainingWeights[j];
if (r <= 0) {
selectedIndex = j;
break;
}
}
result.push(remaining[selectedIndex]);
remaining.splice(selectedIndex, 1);
remainingWeights.splice(selectedIndex, 1);
}
return result;
}

View File

@@ -0,0 +1,177 @@
/**
* @zh 加权随机工具
* @en Weighted Random Utilities
*/
import type { SeededRandom } from './SeededRandom';
/**
* @zh 加权项
* @en Weighted item
*/
export interface WeightedItem<T> {
/**
* @zh 项目值
* @en Item value
*/
value: T;
/**
* @zh 权重(> 0
* @en Weight (> 0)
*/
weight: number;
}
/**
* @zh 加权随机选择器
* @en Weighted random selector
*/
export class WeightedRandom<T> {
private readonly _items: WeightedItem<T>[];
private readonly _cumulativeWeights: number[];
private readonly _totalWeight: number;
/**
* @zh 创建加权随机选择器
* @en Create weighted random selector
*
* @param items - @zh 加权项数组 @en Array of weighted items
*/
constructor(items: WeightedItem<T>[]) {
if (items.length === 0) {
throw new Error('Items array cannot be empty');
}
this._items = [...items];
this._cumulativeWeights = [];
let total = 0;
for (const item of this._items) {
if (item.weight <= 0) {
throw new Error('Weights must be positive');
}
total += item.weight;
this._cumulativeWeights.push(total);
}
this._totalWeight = total;
}
/**
* @zh 随机选择一个项目
* @en Randomly select an item
*
* @param rng - @zh 随机数生成器 @en Random number generator
*/
pick(rng: SeededRandom | { next(): number }): T {
const r = rng.next() * this._totalWeight;
// Binary search for the selected item
let left = 0;
let right = this._cumulativeWeights.length - 1;
while (left < right) {
const mid = (left + right) >>> 1;
if (this._cumulativeWeights[mid] < r) {
left = mid + 1;
} else {
right = mid;
}
}
return this._items[left].value;
}
/**
* @zh 使用 Math.random 选择
* @en Pick using Math.random
*/
pickRandom(): T {
return this.pick({ next: () => Math.random() });
}
/**
* @zh 获取项目的选中概率
* @en Get selection probability of an item
*/
getProbability(index: number): number {
if (index < 0 || index >= this._items.length) {
throw new Error('Index out of bounds');
}
return this._items[index].weight / this._totalWeight;
}
/**
* @zh 获取所有项目数量
* @en Get total item count
*/
get size(): number {
return this._items.length;
}
/**
* @zh 获取总权重
* @en Get total weight
*/
get totalWeight(): number {
return this._totalWeight;
}
}
/**
* @zh 从加权数组中随机选择
* @en Pick from weighted array
*
* @param items - @zh 加权项数组 @en Array of weighted items
* @param rng - @zh 随机数生成器 @en Random number generator
*/
export function weightedPick<T>(
items: WeightedItem<T>[],
rng: SeededRandom | { next(): number }
): T {
if (items.length === 0) {
throw new Error('Items array cannot be empty');
}
let totalWeight = 0;
for (const item of items) {
totalWeight += item.weight;
}
let r = rng.next() * totalWeight;
for (const item of items) {
r -= item.weight;
if (r <= 0) {
return item.value;
}
}
return items[items.length - 1].value;
}
/**
* @zh 从权重映射中随机选择
* @en Pick from weight map
*
* @param weights - @zh 值到权重的映射 @en Map of values to weights
* @param rng - @zh 随机数生成器 @en Random number generator
*/
export function weightedPickFromMap<T extends string | number>(
weights: Record<T, number>,
rng: SeededRandom | { next(): number }
): T {
const items: WeightedItem<T>[] = [];
for (const key in weights) {
items.push({ value: key as T, weight: weights[key] });
}
return weightedPick(items, rng);
}
/**
* @zh 创建加权随机选择器
* @en Create weighted random selector
*/
export function createWeightedRandom<T>(items: WeightedItem<T>[]): WeightedRandom<T> {
return new WeightedRandom(items);
}

View File

@@ -0,0 +1,17 @@
/**
* @zh 随机工具模块
* @en Random Utilities Module
*/
export { SeededRandom, createSeededRandom } from './SeededRandom';
export { WeightedRandom, weightedPick, weightedPickFromMap, createWeightedRandom } from './WeightedRandom';
export type { WeightedItem } from './WeightedRandom';
export {
shuffle,
shuffleCopy,
pickOne,
sample,
sampleWithReplacement,
randomIntegers,
weightedSample
} from './Shuffle';