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:
192
packages/framework/procgen/src/random/SeededRandom.ts
Normal file
192
packages/framework/procgen/src/random/SeededRandom.ts
Normal 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);
|
||||
}
|
||||
193
packages/framework/procgen/src/random/Shuffle.ts
Normal file
193
packages/framework/procgen/src/random/Shuffle.ts
Normal 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;
|
||||
}
|
||||
177
packages/framework/procgen/src/random/WeightedRandom.ts
Normal file
177
packages/framework/procgen/src/random/WeightedRandom.ts
Normal 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);
|
||||
}
|
||||
17
packages/framework/procgen/src/random/index.ts
Normal file
17
packages/framework/procgen/src/random/index.ts
Normal 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';
|
||||
Reference in New Issue
Block a user