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,64 @@
# @esengine/platform-wechat
微信小游戏平台适配器,为 ECS Framework 提供微信小游戏环境支持。
## 安装
```bash
npm install @esengine/platform-wechat
```
## 使用
```typescript
import { PlatformManager } from '@esengine/ecs-framework';
import { WeChatAdapter } from '@esengine/platform-wechat';
// 注册微信小游戏适配器
const adapter = new WeChatAdapter();
PlatformManager.getInstance().registerAdapter(adapter);
// 使用子系统
const canvas = adapter.canvas.createCanvas();
const ctx = canvas.getContext('webgl');
// 加载 WASM 模块
const instance = await adapter.wasm.instantiate('path/to/module.wasm');
```
## 子系统
| 子系统 | 描述 |
|--------|------|
| `canvas` | Canvas 创建、WebGL 上下文 |
| `audio` | 音频播放、音量控制 |
| `storage` | 本地存储 |
| `network` | 网络请求、WebSocket |
| `input` | 触摸输入 |
| `file` | 文件系统操作 |
| `wasm` | WebAssembly 加载 |
## 平台限制
- **SharedArrayBuffer**: 不支持
- **Worker**: 支持,但有限制(需独立文件,最多 1 个)
- **eval**: 不支持
- **WASM**: 支持,使用 `WXWebAssembly`
## game.json 配置
```json
{
"workers": "workers",
"subpackages": [
{
"name": "wasm",
"root": "wasm/"
}
]
}
```
## License
MIT

View File

@@ -0,0 +1,56 @@
{
"name": "@esengine/platform-wechat",
"version": "1.0.0",
"description": "微信小游戏平台适配器",
"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",
"prepublishOnly": "npm run build"
},
"keywords": [
"ecs",
"wechat",
"minigame",
"platform",
"adapter"
],
"author": "yhh",
"license": "MIT",
"dependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/platform-common": "workspace:*"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^11.1.6",
"minigame-api-typings": "^3.8.12",
"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/esengine.git",
"directory": "packages/platform-wechat"
}
}

View File

@@ -0,0 +1,44 @@
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import dts from 'rollup-plugin-dts';
const external = ['@esengine/ecs-framework', '@esengine/platform-common'];
export default [
// ESM and CJS builds
{
input: 'src/index.ts',
output: [
{
file: 'dist/index.mjs',
format: 'esm',
sourcemap: true
},
{
file: 'dist/index.js',
format: 'cjs',
sourcemap: true
}
],
external,
plugins: [
resolve(),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
declaration: false
})
]
},
// Type declarations
{
input: 'src/index.ts',
output: {
file: 'dist/index.d.ts',
format: 'esm'
},
external,
plugins: [dts()]
}
];

View File

