mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
tests/games: browser-linux --lazy DIR, pointer events in --input, Hollow Knight's page script
--lazy DIR serves the directory at /gex/lazy/NAME/ (a link) with a manifest at /gex/nt/NAME.lazy.json, so a game's whole install is there without bundling it. --input scripts take move X Y / click X Y (CDP mouse events offset by the window's place in the compositor). hollow-knight-page-input.txt is the native New Game script (tests/games/hollow-knight-input.txt) on the page's clock. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_1896c5b302a54696aaa92c5f4047f20b
This commit is contained in:
1 parent
1e9154ad0d
commit
2ea7e6f278
2 files changed
+75
-2
No files matched your search
@@ -9,11 +9,15 @@
|
||||
// [--cwd REL] [--args ARGS] [--libs DIR:DIR] [--files LIST] [--home DIR]
|
||||
// [--input SCRIPT.wl] [--size WxH] [--minutes N] [--shot DIR]
|
||||
// [--profile AT,SECS[,MATCH]] [--gameplay N] [--mem64] [--fjit] [--wasmdlls SO=PATH] [--query K=V&...] [--env K=V,K=V] [--end-on-exit]
|
||||
// [--lazy DIR]
|
||||
//
|
||||
// --files lists host files the game opens, one per line (collected from a
|
||||
// native `gexc run --strace`; tests/games/retrocycles-linux-files.txt).
|
||||
// --gameplay N: report when the page has presented N GL frames.
|
||||
// --mem64: a 64-bit guest memory (the wasm64 shim, `?mem64=1`; docs/unity-dotnet.md P5).
|
||||
// --lazy DIR: every file under DIR the bundle lacks, at /game/REL, read from the
|
||||
// page's server as the game reads it (a megabyte at a time; `?lazy=`), not bundled.
|
||||
// The input script's `move X Y` / `click X Y` are in the game window's pixels.
|
||||
// --env: more environment for the guest. --end-on-exit: a program that exits is a result
|
||||
// (its output and status are printed), not a failure.
|
||||
// Needs web/public/gex built (shims/browser/build.sh), Mesa's wasm
|
||||
@@ -115,6 +119,29 @@ function bundle() {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** --lazy DIR: served at /gex/lazy/NAME/ (a link), listed in /gex/nt/NAME.lazy.json. */
|
||||
function lazyManifest() {
|
||||
const dir = opt('--lazy', null);
|
||||
if (!dir) return null;
|
||||
const link = join(WEB, `public/gex/lazy/${name}`);
|
||||
mkdirSync(dirname(link), { recursive: true });
|
||||
rmSync(link, { force: true });
|
||||
symlinkSync(dir, link);
|
||||
const files = [];
|
||||
const walk = (rel) => {
|
||||
for (const e of readdirSync(join(dir, rel))) {
|
||||
const r = rel ? `${rel}/${e}` : e;
|
||||
const st = statSync(join(dir, r));
|
||||
if (st.isDirectory()) walk(r);
|
||||
else if (st.isFile()) files.push([`/game/${r}`, st.size, r]);
|
||||
}
|
||||
};
|
||||
walk('');
|
||||
writeFileSync(join(WEB, `public/gex/nt/${name}.lazy.json`), JSON.stringify({ base: `/gex/lazy/${name}/`, files }));
|
||||
log(`lazy: ${files.length} files, ${(files.reduce((a, f) => a + f[1], 0) / 1048576).toFixed(0)} MiB under ${dir}`);
|
||||
return `/gex/nt/${name}.lazy.json`;
|
||||
}
|
||||
|
||||
/** A GEX_WL_INPUT script: `frames N`, then `SECONDS tap|key NAME [1|0]`. */
|
||||
function script() {
|
||||
const file = opt('--input', null);
|
||||
@@ -128,6 +155,7 @@ function script() {
|
||||
const t = Number(w[0]);
|
||||
if (w[1] === 'tap') { events.push([t, w[2], 'keydown'], [t + 0.1, w[2], 'keyup']); }
|
||||
else if (w[1] === 'key') events.push([t, w[2], w[3] === '1' ? 'keydown' : 'keyup']);
|
||||
else if (w[1] === 'move' || w[1] === 'click') events.push([t, `${w[2]},${w[3]}`, w[1]]);
|
||||
}
|
||||
events.sort((a, b) => a[0] - b[0]);
|
||||
return { after, events };
|
||||
@@ -151,6 +179,7 @@ async function main() {
|
||||
if (!existsSync(join(WEB, 'public/gex', f))) throw new Error(`no web/public/gex/${f}`);
|
||||
}
|
||||
const tar = bundle();
|
||||
const lazy = lazyManifest();
|
||||
const libs = (opt('--libs', '') || '').split(':').filter(Boolean).map((_, i) => `/gex/libs/${i}`);
|
||||
const genv = [
|
||||
`LD_LIBRARY_PATH=${['/gex/gl', ...libs, `/game/${dirname(exe)}`].join(':')}`,
|
||||
@@ -159,11 +188,12 @@ async function main() {
|
||||
...(opt('--env', '') || '').split(/,(?=[A-Za-z_][A-Za-z0-9_]*=)/).filter(Boolean),
|
||||
].join(',');
|
||||
const t0 = Date.now();
|
||||
const { evaluate, state, shot, consoleLines, profile } = await openPage(
|
||||
const { browser, page, evaluate, state, shot, consoleLines, profile } = await openPage(
|
||||
`tar=/gex/nt/${basename(tar)}&exe=/game/${exe}&cwd=/game/${cwd}&gl=1&glbackend=wgpu&hostx=1`
|
||||
+ (argv.includes('--mem64') ? '&mem64=1' : '') + (argv.includes('--fjit') ? '&fjit=1' : '')
|
||||
+ (opt('--wasmdlls', null) ? `&wasmdlls=${encodeURIComponent(opt('--wasmdlls'))}` : '')
|
||||
+ (opt('--query', null) ? `&${opt('--query')}` : '')
|
||||
+ (lazy ? `&lazy=${encodeURIComponent(lazy)}` : '')
|
||||
+ `&size=${width}x${height}&trapstack=1&env=${encodeURIComponent(genv)}&args=${encodeURIComponent(opt('--args', ''))}`,
|
||||
{ width: width + 20, height: height + 100, shotDir },
|
||||
);
|
||||
@@ -178,6 +208,20 @@ async function main() {
|
||||
let last = 0;
|
||||
const key = (c, type) => evaluate(
|
||||
`(() => { for (const t of document.querySelectorAll('canvas')) t.dispatchEvent(new KeyboardEvent(${JSON.stringify(type)}, { code: ${JSON.stringify(c)}, key: ${JSON.stringify(c)}, bubbles: true })); return 1; })()`, true);
|
||||
// The game window's place on the page: the compositor's toplevel
|
||||
// (`role=1 WxH@X,Y`), the canvas at the page's origin (as browser-gui.mjs).
|
||||
const pointer = async (type, xy) => {
|
||||
const [x, y] = xy.split(',').map(Number);
|
||||
const d = (await evaluate('globalThis.__gexCompositor?.describe()', true)) ?? {};
|
||||
const m = /role=1 (\d+)x(\d+)@(-?\d+),(-?\d+)/.exec(JSON.stringify(d));
|
||||
const [ox, oy] = m ? [Number(m[3]), Number(m[4])] : [0, 0];
|
||||
const at = { x: ox + x, y: oy + y };
|
||||
await browser.send('Input.dispatchMouseEvent', { type: 'mouseMoved', ...at, button: 'none', buttons: 0 }, page);
|
||||
if (type === 'click') {
|
||||
await browser.send('Input.dispatchMouseEvent', { type: 'mousePressed', ...at, button: 'left', clickCount: 1, buttons: 1 }, page);
|
||||
await browser.send('Input.dispatchMouseEvent', { type: 'mouseReleased', ...at, button: 'left', clickCount: 1, buttons: 0 }, page);
|
||||
}
|
||||
};
|
||||
const { after, events } = script();
|
||||
let first = null, start = null, next = 0, playAt = null, shots = 0, nextShot = Date.now() + 30000;
|
||||
const profSpec = opt('--profile', null) ? (([at = '300', secs = '20', match = '']) => ({ at: +at, secs: +secs, match }))(opt('--profile').split(',')) : null;
|
||||
@@ -216,7 +260,8 @@ async function main() {
|
||||
}
|
||||
while (start && next < events.length && Date.now() - start >= events[next][0] * 1000) {
|
||||
const [t, k, type] = events[next++];
|
||||
await key(code(k), type);
|
||||
if (type === 'move' || type === 'click') await pointer(type, k);
|
||||
else await key(code(k), type);
|
||||
log(`input at ${t} s: ${type} ${k} (present ${n})`);
|
||||
}
|
||||
if (gameplay && !playAt && n >= gameplay) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Hollow Knight in the page (tests/games/browser-linux.mjs --input): main menu -> Start Game -> profile 1
|
||||
# (new) -> screen-scale page -> brightness page -> intro (skipped) -> King's Pass. As
|
||||
# hollow-knight-input.txt (native), timed from the page's 600th GL present (the menu is up and
|
||||
# taking input by then on edger); keys held 2 s, steps 40 s apart (a new game's scene loads come
|
||||
# through ?lazy= a megabyte at a time).
|
||||
frames 600
|
||||
8 move 2 2
|
||||
12 key enter 1 # Start Game (selected by default)
|
||||
14 key enter 0
|
||||
50 key enter 1 # profile 1 (selected by default): new game
|
||||
52 key enter 0
|
||||
110 move 400 434 # screen-scale page: Done
|
||||
112 click 400 434
|
||||
150 click 400 434 # brightness page: Done
|
||||
190 click 400 434 # (again, in case the page came up late)
|
||||
195 move 2 2
|
||||
230 key enter 1 # the intro cinematic: skip prompt
|
||||
232 key enter 0
|
||||
240 key enter 1 # confirm the skip
|
||||
242 key enter 0
|
||||
400 key right 1 # King's Pass: walk right
|
||||
430 key right 0
|
||||
435 key z 1 # jump
|
||||
437 key z 0
|
||||
445 key x 1 # attack
|
||||
446 key x 0
|
||||
460 key left 1 # walk back left
|
||||
480 key left 0
|
||||
Reference in new issue
Block a user