Skip to content

Commit 2bbfbab

Browse files
committed
fix: improve WeChat MiniGame platform adapter and refactor SDK defaults
- Refactor platform types and add WeChat runtime entry point - Extract component defaults to dedicated module - Improve WeChat fs error messages and asset path handling - Simplify builder templates and add playable emitter tests - Sync SDK build artifacts to desktop
1 parent dacd828 commit 2bbfbab

23 files changed

Lines changed: 506 additions & 339 deletions

File tree

desktop/public/sdk/cjs/esengine.wechat.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/public/sdk/cjs/index.wechat.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/public/sdk/esm/esengine.d.ts

Lines changed: 23 additions & 39 deletions
Large diffs are not rendered by default.

desktop/public/sdk/esm/esengine.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/public/sdk/esm/physics/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

desktop/public/sdk/esm/spine/index.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

editor/src/__tests__/builder/playableEmitter.test.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2+
import { toBuildPath } from 'esengine';
23

34
/**
45
* Extracts the asset-skip logic from PlayableEmitter.collectInlineAssets
@@ -37,3 +38,119 @@ describe('PlayableEmitter: packed texture embedding', () => {
3738
expect(shouldIncludeAsset('assets/prefabs/Star.esprefab', packedPaths)).toBe(true);
3839
});
3940
});
41+
42+
describe('PlayableEmitter: embedded asset path resolution', () => {
43+
it('should find prefabs stored with build path when looked up with original extension', () => {
44+
const embedded = new Map<string, string>();
45+
// PlayableEmitter stores with toBuildPath key: .esprefab -> .json
46+
embedded.set('assets/prefabs/Star.json', 'data:application/json;base64,abc');
47+
48+
// Runtime looks up with original .esprefab path
49+
const localPath = 'assets/prefabs/Star.esprefab';
50+
const buildPath = toBuildPath(localPath);
51+
52+
// Direct lookup fails (this is the current bug)
53+
expect(embedded.get(localPath)).toBeUndefined();
54+
// Build path lookup should work
55+
expect(embedded.get(buildPath)).toBe('data:application/json;base64,abc');
56+
// toBuildPath correctly converts .esprefab to .json
57+
expect(buildPath).toBe('assets/prefabs/Star.json');
58+
});
59+
60+
it('should find prefabs with leading slash after normalization', () => {
61+
const embedded = new Map<string, string>();
62+
embedded.set('assets/prefabs/EnemyA.json', 'data:application/json;base64,def');
63+
64+
// Runtime passes /assets/prefabs/EnemyA.esprefab → toLocalPath strips /
65+
const runtimePath = '/assets/prefabs/EnemyA.esprefab';
66+
const localPath = runtimePath.startsWith('/') ? runtimePath.substring(1) : runtimePath;
67+
const buildPath = toBuildPath(localPath);
68+
69+
expect(embedded.get(localPath)).toBeUndefined();
70+
expect(embedded.get(buildPath)).toBe('data:application/json;base64,def');
71+
});
72+
73+
it('should still find assets stored with original extension', () => {
74+
const embedded = new Map<string, string>();
75+
embedded.set('assets/textures/bg.png', 'data:image/png;base64,xyz');
76+
77+
// Non-custom extensions: toBuildPath is identity
78+
const localPath = 'assets/textures/bg.png';
79+
expect(toBuildPath(localPath)).toBe(localPath);
80+
expect(embedded.get(localPath)).toBe('data:image/png;base64,xyz');
81+
});
82+
});
83+
84+
describe('WeChat runtime: manifest path resolution', () => {
85+
function buildManifestIndex(groups: Record<string, Record<string, { path: string }>>) {
86+
const assetIndex: Record<string, { path: string }> = {};
87+
const pathIndex: Record<string, { path: string }> = {};
88+
for (const groupName in groups) {
89+
const assets = groups[groupName];
90+
for (const uuid in assets) {
91+
const entry = assets[uuid];
92+
assetIndex[uuid] = entry;
93+
pathIndex[entry.path] = entry;
94+
}
95+
}
96+
return { assetIndex, pathIndex };
97+
}
98+
99+
function createPathResolver(index: ReturnType<typeof buildManifestIndex>) {
100+
const { assetIndex, pathIndex } = index;
101+
return (ref: string): string => {
102+
const resolved = toBuildPath(ref);
103+
const entry = assetIndex[ref] || assetIndex[resolved]
104+
|| pathIndex[resolved] || pathIndex[ref];
105+
return entry ? entry.path : resolved;
106+
};
107+
}
108+
109+
it('should resolve prefab path via toBuildPath when referenced with custom extension', () => {
110+
const index = buildManifestIndex({
111+
default: {
112+
'uuid-1': { path: 'assets/prefabs/Star.json' },
113+
},
114+
});
115+
const resolve = createPathResolver(index);
116+
117+
expect(resolve('assets/prefabs/Star.esprefab')).toBe('assets/prefabs/Star.json');
118+
});
119+
120+
it('should resolve prefab path when referenced with build extension', () => {
121+
const index = buildManifestIndex({
122+
default: {
123+
'uuid-1': { path: 'assets/prefabs/Star.json' },
124+
},
125+
});
126+
const resolve = createPathResolver(index);
127+
128+
expect(resolve('assets/prefabs/Star.json')).toBe('assets/prefabs/Star.json');
129+
});
130+
131+
it('should resolve by UUID', () => {
132+
const index = buildManifestIndex({
133+
default: {
134+
'abc-123': { path: 'assets/prefabs/Enemy.json' },
135+
},
136+
});
137+
const resolve = createPathResolver(index);
138+
139+
expect(resolve('abc-123')).toBe('assets/prefabs/Enemy.json');
140+
});
141+
142+
it('should fall back to toBuildPath for unknown refs', () => {
143+
const index = buildManifestIndex({ default: {} });
144+
const resolve = createPathResolver(index);
145+
146+
expect(resolve('assets/prefabs/Missing.esprefab')).toBe('assets/prefabs/Missing.json');
147+
expect(resolve('assets/textures/bg.png')).toBe('assets/textures/bg.png');
148+
});
149+
150+
it('should apply toBuildPath before wxReadTextFile to avoid double conversion', () => {
151+
const ref = 'assets/prefabs/Star.esprefab';
152+
const buildPath = toBuildPath(ref);
153+
expect(buildPath).toBe('assets/prefabs/Star.json');
154+
expect(toBuildPath(buildPath)).toBe('assets/prefabs/Star.json');
155+
});
156+
});

editor/src/builder/templates.ts

Lines changed: 15 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @brief Build output templates for platform emitters
44
*/
55