@@ -0,0 +1,235 @@
/**
* Rust 引擎桥接层
* 负责在微信小游戏环境中初始化和管理 Rust WASM 引擎
*/
import type { IPlatformCanvas } from '@esengine/platform-common';
import { WeChatAdapter } from './WeChatAdapter';
/**
* 引擎配置
*/
export interface EngineBridgeConfig {
wasmPath: string;
canvasWidth?: number;
canvasHeight?: number;
enableWebGL2?: boolean;
}
/**
* 引擎桥接层
* 将微信平台能力桥接到 Rust WASM 引擎
*/
export class EngineBridge {
private _adapter: WeChatAdapter;
private _canvas: IPlatformCanvas;
private _gl: WebGLRenderingContext | WebGL2RenderingContext | null = null;
private _wasmInstance: any = null;
private _config: EngineBridgeConfig;
constructor(adapter: WeChatAdapter, config: EngineBridgeConfig) {
this._adapter = adapter;
this._config = config;
// 创建主 Canvas
const windowInfo = adapter.getSystemInfo();
const width = config.canvasWidth ?? windowInfo.windowWidth;
const height = config.canvasHeight ?? windowInfo.windowHeight;
this._canvas = adapter.canvas.createCanvas(width, height);
}
/**
* 初始化引擎
*/
async initialize(): Promise<void> {
// 获取 WebGL 上下文
this._gl = this._getWebGLContext();
if (!this._gl) {
throw new Error('无法获取 WebGL 上下文');
}
// 加载 WASM 模块
const imports = this._createWASMImports();
this._wasmInstance = await this._adapter.wasm.instantiate(
this._config.wasmPath,
imports
);
// 初始化引擎
if (this._wasmInstance.exports.init) {
this._wasmInstance.exports.init();
}
}
/**
* 获取 WebGL 上下文
*/
private _getWebGLContext(): WebGLRenderingContext | WebGL2RenderingContext | null {
const contextType = this._config.enableWebGL2 ? 'webgl2' : 'webgl';
const gl = this._canvas.getContext(contextType, {
alpha: false,
antialias: false,
depth: false,
stencil: false,
premultipliedAlpha: true,
preserveDrawingBuffer: false
});
return gl as WebGLRenderingContext | WebGL2RenderingContext | null;
}
/**
* 创建 WASM 导入对象
*/
private _createWASMImports(): Record<string, Record<string, any>> {
return {
env: {
// 内存
memory: this._adapter.wasm.createMemory(256, 16384),
// 平台桥接函数
platform_log: (ptr: number, len: number) => {
const message = this._readString(ptr, len);
console.log('[Engine]', message);
},
platform_error: (ptr: number, len: number) => {
const message = this._readString(ptr, len);
console.error('[Engine]', message);
},
platform_now: () => {
return performance.now();
},
// WebGL 桥接
gl_bindBuffer: (target: number, buffer: number) => {
this._gl?.bindBuffer(target, this._getGLObject(buffer));
},
gl_bufferData: (target: number, dataPtr: number, dataLen: number, usage: number) => {
const data = this._readBuffer(dataPtr, dataLen);
this._gl?.bufferData(target, data, usage);
},
gl_clear: (mask: number) => {
this._gl?.clear(mask);
},
gl_clearColor: (r: number, g: number, b: number, a: number) => {
this._gl?.clearColor(r, g, b, a);
},
gl_drawArrays: (mode: number, first: number, count: number) => {
this._gl?.drawArrays(mode, first, count);
},
gl_drawElements: (mode: number, count: number, type: number, offset: number) => {
this._gl?.drawElements(mode, count, type, offset);
},
gl_enable: (cap: number) => {
this._gl?.enable(cap);
},
gl_disable: (cap: number) => {
this._gl?.disable(cap);
},
gl_viewport: (x: number, y: number, width: number, height: number) => {
this._gl?.viewport(x, y, width, height);
}
}
};
}
/**
* 从 WASM 内存读取字符串
*/
private _readString(ptr: number, len: number): string {
const memory = this._wasmInstance?.exports.memory as WebAssembly.Memory;
if (!memory) return '';
const bytes = new Uint8Array(memory.buffer, ptr, len);
return new TextDecoder().decode(bytes);
}
/**
* 从 WASM 内存读取缓冲区
*/
private _readBuffer(ptr: number, len: number): ArrayBuffer {
const memory = this._wasmInstance?.exports.memory as WebAssembly.Memory;
if (!memory) return new ArrayBuffer(0);
return memory.buffer.slice(ptr, ptr + len);
}
/**
* 获取 WebGL 对象(暂时简化实现)
*/
private _getGLObject(_id: number): WebGLBuffer | null {
// TODO: 实现 WebGL 对象管理
return null;
}
/**
* 获取 Canvas
*/
get canvas(): IPlatformCanvas {
return this._canvas;
}
/**
* 获取 WebGL 上下文
*/
get gl(): WebGLRenderingContext | WebGL2RenderingContext | null {
return this._gl;
}
/**
* 获取 WASM 实例
*/
get wasmInstance(): any {
return this._wasmInstance;
}
/**
* 清屏
*/
clear(r: number, g: number, b: number, a: number): void {
if (this._gl) {
this._gl.clearColor(r, g, b, a);
this._gl.clear(this._gl.COLOR_BUFFER_BIT);
}
}
/**
* 渲染一帧
*/
render(): void {
if (this._wasmInstance?.exports.render) {
this._wasmInstance.exports.render();
}
}
/**
* 更新逻辑
*/
update(deltaTime: number): void {
if (this._wasmInstance?.exports.update) {
this._wasmInstance.exports.update(deltaTime);
}
}
/**
* 销毁引擎
*/
dispose(): void {
if (this._wasmInstance?.exports.dispose) {
this._wasmInstance.exports.dispose();
}
this._wasmInstance = null;
this._gl = null;
}
}

View File

