@playcraft/build 0.0.2

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.
Files changed (74) hide show
  1. package/README.md +96 -0
  2. package/dist/base-builder.d.ts +66 -0
  3. package/dist/base-builder.js +415 -0
  4. package/dist/converter.d.ts +35 -0
  5. package/dist/converter.js +148 -0
  6. package/dist/generators/config-generator.d.ts +7 -0
  7. package/dist/generators/config-generator.js +122 -0
  8. package/dist/generators/settings-generator.d.ts +14 -0
  9. package/dist/generators/settings-generator.js +100 -0
  10. package/dist/index.d.ts +14 -0
  11. package/dist/index.js +14 -0
  12. package/dist/loaders/playcanvas-loader.d.ts +10 -0
  13. package/dist/loaders/playcanvas-loader.js +18 -0
  14. package/dist/loaders/playcraft-loader.d.ts +10 -0
  15. package/dist/loaders/playcraft-loader.js +51 -0
  16. package/dist/platforms/applovin.d.ts +10 -0
  17. package/dist/platforms/applovin.js +67 -0
  18. package/dist/platforms/base.d.ts +29 -0
  19. package/dist/platforms/base.js +11 -0
  20. package/dist/platforms/bigo.d.ts +15 -0
  21. package/dist/platforms/bigo.js +77 -0
  22. package/dist/platforms/facebook.d.ts +9 -0
  23. package/dist/platforms/facebook.js +37 -0
  24. package/dist/platforms/google.d.ts +10 -0
  25. package/dist/platforms/google.js +53 -0
  26. package/dist/platforms/index.d.ts +14 -0
  27. package/dist/platforms/index.js +47 -0
  28. package/dist/platforms/ironsource.d.ts +10 -0
  29. package/dist/platforms/ironsource.js +71 -0
  30. package/dist/platforms/liftoff.d.ts +10 -0
  31. package/dist/platforms/liftoff.js +56 -0
  32. package/dist/platforms/moloco.d.ts +10 -0
  33. package/dist/platforms/moloco.js +53 -0
  34. package/dist/platforms/snapchat.d.ts +10 -0
  35. package/dist/platforms/snapchat.js +59 -0
  36. package/dist/platforms/tiktok.d.ts +15 -0
  37. package/dist/platforms/tiktok.js +65 -0
  38. package/dist/platforms/unity.d.ts +10 -0
  39. package/dist/platforms/unity.js +69 -0
  40. package/dist/playable-builder.d.ts +97 -0
  41. package/dist/playable-builder.js +590 -0
  42. package/dist/types.d.ts +90 -0
  43. package/dist/types.js +1 -0
  44. package/dist/vite/config-builder.d.ts +15 -0
  45. package/dist/vite/config-builder.js +212 -0
  46. package/dist/vite/platform-configs.d.ts +38 -0
  47. package/dist/vite/platform-configs.js +257 -0
  48. package/dist/vite/plugin-model-compression.d.ts +11 -0
  49. package/dist/vite/plugin-model-compression.js +63 -0
  50. package/dist/vite/plugin-platform.d.ts +17 -0
  51. package/dist/vite/plugin-platform.js +241 -0
  52. package/dist/vite/plugin-playcanvas.d.ts +18 -0
  53. package/dist/vite/plugin-playcanvas.js +711 -0
  54. package/dist/vite/plugin-source-builder.d.ts +15 -0
  55. package/dist/vite/plugin-source-builder.js +344 -0
  56. package/dist/vite-builder.d.ts +51 -0
  57. package/dist/vite-builder.js +122 -0
  58. package/package.json +51 -0
  59. package/templates/__loading__.js +100 -0
  60. package/templates/__modules__.js +47 -0
  61. package/templates/__settings__.template.js +20 -0
  62. package/templates/__start__.js +332 -0
  63. package/templates/index.html +18 -0
  64. package/templates/logo.png +0 -0
  65. package/templates/manifest.json +1 -0
  66. package/templates/patches/cannon.min.js +28 -0
  67. package/templates/patches/lz4.js +10 -0
  68. package/templates/patches/one-page-http-get.js +20 -0
  69. package/templates/patches/one-page-inline-game-scripts.js +20 -0
  70. package/templates/patches/one-page-mraid-resize-canvas.js +46 -0
  71. package/templates/patches/p2.min.js +27 -0
  72. package/templates/patches/playcraft-no-xhr.js +52 -0
  73. package/templates/playcanvas-stable.min.js +16363 -0
  74. package/templates/styles.css +43 -0