6-
import { getCustomExtensions, getAssetTypeEntry } from 'esengine';
6+
import type { RuntimeBuildConfig } from './BuildService';
77

88
// =============================================================================
99
// Playable HTML Template
@@ -81,219 +81,35 @@ export interface WeChatGameJsParams {
8181
hasSpine: boolean;
8282
hasPhysics: boolean;
8383
physicsConfig: string;
84-
runtimeConfig?: {
85-
sceneTransitionDuration?: number;
86-
sceneTransitionColor?: string;
87-
defaultFontFamily?: string;
88-
canvasScaleMode?: string;
89-
canvasMatchWidthOrHeight?: number;
90-
maxDeltaTime?: number;
91-
maxFixedSteps?: number;
92-
textCanvasSize?: number;
93-
};
94-
}
95-
96-
function generateRuntimeConfigBlock(rc?: WeChatGameJsParams['runtimeConfig']): string {
97-
if (!rc) return '';
98-
const lines: string[] = [];
99-
if (rc.maxDeltaTime !== undefined) {
100-
lines.push(` SDK.RuntimeConfig.maxDeltaTime = ${rc.maxDeltaTime};`);
101-
lines.push(` app.setMaxDeltaTime(${rc.maxDeltaTime});`);
102-
}
103-
if (rc.maxFixedSteps !== undefined) {
104-
lines.push(` SDK.RuntimeConfig.maxFixedSteps = ${rc.maxFixedSteps};`);
105-
lines.push(` app.setMaxFixedSteps(${rc.maxFixedSteps});`);
106-
}
107-
if (rc.textCanvasSize !== undefined) lines.push(` SDK.RuntimeConfig.textCanvasSize = ${rc.textCanvasSize};`);
108-
if (rc.defaultFontFamily !== undefined) lines.push(` SDK.RuntimeConfig.defaultFontFamily = ${JSON.stringify(rc.defaultFontFamily)};`);
109-
if (rc.sceneTransitionDuration !== undefined) lines.push(` SDK.RuntimeConfig.sceneTransitionDuration = ${rc.sceneTransitionDuration};`);
110-
if (rc.sceneTransitionColor) {
111-
const hex = rc.sceneTransitionColor.replace('#', '');
112-
const r = parseInt(hex.substring(0, 2), 16) / 255;
113-
const g = parseInt(hex.substring(2, 4), 16) / 255;
114-
const b = parseInt(hex.substring(4, 6), 16) / 255;
115-
lines.push(` SDK.RuntimeConfig.sceneTransitionColor = {r:${r},g:${g},b:${b},a:1};`);
116-
}
117-
if (rc.canvasScaleMode !== undefined) {
118-
const modeMap: Record<string, number> = { FixedWidth: 0, FixedHeight: 1, Expand: 2, Shrink: 3, Match: 4 };
119-
lines.push(` SDK.RuntimeConfig.canvasScaleMode = ${modeMap[rc.canvasScaleMode] ?? 1};`);
120-
}
121-
if (rc.canvasMatchWidthOrHeight !== undefined) lines.push(` SDK.RuntimeConfig.canvasMatchWidthOrHeight = ${rc.canvasMatchWidthOrHeight};`);
122-
return lines.join('\n');
84+
runtimeConfig?: RuntimeBuildConfig;
12385
}
12486