@@ -0,0 +1,289 @@
/**
* 微信小游戏平台适配器
*/
import type {
IPlatformAdapter,
PlatformWorker,
WorkerCreationOptions,
PlatformConfig
} from '@esengine/ecs-framework';
import type { SystemInfo } from '@esengine/platform-common';
import { WeChatCanvasSubsystem } from './subsystems/WeChatCanvasSubsystem';
import { WeChatAudioSubsystem } from './subsystems/WeChatAudioSubsystem';
import { WeChatStorageSubsystem } from './subsystems/WeChatStorageSubsystem';
import { WeChatNetworkSubsystem } from './subsystems/WeChatNetworkSubsystem';
import { WeChatInputSubsystem } from './subsystems/WeChatInputSubsystem';
import { WeChatFileSubsystem } from './subsystems/WeChatFileSubsystem';
import { WeChatWASMSubsystem } from './subsystems/WeChatWASMSubsystem';
import { getWx, isWeChatMiniGame } from './utils';
/**
* 微信小游戏 Worker 包装
*/
class WeChatWorker implements PlatformWorker {
private _worker: WechatMinigame.Worker;
private _state: 'running' | 'terminated' = 'running';
constructor(worker: WechatMinigame.Worker) {
this._worker = worker;
}
get state(): 'running' | 'terminated' {
return this._state;
}
postMessage(message: any, _transfer?: Transferable[]): void {
this._worker.postMessage(message);
}
onMessage(handler: (event: { data: any }) => void): void {
this._worker.onMessage((res) => {
handler({ data: res });
});
}
onError(handler: (error: ErrorEvent) => void): void {
this._worker.onError((error) => {
handler(error as unknown as ErrorEvent);
});
}
terminate(): void {
this._worker.terminate();
this._state = 'terminated';
}
}
/**
* 微信小游戏平台适配器
*/
export class WeChatAdapter implements IPlatformAdapter {
readonly name = 'wechat-minigame';
readonly version: string;
// 子系统实例
private _canvas: WeChatCanvasSubsystem | null = null;
private _audio: WeChatAudioSubsystem | null = null;
private _storage: WeChatStorageSubsystem | null = null;
private _network: WeChatNetworkSubsystem | null = null;
private _input: WeChatInputSubsystem | null = null;
private _file: WeChatFileSubsystem | null = null;
private _wasm: WeChatWASMSubsystem | null = null;
private _deviceInfo: WechatMinigame.DeviceInfo | null = null;
private _windowInfo: WechatMinigame.WindowInfo | null = null;
private _appBaseInfo: WechatMinigame.AppBaseInfo | null = null;
constructor() {
if (!isWeChatMiniGame()) {
throw new Error('当前环境不是微信小游戏环境');
}
// 使用新的分离 API 获取系统信息
const wxApi = getWx();
this._deviceInfo = wxApi.getDeviceInfo();
this._windowInfo = wxApi.getWindowInfo();
this._appBaseInfo = wxApi.getAppBaseInfo();
this.version = this._appBaseInfo.SDKVersion;
}
// ========================================================================
// IPlatformAdapter 基础实现
// ========================================================================
isWorkerSupported(): boolean {
// 微信小游戏支持 Worker但有限制
return typeof getWx().createWorker === 'function';
}
isSharedArrayBufferSupported(): boolean {
// 微信小游戏不支持 SharedArrayBuffer
return false;
}
getHardwareConcurrency(): number {
// 微信小游戏无法获取真实核心数,返回保守值
return 2;
}
createWorker(script: string, options?: WorkerCreationOptions): PlatformWorker {
// 微信小游戏 Worker 需要指定文件路径,不支持内联脚本
// script 参数应该是 worker 文件的路径
const worker = getWx().createWorker(script, {
useExperimentalWorker: true
});
return new WeChatWorker(worker);
}
createSharedArrayBuffer(_length: number): SharedArrayBuffer | null {
// 微信小游戏不支持 SharedArrayBuffer
return null;
}
getHighResTimestamp(): number {
return Date.now();
}
getPlatformConfig(): PlatformConfig {
return {
maxWorkerCount: 1,
supportsModuleWorker: false,
supportsTransferableObjects: true,
maxSharedArrayBufferSize: 0,
workerScriptPrefix: '',
limitations: {
noEval: true,
requiresWorkerInit: true,
memoryLimit: this._deviceInfo?.memorySize
? parseInt(String(this._deviceInfo.memorySize)) * 1024 * 1024
: 256 * 1024 * 1024,
workerNotSupported: false,
workerLimitations: [
'Worker 必须使用独立文件,不支持内联脚本',
'仅支持 1 个 Worker 实例',
'不支持 SharedArrayBuffer',
'Worker 文件需要在 game.json 中配置'
]
},
extensions: {
platform: 'wechat-minigame',
sdkVersion: this._appBaseInfo?.SDKVersion
}
};
}
async getPlatformConfigAsync(): Promise<PlatformConfig> {
return this.getPlatformConfig();
}
// ========================================================================
// 子系统访问器
// ========================================================================
/**
* 获取 Canvas 子系统
*/
get canvas(): WeChatCanvasSubsystem {
if (!this._canvas) {
this._canvas = new WeChatCanvasSubsystem();
}
return this._canvas;
}
/**
* 获取音频子系统
*/
get audio(): WeChatAudioSubsystem {
if (!this._audio) {
this._audio = new WeChatAudioSubsystem();
}
return this._audio;
}
/**
* 获取存储子系统
*/
get storage(): WeChatStorageSubsystem {
if (!this._storage) {
this._storage = new WeChatStorageSubsystem();
}
return this._storage;
}
/**
* 获取网络子系统
*/
get network(): WeChatNetworkSubsystem {
if (!this._network) {
this._network = new WeChatNetworkSubsystem();
}
return this._network;
}
/**
* 获取输入子系统
*/
get input(): WeChatInputSubsystem {
if (!this._input) {
this._input = new WeChatInputSubsystem();
}
return this._input;
}
/**
* 获取文件系统子系统
*/
get file(): WeChatFileSubsystem {
if (!this._file) {
this._file = new WeChatFileSubsystem();
}
return this._file;
}
/**
* 获取 WASM 子系统
*/
get wasm(): WeChatWASMSubsystem {
if (!this._wasm) {
this._wasm = new WeChatWASMSubsystem();
}
return this._wasm;
}
// ========================================================================
// 系统信息
// ========================================================================
/**
* 获取系统信息
*/
getSystemInfo(): SystemInfo {
const device = this._deviceInfo!;
const window = this._windowInfo!;
const app = this._appBaseInfo!;
return {
brand: device.brand,
model: device.model,
pixelRatio: window.pixelRatio,
screenWidth: window.screenWidth,
screenHeight: window.screenHeight,
windowWidth: window.windowWidth,
windowHeight: window.windowHeight,
statusBarHeight: window.statusBarHeight || 0,
system: device.system,
platform: device.platform as SystemInfo['platform'],
SDKVersion: app.SDKVersion,
benchmarkLevel: device.benchmarkLevel || 0,
memorySize: device.memorySize ? parseInt(String(device.memorySize)) : undefined
};
}
/**
* 比较基础库版本
*/
compareVersion(v1: string, v2: string): number {
const a1 = v1.split('.').map(Number);
const a2 = v2.split('.').map(Number);
const len = Math.max(a1.length, a2.length);
for (let i = 0; i < len; i++) {
const n1 = a1[i] || 0;
const n2 = a2[i] || 0;
if (n1 > n2) return 1;
if (n1 < n2) return -1;
}
return 0;
}
/**
* 检查是否支持某个 API
*/
canIUse(schema: string): boolean {
return getWx().canIUse(schema);
}
}

