@energy8platform/game-engine 0.15.2 → 0.16.0

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 (55) hide show
  1. package/README.md +22 -12
  2. package/dist/assets.d.ts +2 -14
  3. package/dist/core.cjs.js +16 -201
  4. package/dist/core.cjs.js.map +1 -1
  5. package/dist/core.d.ts +4 -35
  6. package/dist/core.esm.js +12 -197
  7. package/dist/core.esm.js.map +1 -1
  8. package/dist/debug.cjs.js +5 -233
  9. package/dist/debug.cjs.js.map +1 -1
  10. package/dist/debug.d.ts +2 -140
  11. package/dist/debug.esm.js +2 -233
  12. package/dist/debug.esm.js.map +1 -1
  13. package/dist/index.cjs.js +21 -432
  14. package/dist/index.cjs.js.map +1 -1
  15. package/dist/index.d.ts +10 -195
  16. package/dist/index.esm.js +14 -429
  17. package/dist/index.esm.js.map +1 -1
  18. package/dist/lua.cjs.js +7 -1493
  19. package/dist/lua.cjs.js.map +1 -1
  20. package/dist/lua.d.ts +1 -403
  21. package/dist/lua.esm.js +1 -1483
  22. package/dist/lua.esm.js.map +1 -1
  23. package/dist/react.cjs.js.map +1 -1
  24. package/dist/react.d.ts +4 -35
  25. package/dist/react.esm.js.map +1 -1
  26. package/dist/vite.cjs.js +19 -175
  27. package/dist/vite.cjs.js.map +1 -1
  28. package/dist/vite.d.ts +8 -3
  29. package/dist/vite.esm.js +10 -173
  30. package/dist/vite.esm.js.map +1 -1
  31. package/package.json +6 -16
  32. package/src/core/GameApplication.ts +14 -18
  33. package/src/debug/index.ts +4 -2
  34. package/src/index.ts +3 -3
  35. package/src/loading/LoadingScene.ts +1 -1
  36. package/src/loading/index.ts +9 -2
  37. package/src/lua/index.ts +3 -31
  38. package/src/types.ts +16 -41
  39. package/src/vite/index.ts +13 -196
  40. package/bin/simulate.ts +0 -139
  41. package/scripts/install-simulate.mjs +0 -101
  42. package/src/debug/DevBridge.ts +0 -305
  43. package/src/loading/CSSPreloader.ts +0 -129
  44. package/src/loading/logo.ts +0 -95
  45. package/src/lua/ActionRouter.ts +0 -132
  46. package/src/lua/LuaEngine.ts +0 -412
  47. package/src/lua/LuaEngineAPI.ts +0 -314
  48. package/src/lua/NativeSimulationRunner.ts +0 -367
  49. package/src/lua/ParallelSimulationRunner.ts +0 -156
  50. package/src/lua/PersistentState.ts +0 -80
  51. package/src/lua/SessionManager.ts +0 -227
  52. package/src/lua/SimulationRunner.ts +0 -192
  53. package/src/lua/SimulationWorker.ts +0 -44
  54. package/src/lua/fengari.d.ts +0 -10
  55. package/src/lua/types.ts +0 -149
@@ -3,7 +3,7 @@ import { Scene } from '../core/Scene';
3
3
  import { Tween } from '../animation/Tween';
4
4
  import { Easing } from '../animation/Easing';
5
5
  import type { LoadingScreenConfig } from '../types';
6
- import { buildLogoSVG, LOADER_BAR_MAX_WIDTH } from './logo';
6
+ import { buildLogoSVG, LOADER_BAR_MAX_WIDTH } from '@energy8platform/platform-core/loading';
7
7
 
