Skip to content

Commit 8a90812

Browse files
tsemachhclaude
andcommitted
Emu: official upstream CI build 54633535; keyboard fire Z/X/Alt for Mac
macOS captures Ctrl+Arrow at the OS level (Mission Control/App Exposé), so the emulator's Ctrl fire key can't be held while steering with the arrows. ARCADE-PATCH now maps Z, X and Alt/Option to the external trigger (capture-phase, so nothing types into the Atari; modifier combos like Cmd+Z pass through untouched). Bundle swapped from the locally-built 27d37558 to the official upstream GitHub-CI artifact of 54633535, which includes the fix for silent deep-link download failures (our issue #86). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 72d0421 commit 8a90812

7 files changed

Lines changed: 283 additions & 33 deletions

File tree

README.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ commit, and the service worker cache is busted on every deploy.
4141
The emulator under `emu/` is **[AltirraSDL](https://github.com/ilmenit/AltirraSDL)**
4242
by Jakub Dębski, a portable fork of **[Altirra](https://www.virtualdub.org/altirra.html)**
4343
by Avery Lee, licensed **GPLv2** (see [`emu/LICENSE`](emu/LICENSE)). The
44-
vendored bundle is built from upstream commit
45-
[`27d37558`](https://github.com/ilmenit/AltirraSDL/commit/27d37558); its
44+
vendored bundle is the official upstream CI build of commit
45+
[`54633535`](https://github.com/ilmenit/AltirraSDL/commit/54633535); its
4646
corresponding source is the upstream repository at that commit. Our
4747
modifications to the emulator's host page (the ARCADE-PATCH block injected
4848
into `emu/index.html`, source in
@@ -68,8 +68,12 @@ launches:
6868
- **TILT toggle + `?tilt=1`** — device-orientation steering (extra
6969
sensitivity on forward/back so the screen stays visible while
7070
accelerating).
71-
- **AUTO FIRE slide switch** — pulses the trigger at ~11 Hz via
72-
`ATWasmSetJoystick`.
71+
- **AUTO FIRE slide switch** — native in-core auto-fire
72+
(`ATWasmSetAutoFire`) with an ~11 Hz `ATWasmSetJoystick` fallback on
73+
older bundles.
74+
- **Keyboard fire: Z / X / Alt(Option)** — macOS grabs Ctrl+Arrow
75+
(Mission Control), so the emulator's Ctrl fire key can't be held
76+
while steering; these alternates work on every desktop OS.
7377
- **`?tv=1`** — smart-TV remote controls (CE-HTML/HbbTV key codes):
7478
D-pad → joystick, OK → fire, Red/Green/Yellow → START/SELECT/OPTION,
7579
Blue → auto-fire, Play/Pause → pause, Back → picker. Open the picker

emu/AltirraSDL.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

emu/AltirraSDL.wasm

150 KB
Binary file not shown.

emu/index.html

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,41 @@
508508
padding: 0 16px;
509509
}
510510

511+
#library-fetch-error {
512+
position: absolute;
513+
inset: 0;
514+
z-index: 30;
515+
display: none;
516+
align-items: center;
517+
justify-content: center;
518+
padding: 24px;
519+
background: rgba(8, 10, 14, .92);
520+
color: #f5f7fa;
521+
text-align: center;
522+
}
523+
#library-fetch-error.active { display: flex; }
524+
#library-fetch-error .error-card { max-width: 520px; }
525+
#library-fetch-error .error-title {
526+
margin-bottom: 10px;
527+
font-size: 1.25rem;
528+
font-weight: 700;
529+
}
530+
#library-fetch-error .error-detail {
531+
margin-bottom: 18px;
532+
color: #c9ced8;
533+
overflow-wrap: anywhere;
534+
}
535+
#library-fetch-retry {
536+
padding: 9px 18px;
537+
border: 1px solid #7aa2f7;
538+
border-radius: 5px;
539+
background: #315a9f;
540+
color: #fff;
541+
cursor: pointer;
542+
font: inherit;
543+
font-weight: 600;
544+
}
545+
511546
/* ── Log panel ───────────────────────────────────────────────── */
512547
#log {
513548
flex: 0 0 auto;
@@ -1432,7 +1467,7 @@
14321467
<h1><a id="brand"
14331468
href="https://github.com/ilmenit/AltirraSDL"
14341469
target="_blank" rel="noopener"
1435-
title="AltirraSDL 4.50 · build 27d3755 (2026-07-18 21:12 UTC) — open the GitHub project in a new tab">AltirraSDL</a></h1>
1470+
title="AltirraSDL 4.50 · build 54633535 (2026-08-01 09:11 UTC) — open the GitHub project in a new tab">AltirraSDL</a></h1>
14361471
<!-- Lobby: navigate back to a curated software library / catalog
14371472
page. Hidden by default so a self-hosted bundle (no
14381473
`config.json`, or no `lobbyUrl` field) doesn't show a button
@@ -1531,6 +1566,18 @@ <h1><a id="brand"
15311566
<div class="play-ring" aria-hidden="true"></div>
15321567
<div class="play-label">Click to play</div>
15331568
</div>
1569+
<div id="library-fetch-error" role="alertdialog" aria-modal="true"
1570+
aria-labelledby="library-fetch-error-title">
1571+
<div class="error-card">
1572+
<div class="error-title" id="library-fetch-error-title">
1573+
Game download failed
1574+
</div>
1575+
<div class="error-detail" id="library-fetch-error-detail">
1576+
Check your connection and try again.
1577+
</div>
1578+
<button id="library-fetch-retry" type="button">Retry download</button>
1579+
</div>
1580+
</div>
15341581
</div>
15351582