View File

@@ -0,0 +1,23 @@
/**
* 微信小游戏平台适配器包
* @packageDocumentation
*/
// 主适配器
export { WeChatAdapter } from './WeChatAdapter';
// 引擎桥接
export { EngineBridge } from './EngineBridge';
export type { EngineBridgeConfig } from './EngineBridge';
// 子系统
export { WeChatCanvasSubsystem } from './subsystems/WeChatCanvasSubsystem';
export { WeChatAudioSubsystem } from './subsystems/WeChatAudioSubsystem';
export { WeChatStorageSubsystem } from './subsystems/WeChatStorageSubsystem';
export { WeChatNetworkSubsystem } from './subsystems/WeChatNetworkSubsystem';
export { WeChatInputSubsystem } from './subsystems/WeChatInputSubsystem';
export { WeChatFileSubsystem } from './subsystems/WeChatFileSubsystem';
export { WeChatWASMSubsystem } from './subsystems/WeChatWASMSubsystem';
// 工具
export { getWx, isWeChatMiniGame } from './utils';

View File

@@ -0,0 +1,88 @@
/**
* 微信小游戏音频子系统
*/
import type {
IPlatformAudioSubsystem,
IPlatformAudioContext
} from '@esengine/platform-common';
import { getWx, promisify } from '../utils';
/**
* 微信音频上下文包装
*/
class WeChatAudioContext implements IPlatformAudioContext {
private _ctx: WechatMinigame.InnerAudioContext;
constructor(ctx: WechatMinigame.InnerAudioContext) {
this._ctx = ctx;
}
get src(): string { return this._ctx.src; }
set src(value: string) { this._ctx.src = value; }
get autoplay(): boolean { return this._ctx.autoplay; }
set autoplay(value: boolean) { this._ctx.autoplay = value; }
get loop(): boolean { return this._ctx.loop; }
set loop(value: boolean) { this._ctx.loop = value; }
get volume(): number { return this._ctx.volume; }
set volume(value: number) { this._ctx.volume = value; }
get duration(): number { return this._ctx.duration; }
get currentTime(): number { return this._ctx.currentTime; }
get paused(): boolean { return this._ctx.paused; }
get buffered(): number { return this._ctx.buffered; }
play(): void { this._ctx.play(); }
pause(): void { this._ctx.pause(); }
stop(): void { this._ctx.stop(); }
seek(position: number): void { this._ctx.seek(position); }
destroy(): void { this._ctx.destroy(); }
onPlay(callback: () => void): void { this._ctx.onPlay(callback); }
onPause(callback: () => void): void { this._ctx.onPause(callback); }
onStop(callback: () => void): void { this._ctx.onStop(callback); }
onEnded(callback: () => void): void { this._ctx.onEnded(callback); }
onError(callback: (error: { errCode: number; errMsg: string }) => void): void {
this._ctx.onError(callback as any);
}
onTimeUpdate(callback: () => void): void { this._ctx.onTimeUpdate(callback); }
onCanplay(callback: () => void): void { this._ctx.onCanplay(callback); }
onSeeking(callback: () => void): void { this._ctx.onSeeking(callback); }
onSeeked(callback: () => void): void { this._ctx.onSeeked(callback); }
offPlay(callback: () => void): void { this._ctx.offPlay(callback); }
offPause(callback: () => void): void { this._ctx.offPause(callback); }
offStop(callback: () => void): void { this._ctx.offStop(callback); }
offEnded(callback: () => void): void { this._ctx.offEnded(callback); }
offError(callback: (error: { errCode: number; errMsg: string }) => void): void {
this._ctx.offError(callback as any);
}
offTimeUpdate(callback: () => void): void { this._ctx.offTimeUpdate(callback); }
}
/**
* 微信小游戏音频子系统实现
*/
export class WeChatAudioSubsystem implements IPlatformAudioSubsystem {
createAudioContext(_options?: { useWebAudioImplement?: boolean }): IPlatformAudioContext {
const ctx = getWx().createInnerAudioContext({
useWebAudioImplement: _options?.useWebAudioImplement
});
return new WeChatAudioContext(ctx);
}
getSupportedFormats(): string[] {
return ['mp3', 'wav', 'aac', 'm4a'];
}
async setInnerAudioOption(options: {
mixWithOther?: boolean;
obeyMuteSwitch?: boolean;
speakerOn?: boolean;
}): Promise<void> {
return promisify(getWx().setInnerAudioOption.bind(getWx()), options);
}
}