8
8
  /**
9
9
  * Build the loading scene variant of the logo SVG.
@@ -1,3 +1,10 @@
1
+ // LoadingScene is pixi-specific and stays in game-engine.
2
+ // CSSPreloader and the Energy8 SVG logo live in platform-core so any
3
+ // renderer (Pixi, Phaser, Three.js) shows the same brand frame.
1
4
  export { LoadingScene } from './LoadingScene';
2
- export { createCSSPreloader, removeCSSPreloader } from './CSSPreloader';
3
- export { buildLogoSVG, LOADER_BAR_MAX_WIDTH } from './logo';
5
+ export {
6
+ createCSSPreloader,
7
+ removeCSSPreloader,
8
+ buildLogoSVG,
9
+ LOADER_BAR_MAX_WIDTH,
10
+ } from '@energy8platform/platform-core/loading';
package/src/lua/index.ts CHANGED
@@ -1,31 +1,3 @@
1
- export { LuaEngine } from './LuaEngine';
2
- export { LuaEngineAPI, createSeededRng } from './LuaEngineAPI';
3
- export { ActionRouter, evaluateCondition } from './ActionRouter';
4
- export { SessionManager } from './SessionManager';
5
- export { PersistentState } from './PersistentState';
6
- export { SimulationRunner, formatSimulationResult } from './SimulationRunner';
7
- export { ParallelSimulationRunner } from './ParallelSimulationRunner';
8
- export { NativeSimulationRunner, findNativeBinary, formatNativeResult } from './NativeSimulationRunner';
9
- export type {
10
- NativeSimulationConfig,
11
- NativeSimulationResult,
12
- StageStats,
13
- DistributionBucket,
14
- } from './NativeSimulationRunner';
15
- export type {
16
- GameDefinition,
17
- ActionDefinition,
18
- TransitionRule,
19
- SessionConfig,
20
- LuaEngineConfig,
21
- LuaPlayResult,
22
- MaxWinConfig,
23
- BuyBonusConfig,
24
- BuyBonusMode,
25
- AnteBetConfig,
26
- PersistentStateConfig,
27
- BetLevelsConfig,
28
- SimulationConfig,
29
- SimulationResult,
30
- SimulationRawAccumulators,
31
- } from './types';
1
+ // Re-export from @energy8platform/platform-core to keep the
2
+ // `@energy8platform/game-engine/lua` sub-path import path stable.
3
+ export * from '@energy8platform/platform-core/lua';
package/src/types.ts CHANGED
@@ -31,48 +31,23 @@ export enum Orientation {
31
31
  ANY = 'any',
32
32
  }
33
33
 
34
- // ─── Loading Screen Config ─────────────────────────────────
35
-
36
- export interface LoadingScreenConfig {
37
- /** Background color (hex number or CSS string) */
38
- backgroundColor?: number | string;
39
- /** Background gradient (CSS string applied to the CSS preloader) */
40
- backgroundGradient?: string;
41
- /** Logo texture alias (must be in 'preload' bundle) */
42
- logoAsset?: string;
43
- /** Logo scale (default: 1) */
44
- logoScale?: number;
45
- /** Show percentage text below the loader bar */
46
- showPercentage?: boolean;
47
- /** Custom progress text formatter */
48
- progressTextFormatter?: (progress: number) => string;
49
- /** Show "Tap to start" after loading (needed for mobile audio unlock) */
50
- tapToStart?: boolean;
51
- /** "Tap to start" label text */
52
- tapToStartText?: string;
53
- /** Minimum display time in ms (so the user sees the brand, even if loading is fast) */
54
- minDisplayTime?: number;
55
- /** CSS preloader custom HTML (shown before PixiJS is ready) */
56
- cssPreloaderHTML?: string;
57
- }
58
-
59
- // ─── Asset Manifest ────────────────────────────────────────
60
-
61
- export interface AssetEntry {
62
- alias: string;
63
- src: string | string[];
64
- /** Optional data to pass to the loader */
65
- data?: Record<string, unknown>;
66
- }
67
-
68
- export interface AssetBundle {
69
- name: string;
70
- assets: AssetEntry[];
71
- }
34
+ // ─── Loading Screen Config + Asset Manifest ───────────────
35
+ // Both are renderer-agnostic and live in @energy8platform/platform-core.
36
+ // Imported for local use inside GameApplicationConfig and re-exported so
37
+ // existing game-engine consumers keep their imports.
38
+ import type {
39
+ LoadingScreenConfig,
40
+ AssetManifest,
41
+ AssetBundle,
42
+ AssetEntry,
43
+ } from '@energy8platform/platform-core';
72
44
 
73
- export interface AssetManifest {
74
- bundles: AssetBundle[];
75
- }
45
+ export type {
46
+ LoadingScreenConfig,
47
+ AssetManifest,
48
+ AssetBundle,
49
+ AssetEntry,
50
+ };
76
51
 
77
52
  // ─── Audio Config ──────────────────────────────────────────
78
53
 
package/src/vite/index.ts CHANGED
@@ -1,4 +1,9 @@
1
1
  import type { UserConfig, Plugin } from 'vite';
