@playcraft/cli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/build-config.js +26 -0
- package/dist/commands/build.js +363 -0
- package/dist/commands/config.js +133 -0
- package/dist/commands/init.js +86 -0
- package/dist/commands/inspect.js +209 -0
- package/dist/commands/logs.js +121 -0
- package/dist/commands/start.js +284 -0
- package/dist/commands/status.js +106 -0
- package/dist/commands/stop.js +58 -0
- package/dist/config.js +31 -0
- package/dist/fs-handler.js +83 -0
- package/dist/index.js +200 -0
- package/dist/logger.js +122 -0
- package/dist/playable/base-builder.js +265 -0
- package/dist/playable/builder.js +1462 -0
- package/dist/playable/converter.js +150 -0
- package/dist/playable/index.js +3 -0
- package/dist/playable/platforms/base.js +12 -0
- package/dist/playable/platforms/facebook.js +37 -0
- package/dist/playable/platforms/index.js +24 -0
- package/dist/playable/platforms/snapchat.js +59 -0
- package/dist/playable/playable-builder.js +521 -0
- package/dist/playable/types.js +1 -0
- package/dist/playable/vite/config-builder.js +136 -0
- package/dist/playable/vite/platform-configs.js +102 -0
- package/dist/playable/vite/plugin-model-compression.js +63 -0
- package/dist/playable/vite/plugin-platform.js +65 -0
- package/dist/playable/vite/plugin-playcanvas.js +454 -0
- package/dist/playable/vite-builder.js +125 -0
- package/dist/port-utils.js +27 -0
- package/dist/process-manager.js +96 -0
- package/dist/server.js +128 -0
- package/dist/socket.js +117 -0
- package/dist/watcher.js +33 -0
- package/package.json +41 -0
- package/templates/playable-ad.html +59 -0
|
@@ -0,0 +1,1462 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import archiver from 'archiver';
|
|
5
|
+
import { OnePageConverter } from './converter.js';
|
|
6
|
+
import { createPlatformAdapter } from './platforms/index.js';
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = path.dirname(__filename);
|
|
9
|
+
export class PlayableBuilder {
|
|
10
|
+
projectDir;
|
|
11
|
+
options;
|
|
12
|
+
manifest = null;
|
|
13
|
+
converter;
|
|
14
|
+
platformAdapter;
|
|
15
|
+
engineCode = '';
|
|
16
|
+
startScript = '';
|
|
17
|
+
modulesScript = ''; // __modules__.js 内容
|
|
18
|
+
configJson = null;
|
|
19
|
+
sizeReport = {
|
|
20
|
+
engine: 0,
|
|
21
|
+
assets: {},
|
|
22
|
+
total: 0,
|
|
23
|
+
limit: 0,
|
|
24
|
+
};
|
|
25
|
+
constructor(projectDir, options) {
|
|
26
|
+
this.projectDir = projectDir;
|
|
27
|
+
this.options = options;
|
|
28
|
+
this.converter = new OnePageConverter(projectDir, options);
|
|
29
|
+
this.platformAdapter = createPlatformAdapter(options);
|
|
30
|
+
this.sizeReport.limit = this.platformAdapter.getSizeLimit();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 加载项目
|
|
34
|
+
*/
|
|
35
|
+
async loadProject() {
|
|
36
|
+
// 优先检查是否是构建后的项目(推荐方式)
|
|
37
|
+
const isBuild = await this.detectBuildProject();
|
|
38
|
+
if (isBuild) {
|
|
39
|
+
console.log('✅ 检测到构建后的项目,使用构建版本(推荐)');
|
|
40
|
+
await this.loadBuildProject();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// 检测项目格式(源代码)
|
|
44
|
+
console.log('⚠️ 未检测到构建版本,将从源代码打包(可能不完整)');
|
|
45
|
+
const format = await this.detectProjectFormat();
|
|
46
|
+
if (format === 'playcraft') {
|
|
47
|
+
await this.loadPlayCraftProject();
|
|
48
|
+
}
|
|
49
|
+
else if (format === 'playcanvas') {
|
|
50
|
+
await this.loadPlayCanvasProject();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
throw new Error('无法识别项目格式。请确保项目包含 manifest.json(PlayCraft)或 assets.json(PlayCanvas)\n\n' +
|
|
54
|
+
'💡 推荐:先使用 PlayCanvas REST API 下载构建版本,然后再打包为 Playable Ad。');
|
|
55
|
+
}
|
|
56
|
+
if (!this.manifest) {
|
|
57
|
+
throw new Error('Manifest is null');
|
|
58
|
+
}
|
|
59
|
+
// 收集所有资产文件
|
|
60
|
+
// 注意:PlayCanvas 场景数据已在 convertPlayCanvasToManifest 中转换为 data URL
|
|
61
|
+
// 只需要添加资产文件
|
|
62
|
+
for (const asset of this.manifest.assets) {
|
|
63
|
+
if (asset.file) {
|
|
64
|
+
this.converter.addAsset(asset.path, asset.type);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 检测是否是构建后的项目
|
|
70
|
+
*/
|
|
71
|
+
async detectBuildProject() {
|
|
72
|
+
const buildIndicators = [
|
|
73
|
+
path.join(this.projectDir, 'index.html'),
|
|
74
|
+
path.join(this.projectDir, '__start__.js'),
|
|
75
|
+
path.join(this.projectDir, 'config.json'),
|
|
76
|
+
];
|
|
77
|
+
// 至少需要 index.html 和 config.json
|
|
78
|
+
try {
|
|
79
|
+
await fs.access(buildIndicators[0]);
|
|
80
|
+
await fs.access(buildIndicators[2]);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* 加载构建后的项目(官方推荐方式)
|
|
89
|
+
*/
|
|
90
|
+
async loadBuildProject() {
|
|
91
|
+
try {
|
|
92
|
+
// 读取 config.json
|
|
93
|
+
const configPath = path.join(this.projectDir, 'config.json');
|
|
94
|
+
const configContent = await fs.readFile(configPath, 'utf-8');
|
|
95
|
+
this.configJson = JSON.parse(configContent);
|
|
96
|
+
// 读取 __start__.js
|
|
97
|
+
const startScriptPath = path.join(this.projectDir, '__start__.js');
|
|
98
|
+
try {
|
|
99
|
+
const originalStartScript = await fs.readFile(startScriptPath, 'utf-8');
|
|
100
|
+
// 包装官方启动脚本,添加日志追踪
|
|
101
|
+
this.startScript = this.wrapOfficialStartScript(originalStartScript);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.warn('未找到 __start__.js,将使用默认启动脚本');
|
|
105
|
+
this.startScript = this.generateDefaultStartScript();
|
|
106
|
+
}
|
|
107
|
+
// 读取 __modules__.js(用于加载 WASM 模块如 Ammo.js)
|
|
108
|
+
const modulesScriptPath = path.join(this.projectDir, '__modules__.js');
|
|
109
|
+
try {
|
|
110
|
+
this.modulesScript = await fs.readFile(modulesScriptPath, 'utf-8');
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
// 如果没有 __modules__.js,使用默认的模块加载器
|
|
114
|
+
this.modulesScript = this.generateDefaultModulesScript();
|
|
115
|
+
}
|
|
116
|
+
// 从 config.json 构建 manifest
|
|
117
|
+
this.manifest = this.convertBuildConfigToManifest(this.configJson);
|
|
118
|
+
// 关键:将所有资产添加到 converter 以进行 Base64 编码
|
|
119
|
+
for (const asset of this.manifest.assets) {
|
|
120
|
+
if (asset.path) {
|
|
121
|
+
this.converter.addAsset(asset.path, asset.type);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// 也处理场景文件(如果不是 data URL)
|
|
125
|
+
for (const scene of this.manifest.scenes) {
|
|
126
|
+
if (scene.path && !scene.path.startsWith('data:')) {
|
|
127
|
+
this.converter.addAsset(scene.path, 'scene');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
console.log(`✅ 成功加载构建项目,共 ${this.manifest.assets.length} 个资产`);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
throw new Error(`无法加载构建项目: ${error.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 生成默认的模块加载脚本
|
|
138
|
+
*/
|
|
139
|
+
generateDefaultModulesScript() {
|
|
140
|
+
return `
|
|
141
|
+
// #region agent log
|
|
142
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:modulesScript:entry',message:'Modules script executing',data:{hasPc:typeof pc!=='undefined'},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
143
|
+
// #endregion
|
|
144
|
+
|
|
145
|
+
var loadModules = function (modules, urlPrefix, doneCallback) {
|
|
146
|
+
// #region agent log
|
|
147
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:modulesScript:loadModulesCalled',message:'loadModules function called',data:{modulesCount:modules?modules.length:0,hasUrlPrefix:!!urlPrefix},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
148
|
+
// #endregion
|
|
149
|
+
|
|
150
|
+
if (typeof modules === "undefined" || modules.length === 0) {
|
|
151
|
+
setTimeout(doneCallback);
|
|
152
|
+
} else {
|
|
153
|
+
var remaining = modules.length;
|
|
154
|
+
var moduleLoaded = function() {
|
|
155
|
+
if (--remaining === 0) {
|
|
156
|
+
// #region agent log
|
|
157
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:modulesScript:allModulesLoaded',message:'All modules loaded',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
158
|
+
// #endregion
|
|
159
|
+
doneCallback();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
modules.forEach(function (m) {
|
|
164
|
+
pc.WasmModule.setConfig(m.moduleName, {
|
|
165
|
+
glueUrl: urlPrefix + m.glueUrl,
|
|
166
|
+
wasmUrl: urlPrefix + m.wasmUrl,
|
|
167
|
+
fallbackUrl: urlPrefix + m.fallbackUrl
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!m.hasOwnProperty('preload') || m.preload) {
|
|
171
|
+
if (m.moduleName === 'BASIS') {
|
|
172
|
+
pc.basisInitialize();
|
|
173
|
+
moduleLoaded();
|
|
174
|
+
} else if (m.moduleName === 'DracoDecoderModule') {
|
|
175
|
+
if (pc.dracoInitialize) {
|
|
176
|
+
pc.dracoInitialize();
|
|
177
|
+
moduleLoaded();
|
|
178
|
+
} else {
|
|
179
|
+
pc.WasmModule.getInstance(m.moduleName, function() { moduleLoaded(); });
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
pc.WasmModule.getInstance(m.moduleName, function() { moduleLoaded(); });
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
moduleLoaded();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
window.loadModules = loadModules;
|
|
191
|
+
|
|
192
|
+
// #region agent log
|
|
193
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:modulesScript:loadModulesDefined',message:'loadModules defined on window',data:{hasLoadModules:typeof window.loadModules==='function'},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
194
|
+
// #endregion
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 将构建配置转换为 manifest 格式
|
|
199
|
+
*/
|
|
200
|
+
convertBuildConfigToManifest(config) {
|
|
201
|
+
const scenes = [];
|
|
202
|
+
const assets = [];
|
|
203
|
+
// 处理场景
|
|
204
|
+
if (config.scenes) {
|
|
205
|
+
for (const scene of config.scenes) {
|
|
206
|
+
scenes.push({
|
|
207
|
+
id: scene.id || scene.name,
|
|
208
|
+
name: scene.name,
|
|
209
|
+
path: scene.url || `${scene.name}.json`,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// 处理资产 - config.assets 是一个对象(以 ID 为 key),不是数组!
|
|
214
|
+
if (config.assets) {
|
|
215
|
+
for (const [id, asset] of Object.entries(config.assets)) {
|
|
216
|
+
const assetData = asset;
|
|
217
|
+
// 从 file.url 中提取不带查询参数的路径
|
|
218
|
+
let filePath = assetData.file?.url || '';
|
|
219
|
+
if (filePath.includes('?')) {
|
|
220
|
+
filePath = filePath.split('?')[0];
|
|
221
|
+
}
|
|
222
|
+
assets.push({
|
|
223
|
+
id: id,
|
|
224
|
+
name: assetData.name,
|
|
225
|
+
type: assetData.type,
|
|
226
|
+
path: filePath,
|
|
227
|
+
// 保留所有原始字段
|
|
228
|
+
preload: assetData.preload !== false,
|
|
229
|
+
tags: assetData.tags || [],
|
|
230
|
+
i18n: assetData.i18n || {},
|
|
231
|
+
data: assetData.data || null,
|
|
232
|
+
file: assetData.file ? {
|
|
233
|
+
filename: assetData.file.filename,
|
|
234
|
+
size: assetData.file.size || 0,
|
|
235
|
+
hash: assetData.file.hash || '',
|
|
236
|
+
url: filePath, // 使用不带查询参数的路径
|
|
237
|
+
} : undefined,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
format: 'playcanvas-build',
|
|
243
|
+
version: '1.0.0',
|
|
244
|
+
project: {
|
|
245
|
+
id: config.project_id || 'build',
|
|
246
|
+
name: config.project_name || 'Build Project',
|
|
247
|
+
},
|
|
248
|
+
scenes,
|
|
249
|
+
assets,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* 检测项目格式
|
|
254
|
+
*/
|
|
255
|
+
async detectProjectFormat() {
|
|
256
|
+
const manifestPath = path.join(this.projectDir, 'manifest.json');
|
|
257
|
+
const assetsJsonPath = path.join(this.projectDir, 'assets.json');
|
|
258
|
+
try {
|
|
259
|
+
await fs.access(manifestPath);
|
|
260
|
+
return 'playcraft';
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
// manifest.json 不存在
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
await fs.access(assetsJsonPath);
|
|
267
|
+
return 'playcanvas';
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
// assets.json 不存在
|
|
271
|
+
}
|
|
272
|
+
return 'unknown';
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* 加载 PlayCraft 格式项目
|
|
276
|
+
*/
|
|
277
|
+
async loadPlayCraftProject() {
|
|
278
|
+
const manifestPath = path.join(this.projectDir, 'manifest.json');
|
|
279
|
+
try {
|
|
280
|
+
const manifestContent = await fs.readFile(manifestPath, 'utf-8');
|
|
281
|
+
this.manifest = JSON.parse(manifestContent);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
throw new Error(`无法加载 manifest.json: ${error.message}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* 加载 PlayCanvas 格式项目
|
|
289
|
+
*/
|
|
290
|
+
async loadPlayCanvasProject() {
|
|
291
|
+
const assetsJsonPath = path.join(this.projectDir, 'assets.json');
|
|
292
|
+
const scenesJsonPath = path.join(this.projectDir, 'scenes.json');
|
|
293
|
+
try {
|
|
294
|
+
// 读取 assets.json
|
|
295
|
+
const assetsContent = await fs.readFile(assetsJsonPath, 'utf-8');
|
|
296
|
+
const assetsJson = JSON.parse(assetsContent);
|
|
297
|
+
// 读取 scenes.json
|
|
298
|
+
let scenesJson = {};
|
|
299
|
+
try {
|
|
300
|
+
const scenesContent = await fs.readFile(scenesJsonPath, 'utf-8');
|
|
301
|
+
scenesJson = JSON.parse(scenesContent);
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
console.warn('未找到 scenes.json,将不包含场景信息');
|
|
305
|
+
}
|
|
306
|
+
// 转换为 PlayCraft manifest 格式
|
|
307
|
+
this.manifest = this.convertPlayCanvasToManifest(assetsJson, scenesJson);
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
throw new Error(`无法加载 PlayCanvas 项目: ${error.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* 将 PlayCanvas 格式转换为 PlayCraft manifest 格式
|
|
315
|
+
*/
|
|
316
|
+
convertPlayCanvasToManifest(assetsJson, scenesJson) {
|
|
317
|
+
const assets = [];
|
|
318
|
+
const scenes = [];
|
|
319
|
+
// 构建文件夹路径映射
|
|
320
|
+
const folderMap = new Map();
|
|
321
|
+
for (const [id, asset] of Object.entries(assetsJson)) {
|
|
322
|
+
const assetData = asset;
|
|
323
|
+
if (assetData.type === 'folder') {
|
|
324
|
+
const folderPath = this.buildFolderPath(assetData, assetsJson, folderMap);
|
|
325
|
+
folderMap.set(id, folderPath);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// 转换资产 - 保留完整的资产信息
|
|
329
|
+
for (const [id, asset] of Object.entries(assetsJson)) {
|
|
330
|
+
const assetData = asset;
|
|
331
|
+
if (assetData.type === 'folder')
|
|
332
|
+
continue;
|
|
333
|
+
const treePath = this.buildAssetTreePath(assetData, folderMap);
|
|
334
|
+
const filePath = assetData.file
|
|
335
|
+
? `files/assets/${id}/1/${assetData.file.filename}`
|
|
336
|
+
: '';
|
|
337
|
+
assets.push({
|
|
338
|
+
id: id,
|
|
339
|
+
name: assetData.name,
|
|
340
|
+
type: assetData.type,
|
|
341
|
+
path: filePath,
|
|
342
|
+
// 保留原始的 preload、tags、i18n、data 字段
|
|
343
|
+
preload: assetData.preload !== false,
|
|
344
|
+
tags: assetData.tags || [],
|
|
345
|
+
i18n: assetData.i18n || {},
|
|
346
|
+
data: assetData.data || null,
|
|
347
|
+
file: assetData.file ? {
|
|
348
|
+
filename: assetData.file.filename,
|
|
349
|
+
size: assetData.file.size || 0,
|
|
350
|
+
hash: assetData.file.hash || '',
|
|
351
|
+
url: assetData.file.url || filePath,
|
|
352
|
+
} : undefined,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
// 转换场景(PlayCanvas 场景数据内嵌在 scenes.json 中)
|
|
356
|
+
for (const [id, scene] of Object.entries(scenesJson)) {
|
|
357
|
+
const sceneData = scene;
|
|
358
|
+
// 将场景数据直接转换为 JSON 字符串,然后 Base64 编码
|
|
359
|
+
const sceneJsonStr = JSON.stringify(sceneData);
|
|
360
|
+
const base64Data = Buffer.from(sceneJsonStr).toString('base64');
|
|
361
|
+
const dataUrl = `data:application/json;base64,${base64Data}`;
|
|
362
|
+
scenes.push({
|
|
363
|
+
id: id,
|
|
364
|
+
name: sceneData.name || id,
|
|
365
|
+
path: dataUrl, // 直接使用 data URL
|
|
366
|
+
isMain: false,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
format: 'playcanvas-converted',
|
|
371
|
+
version: '1.0.0',
|
|
372
|
+
project: {
|
|
373
|
+
id: 'unknown',
|
|
374
|
+
name: 'Converted Project',
|
|
375
|
+
description: 'PlayCanvas project converted to PlayCraft format',
|
|
376
|
+
},
|
|
377
|
+
scenes,
|
|
378
|
+
assets,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* 构建文件夹路径
|
|
383
|
+
*/
|
|
384
|
+
buildFolderPath(folder, assetsJson, folderMap) {
|
|
385
|
+
if (!folder.path || folder.path.length === 0) {
|
|
386
|
+
return folder.name;
|
|
387
|
+
}
|
|
388
|
+
const parentId = folder.path[folder.path.length - 1];
|
|
389
|
+
const parentPath = folderMap.get(parentId);
|
|
390
|
+
if (parentPath) {
|
|
391
|
+
return `${parentPath}/${folder.name}`;
|
|
392
|
+
}
|
|
393
|
+
// 如果父文件夹还没有处理,递归处理
|
|
394
|
+
const parentFolder = assetsJson[parentId];
|
|
395
|
+
if (parentFolder && parentFolder.type === 'folder') {
|
|
396
|
+
const parentPath = this.buildFolderPath(parentFolder, assetsJson, folderMap);
|
|
397
|
+
folderMap.set(parentId, parentPath);
|
|
398
|
+
return `${parentPath}/${folder.name}`;
|
|
399
|
+
}
|
|
400
|
+
return folder.name;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* 构建资产树形路径
|
|
404
|
+
*/
|
|
405
|
+
buildAssetTreePath(asset, folderMap) {
|
|
406
|
+
if (!asset.path || asset.path.length === 0) {
|
|
407
|
+
return '';
|
|
408
|
+
}
|
|
409
|
+
const paths = [];
|
|
410
|
+
for (const folderId of asset.path) {
|
|
411
|
+
const folderPath = folderMap.get(folderId);
|
|
412
|
+
if (folderPath) {
|
|
413
|
+
paths.push(folderPath);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return paths.join('/');
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* 编码资产为 Base64
|
|
420
|
+
*/
|
|
421
|
+
async encodeAssets() {
|
|
422
|
+
await this.converter.encodeAssets();
|
|
423
|
+
// 更新大小报告
|
|
424
|
+
const assets = this.converter.getAssets();
|
|
425
|
+
for (const asset of assets) {
|
|
426
|
+
const name = path.basename(asset.path);
|
|
427
|
+
this.sizeReport.assets[name] = asset.size;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* 生成运行时配置
|
|
432
|
+
*/
|
|
433
|
+
async generateConfig() {
|
|
434
|
+
// 加载 PlayCanvas Engine
|
|
435
|
+
await this.loadEngine();
|
|
436
|
+
// 对于构建后的项目,使用原始配置并更新 URL
|
|
437
|
+
// 对于源码项目,生成新配置
|
|
438
|
+
if (this.manifest?.format === 'playcanvas-build' && this.configJson) {
|
|
439
|
+
await this.updateBuildConfigUrls();
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
await this.generateRuntimeConfig();
|
|
443
|
+
}
|
|
444
|
+
// 生成启动脚本
|
|
445
|
+
await this.generateStartScript();
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* 更新构建配置中的资产 URL 为 data URL
|
|
449
|
+
* 这样可以保留原始配置的所有属性,只替换 URL
|
|
450
|
+
*/
|
|
451
|
+
async updateBuildConfigUrls() {
|
|
452
|
+
if (!this.configJson || !this.configJson.assets) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
let updatedCount = 0;
|
|
456
|
+
// 遍历所有资产,更新 URL 为 data URL
|
|
457
|
+
for (const [id, asset] of Object.entries(this.configJson.assets)) {
|
|
458
|
+
const assetData = asset;
|
|
459
|
+
if (assetData.file && assetData.file.url) {
|
|
460
|
+
// 移除查询参数
|
|
461
|
+
let cleanUrl = assetData.file.url;
|
|
462
|
+
if (cleanUrl.includes('?')) {
|
|
463
|
+
cleanUrl = cleanUrl.split('?')[0];
|
|
464
|
+
}
|
|
465
|
+
// 获取 data URL
|
|
466
|
+
const dataUrl = this.converter.getDataUrl(cleanUrl);
|
|
467
|
+
if (dataUrl) {
|
|
468
|
+
// 更新 URL 为 data URL
|
|
469
|
+
assetData.file.url = dataUrl;
|
|
470
|
+
// 关键:清空 hash,防止 PlayCanvas 追加 ?t=hash
|
|
471
|
+
assetData.file.hash = '';
|
|
472
|
+
updatedCount++;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// 更新场景 URL
|
|
477
|
+
if (this.configJson.scenes) {
|
|
478
|
+
for (const scene of this.configJson.scenes) {
|
|
479
|
+
if (scene.url && !scene.url.startsWith('data:')) {
|
|
480
|
+
// 移除查询参数
|
|
481
|
+
let cleanUrl = scene.url;
|
|
482
|
+
if (cleanUrl.includes('?')) {
|
|
483
|
+
cleanUrl = cleanUrl.split('?')[0];
|
|
484
|
+
}
|
|
485
|
+
const dataUrl = this.converter.getDataUrl(cleanUrl);
|
|
486
|
+
if (dataUrl) {
|
|
487
|
+
scene.url = dataUrl;
|
|
488
|
+
updatedCount++;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
console.log(`✅ 已更新 ${updatedCount} 个资产 URL 为内嵌 data URL`);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* 加载 PlayCanvas Engine
|
|
497
|
+
*/
|
|
498
|
+
async loadEngine() {
|
|
499
|
+
// 1. 首先尝试从本地加载(如果提供了路径)
|
|
500
|
+
if (this.options.enginePath) {
|
|
501
|
+
try {
|
|
502
|
+
this.engineCode = await fs.readFile(this.options.enginePath, 'utf-8');
|
|
503
|
+
this.sizeReport.engine = Buffer.from(this.engineCode).length;
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
console.warn(`无法从本地加载 engine: ${error.message}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
// 2. 尝试从项目目录加载
|
|
511
|
+
const localEnginePaths = [
|
|
512
|
+
path.join(this.projectDir, 'playcanvas-stable.min.js'),
|
|
513
|
+
path.join(this.projectDir, 'playcanvas.min.js'),
|
|
514
|
+
path.join(this.projectDir, '__lib__.js'),
|
|
515
|
+
];
|
|
516
|
+
for (const enginePath of localEnginePaths) {
|
|
517
|
+
try {
|
|
518
|
+
this.engineCode = await fs.readFile(enginePath, 'utf-8');
|
|
519
|
+
this.sizeReport.engine = Buffer.from(this.engineCode).length;
|
|
520
|
+
console.log(`从本地加载 Engine: ${enginePath}`);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
catch (error) {
|
|
524
|
+
// 继续尝试下一个
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// 3. 从 CDN 下载(如果提供了 URL)
|
|
528
|
+
if (this.options.engineUrl) {
|
|
529
|
+
try {
|
|
530
|
+
const response = await fetch(this.options.engineUrl, {
|
|
531
|
+
headers: {
|
|
532
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
533
|
+
'Accept': '*/*',
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
if (response.ok) {
|
|
537
|
+
this.engineCode = await response.text();
|
|
538
|
+
this.sizeReport.engine = Buffer.from(this.engineCode).length;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
console.warn(`无法从 URL 下载 engine: ${error.message}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// 4. 使用默认 CDN 版本
|
|
547
|
+
const version = this.options.engineVersion || '2.15.1';
|
|
548
|
+
// 尝试多个 CDN(优先使用官方 GitHub CDN)
|
|
549
|
+
const cdnUrls = [
|
|
550
|
+
// 官方 PlayCanvas CDN (GitHub releases)
|
|
551
|
+
`https://code.playcanvas.com/playcanvas-${version}.min.js`,
|
|
552
|
+
// jsDelivr (NPM 镜像)
|
|
553
|
+
`https://cdn.jsdelivr.net/npm/playcanvas@${version}/build/playcanvas.min.js`,
|
|
554
|
+
// unpkg (备用)
|
|
555
|
+
`https://unpkg.com/playcanvas@${version}/build/playcanvas.min.js`,
|
|
556
|
+
];
|
|
557
|
+
let lastError = null;
|
|
558
|
+
for (const cdnUrl of cdnUrls) {
|
|
559
|
+
try {
|
|
560
|
+
console.log(`从 CDN 下载 Engine: ${cdnUrl}`);
|
|
561
|
+
const response = await fetch(cdnUrl, {
|
|
562
|
+
headers: {
|
|
563
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
|
|
564
|
+
'Accept': '*/*',
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
if (response.ok) {
|
|
568
|
+
this.engineCode = await response.text();
|
|
569
|
+
this.sizeReport.engine = Buffer.from(this.engineCode).length;
|
|
570
|
+
console.log(`✓ 成功下载 Engine (${(this.sizeReport.engine / 1024 / 1024).toFixed(2)} MB)`);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
lastError = new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
lastError = error;
|
|
579
|
+
console.warn(`CDN 失败: ${cdnUrl}`);
|
|
580
|
+
// 继续尝试下一个 CDN
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
throw new Error(`无法从任何 CDN 加载 PlayCanvas Engine: ${lastError?.message}。请提供本地 engine 文件或检查网络连接。`);
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* 生成运行时配置 - 使用官方 PlayCanvas config.json 格式
|
|
587
|
+
*/
|
|
588
|
+
async generateRuntimeConfig() {
|
|
589
|
+
if (!this.manifest) {
|
|
590
|
+
throw new Error('Manifest not loaded');
|
|
591
|
+
}
|
|
592
|
+
// 构建官方格式的 config.json
|
|
593
|
+
// assets 是以 ID 为 key 的对象,不是数组
|
|
594
|
+
const assetsObject = {};
|
|
595
|
+
for (const asset of this.manifest.assets) {
|
|
596
|
+
// 从路径中移除查询参数,用于查找 data URL
|
|
597
|
+
let cleanPath = asset.path;
|
|
598
|
+
if (cleanPath.includes('?')) {
|
|
599
|
+
cleanPath = cleanPath.split('?')[0];
|
|
600
|
+
}
|
|
601
|
+
// 尝试获取 data URL
|
|
602
|
+
let dataUrl = this.converter.getDataUrl(cleanPath);
|
|
603
|
+
// 如果没有找到,尝试用原始文件的 URL(去掉查询参数)
|
|
604
|
+
if (!dataUrl && asset.file?.url) {
|
|
605
|
+
let cleanFileUrl = asset.file.url;
|
|
606
|
+
if (cleanFileUrl.includes('?')) {
|
|
607
|
+
cleanFileUrl = cleanFileUrl.split('?')[0];
|
|
608
|
+
}
|
|
609
|
+
dataUrl = this.converter.getDataUrl(cleanFileUrl);
|
|
610
|
+
}
|
|
611
|
+
// 如果还是找不到,尝试用文件名
|
|
612
|
+
if (!dataUrl && asset.file?.filename) {
|
|
613
|
+
dataUrl = this.converter.getDataUrl(asset.file.filename);
|
|
614
|
+
}
|
|
615
|
+
// 构建资产配置
|
|
616
|
+
const assetConfig = {
|
|
617
|
+
name: asset.name,
|
|
618
|
+
type: asset.type,
|
|
619
|
+
preload: asset.preload !== false,
|
|
620
|
+
tags: asset.tags || [],
|
|
621
|
+
i18n: asset.i18n || {},
|
|
622
|
+
id: parseInt(asset.id) || asset.id, // 确保 ID 是数字(如果原本是数字)
|
|
623
|
+
data: asset.data || null,
|
|
624
|
+
};
|
|
625
|
+
// 只有有文件的资产才设置 file 字段
|
|
626
|
+
if (asset.file || dataUrl) {
|
|
627
|
+
// 关键修复:如果使用 data URL,清空 hash,防止 ?t=hash 被追加
|
|
628
|
+
const isDataUrl = dataUrl && dataUrl.startsWith('data:');
|
|
629
|
+
assetConfig.file = {
|
|
630
|
+
filename: asset.file?.filename || path.basename(cleanPath),
|
|
631
|
+
size: asset.file?.size || 0,
|
|
632
|
+
// 关键:如果 URL 是 data URL,清空 hash,否则 PlayCanvas 会追加 ?t=hash
|
|
633
|
+
hash: isDataUrl ? '' : (asset.file?.hash || ''),
|
|
634
|
+
// 使用 data URL 替换原始 URL
|
|
635
|
+
url: dataUrl || cleanPath,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
assetsObject[asset.id] = assetConfig;
|
|
639
|
+
}
|
|
640
|
+
// 场景配置 - 使用官方格式
|
|
641
|
+
const scenes = this.manifest.scenes.map(scene => ({
|
|
642
|
+
name: scene.name,
|
|
643
|
+
// 场景 URL 使用文件名格式(会被 Base64 编码后的 data URL 替换)
|
|
644
|
+
url: scene.path.startsWith('data:')
|
|
645
|
+
? scene.path
|
|
646
|
+
: (this.converter.getDataUrl(scene.path) || `${scene.id}.json`),
|
|
647
|
+
}));
|
|
648
|
+
// 官方 config.json 格式
|
|
649
|
+
this.configJson = {
|
|
650
|
+
application_properties: {
|
|
651
|
+
width: 1280,
|
|
652
|
+
height: 720,
|
|
653
|
+
fillMode: 'FILL_WINDOW',
|
|
654
|
+
resolutionMode: 'AUTO',
|
|
655
|
+
useDevicePixelRatio: true,
|
|
656
|
+
antiAlias: true,
|
|
657
|
+
transparentCanvas: false,
|
|
658
|
+
preserveDrawingBuffer: false,
|
|
659
|
+
useLegacyScripts: false,
|
|
660
|
+
useKeyboard: true,
|
|
661
|
+
useMouse: true,
|
|
662
|
+
useTouch: true,
|
|
663
|
+
useGamepads: false,
|
|
664
|
+
scripts: [],
|
|
665
|
+
layers: {
|
|
666
|
+
'0': { name: 'World', opaqueSortMode: 2, transparentSortMode: 3 },
|
|
667
|
+
'1': { name: 'Depth', opaqueSortMode: 2, transparentSortMode: 3 },
|
|
668
|
+
'2': { name: 'Skybox', opaqueSortMode: 0, transparentSortMode: 3 },
|
|
669
|
+
'3': { name: 'Immediate', opaqueSortMode: 0, transparentSortMode: 3 },
|
|
670
|
+
'4': { name: 'UI', opaqueSortMode: 1, transparentSortMode: 1 },
|
|
671
|
+
},
|
|
672
|
+
layerOrder: [
|
|
673
|
+
{ layer: 0, transparent: false, enabled: true },
|
|
674
|
+
{ layer: 1, transparent: false, enabled: true },
|
|
675
|
+
{ layer: 2, transparent: false, enabled: true },
|
|
676
|
+
{ layer: 0, transparent: true, enabled: true },
|
|
677
|
+
{ layer: 3, transparent: false, enabled: true },
|
|
678
|
+
{ layer: 3, transparent: true, enabled: true },
|
|
679
|
+
{ layer: 4, transparent: true, enabled: true },
|
|
680
|
+
],
|
|
681
|
+
batchGroups: [],
|
|
682
|
+
i18nAssets: [],
|
|
683
|
+
externalScripts: [],
|
|
684
|
+
libraries: [],
|
|
685
|
+
},
|
|
686
|
+
scenes: scenes,
|
|
687
|
+
assets: assetsObject,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* 生成启动脚本
|
|
692
|
+
*/
|
|
693
|
+
async generateStartScript() {
|
|
694
|
+
// 如果已经有启动脚本(从 loadBuildProject 加载),则跳过
|
|
695
|
+
if (this.startScript) {
|
|
696
|
+
console.log('✅ 使用已加载的启动脚本');
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
// 尝试从项目目录读取启动脚本
|
|
700
|
+
const startScriptPaths = [
|
|
701
|
+
path.join(this.projectDir, '__start__.js'),
|
|
702
|
+
path.join(this.projectDir, 'start.js'),
|
|
703
|
+
];
|
|
704
|
+
for (const scriptPath of startScriptPaths) {
|
|
705
|
+
try {
|
|
706
|
+
const originalScript = await fs.readFile(scriptPath, 'utf-8');
|
|
707
|
+
this.startScript = this.wrapOfficialStartScript(originalScript);
|
|
708
|
+
console.log(`✅ 从 ${path.basename(scriptPath)} 加载启动脚本`);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
// 继续尝试
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// 如果找不到,生成默认启动脚本
|
|
716
|
+
console.log('⚠️ 未找到启动脚本,使用默认脚本');
|
|
717
|
+
this.startScript = this.generateDefaultStartScript();
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* 包装官方启动脚本,添加日志追踪
|
|
721
|
+
*/
|
|
722
|
+
wrapOfficialStartScript(originalScript) {
|
|
723
|
+
// 在脚本开头添加日志
|
|
724
|
+
const logEntry = `// #region agent log
|
|
725
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:officialStartScript:entry',message:'Official start script executing',data:{hasWindowConfig:!!window.CONFIG,hasConfigFilename:!!window.CONFIG_FILENAME,hasScenePath:!!window.SCENE_PATH,hasPreloadModules:!!window.PRELOAD_MODULES,hasLoadModules:typeof window.loadModules==='function'},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
726
|
+
// #endregion
|
|
727
|
+
`;
|
|
728
|
+
// 在 main() 函数调用前添加日志
|
|
729
|
+
const logBeforeMain = `// #region agent log
|
|
730
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:officialStartScript:beforeMain',message:'Before main() call',data:{hasCreateGraphicsDevice:typeof createGraphicsDevice==='function',hasInitApp:typeof initApp==='function',hasConfigure:typeof configure==='function'},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
731
|
+
// #endregion
|
|
732
|
+
`;
|
|
733
|
+
// 简单包装:在开头和 main() 调用前添加日志
|
|
734
|
+
let wrappedScript = originalScript;
|
|
735
|
+
// 在脚本开头添加入口日志(查找第一个 (function() { 或类似的模式)
|
|
736
|
+
if (wrappedScript.trim().startsWith('(function')) {
|
|
737
|
+
wrappedScript = wrappedScript.replace(/^\(function\s*\(\)\s*\{/, `(function () {${logEntry}`);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
wrappedScript = logEntry + wrappedScript;
|
|
741
|
+
}
|
|
742
|
+
// 在 main() 调用前添加日志
|
|
743
|
+
wrappedScript = wrappedScript.replace(/(\s*)main\(\);/g, `${logBeforeMain}$1main();`);
|
|
744
|
+
return wrappedScript;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* 生成默认启动脚本 - 使用官方 PlayCanvas 初始化方式
|
|
748
|
+
* 关键:使用 app.configure() 加载配置,这是官方推荐的方式
|
|
749
|
+
*/
|
|
750
|
+
generateDefaultStartScript() {
|
|
751
|
+
return `
|
|
752
|
+
(function () {
|
|
753
|
+
// #region agent log
|
|
754
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:startScript:entry',message:'Start script executing',data:{hasWindowConfig:!!window.CONFIG,hasPlayCanvasConfig:!!window.PLAYCANVAS_CONFIG,hasConfigFilename:!!window.CONFIG_FILENAME},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
755
|
+
// #endregion
|
|
756
|
+
|
|
757
|
+
var CANVAS_ID = 'application-canvas';
|
|
758
|
+
|
|
759
|
+
// 创建或获取 Canvas
|
|
760
|
+
var canvas = document.getElementById(CANVAS_ID);
|
|
761
|
+
if (!canvas) {
|
|
762
|
+
canvas = document.createElement('canvas');
|
|
763
|
+
canvas.setAttribute('id', CANVAS_ID);
|
|
764
|
+
canvas.setAttribute('tabindex', 0);
|
|
765
|
+
canvas.onselectstart = function () { return false; };
|
|
766
|
+
canvas.style['-webkit-user-select'] = 'none';
|
|
767
|
+
document.body.appendChild(canvas);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// 获取内联配置 - 优先使用 window.CONFIG(与注入的变量名一致)
|
|
771
|
+
var CONFIG = window.CONFIG || window.PLAYCANVAS_CONFIG;
|
|
772
|
+
if (!CONFIG) {
|
|
773
|
+
// #region agent log
|
|
774
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:startScript:configNotFound',message:'Config not found',data:{hasWindowConfig:!!window.CONFIG,hasPlayCanvasConfig:!!window.PLAYCANVAS_CONFIG,hasConfigFilename:!!window.CONFIG_FILENAME},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
775
|
+
// #endregion
|
|
776
|
+
console.error('CONFIG not found');
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// #region agent log
|
|
781
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:startScript:configFound',message:'Config found, creating app',data:{hasScenes:!!(CONFIG.scenes&&CONFIG.scenes.length>0),scenesCount:CONFIG.scenes?CONFIG.scenes.length:0},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
782
|
+
// #endregion
|
|
783
|
+
|
|
784
|
+
// 将配置转为 data URL,供 app.configure() 使用
|
|
785
|
+
var configJson = JSON.stringify(CONFIG);
|
|
786
|
+
var configDataUrl = 'data:application/json;base64,' + btoa(unescape(encodeURIComponent(configJson)));
|
|
787
|
+
|
|
788
|
+
// 创建应用 - 使用旧版兼容的 pc.Application
|
|
789
|
+
try {
|
|
790
|
+
var app = new pc.Application(canvas, {
|
|
791
|
+
mouse: new pc.Mouse(canvas),
|
|
792
|
+
touch: pc.platform && pc.platform.touch ? new pc.TouchDevice(canvas) : null,
|
|
793
|
+
keyboard: new pc.Keyboard(window),
|
|
794
|
+
graphicsDeviceOptions: {
|
|
795
|
+
alpha: false,
|
|
796
|
+
antialias: true,
|
|
797
|
+
depth: true,
|
|
798
|
+
stencil: true,
|
|
799
|
+
preserveDrawingBuffer: false
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
// #region agent log
|
|
804
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:startScript:appCreated',message:'App created successfully',data:{hasApp:!!app,appIsPlaying:app?app.isPlaying:false},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
805
|
+
// #endregion
|
|
806
|
+
|
|
807
|
+
// 将 app 暴露到全局作用域,以便调试
|
|
808
|
+
window.app = app;
|
|
809
|
+
|
|
810
|
+
app.setCanvasFillMode(pc.FILLMODE_FILL_WINDOW);
|
|
811
|
+
app.setCanvasResolution(pc.RESOLUTION_AUTO);
|
|
812
|
+
|
|
813
|
+
window.addEventListener('resize', function() {
|
|
814
|
+
app.resizeCanvas();
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
// 使用 app.configure() 加载配置 - 这是官方推荐的方式
|
|
818
|
+
app.configure(configDataUrl, function(err) {
|
|
819
|
+
// #region agent log
|
|
820
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:startScript:configureCallback',message:'app.configure callback',data:{hasError:!!err,error:err?err.message:null},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
821
|
+
// #endregion
|
|
822
|
+
|
|
823
|
+
if (err) {
|
|
824
|
+
console.error('Configure error:', err);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// 预加载资产
|
|
829
|
+
app.preload(function(err) {
|
|
830
|
+
// #region agent log
|
|
831
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:startScript:preloadCallback',message:'app.preload callback',data:{hasError:!!err,error:err?err.message:null},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
832
|
+
// #endregion
|
|
833
|
+
|
|
834
|
+
if (err) {
|
|
835
|
+
console.error('Preload error:', err);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// 加载场景
|
|
839
|
+
if (CONFIG.scenes && CONFIG.scenes.length > 0) {
|
|
840
|
+
var sceneUrl = CONFIG.scenes[0].url;
|
|
841
|
+
app.scenes.loadScene(sceneUrl, function(err, entity) {
|
|
842
|
+
// #region agent log
|
|
843
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:startScript:loadSceneCallback',message:'app.scenes.loadScene callback',data:{hasError:!!err,error:err?err.message:null,hasEntity:!!entity},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
844
|
+
// #endregion
|
|
845
|
+
|
|
846
|
+
if (err) {
|
|
847
|
+
console.error('Scene load error:', err);
|
|
848
|
+
}
|
|
849
|
+
// 启动应用
|
|
850
|
+
app.start();
|
|
851
|
+
// #region agent log
|
|
852
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:startScript:appStarted',message:'app.start called',data:{appIsPlaying:app.isPlaying},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
853
|
+
// #endregion
|
|
854
|
+
});
|
|
855
|
+
} else {
|
|
856
|
+
app.start();
|
|
857
|
+
// #region agent log
|
|
858
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:startScript:appStarted',message:'app.start called (no scenes)',data:{appIsPlaying:app.isPlaying},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
859
|
+
// #endregion
|
|
860
|
+
}
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
} catch (e) {
|
|
864
|
+
// #region agent log
|
|
865
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:startScript:error',message:'Error creating app',data:{error:e.message,stack:e.stack?e.stack.substring(0,500):''},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
866
|
+
// #endregion
|
|
867
|
+
console.error('Error creating app:', e);
|
|
868
|
+
}
|
|
869
|
+
})();
|
|
870
|
+
`;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* 应用平台配置
|
|
874
|
+
*/
|
|
875
|
+
async applyPlatformConfig() {
|
|
876
|
+
this.platformAdapter.validateOptions();
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* 输出文件
|
|
880
|
+
*/
|
|
881
|
+
async output() {
|
|
882
|
+
const outputDir = this.options.outputDir || path.join(this.projectDir, 'dist');
|
|
883
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
884
|
+
const format = this.options.format || this.platformAdapter.getDefaultFormat();
|
|
885
|
+
if (format === 'html') {
|
|
886
|
+
return await this.outputHTML(outputDir);
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
return await this.outputZIP(outputDir);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* 输出 HTML 文件
|
|
894
|
+
*/
|
|
895
|
+
async outputHTML(outputDir) {
|
|
896
|
+
const htmlTemplate = await this.loadHTMLTemplate();
|
|
897
|
+
const assets = this.converter.getAssets();
|
|
898
|
+
// 创建 URL 映射
|
|
899
|
+
const urlMap = new Map();
|
|
900
|
+
for (const asset of assets) {
|
|
901
|
+
const dataUrl = this.converter.getDataUrl(asset.path);
|
|
902
|
+
if (dataUrl) {
|
|
903
|
+
urlMap.set(asset.path, dataUrl);
|
|
904
|
+
urlMap.set(`/${asset.path}`, dataUrl);
|
|
905
|
+
const fileName = path.basename(asset.path);
|
|
906
|
+
urlMap.set(fileName, dataUrl);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
// 替换 URL
|
|
910
|
+
let html = this.converter.replaceUrlsWithBase64(htmlTemplate, urlMap);
|
|
911
|
+
// 内联脚本(如果需要)
|
|
912
|
+
if (this.options.inlineGameScripts !== false) {
|
|
913
|
+
html = await this.converter.inlineScripts(html);
|
|
914
|
+
}
|
|
915
|
+
// 注入 PlayCanvas Engine
|
|
916
|
+
const engineScript = `<script>${this.engineCode}</script>`;
|
|
917
|
+
html = html.replace('</head>', `${engineScript}</head>`);
|
|
918
|
+
// 生成配置 data URL(官方启动脚本使用 app.configure(CONFIG_FILENAME) 来加载配置)
|
|
919
|
+
const configJsonStr = JSON.stringify(this.configJson);
|
|
920
|
+
const configBase64 = Buffer.from(configJsonStr).toString('base64');
|
|
921
|
+
const configDataUrl = `data:application/json;base64,${configBase64}`;
|
|
922
|
+
// 同时内联配置对象到 window.CONFIG,以便启动脚本可以直接使用
|
|
923
|
+
// 这样可以避免通过 data URL 加载时可能的数据丢失问题
|
|
924
|
+
const configInlineScript = `<script>
|
|
925
|
+
window.CONFIG = ${configJsonStr};
|
|
926
|
+
// #region agent log
|
|
927
|
+
(function(){var fontAsset=window.CONFIG&&window.CONFIG.assets&&window.CONFIG.assets['269207482']?window.CONFIG.assets['269207482']:null;fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:configInline:validation',message:'Config object validation',data:{hasFontAsset:!!fontAsset,fontAssetHasData:!!(fontAsset&&fontAsset.data),fontAssetHasInfo:!!(fontAsset&&fontAsset.data&&fontAsset.data.info),fontAssetHasMaps:!!(fontAsset&&fontAsset.data&&fontAsset.data.info&&fontAsset.data.info.maps),mapsValue:fontAsset&&fontAsset.data&&fontAsset.data.info?fontAsset.data.info.maps:null},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'E'})}).catch(()=>{});})();
|
|
928
|
+
// #endregion
|
|
929
|
+
</script>`;
|
|
930
|
+
html = html.replace('</head>', `${configInlineScript}</head>`);
|
|
931
|
+
// 生成场景 data URL(如果有场景)
|
|
932
|
+
let sceneDataUrl = '';
|
|
933
|
+
if (this.configJson.scenes && this.configJson.scenes.length > 0) {
|
|
934
|
+
const sceneUrl = this.configJson.scenes[0].url;
|
|
935
|
+
if (sceneUrl && sceneUrl.startsWith('data:')) {
|
|
936
|
+
sceneDataUrl = sceneUrl;
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
// 尝试获取场景的 data URL
|
|
940
|
+
const scenePath = this.configJson.scenes[0].url || '';
|
|
941
|
+
sceneDataUrl = this.converter.getDataUrl(scenePath) || scenePath;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
// 生成设置脚本 - 使用 data URL 替代文件路径
|
|
945
|
+
// 这样官方的 __start__.js 可以正常工作
|
|
946
|
+
// 同时添加补丁脚本,让启动脚本优先使用 window.CONFIG
|
|
947
|
+
const settingsScript = this.generateSettingsScript(configDataUrl, sceneDataUrl);
|
|
948
|
+
html = html.replace('</head>', `<script>${settingsScript}</script></head>`);
|
|
949
|
+
// 添加补丁脚本,修改 app.configure 来使用内联的配置对象
|
|
950
|
+
const configPatchScript = this.generateConfigPatchScript();
|
|
951
|
+
html = html.replace('</head>', `<script>${configPatchScript}</script></head>`);
|
|
952
|
+
// 注入模块加载脚本(用于 WASM 模块如 Ammo.js)
|
|
953
|
+
if (this.modulesScript) {
|
|
954
|
+
const scriptLength = this.modulesScript.length;
|
|
955
|
+
const modulesScriptTag = `<script>
|
|
956
|
+
// #region agent log
|
|
957
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:outputHTML:modulesScriptInjected',message:'Modules script injected',data:{scriptLength:${scriptLength}},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
958
|
+
// #endregion
|
|
959
|
+
${this.modulesScript}</script>`;
|
|
960
|
+
html = html.replace('</body>', `${modulesScriptTag}</body>`);
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
// 添加日志:模块脚本为空
|
|
964
|
+
const logScript = `<script>
|
|
965
|
+
// #region agent log
|
|
966
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:outputHTML:modulesScriptEmpty',message:'Modules script is empty',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
967
|
+
// #endregion
|
|
968
|
+
</script>`;
|
|
969
|
+
html = html.replace('</body>', `${logScript}</body>`);
|
|
970
|
+
}
|
|
971
|
+
// 注入启动脚本
|
|
972
|
+
if (this.startScript) {
|
|
973
|
+
const scriptLength = this.startScript.length;
|
|
974
|
+
const startScriptTag = `<script>
|
|
975
|
+
// #region agent log
|
|
976
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:outputHTML:startScriptInjected',message:'Start script injected',data:{scriptLength:${scriptLength}},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
977
|
+
try {
|
|
978
|
+
// #endregion
|
|
979
|
+
${this.startScript}
|
|
980
|
+
// #region agent log
|
|
981
|
+
} catch(e) {
|
|
982
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:outputHTML:startScriptError',message:'Start script execution error',data:{error:e.message,stack:e.stack?e.stack.substring(0,500):''},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
983
|
+
throw e;
|
|
984
|
+
}
|
|
985
|
+
// #endregion
|
|
986
|
+
</script>`;
|
|
987
|
+
html = html.replace('</body>', `${startScriptTag}</body>`);
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
// 添加日志:启动脚本为空
|
|
991
|
+
const logScript = `<script>
|
|
992
|
+
// #region agent log
|
|
993
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:outputHTML:startScriptEmpty',message:'Start script is empty',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'G'})}).catch(()=>{});
|
|
994
|
+
// #endregion
|
|
995
|
+
</script>`;
|
|
996
|
+
html = html.replace('</body>', `${logScript}</body>`);
|
|
997
|
+
}
|
|
998
|
+
// 应用平台特定的修改
|
|
999
|
+
html = this.platformAdapter.modifyHTML(html, assets);
|
|
1000
|
+
// 添加平台脚本
|
|
1001
|
+
const platformScript = this.platformAdapter.getPlatformScript();
|
|
1002
|
+
html = html.replace('</body>', `<script>${platformScript}</script></body>`);
|
|
1003
|
+
const outputPath = path.join(outputDir, 'index.html');
|
|
1004
|
+
await fs.writeFile(outputPath, html, 'utf-8');
|
|
1005
|
+
// 更新大小报告
|
|
1006
|
+
const htmlSize = Buffer.from(html, 'utf-8').length;
|
|
1007
|
+
this.sizeReport.total = htmlSize;
|
|
1008
|
+
this.sizeReport.assets['index.html'] = htmlSize;
|
|
1009
|
+
this.sizeReport.assets['PlayCanvas Engine'] = this.sizeReport.engine;
|
|
1010
|
+
return outputPath;
|
|
1011
|
+
}
|
|
1012
|
+
/**
|
|
1013
|
+
* 生成配置补丁脚本
|
|
1014
|
+
* 修改 app.configure 方法,优先使用内联的 window.CONFIG 对象
|
|
1015
|
+
* 这样可以避免通过 data URL 加载时可能的数据丢失问题
|
|
1016
|
+
*/
|
|
1017
|
+
generateConfigPatchScript() {
|
|
1018
|
+
return `
|
|
1019
|
+
(function() {
|
|
1020
|
+
// #region agent log
|
|
1021
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:init',message:'Patch script started',data:{hasWindowConfig:!!window.CONFIG},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'B'})}).catch(()=>{});
|
|
1022
|
+
// #endregion
|
|
1023
|
+
|
|
1024
|
+
// 等待 PlayCanvas 引擎加载
|
|
1025
|
+
function patchConfigure() {
|
|
1026
|
+
if (typeof pc === 'undefined' || !pc.Application) {
|
|
1027
|
+
setTimeout(patchConfigure, 10);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// #region agent log
|
|
1032
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:pcReady',message:'PlayCanvas engine loaded, patching configure',data:{hasWindowConfig:!!window.CONFIG,hasFontAsset:!!(window.CONFIG&&window.CONFIG.assets&&window.CONFIG.assets['269207482'])},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'B'})}).catch(()=>{});
|
|
1033
|
+
// #endregion
|
|
1034
|
+
|
|
1035
|
+
var ApplicationPrototype = pc.Application.prototype;
|
|
1036
|
+
var originalConfigure = ApplicationPrototype.configure;
|
|
1037
|
+
|
|
1038
|
+
// 检查字体资产数据完整性
|
|
1039
|
+
if (window.CONFIG && window.CONFIG.assets && window.CONFIG.assets['269207482']) {
|
|
1040
|
+
var fontAsset = window.CONFIG.assets['269207482'];
|
|
1041
|
+
// #region agent log
|
|
1042
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:checkFontAsset',message:'Font asset data check',data:{hasData:!!fontAsset.data,hasInfo:!!(fontAsset.data&&fontAsset.data.info),hasMaps:!!(fontAsset.data&&fontAsset.data.info&&fontAsset.data.info.maps),mapsValue:fontAsset.data&&fontAsset.data.info?fontAsset.data.info.maps:null},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
|
1043
|
+
// #endregion
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// 拦截 _parseApplicationProperties 方法
|
|
1047
|
+
if (ApplicationPrototype._parseApplicationProperties) {
|
|
1048
|
+
var originalParseApplicationProperties = ApplicationPrototype._parseApplicationProperties;
|
|
1049
|
+
ApplicationPrototype._parseApplicationProperties = function(config, callback) {
|
|
1050
|
+
// #region agent log
|
|
1051
|
+
var fontAsset = config&&config.assets&&config.assets['269207482']?config.assets['269207482']:null;
|
|
1052
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:beforeParseApplicationProperties',message:'Before _parseApplicationProperties',data:{hasFontAsset:!!fontAsset,fontAssetHasMaps:!!(fontAsset&&fontAsset.data&&fontAsset.data.info&&fontAsset.data.info.maps)},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
|
1053
|
+
// #endregion
|
|
1054
|
+
|
|
1055
|
+
try {
|
|
1056
|
+
var result = originalParseApplicationProperties.call(this, config, callback);
|
|
1057
|
+
// #region agent log
|
|
1058
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:afterParseApplicationProperties',message:'After _parseApplicationProperties',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
|
1059
|
+
// #endregion
|
|
1060
|
+
return result;
|
|
1061
|
+
} catch (e) {
|
|
1062
|
+
// #region agent log
|
|
1063
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:parseApplicationPropertiesError',message:'Error in _parseApplicationProperties',data:{error:e.message},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
|
1064
|
+
// #endregion
|
|
1065
|
+
throw e;
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// 拦截 _parseAssets 方法来追踪字体资产解析
|
|
1071
|
+
if (ApplicationPrototype._parseAssets) {
|
|
1072
|
+
var originalParseAssets = ApplicationPrototype._parseAssets;
|
|
1073
|
+
ApplicationPrototype._parseAssets = function(config) {
|
|
1074
|
+
// #region agent log
|
|
1075
|
+
var fontAsset = config&&config.assets&&config.assets['269207482']?config.assets['269207482']:null;
|
|
1076
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:beforeParseAssets',message:'Before _parseAssets',data:{hasFontAsset:!!fontAsset,fontAssetHasData:!!(fontAsset&&fontAsset.data),fontAssetHasInfo:!!(fontAsset&&fontAsset.data&&fontAsset.data.info),fontAssetHasMaps:!!(fontAsset&&fontAsset.data&&fontAsset.data.info&&fontAsset.data.info.maps)},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
|
1077
|
+
// #endregion
|
|
1078
|
+
|
|
1079
|
+
try {
|
|
1080
|
+
var result = originalParseAssets.call(this, config);
|
|
1081
|
+
|
|
1082
|
+
// #region agent log
|
|
1083
|
+
var fontAssetAfter = this.assets&&this.assets.get('269207482')?this.assets.get('269207482'):null;
|
|
1084
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:afterParseAssets',message:'After _parseAssets',data:{hasFontAsset:!!fontAssetAfter,fontAssetHasData:!!(fontAssetAfter&&fontAssetAfter.data),fontAssetHasInfo:!!(fontAssetAfter&&fontAssetAfter.data&&fontAssetAfter.data.info),fontAssetHasMaps:!!(fontAssetAfter&&fontAssetAfter.data&&fontAssetAfter.data.info&&fontAssetAfter.data.info.maps)},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
|
1085
|
+
// #endregion
|
|
1086
|
+
|
|
1087
|
+
return result;
|
|
1088
|
+
} catch (e) {
|
|
1089
|
+
// #region agent log
|
|
1090
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:parseAssetsError',message:'Error in _parseAssets',data:{error:e.message,stack:e.stack?e.stack.substring(0,500):''},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
|
1091
|
+
// #endregion
|
|
1092
|
+
throw e;
|
|
1093
|
+
}
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// 重写 configure 方法
|
|
1098
|
+
ApplicationPrototype.configure = function(url, callback) {
|
|
1099
|
+
// #region agent log
|
|
1100
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:configureCalled',message:'app.configure called',data:{urlType:typeof url,urlIsDataUrl:typeof url==='string'&&url.startsWith('data:'),hasWindowConfig:!!window.CONFIG},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
|
|
1101
|
+
// #endregion
|
|
1102
|
+
|
|
1103
|
+
// 如果 window.CONFIG 存在且 url 是 data URL,直接使用 window.CONFIG
|
|
1104
|
+
if (window.CONFIG && typeof url === 'string' && url.startsWith('data:')) {
|
|
1105
|
+
// #region agent log
|
|
1106
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:usingInlineConfig',message:'Using inline CONFIG instead of data URL',data:{fontAssetHasMaps:!!(window.CONFIG.assets&&window.CONFIG.assets['269207482']&&window.CONFIG.assets['269207482'].data&&window.CONFIG.assets['269207482'].data.info&&window.CONFIG.assets['269207482'].data.info.maps)},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
|
|
1107
|
+
// #endregion
|
|
1108
|
+
|
|
1109
|
+
// 使用内部方法直接解析配置对象
|
|
1110
|
+
try {
|
|
1111
|
+
// #region agent log
|
|
1112
|
+
var configBeforeParse = JSON.stringify(window.CONFIG.assets&&window.CONFIG.assets['269207482']?window.CONFIG.assets['269207482'].data:null).substring(0,200);
|
|
1113
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:beforeParse',message:'Before _parseApplicationProperties',data:{fontAssetDataPreview:configBeforeParse},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
|
1114
|
+
// #endregion
|
|
1115
|
+
|
|
1116
|
+
this._parseApplicationProperties(window.CONFIG, callback);
|
|
1117
|
+
} catch (e) {
|
|
1118
|
+
// #region agent log
|
|
1119
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:parseError',message:'Error in _parseApplicationProperties',data:{error:e.message,stack:e.stack?e.stack.substring(0,500):''},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
|
1120
|
+
// #endregion
|
|
1121
|
+
|
|
1122
|
+
// 如果失败,回退到原始方法
|
|
1123
|
+
if (originalConfigure) {
|
|
1124
|
+
return originalConfigure.call(this, url, callback);
|
|
1125
|
+
} else {
|
|
1126
|
+
if (callback) callback(e);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// #region agent log
|
|
1133
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:usingOriginal',message:'Using original configure method',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
|
|
1134
|
+
// #endregion
|
|
1135
|
+
|
|
1136
|
+
// 否则使用原始方法
|
|
1137
|
+
if (originalConfigure) {
|
|
1138
|
+
return originalConfigure.call(this, url, callback);
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
// 拦截关键方法以追踪初始化流程
|
|
1143
|
+
if (ApplicationPrototype.preload) {
|
|
1144
|
+
var originalPreload = ApplicationPrototype.preload;
|
|
1145
|
+
ApplicationPrototype.preload = function(callback) {
|
|
1146
|
+
// #region agent log
|
|
1147
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:preloadCalled',message:'app.preload called',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H'})}).catch(()=>{});
|
|
1148
|
+
// #endregion
|
|
1149
|
+
return originalPreload.call(this, function(err) {
|
|
1150
|
+
// #region agent log
|
|
1151
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:preloadComplete',message:'app.preload complete',data:{hasError:!!err,error:err?err.message:null},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H'})}).catch(()=>{});
|
|
1152
|
+
// #endregion
|
|
1153
|
+
if (callback) callback(err);
|
|
1154
|
+
});
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (ApplicationPrototype.scenes && ApplicationPrototype.scenes.loadScene) {
|
|
1159
|
+
var originalLoadScene = ApplicationPrototype.scenes.loadScene;
|
|
1160
|
+
ApplicationPrototype.scenes.loadScene = function(url, callback) {
|
|
1161
|
+
// #region agent log
|
|
1162
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:loadSceneCalled',message:'app.scenes.loadScene called',data:{url:typeof url==='string'?url.substring(0,100):url},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H'})}).catch(()=>{});
|
|
1163
|
+
// #endregion
|
|
1164
|
+
return originalLoadScene.call(this, url, function(err, entity) {
|
|
1165
|
+
// #region agent log
|
|
1166
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:loadSceneComplete',message:'app.scenes.loadScene complete',data:{hasError:!!err,error:err?err.message:null,hasEntity:!!entity},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H'})}).catch(()=>{});
|
|
1167
|
+
// #endregion
|
|
1168
|
+
if (callback) callback(err, entity);
|
|
1169
|
+
});
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (ApplicationPrototype.start) {
|
|
1174
|
+
var originalStart = ApplicationPrototype.start;
|
|
1175
|
+
ApplicationPrototype.start = function() {
|
|
1176
|
+
var appInstance = this;
|
|
1177
|
+
// #region agent log
|
|
1178
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:startCalled',message:'app.start called',data:{isPlaying:appInstance.isPlaying},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'I'})}).catch(()=>{});
|
|
1179
|
+
// #endregion
|
|
1180
|
+
var result = originalStart.call(appInstance);
|
|
1181
|
+
// #region agent log
|
|
1182
|
+
setTimeout(function() {
|
|
1183
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:startComplete',message:'app.start complete',data:{isPlaying:appInstance.isPlaying},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'J'})}).catch(()=>{});
|
|
1184
|
+
}, 100);
|
|
1185
|
+
// #endregion
|
|
1186
|
+
return result;
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// #region agent log
|
|
1191
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:patchComplete',message:'Patch applied successfully',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
|
1192
|
+
// #endregion
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
patchConfigure();
|
|
1196
|
+
|
|
1197
|
+
// 拦截 loadModules 函数(启动脚本中的关键函数)
|
|
1198
|
+
if (window.loadModules) {
|
|
1199
|
+
var originalLoadModules = window.loadModules;
|
|
1200
|
+
window.loadModules = function(modules, urlPrefix, doneCallback) {
|
|
1201
|
+
// #region agent log
|
|
1202
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:loadModulesCalled',message:'loadModules called',data:{modulesCount:modules?modules.length:0,hasUrlPrefix:!!urlPrefix},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
1203
|
+
// #endregion
|
|
1204
|
+
return originalLoadModules.call(this, modules, urlPrefix, function() {
|
|
1205
|
+
// #region agent log
|
|
1206
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:loadModulesComplete',message:'loadModules complete',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
1207
|
+
// #endregion
|
|
1208
|
+
if (doneCallback) doneCallback();
|
|
1209
|
+
});
|
|
1210
|
+
};
|
|
1211
|
+
} else {
|
|
1212
|
+
// #region agent log
|
|
1213
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:loadModulesNotDefined',message:'loadModules not defined yet',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
1214
|
+
// #endregion
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// 追踪启动脚本执行
|
|
1218
|
+
// #region agent log
|
|
1219
|
+
var originalOnLoad = window.onload;
|
|
1220
|
+
window.onload = function() {
|
|
1221
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:windowOnLoad',message:'window.onload fired',data:{hasCONFIG_FILENAME:!!window.CONFIG_FILENAME,hasSCENE_PATH:!!window.SCENE_PATH,hasPRELOAD_MODULES:!!window.PRELOAD_MODULES,preloadModulesCount:window.PRELOAD_MODULES?window.PRELOAD_MODULES.length:0},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
1222
|
+
if (originalOnLoad) originalOnLoad();
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
// 检查启动脚本是否会在 DOMContentLoaded 时执行
|
|
1226
|
+
if (document.readyState === 'loading') {
|
|
1227
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1228
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:domContentLoaded',message:'DOMContentLoaded fired',data:{hasCONFIG_FILENAME:!!window.CONFIG_FILENAME,hasSCENE_PATH:!!window.SCENE_PATH,hasPRELOAD_MODULES:!!window.PRELOAD_MODULES},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
1229
|
+
});
|
|
1230
|
+
} else {
|
|
1231
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:domAlreadyLoaded',message:'DOM already loaded',data:{readyState:document.readyState,hasCONFIG_FILENAME:!!window.CONFIG_FILENAME,hasSCENE_PATH:!!window.SCENE_PATH,hasPRELOAD_MODULES:!!window.PRELOAD_MODULES},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// 添加定时器来检查启动脚本是否执行
|
|
1235
|
+
setTimeout(function() {
|
|
1236
|
+
// #region agent log
|
|
1237
|
+
var appExists = typeof app !== 'undefined';
|
|
1238
|
+
var canvas = document.getElementById('application-canvas');
|
|
1239
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:startupCheck2s',message:'Startup check after 2s',data:{hasApp:appExists,appIsPlaying:appExists&&app.isPlaying,hasCanvas:!!canvas,canvasWidth:canvas?canvas.width:0,canvasHeight:canvas?canvas.height:0},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
1240
|
+
// #endregion
|
|
1241
|
+
}, 2000);
|
|
1242
|
+
|
|
1243
|
+
setTimeout(function() {
|
|
1244
|
+
// #region agent log
|
|
1245
|
+
var appExists = typeof app !== 'undefined';
|
|
1246
|
+
var canvas = document.getElementById('application-canvas');
|
|
1247
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:startupCheck5s',message:'Startup check after 5s',data:{hasApp:appExists,appIsPlaying:appExists&&app.isPlaying,hasCanvas:!!canvas,canvasWidth:canvas?canvas.width:0,canvasHeight:canvas?canvas.height:0},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
1248
|
+
// #endregion
|
|
1249
|
+
}, 5000);
|
|
1250
|
+
|
|
1251
|
+
setTimeout(function() {
|
|
1252
|
+
// #region agent log
|
|
1253
|
+
var appExists = typeof app !== 'undefined';
|
|
1254
|
+
fetch('http://127.0.0.1:7243/ingest/cd641f99-8aa7-4e81-8bdc-9c76de322a79',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'builder.ts:patchConfigure:startupCheck5s',message:'Startup check after 5s',data:{hasApp:appExists,appIsPlaying:appExists&&app.isPlaying,hasCanvas:!!document.getElementById('application-canvas'),canvasWidth:document.getElementById('application-canvas')?document.getElementById('application-canvas').width:0,canvasHeight:document.getElementById('application-canvas')?document.getElementById('application-canvas').height:0},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'F'})}).catch(()=>{});
|
|
1255
|
+
// #endregion
|
|
1256
|
+
}, 5000);
|
|
1257
|
+
// #endregion
|
|
1258
|
+
})();
|
|
1259
|
+
`;
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* 生成设置脚本 - 替代原始的 __settings__.js
|
|
1263
|
+
* 使用 data URL 替代文件路径,以支持单 HTML 文件运行
|
|
1264
|
+
*/
|
|
1265
|
+
generateSettingsScript(configDataUrl, sceneDataUrl) {
|
|
1266
|
+
const appProps = this.configJson.application_properties || {};
|
|
1267
|
+
const scripts = appProps.scripts || [];
|
|
1268
|
+
// 处理预加载模块(如 Ammo.js)- 转换为 data URL
|
|
1269
|
+
const preloadModules = this.generatePreloadModules();
|
|
1270
|
+
return `
|
|
1271
|
+
window.ASSET_PREFIX = "";
|
|
1272
|
+
window.SCRIPT_PREFIX = "";
|
|
1273
|
+
window.SCENE_PATH = "${sceneDataUrl || ''}";
|
|
1274
|
+
window.CONTEXT_OPTIONS = {
|
|
1275
|
+
'antialias': ${appProps.antiAlias !== false},
|
|
1276
|
+
'alpha': ${appProps.transparentCanvas === true},
|
|
1277
|
+
'preserveDrawingBuffer': ${appProps.preserveDrawingBuffer === true},
|
|
1278
|
+
'deviceTypes': ['webgl2', 'webgl1'],
|
|
1279
|
+
'powerPreference': "default"
|
|
1280
|
+
};
|
|
1281
|
+
window.SCRIPTS = [${scripts.join(', ')}];
|
|
1282
|
+
window.CONFIG_FILENAME = "${configDataUrl}";
|
|
1283
|
+
window.INPUT_SETTINGS = {
|
|
1284
|
+
useKeyboard: ${appProps.useKeyboard !== false},
|
|
1285
|
+
useMouse: ${appProps.useMouse !== false},
|
|
1286
|
+
useGamepads: ${appProps.useGamepads === true},
|
|
1287
|
+
useTouch: ${appProps.useTouch !== false}
|
|
1288
|
+
};
|
|
1289
|
+
pc.script.legacy = ${appProps.useLegacyScripts === true};
|
|
1290
|
+
window.PRELOAD_MODULES = ${JSON.stringify(preloadModules)};
|
|
1291
|
+
`;
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* 生成预加载模块配置
|
|
1295
|
+
* 对于 playable ads,只使用 JS fallback 版本,跳过 WASM(避免 data URL 加载问题)
|
|
1296
|
+
*/
|
|
1297
|
+
generatePreloadModules() {
|
|
1298
|
+
const modules = [];
|
|
1299
|
+
// 查找 wasm 类型的资产(如 Ammo.js)
|
|
1300
|
+
for (const [id, asset] of Object.entries(this.configJson.assets || {})) {
|
|
1301
|
+
const assetData = asset;
|
|
1302
|
+
if (assetData.type === 'wasm' && assetData.data) {
|
|
1303
|
+
const moduleData = assetData.data;
|
|
1304
|
+
const fallbackAssetId = moduleData.fallbackScriptId;
|
|
1305
|
+
let fallbackUrl = '';
|
|
1306
|
+
// 只获取 fallback script 的 data URL(纯 JS 版本)
|
|
1307
|
+
if (fallbackAssetId && this.configJson.assets[fallbackAssetId]) {
|
|
1308
|
+
const fallbackAsset = this.configJson.assets[fallbackAssetId];
|
|
1309
|
+
fallbackUrl = fallbackAsset.file?.url || '';
|
|
1310
|
+
}
|
|
1311
|
+
// 只使用 fallback URL,跳过 WASM 和 glue script
|
|
1312
|
+
// PlayCanvas 会自动使用 fallback 当 wasmUrl 和 glueUrl 为空时
|
|
1313
|
+
if (moduleData.moduleName && fallbackUrl) {
|
|
1314
|
+
modules.push({
|
|
1315
|
+
moduleName: moduleData.moduleName,
|
|
1316
|
+
glueUrl: '', // 清空 - 不使用 WASM 胶水脚本
|
|
1317
|
+
wasmUrl: '', // 清空 - 不使用 WASM 二进制
|
|
1318
|
+
fallbackUrl: fallbackUrl, // 只使用纯 JS fallback
|
|
1319
|
+
preload: true
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return modules;
|
|
1325
|
+
}
|
|
1326
|
+
/**
|
|
1327
|
+
* 输出 ZIP 文件
|
|
1328
|
+
*/
|
|
1329
|
+
async outputZIP(outputDir) {
|
|
1330
|
+
return new Promise(async (resolve, reject) => {
|
|
1331
|
+
const zipPath = path.join(outputDir, 'playable.zip');
|
|
1332
|
+
const { createWriteStream } = await import('fs');
|
|
1333
|
+
const stream = createWriteStream(zipPath);
|
|
1334
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
1335
|
+
archive.pipe(stream);
|
|
1336
|
+
try {
|
|
1337
|
+
// 先创建临时 HTML 文件
|
|
1338
|
+
const tempHtmlPath = path.join(outputDir, 'temp-index.html');
|
|
1339
|
+
const htmlTemplate = await this.loadHTMLTemplate();
|
|
1340
|
+
const assets = this.converter.getAssets();
|
|
1341
|
+
// 创建 URL 映射
|
|
1342
|
+
const urlMap = new Map();
|
|
1343
|
+
for (const asset of assets) {
|
|
1344
|
+
const dataUrl = this.converter.getDataUrl(asset.path);
|
|
1345
|
+
if (dataUrl) {
|
|
1346
|
+
urlMap.set(asset.path, dataUrl);
|
|
1347
|
+
urlMap.set(`/${asset.path}`, dataUrl);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
let html = this.converter.replaceUrlsWithBase64(htmlTemplate, urlMap);
|
|
1351
|
+
if (this.options.inlineGameScripts !== false) {
|
|
1352
|
+
html = await this.converter.inlineScripts(html);
|
|
1353
|
+
}
|
|
1354
|
+
html = this.platformAdapter.modifyHTML(html, assets);
|
|
1355
|
+
const platformScript = this.platformAdapter.getPlatformScript();
|
|
1356
|
+
html = html.replace('</body>', `<script>${platformScript}</script></body>`);
|
|
1357
|
+
await fs.writeFile(tempHtmlPath, html, 'utf-8');
|
|
1358
|
+
archive.file(tempHtmlPath, { name: 'index.html' });
|
|
1359
|
+
// 如果启用外部文件,添加资产文件
|
|
1360
|
+
if (this.options.externFiles) {
|
|
1361
|
+
const assets = this.converter.getAssets();
|
|
1362
|
+
const folderName = this.options.externFiles?.folderName || 'assets';
|
|
1363
|
+
for (const asset of assets) {
|
|
1364
|
+
const fullPath = path.join(this.projectDir, asset.path);
|
|
1365
|
+
try {
|
|
1366
|
+
await fs.access(fullPath);
|
|
1367
|
+
archive.file(fullPath, { name: `${folderName}/${path.basename(asset.path)}` });
|
|
1368
|
+
}
|
|
1369
|
+
catch (error) {
|
|
1370
|
+
console.warn(`Warning: Failed to add asset to ZIP: ${asset.path}`);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
await archive.finalize();
|
|
1375
|
+
stream.on('close', async () => {
|
|
1376
|
+
const { statSync } = await import('fs');
|
|
1377
|
+
const stats = statSync(zipPath);
|
|
1378
|
+
this.sizeReport.total = stats.size;
|
|
1379
|
+
this.sizeReport.assets['playable.zip'] = stats.size;
|
|
1380
|
+
// 清理临时文件
|
|
1381
|
+
try {
|
|
1382
|
+
await fs.unlink(tempHtmlPath);
|
|
1383
|
+
}
|
|
1384
|
+
catch (e) {
|
|
1385
|
+
// 忽略清理错误
|
|
1386
|
+
}
|
|
1387
|
+
resolve(zipPath);
|
|
1388
|
+
});
|
|
1389
|
+
stream.on('error', reject);
|
|
1390
|
+
archive.on('error', reject);
|
|
1391
|
+
}
|
|
1392
|
+
catch (error) {
|
|
1393
|
+
reject(error);
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* 加载 HTML 模板
|
|
1399
|
+
*/
|
|
1400
|
+
async loadHTMLTemplate() {
|
|
1401
|
+
// 尝试多个可能的路径
|
|
1402
|
+
const possiblePaths = [
|
|
1403
|
+
// 开发环境:从源目录
|
|
1404
|
+
path.join(__dirname, '../../templates/playable-ad.html'),
|
|
1405
|
+
// 生产环境:从 dist 目录
|
|
1406
|
+
path.join(__dirname, '../templates/playable-ad.html'),
|
|
1407
|
+
// 从项目根目录
|
|
1408
|
+
path.join(this.projectDir, 'templates/playable-ad.html'),
|
|
1409
|
+
];
|
|
1410
|
+
for (const templatePath of possiblePaths) {
|
|
1411
|
+
try {
|
|
1412
|
+
return await fs.readFile(templatePath, 'utf-8');
|
|
1413
|
+
}
|
|
1414
|
+
catch (error) {
|
|
1415
|
+
// 继续尝试下一个路径
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
// 如果所有路径都失败,返回默认模板
|
|
1419
|
+
return this.getDefaultTemplate();
|
|
1420
|
+
}
|
|
1421
|
+
/**
|
|
1422
|
+
* 获取默认 HTML 模板
|
|
1423
|
+
*/
|
|
1424
|
+
getDefaultTemplate() {
|
|
1425
|
+
return `<!DOCTYPE html>
|
|
1426
|
+
<html lang="zh-CN">
|
|
1427
|
+
<head>
|
|
1428
|
+
<meta charset="UTF-8">
|
|
1429
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1430
|
+
<title>Playable Ad</title>
|
|
1431
|
+
<style>
|
|
1432
|
+
* {
|
|
1433
|
+
margin: 0;
|
|
1434
|
+
padding: 0;
|
|
1435
|
+
box-sizing: border-box;
|
|
1436
|
+
}
|
|
1437
|
+
html, body {
|
|
1438
|
+
width: 100%;
|
|
1439
|
+
height: 100%;
|
|
1440
|
+
overflow: hidden;
|
|
1441
|
+
background: #000;
|
|
1442
|
+
}
|
|
1443
|
+
#application-canvas {
|
|
1444
|
+
display: block;
|
|
1445
|
+
width: 100%;
|
|
1446
|
+
height: 100%;
|
|
1447
|
+
}
|
|
1448
|
+
</style>
|
|
1449
|
+
</head>
|
|
1450
|
+
<body>
|
|
1451
|
+
<canvas id="application-canvas"></canvas>
|
|
1452
|
+
<!-- PlayCanvas Engine, Config 和 Start Script 将在这里注入 -->
|
|
1453
|
+
</body>
|
|
1454
|
+
</html>`;
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* 获取大小报告
|
|
1458
|
+
*/
|
|
1459
|
+
getSizeReport() {
|
|
1460
|
+
return this.sizeReport;
|
|
1461
|
+
}
|
|
1462
|
+
}
|