View File

@@ -0,0 +1,209 @@
/**
* 微信小游戏 Canvas 子系统
*/
import type {
IPlatformCanvasSubsystem,
IPlatformCanvas,
IPlatformImage,
TempFilePathOptions,
CanvasContextAttributes
} from '@esengine/platform-common';
import { getWx } from '../utils';
/**
* 微信小游戏 Canvas 包装
*/
class WeChatCanvas implements IPlatformCanvas {
private _canvas: WechatMinigame.Canvas;
constructor(canvas: WechatMinigame.Canvas) {
this._canvas = canvas;
}
get width(): number {
return this._canvas.width;
}
set width(value: number) {
this._canvas.width = value;
}
get height(): number {
return this._canvas.height;
}
set height(value: number) {
this._canvas.height = value;
}
getContext(
contextType: '2d' | 'webgl' | 'webgl2',
contextAttributes?: CanvasContextAttributes
): RenderingContext | null {
const wxAttributes: WechatMinigame.ContextAttributes | undefined = contextAttributes ? {
alpha: typeof contextAttributes.alpha === 'boolean'
? (contextAttributes.alpha ? 1 : 0)
: contextAttributes.alpha,
antialias: contextAttributes.antialias,
preserveDrawingBuffer: contextAttributes.preserveDrawingBuffer,
antialiasSamples: contextAttributes.antialiasSamples
} : undefined;
return this._canvas.getContext(contextType, wxAttributes);
}
toDataURL(): string {
return this._canvas.toDataURL();
}
toTempFilePath(options: TempFilePathOptions): void {
this._canvas.toTempFilePath({
x: options.x,
y: options.y,
width: options.width,
height: options.height,
destWidth: options.destWidth,
destHeight: options.destHeight,
fileType: options.fileType,
quality: options.quality,
success: options.success,
fail: options.fail,
complete: options.complete
});
}
/**
* 获取原始微信 Canvas 对象
*/
getNativeCanvas(): WechatMinigame.Canvas {
return this._canvas;
}
}
/**
* 微信小游戏 Image 包装
*/
class WeChatImage implements IPlatformImage {
private _image: WechatMinigame.Image;
constructor(image: WechatMinigame.Image) {
this._image = image;
}
get src(): string {
return this._image.src;
}
set src(value: string) {
this._image.src = value;
}
get width(): number {
return this._image.width;
}
get height(): number {
return this._image.height;
}
get onload(): (() => void) | null {
return this._image.onload as (() => void) | null;
}
set onload(value: (() => void) | null) {
this._image.onload = value as any;
}
get onerror(): ((error: any) => void) | null {
return this._image.onerror as ((error: any) => void) | null;
}
set onerror(value: ((error: any) => void) | null) {
this._image.onerror = value as any;
}
/**
* 获取原始微信 Image 对象
*/
getNativeImage(): WechatMinigame.Image {
return this._image;
}
}
/**
* 微信小游戏 Canvas 子系统实现
*/
export class WeChatCanvasSubsystem implements IPlatformCanvasSubsystem {
private _mainCanvas: WeChatCanvas | null = null;
private _windowInfo: WechatMinigame.WindowInfo;
constructor() {
this._windowInfo = getWx().getWindowInfo();
}
createCanvas(width?: number, height?: number): IPlatformCanvas {
const canvas = getWx().createCanvas();
// 设置尺寸
if (width !== undefined) {
canvas.width = width;
}
if (height !== undefined) {
canvas.height = height;
}
const wrappedCanvas = new WeChatCanvas(canvas);
// 首次创建的是主 Canvas
if (!this._mainCanvas) {
this._mainCanvas = wrappedCanvas;
}
return wrappedCanvas;
}
createImage(): IPlatformImage {
const image = getWx().createImage();
return new WeChatImage(image);
}
createImageData(width: number, height: number): ImageData {
// 微信小游戏 3.4.10+ 支持 createImageData
if (typeof getWx().createImageData === 'function') {
return getWx().createImageData(width, height) as unknown as ImageData;
}
// 降级方案:创建标准 ImageData
const data = new Uint8ClampedArray(width * height * 4);
return {
data,
width,
height,
colorSpace: 'srgb'
} as ImageData;
}
getScreenWidth(): number {
return this._windowInfo.screenWidth;
}
getScreenHeight(): number {
return this._windowInfo.screenHeight;
}
getDevicePixelRatio(): number {
return this._windowInfo.pixelRatio;
}
getMainCanvas(): IPlatformCanvas | null {
return this._mainCanvas;
}
getWindowWidth(): number {
return this._windowInfo.windowWidth;
}
getWindowHeight(): number {
return this._windowInfo.windowHeight;
}
}

View File