2
+ import { devBridgePlugin, luaPlugin } from '@energy8platform/platform-core/vite';
3
+
4
+ // Re-export so users importing from `@energy8platform/game-engine/vite`
5
+ // can still grab the plugins directly without a separate platform-core import.
6
+ export { devBridgePlugin, luaPlugin } from '@energy8platform/platform-core/vite';
2
7
 
3
8
  // ─── Types ───────────────────────────────────────────────
4
9
 
@@ -16,208 +21,20 @@ export interface GameConfig {
16
21
  vite?: UserConfig;
17
22
  }
18
23
 
19
- // ─── DevBridge Plugin ────────────────────────────────────
20
-
21
- /**
22
- * Vite plugin that auto-injects the DevBridge script into the
23
- * HTML during development, so the game can communicate with a
24
- * mock casino host without manual setup.
25
- */
26
- const VIRTUAL_ID = '/@dev-bridge-entry.js';
27
-
28
- function devBridgePlugin(configPath: string): Plugin {
29
- let entrySrc = '';
30
- let viteRoot = '';
31
- let resolvedConfigPath = configPath;
32
-
33
- return {
34
- name: 'game-engine:dev-bridge',
35
- apply: 'serve', // dev only
36
- enforce: 'pre',
37
-
38
- configResolved(config) {
39
- viteRoot = config.root;
40
- // Resolve relative config path against Vite root so the virtual
41
- // module can import it with an absolute path.
42
- if (configPath.startsWith('.')) {
43
- resolvedConfigPath = config.root + '/' + configPath.replace(/^\.\//, '');
44
- }
45
- },
46
-
47
- resolveId(id) {
48
- if (id === VIRTUAL_ID) return id;
49
- },
50
-
51
- load(id) {
52
- if (id === VIRTUAL_ID) {
53
- // This goes through Vite's pipeline so bare imports are resolved
54
- return `
55
- import { DevBridge } from '@energy8platform/game-engine/debug';
56
-
57
- try {
58
- const mod = await import('${resolvedConfigPath}');
59
- const config = mod.default ?? mod.config ?? mod;
60
- new DevBridge(config).start();
61
- } catch (e) {
62
- console.warn('[DevBridge] Failed to load config:', e);
63
- }
64
-
65
- await import('${entrySrc}');
66
- `;
67
- }
68
- },
69
-
70
- transformIndexHtml(html) {
71
- // Find the app's entry module script (skip Vite internal /@... scripts)
72
- const scriptRegex = /<script\s+type="module"\s+src="((?!\/@)[^"]+)"\s*>\s*<\/script>/;
73
- const match = html.match(scriptRegex);
74
-
75
- if (!match) {
76
- console.warn('[DevBridge] Could not find entry module script in index.html');
77
- return html;
78
- }
79
-
80
- entrySrc = match[1];
81
- if (entrySrc.startsWith('.')) {
82
- entrySrc = viteRoot + '/' + entrySrc.replace(/^\.\//, '');
83
- } else if (entrySrc.startsWith('/')) {
84
- entrySrc = viteRoot + entrySrc;
85
- }
86
- return html.replace(match[0], `<script type="module" src="${VIRTUAL_ID}"></script>`);
87
- },
88
- };
89
- }
90
-
91
- // ─── Lua Plugin ─────────────────────────────────────────
92
-
93
- /**
94
- * Vite plugin that:
95
- * 1. Enables importing `.lua` files as raw strings with HMR
96
- * 2. Runs a LuaEngine on the Vite dev server (Node.js) via `POST /__lua-play`
97
- *
98
- * fengari runs server-side only — no browser shims needed.
99
- */
100
- function luaPlugin(configPath: string): Plugin {
101
- let luaEngine: any = null;
102
- let viteServer: any = null;
103
-
104
- async function initEngine() {
105
- if (!viteServer) return;
106
-
107
- try {
108
- // Invalidate cached modules so HMR picks up changes
109
- const root = viteServer.config.root;
110
- const fullConfigPath = configPath.startsWith('.')
111
- ? root + '/' + configPath.replace(/^\.\//, '')
112
- : configPath;
113
-
114
- // Invalidate the config module and its dependencies
115
- const configMod = viteServer.moduleGraph.getModuleById(fullConfigPath);
116
- if (configMod) viteServer.moduleGraph.invalidateModule(configMod);
117
-
118
- // ssrLoadModule handles TS transpilation and resolves all imports
119
- const mod = await viteServer.ssrLoadModule(fullConfigPath);
120
- const config = mod.default ?? mod.config ?? mod;
121
-
122
- if (!config.luaScript || !config.gameDefinition) {
123
- console.log('[LuaPlugin] No luaScript/gameDefinition in config — Lua server disabled');
124
- luaEngine = null;
125
- return;
126
- }
127
-
128
- // Load LuaEngine via SSR (fengari runs natively in Node.js)
129
- const luaMod = await viteServer.ssrLoadModule('@energy8platform/game-engine/lua');
130
- const { LuaEngine } = luaMod;
131
-
132
- if (luaEngine) luaEngine.destroy();
133
- luaEngine = new LuaEngine({
134
- script: config.luaScript,
135
- gameDefinition: config.gameDefinition,
136
- seed: config.luaSeed,
137
- });
138
- console.log('[LuaPlugin] LuaEngine initialized (server-side)');
139
- } catch (e: any) {
140
- console.warn('[LuaPlugin] Failed to initialize LuaEngine:', e.message);
141
- luaEngine = null;
142
- }
143
- }
144
-
145
- return {
146
- name: 'game-engine:lua',
147
- apply: 'serve',
148
-
149
- async configureServer(server) {
150
- viteServer = server;
151
- await initEngine();
152
-
153
- // POST /__lua-play — execute Lua on the server
154
- server.middlewares.use('/__lua-play', (req: any, res: any) => {
155
- if (req.method !== 'POST') {
156
- res.statusCode = 405;
157
- res.end('Method Not Allowed');
158
- return;
159
- }
160
-
161
- let body = '';
162
- req.on('data', (chunk: string) => { body += chunk; });
163
- req.on('end', () => {
164
- try {
165
- if (!luaEngine) {
166
- res.statusCode = 503;
167
- res.setHeader('Content-Type', 'application/json');
168
- res.end(JSON.stringify({ error: 'LuaEngine not initialized' }));
169
- return;
170
- }
171
-
172
- const params = JSON.parse(body);
173
- const result = luaEngine.execute(params);
174
-
175
- res.statusCode = 200;
176
- res.setHeader('Content-Type', 'application/json');
177
- res.end(JSON.stringify(result));
178
- } catch (e: any) {
179
- res.statusCode = 500;
180
- res.setHeader('Content-Type', 'application/json');
181
- res.end(JSON.stringify({ error: e.message }));
182
- }
183
- });
184
- });
185
- },
186
-
187
- transform(code: string, id: string) {
188
- if (id.endsWith('.lua')) {
189
- return {
190
- code: `export default ${JSON.stringify(code)};`,
191
- map: null,
192
- };
193
- }
194
- },
195
-
196
- async handleHotUpdate({ file, server }: { file: string; server: any }) {
197
- if (file.endsWith('.lua') || file.includes('dev.config')) {
198
- console.log('[LuaPlugin] Reloading LuaEngine...');
199
-
200
- // Invalidate all SSR modules so ssrLoadModule picks up fresh code
201
- server.moduleGraph.invalidateAll();
202
-
203
- await initEngine();
204
- server.ws.send({ type: 'full-reload' });
205
- return [];
206
- }
207
- },
208
- };
209
- }
210
-
211
24
  // ─── defineGameConfig ────────────────────────────────────
212
25
 
213
26
  /**
214
- * Define a Vite configuration tailored for Energy8 casino games.
27
+ * Define a Vite configuration tailored for Energy8 casino games on
28
+ * @energy8platform/game-engine (PixiJS).
215
29
  *
216
30
  * Merges sensible defaults for iGaming projects:
217
31
  * - Build target: ESNext
218
32
  * - Asset inlining threshold: 8KB
219
- * - Source maps for dev, none for prod
220
- * - Optional DevBridge auto-injection in dev mode
33
+ * - PixiJS-specific dedupe / chunk splitting / prebundle hints
34
+ * - Optional DevBridge auto-injection in dev mode (with Lua engine)
35
+ *
36
+ * For Phaser/Three/custom engines, import the bare plugins directly
37
+ * from `@energy8platform/platform-core/vite` and write your own config.
221
38
  *
222
39
  * @example
223
40
  * ```ts
@@ -242,7 +59,7 @@ export function defineGameConfig(config: GameConfig = {}): UserConfig {
242
59
  const userVite = config.vite ?? {};
243
60
 
244
61
  return {
245
- base: config.base ?? '/',
62
+ base: config.base ?? './',
246
63
 
247
64
  plugins: [
248
65
  ...plugins,
package/bin/simulate.ts DELETED
@@ -1,139 +0,0 @@
1
- #!/usr/bin/env npx tsx
2
- import { SimulationRunner, formatSimulationResult } from '../src/lua/SimulationRunner';
3
- import { ParallelSimulationRunner } from '../src/lua/ParallelSimulationRunner';
4
- import { NativeSimulationRunner, findNativeBinary, formatNativeResult } from '../src/lua/NativeSimulationRunner';
5
- import { cpus } from 'os';
6
- import { resolve, dirname } from 'path';
7
- import { fileURLToPath } from 'url';
8
-
9
- // ─── Argument Parsing ───────────────────────────────────
10
-
11
- function parseArgs(argv: string[]): Record<string, string> {
12
- const args: Record<string, string> = {};
13
- for (let i = 2; i < argv.length; i++) {
14
- const arg = argv[i];
15
- if (arg.startsWith('--')) {
16
- const key = arg.slice(2);
17
- // Boolean flags (no value)
18
- if (key === 'native' || key === 'js') {
19
- args[key] = 'true';
20
- } else if (i + 1 < argv.length) {
21
- args[key] = argv[++i];
22
- }
23
- }
24
- }
25
- return args;
26
- }
27
-
28
- async function main() {
29
- const args = parseArgs(process.argv);
30
-
31
- const configPath = resolve(process.cwd(), args.config ?? './dev.config.ts');
32
- const iterations = parseInt(args.iterations ?? '1000000', 10);
33
- const bet = parseFloat(args.bet ?? '1');
34
- const seed = args.seed ? parseInt(args.seed, 10) : undefined;
35
- const action = args.action ?? 'spin';
36
- const params = args.params ? JSON.parse(args.params) : undefined;
37
- const workers = args.workers ? parseInt(args.workers, 10) : cpus().length;
38
- const useNative = args.native === 'true';
39
- const useJs = args.js === 'true';
40
-
41
- // Load dev config
42
- let config: any;
43
- try {
44
- const mod = await import(configPath);
45
- config = mod.default ?? mod.config ?? mod;
46
- } catch (e: any) {
47
- console.error(`Failed to load config from ${configPath}:`);
48
- console.error(e.message);
49
- process.exit(1);
50
- }
51
-
52
- if (!config.luaScript) {
53
- console.error('Config must contain `luaScript` (Lua source code string).');
54
- console.error('Make sure your dev.config.ts exports luaScript and gameDefinition.');
55
- process.exit(1);
56
- }
57
-
58
- if (!config.gameDefinition) {
59
- console.error('Config must contain `gameDefinition` (GameDefinition object).');
60
- process.exit(1);
61
- }
62
-
63
- const gameId = config.gameDefinition.id ?? 'unknown';
64
-
65
- // ─── Native binary detection ────────────────────────────
66
- // Search in config dir first, then in the game-engine package root
67
- const engineRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
68
- const binaryPath = args.binary ?? (useJs ? null : (findNativeBinary(dirname(configPath)) ?? findNativeBinary(engineRoot)));
69
-
70
- if (useNative && !binaryPath) {
71
- console.error('Native simulation binary not found.');
72
- console.error('Use --binary <path> or set SIMULATE_BINARY environment variable.');
73
- process.exit(1);
74
- }
75
-
76
- const onProgress = (completed: number, total: number) => {
77
- const pct = Math.round((completed / total) * 100);
78
- console.log(`Progress: ${completed.toLocaleString()}/${total.toLocaleString()} (${pct}%)`);
79
- };
80
-
81
- // ─── Native binary path ─────────────────────────────────
82
- if (binaryPath) {
83
- console.log(`Using native binary: ${binaryPath}`);
84
- console.log(`Starting simulation for ${gameId} (${iterations.toLocaleString()} iterations, action: ${action})...`);
85
-
86
- const runner = new NativeSimulationRunner({
87
- binaryPath,
88
- script: config.luaScript,
89
- gameDefinition: config.gameDefinition,
90
- iterations,
91
- bet,
92
- action,
93
- params,
94
- });
95
- const result = await runner.run();
96
- console.log(formatNativeResult(result));
97
- return;
98
- }
99
-
100
- // ─── JS simulation path ─────────────────────────────────
101
- const useParallel = workers > 1;
102
- console.log(`Starting simulation for ${gameId} (${iterations.toLocaleString()} iterations, action: ${action}, workers: ${useParallel ? workers : 1})...`);
103
-
104
- let result;
105
-
106
- if (useParallel) {
107
- const runner = new ParallelSimulationRunner({
108
- script: config.luaScript,
109
- gameDefinition: config.gameDefinition,
110
- iterations,
111
- bet,
112
- seed,
113
- action,
114
- params,
115
- workerCount: workers,
116
- onProgress,
117
- });
118
- result = await runner.run();
119
- } else {
120
- const runner = new SimulationRunner({
121
- script: config.luaScript,
122
- gameDefinition: config.gameDefinition,
123
- iterations,
124
- bet,
125
- seed,
126
- action,
127
- params,
128
- onProgress,
129
- });
130
- result = runner.run();
131
- }
132
-
133
- console.log(formatSimulationResult(result));
134
- }
135
-
136
- main().catch((err) => {
137
- console.error(err);
138
- process.exit(1);
139
- });
@@ -1,101 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Postinstall script: downloads the native simulation binary for the current
5
- * platform from GitHub Releases. Falls back silently — the JS simulation
6
- * will be used if the binary is unavailable.
7
- */
8
-
9
- import { createWriteStream, chmodSync, existsSync, mkdirSync } from 'fs';
10
- import { join, dirname } from 'path';
11
- import { fileURLToPath } from 'url';
12
- import { get } from 'https';
13
-
14
- const __dirname = dirname(fileURLToPath(import.meta.url));
15
- const BIN_DIR = join(__dirname, '..', 'bin');
16
-
17
- // ─── Config ─────────────────────────────────────────────
18
-
19
- const REPO = 'energy8platform/game-engine';
20
- // Binary version — update this when new Go binaries are built and uploaded to GitHub Releases.
21
- // The binary is backwards-compatible: it runs any Lua script, so it doesn't need to match
22
- // the engine version exactly. Only bump when the Go simulate CLI itself changes.
23
- const BINARY_VERSION = '0.13.0';
24
-
25
- const PLATFORM_MAP = {
26
- 'darwin-arm64': 'simulate-darwin-arm64',
27
- 'darwin-x64': 'simulate-darwin-amd64',
28
- 'linux-x64': 'simulate-linux-amd64',
29
- 'linux-arm64': 'simulate-linux-arm64',
30
- 'win32-x64': 'simulate-windows-amd64.exe',
31
- };
32
-
33
- // ─── Main ───────────────────────────────────────────────
34
-
35
- async function main() {
36
- const key = `${process.platform}-${process.arch}`;
37
- const binaryName = PLATFORM_MAP[key];
38
-
39
- if (!binaryName) {
40
- console.log(`[simulate] No native binary available for ${key}, will use JS simulation.`);
41
- return;
42
- }
43
-
44
- const dest = join(BIN_DIR, binaryName);
45
-
46
- // Skip if already downloaded
47
- if (existsSync(dest)) {
48
- return;
49
- }
50
-
51
- const tag = `v${BINARY_VERSION}`;
52
-
53
- const url = `https://github.com/${REPO}/releases/download/${tag}/${binaryName}`;
54
-
55
- console.log(`[simulate] Downloading native binary for ${key}...`);
56
-
57
- try {
58
- if (!existsSync(BIN_DIR)) {
59
- mkdirSync(BIN_DIR, { recursive: true });
60
- }
61
- await download(url, dest);
62
- chmodSync(dest, 0o755);
63
- console.log(`[simulate] Installed ${binaryName}`);
64
- } catch (err) {
65
- // Non-fatal — JS simulation is the fallback
66
- console.log(`[simulate] Could not download native binary: ${err.message}`);
67
- console.log(`[simulate] Will use JS simulation instead.`);
68
- }
69
- }
70
-
71
- // ─── Download with redirect following ───────────────────
72
-
73
- function download(url, dest, redirects = 5) {
74
- return new Promise((resolve, reject) => {
75
- if (redirects <= 0) {
76
- return reject(new Error('Too many redirects'));
77
- }
78
-
79
- get(url, { headers: { 'User-Agent': 'game-engine-postinstall' } }, (res) => {
80
- // Follow redirects (GitHub sends 302 to S3)
81
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
82
- res.resume();
83
- return download(res.headers.location, dest, redirects - 1).then(resolve, reject);
84
- }
85
-
86
- if (res.statusCode !== 200) {
87
- res.resume();
88
- return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
89
- }
90
-
91
- const file = createWriteStream(dest);
92
- res.pipe(file);
93
- file.on('finish', () => file.close(resolve));
94
- file.on('error', reject);
95
- }).on('error', reject);
96
- });
97
- }
98
-
99
- main().catch(() => {
100
- // Never fail the install
101
- });