15361583
<div id="log" class="hidden">
@@ -1898,7 +1945,7 @@ <h2 id="fw-title">Download standard Atari ROMs?</h2>
18981945
build. See src/AltirraSDL/cmake/wasm_lib_deeplink.js for the
18991946
contract.
19001947
-->
1901-
<script src="wasm_lib_deeplink.js?v=27d3755"></script>
1948+
<script src="wasm_lib_deeplink.js?v=54633535"></script>
19021949

19031950
<script>
19041951
(() => {
@@ -7209,6 +7256,22 @@ <h2 id="fw-title">Download standard Atari ROMs?</h2>
72097256
arguments: __wasmCliArgs,
72107257
print: (t) => appendLog('[stdout] ' + t),
72117258
printErr: (t) => appendLog('[stderr] ' + t),
7259+
onLibraryFetchError: (failures) => {
7260+
const overlay = document.getElementById('library-fetch-error');
7261+
const detail = document.getElementById('library-fetch-error-detail');
7262+
const retry = document.getElementById('library-fetch-retry');
7263+
const names = (failures || []).map((failure) => failure.path);
7264+
if (detail) {
7265+
detail.textContent = names.length
7266+
? 'Could not download ' + names.join(', ')
7267+
+ '. Check your connection and try again.'
7268+
: 'Check your connection and try again.';
7269+
}
7270+
if (retry) retry.onclick = () => window.location.reload();
7271+
if (overlay) overlay.classList.add('active');
7272+
if (retry) retry.focus();
7273+
statusEl.textContent = 'Game download failed';
7274+
},
72127275
// Emscripten calls setStatus with messages like "Downloading
72137276
// data... (1234567/10485760)" while the .wasm is still in flight.
72147277
// Rewrite the prefix so non-technical users see what's actually
@@ -7318,8 +7381,8 @@ <h2 id="fw-title">Download standard Atari ROMs?</h2>
73187381
// opens the File Manager the bar shows real numbers
73197382
// immediately rather than "checking…" for a frame.
73207383
refreshStorageInfo();
7321-
jlog('runtime ready, build=27d3755',
7322-
'(2026-07-18 21:12 UTC)');
7384+
jlog('runtime ready, build=54633535',
7385+
'(2026-08-01 09:11 UTC)');
73237386
// Log what's on disk after IDBFS syncfs completed so we can
73247387
// confirm uploads survive reloads. readdir throws if the
73257388
// dir doesn't exist yet — swallow.
@@ -8021,6 +8084,42 @@ <h2 id="fw-title">Download standard Atari ROMs?</h2>
80218084
}, true);
80228085
})();
80238086