@@ -0,0 +1,204 @@
/**
* 微信小游戏文件系统子系统
*/
import type {
IPlatformFileSubsystem,
FileInfo
} from '@esengine/platform-common';
import { getWx } from '../utils';
/**
* 微信小游戏文件系统子系统实现
*/
export class WeChatFileSubsystem implements IPlatformFileSubsystem {
private _fs: WechatMinigame.FileSystemManager;
constructor() {
this._fs = getWx().getFileSystemManager();
}
async readFile(options: {
filePath: string;
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8';
position?: number;
length?: number;
}): Promise<string | ArrayBuffer> {
return new Promise((resolve, reject) => {
this._fs.readFile({
filePath: options.filePath,
encoding: options.encoding as any,
position: options.position,
length: options.length,
success: (res) => resolve(res.data),
fail: reject
});
});
}
readFileSync(
filePath: string,
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8',
position?: number,
length?: number
): string | ArrayBuffer {
return this._fs.readFileSync(filePath, encoding as any, position, length);
}
async writeFile(options: {
filePath: string;
data: string | ArrayBuffer;
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8';
}): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.writeFile({
filePath: options.filePath,
data: options.data,
encoding: options.encoding as any,
success: () => resolve(),
fail: reject
});
});
}
writeFileSync(
filePath: string,
data: string | ArrayBuffer,
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8'
): void {
this._fs.writeFileSync(filePath, data, encoding as any);
}
async appendFile(options: {
filePath: string;
data: string | ArrayBuffer;
encoding?: 'ascii' | 'base64' | 'binary' | 'hex' | 'ucs2' | 'utf-8' | 'utf8';
}): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.appendFile({
filePath: options.filePath,
data: options.data,
encoding: options.encoding as any,
success: () => resolve(),
fail: reject
});
});
}
async unlink(filePath: string): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.unlink({
filePath,
success: () => resolve(),
fail: reject
});
});
}
async mkdir(options: {
dirPath: string;
recursive?: boolean;
}): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.mkdir({
dirPath: options.dirPath,
recursive: options.recursive,
success: () => resolve(),
fail: reject
});
});
}
async rmdir(options: {
dirPath: string;
recursive?: boolean;
}): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.rmdir({
dirPath: options.dirPath,
recursive: options.recursive,
success: () => resolve(),
fail: reject
});
});
}
async readdir(dirPath: string): Promise<string[]> {
return new Promise((resolve, reject) => {
this._fs.readdir({
dirPath,
success: (res) => resolve(res.files),
fail: reject
});
});
}
async stat(path: string): Promise<FileInfo> {
return new Promise((resolve, reject) => {
this._fs.stat({
path,
success: (res) => {
const stats = res.stats as WechatMinigame.Stats;
resolve({
size: stats.size,
createTime: stats.lastAccessedTime,
modifyTime: stats.lastModifiedTime,
isDirectory: stats.isDirectory(),
isFile: stats.isFile()
});
},
fail: reject
});
});
}
async access(path: string): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.access({
path,
success: () => resolve(),
fail: reject
});
});
}
async rename(oldPath: string, newPath: string): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.rename({
oldPath,
newPath,
success: () => resolve(),
fail: reject
});
});
}
async copyFile(srcPath: string, destPath: string): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.copyFile({
srcPath,
destPath,
success: () => resolve(),
fail: reject
});
});
}
getUserDataPath(): string {
return `${getWx().env.USER_DATA_PATH}`;
}
async unzip(options: {
zipFilePath: string;
targetPath: string;
}): Promise<void> {
return new Promise((resolve, reject) => {
this._fs.unzip({
zipFilePath: options.zipFilePath,
targetPath: options.targetPath,
success: () => resolve(),
fail: reject
});
});
}
}

View File

@@ -0,0 +1,77 @@
/**
* 微信小游戏输入子系统
*/
import type {
IPlatformInputSubsystem,
TouchHandler,
TouchEvent
} from '@esengine/platform-common';
import { getWx } from '../utils';
/**
* 微信小游戏输入子系统实现
*/
export class WeChatInputSubsystem implements IPlatformInputSubsystem {
onTouchStart(handler: TouchHandler): void {
getWx().onTouchStart((res) => {
handler(this.convertTouchEvent(res));
});
}
onTouchMove(handler: TouchHandler): void {
getWx().onTouchMove((res) => {
handler(this.convertTouchEvent(res));
});
}
onTouchEnd(handler: TouchHandler): void {
getWx().onTouchEnd((res) => {
handler(this.convertTouchEvent(res));
});
}
onTouchCancel(handler: TouchHandler): void {
getWx().onTouchCancel((res) => {
handler(this.convertTouchEvent(res));
});
}
offTouchStart(handler: TouchHandler): void {
getWx().offTouchStart(handler as any);
}
offTouchMove(handler: TouchHandler): void {
getWx().offTouchMove(handler as any);
}
offTouchEnd(handler: TouchHandler): void {
getWx().offTouchEnd(handler as any);
}
offTouchCancel(handler: TouchHandler): void {
getWx().offTouchCancel(handler as any);
}
supportsPressure(): boolean {
return true;
}
private convertTouchEvent(res: WechatMinigame.OnTouchStartListenerResult): TouchEvent {
return {
touches: res.touches.map((t: WechatMinigame.Touch) => ({
identifier: t.identifier,
x: t.clientX,
y: t.clientY,
force: t.force
})),
changedTouches: res.changedTouches.map((t: WechatMinigame.Touch) => ({
identifier: t.identifier,
x: t.clientX,
y: t.clientY,
force: t.force
})),
timeStamp: res.timeStamp
};
}
}