@@ -0,0 +1,711 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { createRequire } from 'module';
5
+ /**
6
+ * PlayCanvas Vite 插件
7
+ * 处理 PlayCanvas 特定的资源转换和内联
8
+ */
9
+ export function vitePlayCanvasPlugin(options) {
10
+ return {
11
+ name: 'vite-plugin-playcanvas',
12
+ enforce: 'pre',
13
+ async transformIndexHtml(html) {
14
+ if (options.ammoReplacement) {
15
+ html = await injectPhysicsLibrary(html, options);
16
+ }
17
+ if (options.outputFormat === 'html') {
18
+ // 1. 内联 PlayCanvas Engine + 引擎补丁
19
+ html = await inlineEngineScript(html, options.baseBuildDir, options);
20
+ // 2. 内联并转换 __settings__.js
21
+ html = await inlineAndConvertSettings(html, options.baseBuildDir, options);
22
+ // 3. 内联 __modules__.js
23
+ html = await inlineModulesScript(html, options.baseBuildDir);
24
+ // 4. 内联 __start__.js
25
+ html = await inlineStartScript(html, options.baseBuildDir, options);
26
+ // 5. 内联 __loading__.js
27
+ html = await inlineLoadingScript(html, options.baseBuildDir);
28
+ // 6. 内联 CSS
29
+ html = await inlineCSS(html, options.baseBuildDir, options);
30
+ // 7. 处理 manifest.json
31
+ html = await inlineManifest(html, options.baseBuildDir, options);
32
+ }
33
+ return html;
34
+ },
35
+ async transform(code, id) {
36
+ // 转换 __settings__.js 中的资源路径为 data URLs
37
+ if (id.endsWith('__settings__.js')) {
38
+ return await convertSettingsToDataUrls(code, options.baseBuildDir, options);
39
+ }
40
+ return code;
41
+ },
42
+ };
43
+ }
44
+ /**
45
+ * 内联 PlayCanvas Engine
46
+ */
47
+ async function inlineEngineScript(html, baseBuildDir, options) {
48
+ const engineNames = [
49
+ 'playcanvas-stable.min.js',
50
+ 'playcanvas.min.js',
51
+ '__lib__.js',
52
+ ];
53
+ for (const engineName of engineNames) {
54
+ const enginePath = path.join(baseBuildDir, engineName);
55
+ try {
56
+ await fs.access(enginePath);
57
+ const engineCode = await fs.readFile(enginePath, 'utf-8');
58
+ const patchScripts = await getEnginePatchScripts(options);
59
+ const engineScript = options.compressEngine
60
+ ? `${await getLz4InlineScript()}${await buildCompressedEngineScript(engineCode)}`
61
+ : `<script>${engineCode}</script>`;
62
+ // 替换 script 标签
63
+ const scriptPattern = new RegExp(`<script[^>]*src=["']${engineName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}["'][^>]*></script>`, 'i');
64
+ html = html.replace(scriptPattern, `${engineScript}${patchScripts}`);
65
+ return html;
66
+ }
67
+ catch (error) {
68
+ // 继续尝试下一个
69
+ }
70
+ }
71
+ return html;
72
+ }
73
+ /**
74
+ * 内联并转换 __settings__.js
75
+ */
76
+ async function inlineAndConvertSettings(html, baseBuildDir, options) {
77
+ const settingsPath = path.join(baseBuildDir, '__settings__.js');
78
+ try {
79
+ await fs.access(settingsPath);
80
+ }
81
+ catch (error) {
82
+ // __settings__.js 不存在,尝试从 config.json 生成
83
+ return await generateAndInlineSettings(html, baseBuildDir, options);
84
+ }
85
+ let settingsCode = await fs.readFile(settingsPath, 'utf-8');
86
+ // 转换资源URL为data URLs
87
+ settingsCode = await convertSettingsToDataUrls(settingsCode, baseBuildDir, options);
88
+ // 替换 script 标签
89
+ const scriptPattern = /<script[^>]*src=["']__settings__\.js["'][^>]*><\/script>/i;
90
+ html = html.replace(scriptPattern, `<script>${settingsCode}</script>`);
91
+ return html;
92
+ }
93
+ /**
94
+ * 生成并内联 settings(如果 __settings__.js 不存在)
95
+ */
96
+ async function generateAndInlineSettings(html, baseBuildDir, options) {
97
+ const configPath = path.join(baseBuildDir, 'config.json');
98
+ const configContent = await fs.readFile(configPath, 'utf-8');
99
+ let configJson = JSON.parse(configContent);
100
+ if (options.convertDataUrls) {
101
+ configJson = await inlineConfigAssetUrls(configJson, baseBuildDir);
102
+ }
103
+ if (options.ammoReplacement) {
104
+ configJson = stripAmmoAssets(configJson);
105
+ }
106
+ if (options.mraidSupport) {
107
+ configJson = applyMraidConfig(configJson);
108
+ }
109
+ // 生成 config 值
110
+ const configValue = buildConfigValue(configJson);
111
+ // 生成 scene data URL
112
+ let sceneDataUrl = '';
113
+ if (configJson.scenes && configJson.scenes.length > 0) {
114
+ const sceneUrl = configJson.scenes[0].url;
115
+ if (sceneUrl && !sceneUrl.startsWith('data:')) {
116
+ const scenePath = path.join(baseBuildDir, sceneUrl);
117
+ try {
118
+ const sceneContent = await fs.readFile(scenePath, 'utf-8');
119
+ sceneDataUrl = `data:application/json;base64,${Buffer.from(sceneContent).toString('base64')}`;
120
+ }
121
+ catch (error) {
122
+ console.warn(`警告: 场景文件不存在: ${sceneUrl}`);
123
+ }
124
+ }
125
+ else {
126
+ sceneDataUrl = sceneUrl;
127
+ }
128
+ }
129
+ const appProps = configJson.application_properties || {};
130
+ const scripts = appProps.scripts || [];
131
+ const preloadModules = extractPreloadModules(configJson);
132
+ const settingsCode = `
133
+ window.ASSET_PREFIX = "";
134
+ window.SCRIPT_PREFIX = "";
135
+ window.SCENE_PATH = "${sceneDataUrl}";
136
+ window.CONTEXT_OPTIONS = {
137
+ 'antialias': ${appProps.antiAlias !== false},
138
+ 'alpha': ${appProps.transparentCanvas === true},
139
+ 'preserveDrawingBuffer': ${appProps.preserveDrawingBuffer === true},
140
+ 'deviceTypes': ['webgl2', 'webgl1'],
141
+ 'powerPreference': "default"
142
+ };
143
+ window.SCRIPTS = [${scripts.join(', ')}];
144
+ window.CONFIG_FILENAME = ${configValue};
145
+ window.INPUT_SETTINGS = {
146
+ useKeyboard: ${appProps.useKeyboard !== false},
147
+ useMouse: ${appProps.useMouse !== false},
148
+ useGamepads: ${appProps.useGamepads === true},
149
+ useTouch: ${appProps.useTouch !== false}
150
+ };
151
+ pc.script.legacy = ${appProps.useLegacyScripts === true};
152
+ window.PRELOAD_MODULES = ${JSON.stringify(preloadModules)};
153
+ `;
154
+ // 在 </head> 之前插入
155
+ html = html.replace('</head>', `<script>${settingsCode}</script>\n</head>`);
156
+ return html;
157
+ }
158
+ /**
159
+ * 转换 settings 代码中的资源URL为 data URLs
160
+ */
161
+ async function convertSettingsToDataUrls(settingsCode, baseBuildDir, options) {
162
+ // 1. 转换 config.json
163
+ settingsCode = await convertConfigUrl(settingsCode, baseBuildDir, options);
164
+ // 2. 转换场景文件
165
+ settingsCode = await convertSceneUrl(settingsCode, baseBuildDir);
166
+ // 3. 转换 PRELOAD_MODULES(优先使用 JS fallback)
167
+ settingsCode = await convertPreloadModules(settingsCode, baseBuildDir, options);
168
+ return settingsCode;
169
+ }
170
+ /**
171
+ * 转换 CONFIG_FILENAME 为 data URL
172
+ */
173
+ async function convertConfigUrl(settingsCode, baseBuildDir, options) {
174
+ const configMatch = settingsCode.match(/window\.CONFIG_FILENAME\s*=\s*"([^"]+)"/);
175
+ if (!configMatch) {
176
+ return settingsCode;
177
+ }
178
+ const configPath = configMatch[1];
179
+ if (configPath.startsWith('data:')) {
180
+ return settingsCode; // 已经是 data URL
181
+ }
182
+ const fullConfigPath = path.join(baseBuildDir, configPath);
183
+ try {
184
+ const configContent = await fs.readFile(fullConfigPath, 'utf-8');
185
+ let configJson = JSON.parse(configContent);
186
+ if (options.convertDataUrls) {
187
+ configJson = await inlineConfigAssetUrls(configJson, baseBuildDir);
188
+ }
189
+ if (options.ammoReplacement) {
190
+ configJson = stripAmmoAssets(configJson);
191
+ }
192
+ if (options.mraidSupport) {
193
+ configJson = applyMraidConfig(configJson);
194
+ }
195
+ const configValue = buildConfigValue(configJson);
196
+ return settingsCode.replace(/window\.CONFIG_FILENAME\s*=\s*"[^"]+"/, `window.CONFIG_FILENAME = ${configValue}`);
197
+ }
198
+ catch (error) {
199
+ console.warn(`警告: 无法读取配置文件: ${configPath}`);
200
+ return settingsCode;
201
+ }
202
+ }
203
+ /**
204
+ * 转换 SCENE_PATH 为 data URL
205
+ */
206
+ async function convertSceneUrl(settingsCode, baseBuildDir) {
207
+ const sceneMatch = settingsCode.match(/window\.SCENE_PATH\s*=\s*"([^"]+)"/);
208
+ if (!sceneMatch) {
209
+ return settingsCode;
210
+ }
211
+ const scenePath = sceneMatch[1];
212
+ if (scenePath.startsWith('data:') || !scenePath) {
213
+ return settingsCode; // 已经是 data URL 或为空
214
+ }
215
+ const fullScenePath = path.join(baseBuildDir, scenePath);
216
+ try {
217
+ const sceneContent = await fs.readFile(fullScenePath, 'utf-8');
218
+ const sceneDataUrl = `data:application/json;base64,${Buffer.from(sceneContent).toString('base64')}`;
219
+ return settingsCode.replace(/window\.SCENE_PATH\s*=\s*"[^"]+"/, `window.SCENE_PATH = "${sceneDataUrl}"`);
220
+ }
221
+ catch (error) {
222
+ console.warn(`警告: 无法读取场景文件: ${scenePath}`);
223
+ return settingsCode;
224
+ }
225
+ }
226
+ /**
227
+ * 转换 PRELOAD_MODULES 中的资源URL
228
+ * 对于 Playable Ads,优先使用 fallback JS 版本(跳过 WASM)
229
+ */
230
+ async function convertPreloadModules(settingsCode, baseBuildDir, options) {
231
+ // 匹配 PRELOAD_MODULES 数组(支持单引号和双引号)
232
+ const modulesMatch = settingsCode.match(/window\.PRELOAD_MODULES\s*=\s*(\[[\s\S]*?\]);/);
233
+ if (!modulesMatch) {
234
+ return settingsCode;
235
+ }
236
+ try {
237
+ // 使用 Function 构造函数安全地解析 JavaScript 对象字面量
238
+ const modulesStr = modulesMatch[1];
239
+ let modules = new Function(`return ${modulesStr}`)();
240
+ if (options.ammoReplacement) {
241
+ const beforeCount = modules.length;
242
+ modules = modules.filter((module) => !isAmmoModule(module));
243
+ const removed = beforeCount - modules.length;
244
+ if (removed > 0) {
245
+ console.warn(`警告: 已移除 ${removed} 个 Ammo 预加载模块,替换为 ${options.ammoReplacement}`);
246
+ }
247
+ }
248
+ // 转换每个模块的URL为data URL
249
+ for (const module of modules) {
250
+ // 对于 Playable Ads,优先使用 fallback JS 版本,跳过 WASM
251
+ if (module.fallbackUrl && !module.fallbackUrl.startsWith('data:')) {
252
+ const fallbackPath = path.join(baseBuildDir, module.fallbackUrl);
253
+ try {
254
+ const fallbackCode = await fs.readFile(fallbackPath, 'utf-8');
255
+ module.fallbackUrl = `data:text/javascript;base64,${Buffer.from(fallbackCode).toString('base64')}`;
256
+ // 清空 WASM 相关 URL,强制使用 fallback
257
+ module.glueUrl = '';
258
+ module.wasmUrl = '';
259
+ }
260
+ catch (error) {
261
+ console.warn(`警告: 无法读取 fallback 文件: ${module.fallbackUrl}`);
262
+ // 如果 fallback 读取失败,尝试读取 WASM 相关文件
263
+ if (module.glueUrl && !module.glueUrl.startsWith('data:')) {
264
+ const gluePath = path.join(baseBuildDir, module.glueUrl);
265
+ try {
266
+ const glueCode = await fs.readFile(gluePath, 'utf-8');
267
+ module.glueUrl = `data:text/javascript;base64,${Buffer.from(glueCode).toString('base64')}`;
268
+ }
269
+ catch (error) {
270
+ console.warn(`警告: 无法读取 glue 文件: ${module.glueUrl}`);
271
+ }
272
+ }
273
+ if (module.wasmUrl && !module.wasmUrl.startsWith('data:')) {
274
+ const wasmPath = path.join(baseBuildDir, module.wasmUrl);
275
+ try {
276
+ const wasmBinary = await fs.readFile(wasmPath);
277
+ module.wasmUrl = `data:application/wasm;base64,${wasmBinary.toString('base64')}`;
278
+ }
279
+ catch (error) {
280
+ console.warn(`警告: 无法读取 WASM 文件: ${module.wasmUrl}`);
281
+ }
282
+ }
283
+ }
284
+ }
285
+ else {
286
+ // 如果没有 fallback,尝试转换 WASM 相关文件
287
+ if (module.glueUrl && !module.glueUrl.startsWith('data:')) {
288
+ const gluePath = path.join(baseBuildDir, module.glueUrl);
289
+ try {
290
+ const glueCode = await fs.readFile(gluePath, 'utf-8');
291
+ module.glueUrl = `data:text/javascript;base64,${Buffer.from(glueCode).toString('base64')}`;
292
+ }
293
+ catch (error) {
294
+ console.warn(`警告: 无法读取 glue 文件: ${module.glueUrl}`);
295
+ }
296
+ }
297
+ if (module.wasmUrl && !module.wasmUrl.startsWith('data:')) {
298
+ const wasmPath = path.join(baseBuildDir, module.wasmUrl);
299
+ try {
300
+ const wasmBinary = await fs.readFile(wasmPath);
301
+ module.wasmUrl = `data:application/wasm;base64,${wasmBinary.toString('base64')}`;
302
+ }
303
+ catch (error) {
304
+ console.warn(`警告: 无法读取 WASM 文件: ${module.wasmUrl}`);
305
+ }
306
+ }
307
+ }
308
+ }
309
+ // 重新生成 PRELOAD_MODULES 配置(使用双引号 JSON 格式)
310
+ const newModulesStr = JSON.stringify(modules, null, 4);
311
+ return settingsCode.replace(/window\.PRELOAD_MODULES\s*=\s*\[[\s\S]*?\];/, `window.PRELOAD_MODULES = ${newModulesStr};`);
312
+ }
313
+ catch (error) {
314
+ console.warn(`警告: 无法解析 PRELOAD_MODULES: ${error instanceof Error ? error.message : String(error)}`);
315
+ return settingsCode;
316
+ }
317
+ }
318
+ /**
319
+ * 从 config.json 提取 PRELOAD_MODULES
320
+ */
321
+ function extractPreloadModules(configJson) {
322
+ const modules = [];
323
+ if (configJson.assets) {
324
+ for (const [id, asset] of Object.entries(configJson.assets)) {
325
+ const assetData = asset;
326
+ if (assetData.type === 'wasm' && assetData.data) {
327
+ const moduleData = assetData.data;
328
+ const fallbackAssetId = moduleData.fallbackScriptId;
329
+ let fallbackUrl = '';
330
+ if (fallbackAssetId && configJson.assets[fallbackAssetId]) {
331
+ const fallbackAsset = configJson.assets[fallbackAssetId];
332
+ fallbackUrl = fallbackAsset.file?.url || '';
333
+ }
334
+ if (moduleData.moduleName && fallbackUrl) {
335
+ modules.push({
336
+ moduleName: moduleData.moduleName,
337
+ glueUrl: '',
338
+ wasmUrl: '',
339
+ fallbackUrl: fallbackUrl,
340
+ preload: true,
341
+ });
342
+ }
343
+ }
344
+ }
345
+ }
346
+ return modules;
347
+ }
348
+ /**
349
+ * 内联 __modules__.js
350
+ */
351
+ async function inlineModulesScript(html, baseBuildDir) {
352
+ const modulesPath = path.join(baseBuildDir, '__modules__.js');
353
+ try {
354
+ await fs.access(modulesPath);
355
+ }
356
+ catch (error) {
357
+ // __modules__.js 不存在,跳过
358
+ return html;
359
+ }
360
+ const modulesCode = await fs.readFile(modulesPath, 'utf-8');
361
+ // 替换 script 标签
362
+ const scriptPattern = /<script[^>]*src=["']__modules__\.js["'][^>]*><\/script>/i;
363
+ html = html.replace(scriptPattern, `<script>${modulesCode}</script>`);
364
+ // 如果 HTML 中没有找到 script 标签,在 <body> 开头添加
365
+ if (!scriptPattern.test(html)) {
366
+ html = html.replace('<body>', `<body>\n<script>${modulesCode}</script>\n`);
367
+ }
368
+ return html;
369
+ }
370
+ /**
371
+ * 内联 __game-scripts.js
372
+ */
373
+ async function inlineGameScripts(html, baseBuildDir) {
374
+ const gameScriptsPath = path.join(baseBuildDir, '__game-scripts.js');
375
+ try {
376
+ await fs.access(gameScriptsPath);
377
+ }
378
+ catch (error) {
379
+ // __game-scripts.js 不存在,跳过
380
+ return html;
381
+ }
382
+ const gameScriptsCode = await fs.readFile(gameScriptsPath, 'utf-8');
383
+ // 在 </head> 之前或第一个 <script> 标签之后插入游戏脚本
384
+ if (html.includes('</head>')) {
385
+ html = html.replace('</head>', `<script>${gameScriptsCode}</script>\n</head>`);
386
+ }
387
+ else {
388
+ // 如果没有 </head>,在第一个 <body> 标签之后插入
389
+ html = html.replace('<body>', `<body>\n<script>${gameScriptsCode}</script>\n`);
390
+ }
391
+ return html;
392
+ }
393
+ /**
394
+ * 内联 __start__.js
395
+ */
396
+ async function inlineStartScript(html, baseBuildDir, options) {
397
+ const startPath = path.join(baseBuildDir, '__start__.js');
398
+ let startCode = await fs.readFile(startPath, 'utf-8');
399
+ if (options.convertDataUrls) {
400
+ startCode = await inlineLogoInStartScript(startCode, baseBuildDir);
401
+ }
402
+ // 替换 script 标签
403
+ const scriptPattern = /<script[^>]*src=["']__start__\.js["'][^>]*><\/script>/i;
404
+ if (scriptPattern.test(html)) {
405
+ html = html.replace(scriptPattern, `<script>${startCode}</script>`);
406
+ }
407
+ else {
408
+ // 如果 HTML 中没有找到 script 标签,在 </body> 之前插入
409
+ html = html.replace('</body>', `<script>${startCode}</script>\n</body>`);
410
+ }
411
+ return html;
412
+ }
413
+ /**
414
+ * 内联 __loading__.js
415
+ */
416
+ async function inlineLoadingScript(html, baseBuildDir) {
417
+ const loadingPath = path.join(baseBuildDir, '__loading__.js');
418
+ const scriptPattern = /<script[^>]*src=["']__loading__\.js["'][^>]*><\/script>/i;
419
+ try {
420
+ await fs.access(loadingPath);
421
+ // 文件存在,内联它
422
+ let loadingCode = await fs.readFile(loadingPath, 'utf-8');
423
+ if (loadingCode.includes('${ASSET_PREFIX}logo.png')) {
424
+ loadingCode = await inlineLogoInStartScript(loadingCode, baseBuildDir);
425
+ }
426
+ if (scriptPattern.test(html)) {
427
+ html = html.replace(scriptPattern, `<script>${loadingCode}</script>`);
428
+ }
429
+ else {
430
+ // 如果 HTML 中没有找到 script 标签,在 </body> 之前添加
431
+ html = html.replace('</body>', `<script>${loadingCode}</script>\n</body>`);
432
+ }
433
+ }
434
+ catch (error) {
435
+ // __loading__.js 不存在,移除 script 标签
436
+ if (scriptPattern.test(html)) {
437
+ html = html.replace(scriptPattern, '');
438
+ }
439
+ }
440
+ return html;
441
+ }
442
+ /**
443
+ * 内联 CSS 文件
444
+ */
445
+ async function inlineCSS(html, baseBuildDir, options) {
446
+ // 匹配 <link rel="stylesheet" href="...">
447
+ const cssPattern = /<link[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']+)["'][^>]*>/gi;
448
+ const matches = Array.from(html.matchAll(cssPattern));
449
+ for (const match of matches) {
450
+ const cssPath = match[1];
451
+ if (cssPath.startsWith('data:') || cssPath.startsWith('http://') || cssPath.startsWith('https://')) {
452
+ continue; // 跳过已经是 data URL 或外部 URL
453
+ }
454
+ const fullCssPath = path.join(baseBuildDir, cssPath);
455
+ try {
456
+ await fs.access(fullCssPath);
457
+ let cssContent = await fs.readFile(fullCssPath, 'utf-8');
458
+ if (options.mraidSupport && !cssContent.includes('fill-mode-NONE')) {
459
+ cssContent += '\n#application-canvas.fill-mode-NONE { margin: 0; width: 100%; height: 100%; }\n';
460
+ }
461
+ // 替换 link 标签为 style 标签
462
+ html = html.replace(match[0], `<style>${cssContent}</style>`);
463
+ }
464
+ catch (error) {
465
+ console.warn(`警告: CSS 文件不存在: ${cssPath},移除引用`);
466
+ // 移除不存在的 CSS 引用
467
+ html = html.replace(match[0], '');
468
+ }
469
+ }
470
+ return html;
471
+ }
472
+ /**
473
+ * 内联 manifest.json
474
+ */
475
+ async function inlineManifest(html, baseBuildDir, options) {
476
+ // 匹配 <link rel="manifest" href="...">
477
+ const manifestPattern = /<link[^>]*rel=["']manifest["'][^>]*href=["']([^"']+)["'][^>]*>/gi;
478
+ const matches = Array.from(html.matchAll(manifestPattern));
479
+ for (const match of matches) {
480
+ const manifestPath = match[1];
481
+ if (options.convertDataUrls) {
482
+ // 单文件输出不需要 manifest,移除引用避免额外产物
483
+ html = html.replace(match[0], '');
484
+ continue;
485
+ }
486
+ if (manifestPath.startsWith('data:') || manifestPath.startsWith('http://') || manifestPath.startsWith('https://')) {
487
+ continue; // 跳过已经是 data URL 或外部 URL
488
+ }
489
+ const fullManifestPath = path.join(baseBuildDir, manifestPath);
490
+ try {
491
+ await fs.access(fullManifestPath);
492
+ const manifestContent = await fs.readFile(fullManifestPath, 'utf-8');
493
+ // 将 manifest 转换为 data URL 并内联到 meta 标签
494
+ const manifestDataUrl = `data:application/manifest+json;base64,${Buffer.from(manifestContent).toString('base64')}`;
495
+ html = html.replace(match[0], `<link rel="manifest" href="${manifestDataUrl}">`);
496
+ }
497
+ catch (error) {
498
+ console.warn(`警告: manifest 文件不存在: ${manifestPath},移除引用`);
499
+ // 移除不存在的 manifest 引用
500
+ html = html.replace(match[0], '');
501
+ }
502
+ }
503
+ return html;
504
+ }
505
+ async function injectPhysicsLibrary(html, options) {
506
+ const library = options.ammoReplacement;
507
+ if (!library) {
508
+ return html;
509
+ }
510
+ const filename = library === 'p2' ? 'p2.min.js' : 'cannon.min.js';
511
+ const code = await readPatchFile(filename);
512
+ if (!code) {
513
+ return html;
514
+ }
515
+ const tag = `<script>${code}</script>`;
516
+ if (html.includes('</head>')) {
517
+ return html.replace('</head>', `${tag}\n</head>`);
518
+ }
519
+ return `${tag}\n${html}`;
520
+ }
521
+ function isAmmoModule(module) {
522
+ const moduleName = module.moduleName?.toLowerCase() ?? '';
523
+ const glueUrl = module.glueUrl?.toLowerCase() ?? '';
524
+ const wasmUrl = module.wasmUrl?.toLowerCase() ?? '';
525
+ const fallbackUrl = module.fallbackUrl?.toLowerCase() ?? '';
526
+ return (moduleName.includes('ammo') ||
527
+ glueUrl.includes('ammo') ||
528
+ wasmUrl.includes('ammo') ||
529
+ fallbackUrl.includes('ammo'));
530
+ }
531
+ async function inlineConfigAssetUrls(configJson, baseBuildDir) {
532
+ if (!configJson?.assets) {
533
+ return configJson;
534
+ }
535
+ const assets = configJson.assets;
536
+ for (const asset of Object.values(assets)) {
537
+ const file = asset?.file;
538
+ if (!file?.url || typeof file.url !== 'string') {
539
+ continue;
540
+ }
541
+ const url = file.url;
542
+ if (url.startsWith('data:') || url.startsWith('http://') || url.startsWith('https://')) {
543
+ continue;
544
+ }
545
+ const cleanUrl = url.split('?')[0];
546
+ const fullPath = path.join(baseBuildDir, cleanUrl);
547
+ try {
548
+ const buffer = await fs.readFile(fullPath);
549
+ const mime = guessMimeType(cleanUrl);
550
+ file.url = `data:${mime};base64,${buffer.toString('base64')}`;
551
+ file.size = buffer.length;
552
+ // data URL 不需要 hash/variants,避免引擎追加 ?t=hash 造成无效 URL
553
+ if ('hash' in file) {
554
+ delete file.hash;
555
+ }
556
+ if ('variants' in file) {
557
+ delete file.variants;
558
+ }
559
+ }
560
+ catch (error) {
561
+ console.warn(`警告: 资源文件不存在: ${url}`);
562
+ }
563
+ }
564
+ return configJson;
565
+ }
566
+ async function inlineLogoInStartScript(startCode, baseBuildDir) {
567
+ const logoPath = path.join(baseBuildDir, 'logo.png');
568
+ try {
569
+ const buffer = await fs.readFile(logoPath);
570
+ const dataUrl = `data:image/png;base64,${buffer.toString('base64')}`;
571
+ return startCode.replace(/\$\{ASSET_PREFIX\}logo\.png/g, dataUrl);
572
+ }
573
+ catch (error) {
574
+ return startCode;
575
+ }
576
+ }
577
+ function guessMimeType(filePath) {
578
+ const ext = path.extname(filePath).toLowerCase();
579
+ switch (ext) {
580
+ case '.png':
581
+ return 'image/png';
582
+ case '.jpg':
583
+ case '.jpeg':
584
+ return 'image/jpeg';
585
+ case '.webp':
586
+ return 'image/webp';
587
+ case '.gif':
588
+ return 'image/gif';
589
+ case '.svg':
590
+ return 'image/svg+xml';
591
+ case '.glb':
592
+ return 'model/gltf-binary';
593
+ case '.gltf':
594
+ return 'model/gltf+json';
595
+ case '.json':
596
+ return 'application/json';
597
+ case '.js':
598
+ return 'text/javascript';
599
+ case '.css':
600
+ return 'text/css';
601
+ case '.wasm':
602
+ return 'application/wasm';
603
+ case '.ttf':
604
+ return 'font/ttf';
605
+ case '.otf':
606
+ return 'font/otf';
607
+ case '.mp3':
608
+ return 'audio/mpeg';
609
+ case '.wav':
610
+ return 'audio/wav';
611
+ case '.ogg':
612
+ return 'audio/ogg';
613
+ case '.mp4':
614
+ return 'video/mp4';
615
+ default:
616
+ return 'application/octet-stream';
617
+ }
618
+ }
619
+ function buildConfigValue(configJson) {
620
+ const configText = JSON.stringify(configJson);
621
+ return `"data:application/json;base64,${Buffer.from(configText).toString('base64')}"`;
622
+ }
623
+ function applyMraidConfig(configJson) {
624
+ const next = { ...configJson };
625
+ const props = { ...(next.application_properties || {}) };
626
+ props.fillMode = 'NONE';
627
+ next.application_properties = props;
628
+ return next;
629
+ }
630
+ function stripAmmoAssets(configJson) {
631
+ const next = { ...configJson };
632
+ if (!next.assets) {
633
+ return next;
634
+ }
635
+ const assets = { ...next.assets };
636
+ for (const [id, asset] of Object.entries(assets)) {
637
+ const assetData = asset;
638
+ const moduleName = assetData?.data?.moduleName?.toLowerCase?.() || '';
639
+ const fileUrl = assetData?.file?.url?.toLowerCase?.() || '';
640
+ const fileName = assetData?.file?.filename?.toLowerCase?.() || '';
641
+ const assetName = assetData?.name?.toLowerCase?.() || '';
642
+ if (moduleName.includes('ammo') ||
643
+ fileUrl.includes('ammo') ||
644
+ fileName.includes('ammo') ||
645
+ assetName.includes('ammo')) {
646
+ delete assets[id];
647
+ }
648
+ }
649
+ next.assets = assets;
650
+ return next;
651
+ }
652
+ async function getEnginePatchScripts(options) {
653
+ const scripts = [];
654
+ if (options.patchXhrOut) {
655
+ const patchCode = await readPatchFile('playcraft-no-xhr.js');
656
+ scripts.push(`<script>${patchCode}</script>`);
657
+ }
658
+ else if (options.configJsonInline) {
659
+ const patchCode = await readPatchFile('one-page-http-get.js');
660
+ scripts.push(`<script>${patchCode}</script>`);
661
+ }
662
+ if (options.inlineGameScripts) {
663
+ const patchCode = await readPatchFile('one-page-inline-game-scripts.js');
664
+ scripts.push(`<script>${patchCode}</script>`);
665
+ }
666
+ if (options.mraidSupport) {
667
+ const patchCode = await readPatchFile('one-page-mraid-resize-canvas.js');
668
+ scripts.push(`<script>${patchCode}</script>`);
669
+ }
670
+ if (options.compressEngine) {
671
+ // Focus patch: 确保 canvas 获得焦点以接收键盘事件
672
+ const focusPatch = `!function(){var e=function(){var e=document.getElementById("application-canvas");if(!e)return!1;try{e.focus()}catch(t){}e.addEventListener("pointerdown",function(){e.focus()}),e.addEventListener("click",function(){e.focus()});return!0},t=0;if(!e()){var n=setInterval(function(){(e()||++t>50)&&clearInterval(n)},100)}}();`;
673
+ scripts.push(`<script>${focusPatch}</script>`);
674
+ // Keyboard patch: 修复压缩后键盘事件不响应的问题
675
+ // 1. 确保 pc.app.keyboard 存在
676
+ // 2. 将键盘事件绑定到 window
677
+ // 3. 关键修复:在轮询成功后也调用事件注册函数 r()
678
+ const keyboardPatch = `!function(){var c=0,d=!1,i=function(){if(!window.pc||!window.pc.app||!window.pc.Keyboard)return!1;var a=window.pc.app;if(!a.keyboard)try{a.keyboard=new window.pc.Keyboard(window)}catch(x){}if(a.keyboard&&a.keyboard.attach)try{a.keyboard.attach(window)}catch(x){}var v=document.getElementById("application-canvas");if(v){v.tabIndex=0;try{v.focus()}catch(x){}}if(a.keyboard){r();return!0}return!1},r=function(){if(d||!window.pc||!window.pc.app||!window.pc.app.keyboard)return;var k=window.pc.app.keyboard;if(k._handleKeyDown){window.addEventListener("keydown",function(e){k._handleKeyDown(e)});window.addEventListener("keyup",function(e){k._handleKeyUp(e)});d=!0}};if(!i()){var o=setInterval(function(){if(i()||++c>50)clearInterval(o)},100)}}();`;
679
+ scripts.push(`<script>${keyboardPatch}</script>`);
680
+ }
681
+ return scripts.join('\n');
682
+ }
683
+ async function getLz4InlineScript() {
684
+ const lz4Code = await readPatchFile('lz4.js');
685
+ return `<script>${lz4Code}</script>`;
686
+ }
687
+ async function buildCompressedEngineScript(engineCode) {
688
+ const lz4 = await loadLz4Module();
689
+ const compressed = lz4.compress(Buffer.from(engineCode));
690
+ const base64 = Buffer.from(compressed).toString('base64');
691
+ // 遵循 PlayCanvas 官方 one-page.js 的实现方式
692
+ // 参考: https://github.com/playcanvas/playcanvas-rest-api-tools/blob/main/one-page.js
693
+ // 1. 使用 new Buffer(base64, "base64") 解码 base64
694
+ // 2. 使用 Buffer.from(lz4.decompress(e)).toString() 解压并转字符串
695
+ // 3. 使用 innerText 设置脚本内容
696
+ // 4. 插入到当前脚本之前(保持执行顺序)
697
+ const wrapper = `!function(){var e=new Buffer("${base64}","base64"),n=Buffer.from(lz4.decompress(e)).toString(),r=document.createElement("script");r.async=!1,r.innerText=n,document.currentScript.parentNode.insertBefore(r,document.currentScript)}();`;
698
+ return `<script>${wrapper}</script>`;
699
+ }
700
+ async function loadLz4Module() {
701
+ const code = await readPatchFile('lz4.js');
702
+ const require = createRequire(import.meta.url);
703
+ const sandboxWindow = {};
704
+ const factory = new Function('require', 'window', 'globalThis', `${code}; return window.lz4 || globalThis.lz4;`);
705
+ return factory(require, sandboxWindow, sandboxWindow);
706
+ }
707
+ async function readPatchFile(name) {
708
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
709
+ const patchPath = path.resolve(currentDir, '../../templates/patches', name);
710
+ return await fs.readFile(patchPath, 'utf-8');
711
+ }