8087+
// ---- desktop keyboard fire: Z / X / Alt(Option) ---------------------
8088+
// The emulator's keyboard fire is Ctrl, but macOS grabs Ctrl+Arrow at
8089+
// the OS level (Mission Control / App Exposé) before the browser ever
8090+
// sees it — you can't hold fire while steering. Offer triggers no OS
8091+
// touches: Z, X, and Alt/Option (on Mac laptops the right Option sits
8092+
// directly beside the arrow cluster). Capture-phase + stop so SDL
8093+
// never types the letter into the Atari; skipped with a modifier held
8094+
// (Cmd+Z stays undo) and in TV mode (own handler above).
8095+
if (params.get('tv') !== '1') (function () {
8096+
var kbFire = false;
8097+
function isFireKey(e) {
8098+
return e.code === 'KeyZ' || e.code === 'KeyX' || e.key === 'Alt';
8099+
}
8100+
function setKbFire(down) {
8101+
if (kbFire === down) return;
8102+
kbFire = down;
8103+
// Auto-fire ON holds the external trigger steady — never release it
8104+
var afHold = afBtn && afBtn.classList.contains('on');
8105+
ext.fire = down || !!afHold;
8106+
pushExt();
8107+
}
8108+
document.addEventListener('keydown', function (e) {
8109+
if (e.metaKey || e.ctrlKey) return;
8110+
if (!isFireKey(e)) return;
8111+
e.preventDefault(); e.stopImmediatePropagation();
8112+
setKbFire(true);
8113+
}, true);
8114+
document.addEventListener('keyup', function (e) {
8115+
if (!isFireKey(e)) return;
8116+
e.preventDefault(); e.stopImmediatePropagation();
8117+
setKbFire(false);
8118+
}, true);
8119+
// Alt+Tab / app switch can eat the keyup — never leave fire stuck
8120+
window.addEventListener('blur', function () { setKbFire(false); });
8121+
})();
8122+
80248123
// ---- ?fs=1 : tap enters fullscreen ---------------------------------
80258124
// Android Chrome rejects fullscreen requests made from start-of-touch
80268125
// events (pointerdown/touchstart) — only end-of-gesture events carry

emu/wasm_lib_deeplink.js

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,26 +1003,49 @@
10031003
}
10041004

10051005
function doFetch(reasonLabel) {
1006-
return fetch(url, { cache: 'no-cache' })
1007-
.then(function (r) {
1008-
// Compact status-only message — the outer .catch logs the
1009-
// full URL alongside, so duplicating it here would just
1010-
// produce "... at URL — HTTP 404 for URL".
1011-
if (!r.ok) throw new Error('HTTP ' + r.status);
1012-
var freshETag = r.headers.get('etag');
1013-
return r.arrayBuffer().then(function (buf) {
1014-
writeFile(buf, freshETag);
1015-
if (reasonLabel === 'refetch') refetched++; else fetched++;
1006+
var maxAttempts = 3;
1007+
1008+
function fetchAttempt(attempt) {
1009+
return fetch(url, { cache: 'no-cache' })
1010+
.then(function (r) {
1011+
// Compact status-only message — the outer .catch logs the
1012+
// full URL alongside, so duplicating it here would just
1013+
// produce "... at URL — HTTP 404 for URL".
1014+
if (!r.ok) {
1015+
var httpError = new Error('HTTP ' + r.status);
1016+
httpError.httpStatus = r.status;
1017+
throw httpError;
1018+
}
1019+
var freshETag = r.headers.get('etag');
1020+
return r.arrayBuffer().then(function (buf) {
1021+
writeFile(buf, freshETag);
1022+
if (reasonLabel === 'refetch') refetched++; else fetched++;
1023+
});
1024+
}).catch(function (err) {
1025+
var status = err && err.httpStatus;
1026+
var retryable = !status || status === 408 || status === 425
1027+
|| status === 429 || status >= 500;
1028+
if (retryable && attempt < maxAttempts) {
1029+
var delay = attempt === 1 ? 400 : 1200;
1030+
log('fetch attempt ' + attempt + ' failed for', e.rel,
1031+
'— retrying in ' + delay + 'ms:',
1032+
err && err.message ? err.message : err);
1033+
return new Promise(function (resolve) {
1034+
setTimeout(resolve, delay);
1035+
}).then(function () { return fetchAttempt(attempt + 1); });
1036+
}
1037+
1038+
// Include the full URL — when self-hosters report "fetch
1039+
// failed", the actual URL the browser tried is the single
1040+
// most useful debug breadcrumb (404 from the wrong path is
1041+
// the #1 cause; HTTP status alone doesn't pinpoint that).
1042+
log('fetch failed for', e.rel, 'at', url, '—',
1043+
err && err.message ? err.message : err);
1044+
throw err;
10161045
});
1017-
})
1018-
.catch(function (err) {
1019-
// Include the full URL — when self-hosters report "fetch
1020-
// failed", the actual URL the browser tried is the single
1021-
// most useful debug breadcrumb (404 from the wrong path is
1022-
// the #1 cause; HTTP status alone doesn't pinpoint that).
1023-
log('fetch failed for', e.rel, 'at', url, '—',
1024-
err && err.message ? err.message : err);
1025-
});
1046+
}
1047+
1048+
return fetchAttempt(1);
10261049
}
10271050

10281051
if (onDiskSize <= 0) {
@@ -1085,7 +1108,32 @@
10851108
}));
10861109

