@energy8platform/platform-core 0.28.2 → 0.29.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.
- package/README.md +127 -150
- package/bin/simulate.ts +35 -98
- package/dist/dev-bridge.cjs.js +3 -3
- package/dist/dev-bridge.cjs.js.map +1 -1
- package/dist/dev-bridge.d.ts +9 -2
- package/dist/dev-bridge.esm.js +3 -3
- package/dist/dev-bridge.esm.js.map +1 -1
- package/dist/game-spec.cjs.js +70 -27
- package/dist/game-spec.cjs.js.map +1 -1
- package/dist/game-spec.d.ts +47 -11
- package/dist/game-spec.esm.js +68 -25
- package/dist/game-spec.esm.js.map +1 -1
- package/dist/index.cjs.js +3 -3
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +28 -2
- package/dist/index.esm.js +3 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/lua.cjs.js +0 -1234
- package/dist/lua.cjs.js.map +1 -1
- package/dist/lua.d.ts +8 -206
- package/dist/lua.esm.js +0 -1225
- package/dist/lua.esm.js.map +1 -1
- package/dist/simulation.cjs.js +48 -179
- package/dist/simulation.cjs.js.map +1 -1
- package/dist/simulation.d.ts +33 -60
- package/dist/simulation.esm.js +48 -178
- package/dist/simulation.esm.js.map +1 -1
- package/dist/vite.cjs.js +323 -109
- package/dist/vite.cjs.js.map +1 -1
- package/dist/vite.d.ts +19 -9
- package/dist/vite.esm.js +322 -109
- package/dist/vite.esm.js.map +1 -1
- package/package.json +6 -5
- package/scripts/install-e8.mjs +113 -0
- package/src/dev-bridge/DevBridge.ts +3 -3
- package/src/game-spec/defineGame.ts +2 -2
- package/src/game-spec/derive.ts +46 -17
- package/src/game-spec/export.ts +28 -8
- package/src/game-spec/index.ts +3 -2
- package/src/game-spec/types.ts +11 -1
- package/src/index.ts +6 -12
- package/src/lua/index.ts +4 -11
- package/src/lua/types.ts +7 -0
- package/src/simulation/NativeSimulationRunner.ts +71 -45
- package/src/simulation/index.ts +2 -4
- package/src/vite/index.ts +4 -121
- package/src/vite/spinPlugin.ts +338 -0
- package/scripts/install-simulate.mjs +0 -101
- package/src/lua/ActionRouter.ts +0 -132
- package/src/lua/LuaEngine.ts +0 -520
- package/src/lua/LuaEngineAPI.ts +0 -314
- package/src/lua/PersistentState.ts +0 -80
- package/src/lua/SessionManager.ts +0 -249
- package/src/lua/SimulationRunner.ts +0 -190
- package/src/lua/fengari.d.ts +0 -10
- package/src/simulation/ParallelSimulationRunner.ts +0 -156
- package/src/simulation/SimulationWorker.ts +0 -44
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spinPlugin — dev-путь фронта поверх e8-server (SpinML, домен-API раундов).
|
|
3
|
+
*
|
|
4
|
+
* Замена luaPlugin для spin-рантайма: тот же роут POST /__lua-play и тот же
|
|
5
|
+
* ответ — DevBridge не меняется. Плагин ТОНКИЙ: машину раунда (сессии /
|
|
6
|
+
* очереди / unlimited / globals / идемпотентность) ведёт сам e8-server
|
|
7
|
+
* (--sessions memory), плагин лишь переводит протокол и держит history для
|
|
8
|
+
* HUD. Горячая перезагрузка .spin — у сервера (--watch): правка файла =
|
|
9
|
+
* новая версия, открытые раунды доигрываются старой.
|
|
10
|
+
*
|
|
11
|
+
* Источник истины протокола: casino-platform/e8/crates/e8-server/proto/
|
|
12
|
+
* engine.proto (репо движка); ниже — синхронизированная копия.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
15
|
+
import { accessSync, constants as fsConstants, mkdtempSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import type { Plugin } from 'vite';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Поиск бинаря e8-server в порядке их NativeSimulationRunner:
|
|
24
|
+
* явный binPath → env E8_SERVER_BINARY →
|
|
25
|
+
* node_modules/@energy8platform/platform-core/bin/e8-server-<platform>-<arch>
|
|
26
|
+
* (его качает install-e8.mjs postinstall'ом) → голый "e8-server" из PATH.
|
|
27
|
+
*/
|
|
28
|
+
function resolveServerBinary(explicit?: string): string {
|
|
29
|
+
const ok = (p: string) => {
|
|
30
|
+
try {
|
|
31
|
+
accessSync(p, fsConstants.X_OK);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
if (explicit) return explicit;
|
|
38
|
+
const env = process.env.E8_SERVER_BINARY;
|
|
39
|
+
if (env && ok(env)) return env;
|
|
40
|
+
const arch = process.arch === 'x64' ? 'amd64' : process.arch;
|
|
41
|
+
const platform = process.platform === 'win32' ? 'windows' : process.platform;
|
|
42
|
+
const ext = process.platform === 'win32' ? '.exe' : '';
|
|
43
|
+
const name = `e8-server-${platform}-${arch}${ext}`;
|
|
44
|
+
// бинарь этого пакета (его качает scripts/install-e8.mjs); раскладки
|
|
45
|
+
// src/vite/*.ts и dist/vite.esm.js отличаются уровнем — пробуем оба
|
|
46
|
+
try {
|
|
47
|
+
const here = fileURLToPath(import.meta.url);
|
|
48
|
+
for (const up of ['..', '../..']) {
|
|
49
|
+
const candidate = join(here, '..', up, 'bin', name);
|
|
50
|
+
if (ok(candidate)) return candidate;
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// import.meta недоступен — падаем на PATH
|
|
54
|
+
}
|
|
55
|
+
return `e8-server${ext}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SpinPluginOptions {
|
|
59
|
+
/** путь к бинарю e8-server (default: E8_SERVER_BINARY → platform-core/bin → PATH) */
|
|
60
|
+
binPath?: string;
|
|
61
|
+
/** .spin-файл игры или каталог */
|
|
62
|
+
spinPath?: string;
|
|
63
|
+
gamesDir?: string;
|
|
64
|
+
/** id игры из декларации game "..." (default: первая загруженная) */
|
|
65
|
+
gameId?: string;
|
|
66
|
+
/** порт gRPC (default 50151) */
|
|
67
|
+
port?: number;
|
|
68
|
+
/** server_seed дев-сессий (детерминированный replay) */
|
|
69
|
+
serverSeed?: string;
|
|
70
|
+
/** подключиться к уже запущенному серверу, не спавнить */
|
|
71
|
+
external?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Копия контракта — источник истины crates/e8-server/proto/engine.proto.
|
|
75
|
+
const ENGINE_PROTO = `
|
|
76
|
+
syntax = "proto3";
|
|
77
|
+
package e8;
|
|
78
|
+
service Engine {
|
|
79
|
+
rpc ListGames(ListGamesRequest) returns (ListGamesResponse);
|
|
80
|
+
rpc GetConfig(ConfigRequest) returns (ConfigResponse);
|
|
81
|
+
rpc StartRound(StartRoundRequest) returns (RoundResponse);
|
|
82
|
+
rpc Step(RoundStepRequest) returns (RoundResponse);
|
|
83
|
+
rpc GetRound(GetRoundRequest) returns (RoundStateResponse);
|
|
84
|
+
rpc Health(HealthRequest) returns (HealthResponse);
|
|
85
|
+
}
|
|
86
|
+
message ListGamesRequest {}
|
|
87
|
+
message GameInfo {
|
|
88
|
+
string game_id = 1;
|
|
89
|
+
string script_sha256 = 2;
|
|
90
|
+
string vars_layout_hash = 3;
|
|
91
|
+
repeated string entry_actions = 4;
|
|
92
|
+
repeated string loaded_versions = 5;
|
|
93
|
+
}
|
|
94
|
+
message ListGamesResponse { repeated GameInfo games = 1; }
|
|
95
|
+
message ConfigRequest { string game_id = 1; }
|
|
96
|
+
message ConfigResponse { string config_json = 1; string error = 2; }
|
|
97
|
+
message StartRoundRequest {
|
|
98
|
+
string game_id = 1;
|
|
99
|
+
string player_id = 2;
|
|
100
|
+
string round_id = 3;
|
|
101
|
+
string server_seed = 4;
|
|
102
|
+
string client_seed = 5;
|
|
103
|
+
int64 nonce = 6;
|
|
104
|
+
string action = 7;
|
|
105
|
+
double bet = 8;
|
|
106
|
+
string params_json = 9;
|
|
107
|
+
string request_id = 10;
|
|
108
|
+
bool recording = 11;
|
|
109
|
+
}
|
|
110
|
+
message RoundStepRequest {
|
|
111
|
+
string round_id = 1;
|
|
112
|
+
string action = 2;
|
|
113
|
+
string params_json = 3;
|
|
114
|
+
string request_id = 4;
|
|
115
|
+
}
|
|
116
|
+
message RoundResponse {
|
|
117
|
+
double win = 1;
|
|
118
|
+
double total_win = 2;
|
|
119
|
+
string data_json = 3;
|
|
120
|
+
string vars_json = 4;
|
|
121
|
+
string globals_json = 5;
|
|
122
|
+
repeated string next_actions = 6;
|
|
123
|
+
bool round_complete = 7;
|
|
124
|
+
int64 spins_remaining = 8;
|
|
125
|
+
uint32 spins_played = 9;
|
|
126
|
+
string script_sha256 = 10;
|
|
127
|
+
string error = 11;
|
|
128
|
+
double bet = 12;
|
|
129
|
+
}
|
|
130
|
+
message GetRoundRequest { string round_id = 1; }
|
|
131
|
+
message RoundStateResponse {
|
|
132
|
+
bool found = 1;
|
|
133
|
+
string game_id = 2;
|
|
134
|
+
string script_sha256 = 3;
|
|
135
|
+
double total_win = 4;
|
|
136
|
+
uint32 spins_played = 5;
|
|
137
|
+
int64 spins_remaining = 6;
|
|
138
|
+
repeated string next_actions = 7;
|
|
139
|
+
bool round_complete = 8;
|
|
140
|
+
string vars_json = 9;
|
|
141
|
+
string error = 10;
|
|
142
|
+
double bet = 11;
|
|
143
|
+
}
|
|
144
|
+
message HealthRequest {}
|
|
145
|
+
message HealthResponse { bool ok = 1; uint32 games_loaded = 2; string sessions_backend = 3; }
|
|
146
|
+
`;
|
|
147
|
+
|
|
148
|
+
export function spinPlugin(opts: SpinPluginOptions = {}): Plugin {
|
|
149
|
+
const port = opts.port ?? 50151;
|
|
150
|
+
const serverSeed = opts.serverSeed ?? 'e8-dev-seed';
|
|
151
|
+
let child: ChildProcess | null = null;
|
|
152
|
+
let client: any = null;
|
|
153
|
+
let gameId = opts.gameId ?? '';
|
|
154
|
+
let entryActions: string[] = [];
|
|
155
|
+
|
|
156
|
+
let roundCounter = 0;
|
|
157
|
+
let reqCounter = 0;
|
|
158
|
+
// презентационное состояние протокола (history / bet раунда)
|
|
159
|
+
const rounds = new Map<string, { bet: number; history: unknown[] }>();
|
|
160
|
+
let activeRoundId: string | null = null;
|
|
161
|
+
|
|
162
|
+
function grpc() {
|
|
163
|
+
// createRequire: vite.config грузится и как ESM, и как CJS-бандл
|
|
164
|
+
const req = createRequire(import.meta.url);
|
|
165
|
+
const grpcJs = req('@grpc/grpc-js');
|
|
166
|
+
const loader = req('@grpc/proto-loader');
|
|
167
|
+
const dir = mkdtempSync(join(tmpdir(), 'e8proto-'));
|
|
168
|
+
writeFileSync(join(dir, 'engine.proto'), ENGINE_PROTO);
|
|
169
|
+
const def = loader.loadSync(join(dir, 'engine.proto'), {
|
|
170
|
+
keepCase: true,
|
|
171
|
+
longs: Number,
|
|
172
|
+
defaults: true,
|
|
173
|
+
});
|
|
174
|
+
const pkg = grpcJs.loadPackageDefinition(def) as any;
|
|
175
|
+
return new pkg.e8.Engine(`127.0.0.1:${port}`, grpcJs.credentials.createInsecure());
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function call<T>(method: string, req: unknown): Promise<T> {
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
client[method](req, (err: Error | null, resp: T) =>
|
|
181
|
+
err ? reject(err) : resolve(resp),
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function toLegacy(r: any, roundId: string) {
|
|
187
|
+
const meta = rounds.get(roundId)!;
|
|
188
|
+
const data = r.data_json ? JSON.parse(r.data_json) : null;
|
|
189
|
+
meta.history.push({
|
|
190
|
+
spinIndex: r.spins_played - 1,
|
|
191
|
+
win: r.win * meta.bet,
|
|
192
|
+
data,
|
|
193
|
+
});
|
|
194
|
+
const hadSession = r.spins_played > 1 || !r.round_complete;
|
|
195
|
+
if (r.round_complete) {
|
|
196
|
+
rounds.delete(roundId);
|
|
197
|
+
if (activeRoundId === roundId) activeRoundId = null;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
totalWin: r.round_complete && r.spins_played > 1 ? r.total_win : r.win,
|
|
201
|
+
data,
|
|
202
|
+
nextActions: r.next_actions,
|
|
203
|
+
session: hadSession
|
|
204
|
+
? {
|
|
205
|
+
spinsRemaining: r.spins_remaining,
|
|
206
|
+
spinsPlayed: r.spins_played,
|
|
207
|
+
totalWin: r.total_win * meta.bet,
|
|
208
|
+
betAmount: meta.bet,
|
|
209
|
+
completed: r.round_complete,
|
|
210
|
+
maxWinReached: false,
|
|
211
|
+
history: meta.history,
|
|
212
|
+
}
|
|
213
|
+
: null,
|
|
214
|
+
variables: r.vars_json ? JSON.parse(r.vars_json) : {},
|
|
215
|
+
globals: r.globals_json ? JSON.parse(r.globals_json) : {},
|
|
216
|
+
creditDeferred: !r.round_complete,
|
|
217
|
+
roundId,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function play(body: any): Promise<unknown> {
|
|
222
|
+
const action: string = body.action ?? 'spin';
|
|
223
|
+
const params = body.params ? JSON.stringify(body.params) : '';
|
|
224
|
+
const rid: string | null = body.roundId ?? activeRoundId;
|
|
225
|
+
reqCounter += 1;
|
|
226
|
+
|
|
227
|
+
// шаг открытого раунда, если действие принадлежит его активной сессии.
|
|
228
|
+
// Маршрутизируем ПО СЕРВЕРУ (GetRound), а не по локальной карте: клиент
|
|
229
|
+
// (DevBridge) - владелец roundId и может пережить перезапуск плагина.
|
|
230
|
+
if (rid) {
|
|
231
|
+
const st: any = await call('GetRound', { round_id: rid });
|
|
232
|
+
if (st.found && !st.round_complete && st.next_actions.includes(action)) {
|
|
233
|
+
if (!rounds.has(rid)) rounds.set(rid, { bet: st.bet || 1.0, history: [] });
|
|
234
|
+
const r: any = await call('Step', {
|
|
235
|
+
round_id: rid,
|
|
236
|
+
action,
|
|
237
|
+
params_json: params,
|
|
238
|
+
request_id: `dev-${reqCounter}`,
|
|
239
|
+
});
|
|
240
|
+
if (r.error) throw new Error(r.error);
|
|
241
|
+
return toLegacy(r, rid);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// entry: новый раунд. roundId КЛИЕНТА уважается (их DevBridge генерит
|
|
246
|
+
// uuid на entry и шлёт его же в session-шаги) — id-пространства совпадают.
|
|
247
|
+
roundCounter += 1;
|
|
248
|
+
const roundId: string = body.roundId ?? `r${roundCounter.toString(16).padStart(8, '0')}`;
|
|
249
|
+
const bet: number = body.bet ?? 1.0;
|
|
250
|
+
rounds.set(roundId, { bet, history: [] });
|
|
251
|
+
const r: any = await call('StartRound', {
|
|
252
|
+
game_id: gameId,
|
|
253
|
+
player_id: 'dev-player',
|
|
254
|
+
round_id: roundId,
|
|
255
|
+
server_seed: serverSeed,
|
|
256
|
+
client_seed: 'dev',
|
|
257
|
+
nonce: roundCounter,
|
|
258
|
+
action,
|
|
259
|
+
bet,
|
|
260
|
+
params_json: params,
|
|
261
|
+
request_id: `dev-${reqCounter}`,
|
|
262
|
+
recording: true,
|
|
263
|
+
});
|
|
264
|
+
if (r.error) throw new Error(r.error);
|
|
265
|
+
if (!r.round_complete) activeRoundId = roundId;
|
|
266
|
+
return toLegacy(r, roundId);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
name: 'e8:spin',
|
|
271
|
+
apply: 'serve',
|
|
272
|
+
// pre: наша мидлвара /__lua-play регистрируется раньше luaPlugin из
|
|
273
|
+
// defineGameConfig — .spin-игры перехватывают роут без правок конфига
|
|
274
|
+
// движка (их LuaEngine на .spin-тексте тихо не поднимется).
|
|
275
|
+
enforce: 'pre',
|
|
276
|
+
|
|
277
|
+
async configureServer(server) {
|
|
278
|
+
if (!opts.external) {
|
|
279
|
+
const args = ['--port', String(port), '--sessions', 'memory', '--watch'];
|
|
280
|
+
if (opts.gamesDir) args.push('--games-dir', opts.gamesDir);
|
|
281
|
+
else if (opts.spinPath) {
|
|
282
|
+
const dir = opts.spinPath.replace(/\/[^/]+$/, '') || '.';
|
|
283
|
+
args.push('--games-dir', dir);
|
|
284
|
+
}
|
|
285
|
+
child = spawn(resolveServerBinary(opts.binPath), args, { stdio: 'inherit' });
|
|
286
|
+
server.httpServer?.on('close', () => child?.kill());
|
|
287
|
+
}
|
|
288
|
+
client = grpc();
|
|
289
|
+
|
|
290
|
+
for (let i = 0; i < 100; i++) {
|
|
291
|
+
try {
|
|
292
|
+
const games: any = await call('ListGames', {});
|
|
293
|
+
if (!gameId) gameId = games.games[0]?.game_id ?? '';
|
|
294
|
+
const info = games.games.find((g: any) => g.game_id === gameId);
|
|
295
|
+
entryActions = info?.entry_actions ?? [];
|
|
296
|
+
console.log(
|
|
297
|
+
`[e8] connected: game=${gameId} script=${info?.script_sha256?.slice(0, 12)} entry=[${entryActions}]`,
|
|
298
|
+
);
|
|
299
|
+
break;
|
|
300
|
+
} catch {
|
|
301
|
+
await new Promise((res) => setTimeout(res, 300));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
server.middlewares.use('/__lua-play', (req: any, res: any) => {
|
|
306
|
+
if (req.method !== 'POST') {
|
|
307
|
+
res.statusCode = 405;
|
|
308
|
+
res.end('Method Not Allowed');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
let body = '';
|
|
312
|
+
req.on('data', (c: string) => (body += c));
|
|
313
|
+
req.on('end', async () => {
|
|
314
|
+
res.setHeader('Content-Type', 'application/json');
|
|
315
|
+
try {
|
|
316
|
+
res.statusCode = 200;
|
|
317
|
+
res.end(JSON.stringify(await play(JSON.parse(body))));
|
|
318
|
+
} catch (e: any) {
|
|
319
|
+
res.statusCode = 500;
|
|
320
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
server.middlewares.use('/config', async (_req: any, res: any) => {
|
|
326
|
+
res.setHeader('Content-Type', 'application/json');
|
|
327
|
+
try {
|
|
328
|
+
const c: any = await call('GetConfig', { game_id: gameId });
|
|
329
|
+
res.statusCode = 200;
|
|
330
|
+
res.end(c.config_json || JSON.stringify({ error: c.error }));
|
|
331
|
+
} catch (e: any) {
|
|
332
|
+
res.statusCode = 500;
|
|
333
|
+
res.end(JSON.stringify({ error: e.message }));
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
}
|
|
@@ -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.19.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
|
-
});
|
package/src/lua/ActionRouter.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import type { ActionDefinition, TransitionRule, GameDefinition } from './types';
|
|
2
|
-
|
|
3
|
-
export interface TransitionMatch {
|
|
4
|
-
rule: TransitionRule;
|
|
5
|
-
nextActions: string[];
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Replicates the platform's action dispatch and transition evaluation.
|
|
10
|
-
* Routes play requests to the correct action, evaluates transition conditions
|
|
11
|
-
* against current variables to determine next actions and session operations.
|
|
12
|
-
*/
|
|
13
|
-
export class ActionRouter {
|
|
14
|
-
private actions: Record<string, ActionDefinition>;
|
|
15
|
-
|
|
16
|
-
constructor(gameDefinition: GameDefinition) {
|
|
17
|
-
this.actions = gameDefinition.actions;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Look up action by name and validate prerequisites */
|
|
21
|
-
resolveAction(actionName: string, hasSession: boolean): ActionDefinition {
|
|
22
|
-
const action = this.actions[actionName];
|
|
23
|
-
if (!action) {
|
|
24
|
-
throw new Error(`Unknown action: "${actionName}". Available: ${Object.keys(this.actions).join(', ')}`);
|
|
25
|
-
}
|
|
26
|
-
if (action.requires_session && !hasSession) {
|
|
27
|
-
throw new Error(`Action "${actionName}" requires an active session`);
|
|
28
|
-
}
|
|
29
|
-
return action;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Evaluate transitions in order, return the first matching rule */
|
|
33
|
-
evaluateTransitions(
|
|
34
|
-
action: ActionDefinition,
|
|
35
|
-
variables: Record<string, number>,
|
|
36
|
-
): TransitionMatch {
|
|
37
|
-
for (const rule of action.transitions) {
|
|
38
|
-
if (evaluateCondition(rule.condition, variables)) {
|
|
39
|
-
return { rule, nextActions: rule.next_actions };
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
throw new Error(
|
|
43
|
-
`No matching transition for action with stage "${action.stage}". ` +
|
|
44
|
-
`Variables: ${JSON.stringify(variables)}`
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ─── Condition Evaluator ────────────────────────────────
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Evaluates a transition condition expression against variables.
|
|
53
|
-
*
|
|
54
|
-
* Supports:
|
|
55
|
-
* - "always" → true
|
|
56
|
-
* - Simple comparisons: "var > 0", "var == 1", "var >= 10", "var != 0", "var < 5", "var <= 3"
|
|
57
|
-
* - Logical connectives: "expr && expr", "expr || expr"
|
|
58
|
-
*
|
|
59
|
-
* This covers all patterns used by the platform's govaluate conditions.
|
|
60
|
-
*/
|
|
61
|
-
export function evaluateCondition(
|
|
62
|
-
condition: string,
|
|
63
|
-
variables: Record<string, number>,
|
|
64
|
-
): boolean {
|
|
65
|
-
const trimmed = condition.trim();
|
|
66
|
-
|
|
67
|
-
if (trimmed === 'always') return true;
|
|
68
|
-
|
|
69
|
-
// Handle || (OR) — lowest precedence
|
|
70
|
-
if (trimmed.includes('||')) {
|
|
71
|
-
const parts = splitOnOperator(trimmed, '||');
|
|
72
|
-
return parts.some(part => evaluateCondition(part, variables));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Handle && (AND)
|
|
76
|
-
if (trimmed.includes('&&')) {
|
|
77
|
-
const parts = splitOnOperator(trimmed, '&&');
|
|
78
|
-
return parts.every(part => evaluateCondition(part, variables));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Single comparison: "variable op value"
|
|
82
|
-
return evaluateComparison(trimmed, variables);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function splitOnOperator(expr: string, operator: string): string[] {
|
|
86
|
-
const parts: string[] = [];
|
|
87
|
-
let depth = 0;
|
|
88
|
-
let current = '';
|
|
89
|
-
|
|
90
|
-
for (let i = 0; i < expr.length; i++) {
|
|
91
|
-
if (expr[i] === '(') depth++;
|
|
92
|
-
else if (expr[i] === ')') depth--;
|
|
93
|
-
|
|
94
|
-
if (depth === 0 && expr.substring(i, i + operator.length) === operator) {
|
|
95
|
-
parts.push(current);
|
|
96
|
-
current = '';
|
|
97
|
-
i += operator.length - 1;
|
|
98
|
-
} else {
|
|
99
|
-
current += expr[i];
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
parts.push(current);
|
|
103
|
-
return parts;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function evaluateComparison(
|
|
107
|
-
expr: string,
|
|
108
|
-
variables: Record<string, number>,
|
|
109
|
-
): boolean {
|
|
110
|
-
// Match: variable_name operator value
|
|
111
|
-
const match = expr.trim().match(
|
|
112
|
-
/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*(>=|<=|!=|==|>|<)\s*(-?\d+(?:\.\d+)?)\s*$/
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
if (!match) {
|
|
116
|
-
throw new Error(`Cannot parse condition: "${expr}"`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const [, varName, op, valueStr] = match;
|
|
120
|
-
const left = variables[varName] ?? 0;
|
|
121
|
-
const right = parseFloat(valueStr);
|
|
122
|
-
|
|
123
|
-
switch (op) {
|
|
124
|
-
case '>': return left > right;
|
|
125
|
-
case '>=': return left >= right;
|
|
126
|
-
case '<': return left < right;
|
|
127
|
-
case '<=': return left <= right;
|
|
128
|
-
case '==': return left === right;
|
|
129
|
-
case '!=': return left !== right;
|
|
130
|
-
default: return false;
|
|
131
|
-
}
|
|
132
|
-
}
|