View File

@@ -0,0 +1,181 @@
/**
* 微信小游戏网络子系统
*/
import type {
IPlatformNetworkSubsystem,
RequestConfig,
RequestResponse,
IDownloadTask,
IUploadTask,
IPlatformWebSocket
} from '@esengine/platform-common';
import { getWx, promisify } from '../utils';
/**
* 微信 WebSocket 包装
*/
class WeChatWebSocket implements IPlatformWebSocket {
private _task: WechatMinigame.SocketTask;
constructor(task: WechatMinigame.SocketTask) {
this._task = task;
}
send(data: string | ArrayBuffer): void {
this._task.send({ data });
}
close(code?: number, reason?: string): void {
this._task.close({ code, reason });
}
onOpen(callback: (res: { header: Record<string, string> }) => void): void {
this._task.onOpen(callback as any);
}
onClose(callback: (res: { code: number; reason: string }) => void): void {
this._task.onClose(callback as any);
}
onError(callback: (error: any) => void): void {
this._task.onError(callback);
}
onMessage(callback: (res: { data: string | ArrayBuffer }) => void): void {
this._task.onMessage(callback as any);
}
}
/**
* 微信小游戏网络子系统实现
*/
export class WeChatNetworkSubsystem implements IPlatformNetworkSubsystem {
async request<T = any>(config: RequestConfig): Promise<RequestResponse<T>> {
return new Promise((resolve, reject) => {
getWx().request({
url: config.url,
method: config.method as any,
data: config.data,
header: config.header,
timeout: config.timeout,
dataType: config.dataType as any,
responseType: config.responseType as any,
success: (res) => {
resolve({
data: res.data as T,
statusCode: res.statusCode,
header: res.header as Record<string, string>
});
},
fail: reject
});
});
}
downloadFile(options: {
url: string;
filePath?: string;
header?: Record<string, string>;
timeout?: number;
}): Promise<{ tempFilePath: string; filePath?: string; statusCode: number }> & IDownloadTask {
const task = getWx().downloadFile({
url: options.url,
filePath: options.filePath,
header: options.header,
timeout: options.timeout,
success: () => {},
fail: () => {}
});
const promise = new Promise<{ tempFilePath: string; filePath?: string; statusCode: number }>((resolve, reject) => {
task.onProgressUpdate(() => {});
getWx().downloadFile({
...options,
success: (res) => resolve({
tempFilePath: res.tempFilePath,
filePath: res.filePath,
statusCode: res.statusCode
}),
fail: reject
});
});
return Object.assign(promise, {
abort: () => task.abort(),
onProgressUpdate: (callback: any) => task.onProgressUpdate(callback),
offProgressUpdate: (callback: any) => task.offProgressUpdate(callback)
});
}
uploadFile(options: {
url: string;
filePath: string;
name: string;
header?: Record<string, string>;
formData?: Record<string, any>;
timeout?: number;
}): Promise<{ data: string; statusCode: number }> & IUploadTask {
const task = getWx().uploadFile({
url: options.url,
filePath: options.filePath,
name: options.name,
header: options.header,
formData: options.formData,
timeout: options.timeout,
success: () => {},
fail: () => {}
});
const promise = new Promise<{ data: string; statusCode: number }>((resolve, reject) => {
getWx().uploadFile({
...options,
success: (res) => resolve({
data: res.data,
statusCode: res.statusCode
}),
fail: reject
});
});
return Object.assign(promise, {
abort: () => task.abort(),
onProgressUpdate: (callback: any) => task.onProgressUpdate(callback),
offProgressUpdate: (callback: any) => task.offProgressUpdate(callback)
});
}
connectSocket(options: {
url: string;
header?: Record<string, string>;
protocols?: string[];
timeout?: number;
}): IPlatformWebSocket {
const task = getWx().connectSocket({
url: options.url,
header: options.header,
protocols: options.protocols,
timeout: options.timeout
});
return new WeChatWebSocket(task);
}
async getNetworkType(): Promise<'wifi' | '2g' | '3g' | '4g' | '5g' | 'unknown' | 'none'> {
const res = await promisify<{ networkType: string }>(
getWx().getNetworkType.bind(getWx()),
{}
);
return res.networkType as any;
}
onNetworkStatusChange(callback: (res: {
isConnected: boolean;
networkType: string;
}) => void): void {
getWx().onNetworkStatusChange(callback);
}
offNetworkStatusChange(callback: Function): void {
getWx().offNetworkStatusChange(callback as any);
}
}

View File