10871110
var totalCount = entries.length + fwEntries.length;
1088-
Promise.all(promises).then(function () {
1111+
Promise.all(promises.map(function (promise, index) {
1112+
return promise.then(function () { return null; }, function (err) {
1113+
var allEntries = entries.concat(fwEntries);
1114+
return {
1115+
path: allEntries[index].rel,
1116+
message: err && err.message ? err.message : String(err),
1117+
};
1118+
});
1119+
})).then(function (failures) {
1120+
failures = failures.filter(function (failure) { return failure; });
1121+
if (failures.length) {
1122+
log('startup blocked: ' + failures.length
1123+
+ ' requested file(s) could not be downloaded');
1124+
if (typeof Module.onLibraryFetchError === 'function') {
1125+
try { Module.onLibraryFetchError(failures); } catch (e) {
1126+
log('onLibraryFetchError failed —',
1127+
e && e.message ? e.message : e);
1128+
}
1129+
}
1130+
// Deliberately retain the run dependency. Starting with CLI
1131+
// paths that do not exist drops an empty Atari into Self Test,
1132+
// which hides the actual download error from the user. The
1133+
// error UI offers a reload that retries the complete startup.
1134+
return;
1135+
}
1136+
10891137
log('lib+firmware: ' + fetched + ' fetched + ' + refetched
10901138
+ ' refetched + ' + cached + ' cached / ' + totalCount
10911139
+ ' file(s)'

scripts/emu-autofullscreen.snippet.html

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,42 @@
465465
}, true);
466466
})();
467467

468+
// ---- desktop keyboard fire: Z / X / Alt(Option) ---------------------
469+
// The emulator's keyboard fire is Ctrl, but macOS grabs Ctrl+Arrow at
470+
// the OS level (Mission Control / App Exposé) before the browser ever
471+
// sees it — you can't hold fire while steering. Offer triggers no OS
472+
// touches: Z, X, and Alt/Option (on Mac laptops the right Option sits
473+
// directly beside the arrow cluster). Capture-phase + stop so SDL
474+
// never types the letter into the Atari; skipped with a modifier held
475+
// (Cmd+Z stays undo) and in TV mode (own handler above).
476+
if (params.get('tv') !== '1') (function () {
477+
var kbFire = false;
478+
function isFireKey(e) {
479+
return e.code === 'KeyZ' || e.code === 'KeyX' || e.key === 'Alt';
480+
}
481+
function setKbFire(down) {
482+
if (kbFire === down) return;
483+
kbFire = down;
484+
// Auto-fire ON holds the external trigger steady — never release it
485+
var afHold = afBtn && afBtn.classList.contains('on');
486+
ext.fire = down || !!afHold;
487+
pushExt();
488+
}
489+
document.addEventListener('keydown', function (e) {
490+
if (e.metaKey || e.ctrlKey) return;
491+
if (!isFireKey(e)) return;
492+
e.preventDefault(); e.stopImmediatePropagation();
493+
setKbFire(true);
494+
}, true);
495+
document.addEventListener('keyup', function (e) {
496+
if (!isFireKey(e)) return;
497+
e.preventDefault(); e.stopImmediatePropagation();
498+
setKbFire(false);
499+
}, true);
500+
// Alt+Tab / app switch can eat the keyup — never leave fire stuck
501+
window.addEventListener('blur', function () { setKbFire(false); });
502+
})();
503+
468504
// ---- ?fs=1 : tap enters fullscreen ---------------------------------
469505
// Android Chrome rejects fullscreen requests made from start-of-touch
470506
// events (pointerdown/touchstart) — only end-of-gesture events carry

0 commit comments

Comments
 (0)