12587
export function generateWeChatGameJs(params: WeChatGameJsParams): string {
12688
const { userCode, firstSceneName, allSceneNames, hasSpine, hasPhysics, physicsConfig, runtimeConfig } = params;
127-
const runtimeConfigBlock = generateRuntimeConfigBlock(runtimeConfig);
128-
129-
const spineInit = hasSpine ? `
130-
async function initSpineModule() {
131-
try {
132-
var SpineFactory = require('./spine.js');
133-
spineModule = await SpineFactory({
134-
instantiateWasm: function(imports, successCallback) {
135-
WXWebAssembly.instantiate('spine.wasm', imports).then(function(result) {
136-
successCallback(result.instance, result.module);
137-
});
138-
return {};
139-
}
140-
});
141-
} catch(e) { console.warn('Spine module not available:', e); }
142-
}` : '';
14389

144-
const physicsInit = hasPhysics ? `
145-
async function initPhysicsModule() {
146-
try {
147-
var PhysicsFactory = require('./physics.js');
148-
physicsModule = await PhysicsFactory({
149-
instantiateWasm: function(imports, successCallback) {
150-
WXWebAssembly.instantiate('physics.wasm', imports).then(function(result) {
151-
successCallback(result.instance, result.module);
152-
});
153-
return {};
154-
}
155-
});
156-
} catch(e) { console.warn('Physics module not available:', e); }
157-
}` : '';
158-
159-
const sceneNamesArray = JSON.stringify(allSceneNames);
160-
161-
const sceneLoading = firstSceneName ? `
162-
try {
163-
var provider = {
164-
loadPixels: function(ref) { return SDK.wxLoadImagePixels(resolvePath(ref)); },
165-
loadPixelsRaw: function(ref) { return SDK.wxLoadImagePixels(resolvePath(ref)); },
166-
readText: function(ref) {
167-
return new Promise(function(resolve, reject) {
168-
wxfs.readFile({ filePath: resolvePath(ref), encoding: 'utf-8',
169-
success: function(res) { resolve(res.data); },
170-
fail: function(err) { reject(new Error(err.errMsg)); }
171-
});
172-
});
173-
},
174-
readBinary: function(ref) {
175-
return new Promise(function(resolve, reject) {
176-
wxfs.readFile({ filePath: resolvePath(ref),
177-
success: function(res) { resolve(new Uint8Array(res.data)); },
178-
fail: function(err) { reject(new Error(err.errMsg)); }
179-
});
180-
});
181-
},
182-
resolvePath: resolvePath
183-
};
184-
185-
function readSceneFile(name) {
186-
return new Promise(function(resolve, reject) {
187-
wxfs.readFile({ filePath: 'scenes/' + name + '.json', encoding: 'utf-8',
188-
success: function(res) { resolve(JSON.parse(res.data)); },
189-
fail: function(err) { reject(new Error(err.errMsg)); }
190-
});
191-
});
192-
}
193-
194-
var sceneNames = ${sceneNamesArray};
195-
var mgr = app.getResource(SDK.SceneManager);
196-
var sceneOpts = { app: app, module: module, provider: provider, spineModule: spineModule, physicsModule: physicsModule, physicsConfig: ${physicsConfig}, manifest: manifest };
197-
198-
for (var i = 0; i < sceneNames.length; i++) {
199-
var sd = await readSceneFile(sceneNames[i]);
200-
mgr.register(SDK.createRuntimeSceneConfig(sceneNames[i], sd, sceneOpts));
201-
}
202-
mgr.setInitial('${firstSceneName}');
203-
await mgr.load('${firstSceneName}');
204-
205-
var screenAspect = canvas.width / canvas.height;
206-
SDK.updateCameraAspectRatio(app.world, screenAspect);
207-
} catch (err) {
208-
console.error('[ESEngine] Failed to load scene:', err);
209-
}
210-
` : '';
90+
const runtimeConfigJson = runtimeConfig ? JSON.stringify(runtimeConfig) : 'undefined';
21191

21292
return `
21393
var ESEngineModule = require('./esengine.js');
21494
var SDK = require('./sdk.js');
21595
globalThis.__esengine_sdk = SDK;
21696
217-
var spineModule = null;
218-
${spineInit}
219-
220-
var physicsModule = null;
221-
${physicsInit}
97+
${userCode}
22298
22399
(async function() {
224-
var wxfs = wx.getFileSystemManager();
225-
var manifest = await new Promise(function(resolve, reject) {
226-
wxfs.readFile({ filePath: 'asset-manifest.json', encoding: 'utf-8',
227-
success: function(res) { resolve(JSON.parse(res.data)); },
228-
fail: function(err) { reject(new Error(err.errMsg)); }
100+
try {
101+
await SDK.initWeChatRuntime({
102+
engineFactory: ESEngineModule,
103+
sceneNames: ${JSON.stringify(allSceneNames)},
104+
firstScene: ${JSON.stringify(firstSceneName)},
105+
runtimeConfig: ${runtimeConfigJson},
106+
physicsConfig: ${physicsConfig},
107+
${hasSpine ? "spineFactory: require('./spine.js')," : ''}
108+
${hasPhysics ? "physicsFactory: require('./physics.js')," : ''}
229109
});
230-
});
231-
var assetIndex = {};
232-
var pathIndex = {};
233-
for (var gn in manifest.groups) {
234-
var g = manifest.groups[gn];
235-
for (var uuid in g.assets) {
236-
assetIndex[uuid] = g.assets[uuid];
237-
pathIndex[g.assets[uuid].path] = g.assets[uuid];
238-
}
239-
}
240-
var _jsonExts = ${JSON.stringify(getCustomExtensions().filter(e => getAssetTypeEntry(e)?.contentType === 'json'))};
241-
function toBuildPath(p) {
242-
for (var i = 0; i < _jsonExts.length; i++) {
243-
if (p.endsWith(_jsonExts[i])) return p.substring(0, p.length - _jsonExts[i].length) + '.json';
244-
}
245-
return p;
246-
}
247-
function resolvePath(ref) {
248-
var resolved = toBuildPath(ref);
249-
var entry = assetIndex[ref] || assetIndex[resolved] || pathIndex[resolved] || pathIndex[ref];
250-
return entry ? entry.path : resolved;
251-
}
252-
253-
var canvas = wx.createCanvas();
254-
var info = wx.getSystemInfoSync();
255-
canvas.width = info.windowWidth * info.pixelRatio;
256-
canvas.height = info.windowHeight * info.pixelRatio;
257-
258-
var module = await ESEngineModule({
259-
canvas: canvas,
260-
instantiateWasm: function(imports, successCallback) {
261-
WXWebAssembly.instantiate('esengine.wasm', imports).then(function(result) {
262-
successCallback(result.instance, result.module);
263-
});
264-
return {};
265-
}
266-
});
267-
268-
var gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
269-
if (!gl) {
270-
console.error('[ESEngine] Failed to create WebGL context');
271-
return;
110+
} catch (err) {
111+
console.error('[ESEngine] Runtime init error:', err);
272112
}
273-
var glHandle = module.GL.registerContext(gl, {
274-
majorVersion: gl.getParameter(gl.VERSION).indexOf('WebGL 2') === 0 ? 2 : 1,
275-
minorVersion: 0,
276-
enableExtensionsByDefault: true
277-
});
278-
279-
var app = SDK.createWebApp(module, {
280-
glContextHandle: glHandle,
281-
getViewportSize: function() {
282-
return { width: canvas.width, height: canvas.height };
283-
}
284-
});
285-
286-
${runtimeConfigBlock}
287-
288-
${userCode}
289-
290-
SDK.flushPendingSystems(app);
291-
292-
${hasSpine ? 'await initSpineModule();' : ''}
293-
${hasPhysics ? 'await initPhysicsModule();' : ''}
294-
295-
${sceneLoading}
296-
app.run();
297113
})();
298114
`;
299115
}

sdk/src/app.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -768,8 +768,8 @@ export function createWebApp(module: ESEngineModule, options?: WebAppOptions): A
768768
}
769769

770770
export function flushPendingSystems(app: App): void {
771-
if (typeof window === 'undefined') return;
772-
const pending = window.__esengine_pendingSystems;
771+
const g = globalThis as any;
772+
const pending = g.__esengine_pendingSystems;
773773
if (!pending || pending.length === 0) return;
774774

775775
for (const entry of pending) {

0 commit comments

Comments
 (0)