@@ -0,0 +1,67 @@
/**
* 微信小游戏存储子系统
*/
import type {
IPlatformStorageSubsystem,
StorageInfo
} from '@esengine/platform-common';
import { getWx, promisify } from '../utils';
/**
* 微信小游戏存储子系统实现
*/
export class WeChatStorageSubsystem implements IPlatformStorageSubsystem {
getStorageSync<T = any>(key: string): T | undefined {
try {
return getWx().getStorageSync<T>(key);
} catch {
return undefined;
}
}
setStorageSync<T = any>(key: string, value: T): void {
getWx().setStorageSync(key, value);
}
removeStorageSync(key: string): void {
getWx().removeStorageSync(key);
}
clearStorageSync(): void {
getWx().clearStorageSync();
}
getStorageInfoSync(): StorageInfo {
const info = getWx().getStorageInfoSync();
return {
keys: info.keys,
currentSize: info.currentSize,
limitSize: info.limitSize
};
}
async getStorage<T = any>(key: string): Promise<T | undefined> {
try {
const res = await promisify<{ data: T }>(
getWx().getStorage.bind(getWx()),
{ key }
);
return res.data;
} catch {
return undefined;
}
}
async setStorage<T = any>(key: string, value: T): Promise<void> {
await promisify(getWx().setStorage.bind(getWx()), { key, data: value });
}
async removeStorage(key: string): Promise<void> {
await promisify(getWx().removeStorage.bind(getWx()), { key });
}
async clearStorage(): Promise<void> {
await promisify(getWx().clearStorage.bind(getWx()), {});
}
}

View File

@@ -0,0 +1,65 @@
/**
* 微信小游戏 WASM 子系统
*/
import type {
IPlatformWASMSubsystem,
IWASMInstance,
WASMImports,
WASMExports
} from '@esengine/platform-common';
/**
* 微信小游戏 WASM 子系统实现
*/
export class WeChatWASMSubsystem implements IPlatformWASMSubsystem {
async instantiate(path: string, imports?: WASMImports): Promise<IWASMInstance> {
// 微信小游戏使用 WXWebAssembly.instantiate
// path 应该是相对于小游戏根目录的 .wasm 文件路径
if (typeof WXWebAssembly === 'undefined') {
throw new Error('当前微信基础库版本不支持 WebAssembly');
}
const wxImports: WXWebAssembly.Imports | undefined = imports as WXWebAssembly.Imports | undefined;
const instance = await WXWebAssembly.instantiate(path, wxImports);
return {
exports: instance.exports as WASMExports
};
}
isSupported(): boolean {
return typeof WXWebAssembly !== 'undefined';
}
/**
* 获取 WASM 内存
* 用于 Rust/WASM 引擎的内存交互
*/
createMemory(initial: number, maximum?: number): WebAssembly.Memory {
if (typeof WXWebAssembly === 'undefined') {
throw new Error('当前微信基础库版本不支持 WebAssembly');
}
return new WXWebAssembly.Memory({
initial,
maximum,
shared: false // 微信小游戏不支持 shared memory
});
}
/**
* 创建 WASM Table
*/
createTable(initial: number, maximum?: number): WebAssembly.Table {
if (typeof WXWebAssembly === 'undefined') {
throw new Error('当前微信基础库版本不支持 WebAssembly');
}
return new WXWebAssembly.Table({
element: 'anyfunc',
initial,
maximum
});
}
}

View File

@@ -0,0 +1,27 @@
/**
* 微信小游戏类型定义扩展
* 补充官方类型定义包缺失的 API
*/
declare namespace WechatMinigame {
interface Wx {
/**
* 判断小程序的 API回调参数组件等是否在当前版本可用
* @param schema 使用 ${API}.${method}.${param}.${option} 或者 ${component}.${attribute}.${option} 方式来调用
* @returns 当前版本是否可用
*
* @example
* ```typescript
* // 对象的属性或方法
* wx.canIUse('console.log')
* wx.canIUse('CameraContext.onCameraFrame')
*
* // wx接口参数、回调或者返回值
* wx.canIUse('openBluetoothAdapter')
* wx.canIUse('getSystemInfoSync.return.safeArea.left')
* wx.canIUse('showToast.object.image')
* ```
*/
canIUse(schema: string): boolean;
}
}

View File

@@ -0,0 +1,46 @@
/**
* 微信小游戏工具函数
*/
/**
* 获取微信全局对象
*/
export function getWx(): WechatMinigame.Wx {
if (typeof wx === 'undefined') {
throw new Error('当前环境不是微信小游戏环境');
}
return wx;
}
/**
* 检测当前是否为微信小游戏环境
*/
export function isWeChatMiniGame(): boolean {
try {
if (typeof wx === 'undefined') {
return false;
}
const wxObj = wx as WechatMinigame.Wx;
return typeof wxObj.getWindowInfo === 'function' &&
typeof wxObj.createCanvas === 'function' &&
typeof wxObj.createImage === 'function';
} catch {
return false;
}
}
/**
* 将微信回调风格 API 转换为 Promise
*/
export function promisify<T>(
fn: (options: any) => void,
options: any = {}
): Promise<T> {
return new Promise((resolve, reject) => {
fn({
...options,
success: (res: T) => resolve(res),
fail: (err: any) => reject(err)
});
});
}

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false,
"types": ["minigame-api-typings"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}