commit afef40a0af426b741289ae0e89306a97987dff72
parent 497c1defe31d3b3bdeeea70e2bcedf13fb54a3e0
Author: Jean Schurger <jean@schurger.org>
Date: Thu, 5 Nov 2026 13:43:56 -0500
Jeux Exelmémoire : .bkp décodé par blocs, lancement EXL 100 automatique (CALL EXEC)
Les sauvegardes .BKP de webmo ne sont pas un simple dump inversé : à partir de l'\''offset $200, 64 blocs de
258 octets ($01 $00 + 256 octets). Sans retirer les marqueurs, l'\''image glissait de 2 octets par bloc et
les CALL des jeux tombaient dans du texte (Kamicar : $8574 attendu, trouvé à $8500). Corrigé dans
get_cram.py et dans la zone de dépôt.
Ces programmes sont du code machine EXL 100 (ils appellent ExelBASIC et le moniteur de l'\''EXL 100) :
la bibliothèque les lance maintenant sur l'\''EXL 100 — volume dans l'\''Exelmémoire, ExelBASIC, puis
« CALL EXEC(entrée) » tapé automatiquement (typist partagé avec le chargement de cassette). Entrée
déduite de l'\''image (BR @ en $8000 = 32768, $AA puis BR = 32769) sinon des étoiles de webmo.
Les 11 volumes démarrent (vérifié headless). L'\''Autostart EXELTEL marche aussi (Kamicar).
Tests : cram-autostart rythmé sur les trames émulées, library-cram sur l'\''EXL 100, CDP_PORT.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QyJB9yqUAYm3wRQi4uytj9
Diffstat:
8 files changed, 231 insertions(+), 48 deletions(-)
diff --git a/test/cdp.mjs b/test/cdp.mjs
@@ -4,7 +4,7 @@ import { writeFileSync } from 'node:fs';
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
-export async function open(url, { port = 9334, width = 1000, height = 1400 } = {}) {
+export async function open(url, { port = Number(process.env.CDP_PORT ?? 9334), width = 1000, height = 1400 } = {}) {
const chrome = spawn(process.env.CHROME ?? 'chromium', [
'--headless=new', '--no-sandbox', '--hide-scrollbars',
'--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader',
diff --git a/test/cram-autostart.mjs b/test/cram-autostart.mjs
@@ -1,7 +1,9 @@
// Volume Exelmémoire sur l'EXELTEL : Périphériques -> Autostart (5e). Captures + trace PC.
+// Le rythme est calé sur les trames émulées (lignes « FRnnnn » de la build de debug), pas sur l'horloge :
+// la build de debug tourne au ralenti pendant le démarrage.
// node test/cram-autostart.mjs <volume.cram> [base-url] [prefix]
import { open, sleep } from './cdp.mjs';
-import { readFileSync } from 'node:fs';
+import { readFileSync, writeFileSync } from 'node:fs';
const img = readFileSync(process.argv[2]);
const base = process.argv[3] ?? 'http://127.0.0.1:8194';
@@ -9,22 +11,34 @@ const prefix = process.argv[4] ?? '/tmp/as';
const page = await open(base + '/?game=exeltel&rom=/roms/exeltel.zip', { width: 900, height: 1100 });
await page.evaluate(`localStorage.setItem('exl100.cram.exeltel', ${JSON.stringify(Buffer.from(img).toString('base64'))})`);
await page.evaluate('location.reload()');
+const frame = () => {
+ for (let i = page.logs.length - 1; i >= 0; i--) {
+ const m = /FR(\d+) PC=/.exec(page.logs[i]);
+ if (m) return Number(m[1]);
+ }
+ return -1;
+};
+const untilFrame = async (n) => { while (frame() < n) await sleep(100); };
+const frames = async (n) => untilFrame(frame() + n);
const key = async (k, code, vk) => {
await page.send('Input.dispatchKeyEvent', { type: 'keyDown', key: k, code, windowsVirtualKeyCode: vk });
- await sleep(60);
+ await frames(4);
await page.send('Input.dispatchKeyEvent', { type: 'keyUp', key: k, code, windowsVirtualKeyCode: vk });
- await sleep(320);
+ await frames(30);
};
const down = () => key('ArrowDown', 'ArrowDown', 40);
const enter = () => key('Enter', 'Enter', 13);
-await sleep(9000);
-await down(); await down(); await down(); await enter(); await sleep(1200); // Périphériques
-await down(); await down(); await down(); await down(); // Autostart (5e)
+await untilFrame(Number(process.env.BOOT_FRAMES ?? 300));
+await down(); await down(); await down(); await enter(); await frames(60); // Périphériques
+await down(); await down(); await down(); await down(); // Autostart (5e)
await page.screenshot(prefix + '-0-hl.png');
-await enter(); await sleep(2500);
-await page.screenshot(prefix + '-1-2s.png');
-await sleep(8000);
-await page.screenshot(prefix + '-2-10s.png');
+const launch = frame();
+await enter(); await frames(100);
+await page.screenshot(prefix + '-1.png');
+await frames(400);
+await page.screenshot(prefix + '-2.png');
+writeFileSync(prefix + '.log', page.logs.join('\n') + '\n');
const fr = page.logs.map((l) => l.replace(/^\[\w+\]\s*/, '')).filter((l) => /^FR\d+ PC=/.test(l));
-console.log('frames PC en CRAM :', fr.filter((l) => /PC=[89ab]/.test(l)).length, '/', fr.length, '; derniers :', fr.slice(-10).map((l) => l.split('PC=')[1]).join(' '));
+console.log('lancement à la trame', launch, '; frames PC en CRAM :', fr.filter((l) => /PC=[89ab]/.test(l)).length, '/', fr.length,
+ '; derniers :', fr.slice(-10).map((l) => l.split('PC=')[1]).join(' '));
page.close();
diff --git a/test/cram-boot.mjs b/test/cram-boot.mjs
@@ -0,0 +1,18 @@
+// Démarre l'EXELTEL avec un volume .cram chargé, sans toucher au clavier : captures à 4 s et 12 s,
+// et part des frames dont le PC est dans la CRAM ($8000-$BFFF).
+// node test/cram-boot.mjs <volume.cram> [base-url] [prefix]
+import { open, sleep } from './cdp.mjs';
+import { readFileSync } from 'node:fs';
+
+const img = readFileSync(process.argv[2]);
+const base = process.argv[3] ?? 'http://127.0.0.1:8194';
+const prefix = process.argv[4] ?? '/tmp/cb';
+const page = await open(base + '/?game=exeltel&rom=/roms/exeltel.zip', { width: 900, height: 1100 });
+await page.evaluate(`localStorage.setItem('exl100.cram.exeltel', ${JSON.stringify(Buffer.from(img).toString('base64'))})`);
+await page.evaluate('location.reload()');
+await sleep(4000); await page.screenshot(prefix + '-1-4s.png');
+await sleep(8000); await page.screenshot(prefix + '-2-12s.png');
+const fr = page.logs.map((l) => l.replace(/^\[\w+\]\s*/, '')).filter((l) => /^FR\d+ PC=/.test(l));
+console.log('frames PC en CRAM :', fr.filter((l) => /PC=[89ab]/.test(l)).length, '/', fr.length,
+ '; derniers :', fr.slice(-8).map((l) => l.split('PC=')[1]).join(' '));
+page.close();
diff --git a/test/cram-exl100-cmd.mjs b/test/cram-exl100-cmd.mjs
@@ -0,0 +1,34 @@
+// EXL 100 + CROS 16K sur un volume .cram donné : tape des commandes (séparées par ;) et capture après chacune.
+// node test/cram-exl100-cmd.mjs <volume.cram> "DIR;REN" [base-url] [prefix]
+import { open, sleep } from './cdp.mjs';
+import { readFileSync } from 'node:fs';
+
+const img = readFileSync(process.argv[2]);
+const cmds = (process.argv[3] ?? 'DIR').split(';');
+const base = process.argv[4] ?? 'http://127.0.0.1:8195';
+const prefix = process.argv[5] ?? '/tmp/xc';
+const page = await open(base + '/?game=exl100&rom=/roms/exl100.zip&exelmem=1', { width: 900, height: 1100 });
+await page.evaluate(`localStorage.setItem('exl100.cram.exl100', ${JSON.stringify(Buffer.from(img).toString('base64'))})`);
+await page.evaluate('location.reload()');
+const ev = (type, k, code, vk, mods = 0) =>
+ page.send('Input.dispatchKeyEvent', { type, key: k, code, windowsVirtualKeyCode: vk, modifiers: mods });
+const key = async (k, code, vk, hold = 80, gap = 400) => { await ev('keyDown', k, code, vk); await sleep(hold); await ev('keyUp', k, code, vk); await sleep(gap); };
+const type = async (s) => {
+ for (const ch of s) {
+ if (ch === ' ') await key(' ', 'Space', 32);
+ else if (ch === '.') await key('.', 'Period', 190);
+ else if (/[0-9]/.test(ch)) await key(ch, 'Digit' + ch, ch.charCodeAt(0));
+ else await key(ch.toLowerCase(), 'Key' + ch.toUpperCase(), ch.toUpperCase().charCodeAt(0));
+ }
+};
+await sleep(9000);
+let n = 0;
+for (const c of cmds) {
+ if (c === 'ESC') await key('Escape', 'Escape', 27);
+ else if (c === 'ENTER') await key('Enter', 'Enter', 13);
+ else { await type(c); await key('Enter', 'Enter', 13); }
+ await sleep(1800);
+ await page.screenshot(`${prefix}-${n++}-${c.replace(/[^A-Za-z0-9]+/g, '_').slice(0, 12)}.png`);
+}
+console.log('captures', prefix + '-*.png');
+page.close();
diff --git a/test/cram-quad2.mjs b/test/cram-quad2.mjs
@@ -0,0 +1,47 @@
+// Exelquad APRÈS initialisation de CROS64 : volume chargé, Périphériques -> Exelmémoire -> Quit,
+// puis Exelquad (et Autostart). Captures + PC en CRAM + entrées CROS64 lues.
+// node test/cram-quad2.mjs <volume.cram> [base-url] [prefix]
+import { open, sleep } from './cdp.mjs';
+import { readFileSync } from 'node:fs';
+
+const img = readFileSync(process.argv[2]);
+const base = process.argv[3] ?? 'http://127.0.0.1:8194';
+const prefix = process.argv[4] ?? '/tmp/q2';
+const page = await open(base + '/?game=exeltel&rom=/roms/exeltel.zip', { width: 900, height: 1100 });
+await page.evaluate(`localStorage.setItem('exl100.cram.exeltel', ${JSON.stringify(Buffer.from(img).toString('base64'))})`);
+await page.evaluate('location.reload()');
+const key = async (k, code, vk) => {
+ await page.send('Input.dispatchKeyEvent', { type: 'keyDown', key: k, code, windowsVirtualKeyCode: vk });
+ await sleep(60);
+ await page.send('Input.dispatchKeyEvent', { type: 'keyUp', key: k, code, windowsVirtualKeyCode: vk });
+ await sleep(320);
+};
+const down = () => key('ArrowDown', 'ArrowDown', 40);
+const enter = () => key('Enter', 'Enter', 13);
+const mark = (s) => page.logs.push('=== ' + s);
+await sleep(9000);
+await down(); await down(); await down(); await enter(); await sleep(1200); await enter(); await sleep(3500); // CROS64
+await page.screenshot(prefix + '-0-cros64.png');
+mark('QUIT');
+for (let i = 0; i < 8; i++) await down(); // Quit (9e)
+await enter(); await sleep(3000);
+await page.screenshot(prefix + '-1-after-quit.png');
+// Après Quit on est revenu dans le sous-menu Périphériques, « Exelmémoire » surligné.
+mark('AUTOSTART');
+for (let i = 0; i < 4; i++) await down(); // Autostart (5e)
+await enter(); await sleep(3000);
+await page.screenshot(prefix + '-2-autostart.png');
+await key('Escape', 'Escape', 27); await sleep(800);
+await key('Escape', 'Escape', 27); await sleep(800); // quitte le sous-menu -> Périphériques surligné
+mark('EXELQUAD');
+await down(); await down(); await down(); // Pages, Terminal, Exelquad
+await enter(); await sleep(6000);
+await page.screenshot(prefix + '-3-exelquad.png');
+const logs = page.logs.map((l) => l.replace(/^\[\w+\]\s*/, ''));
+for (const ph of ['AUTOSTART', 'EXELQUAD']) {
+ const i = logs.indexOf('=== ' + ph); const j = ph === 'AUTOSTART' ? logs.indexOf('=== EXELQUAD') : logs.length;
+ const seg = logs.slice(i, j); const fr = seg.filter((l) => /^FR\d+ PC=/.test(l));
+ console.log(ph, ': frames PC en CRAM', fr.filter((l) => /PC=[89ab]/.test(l)).length, '/', fr.length,
+ '; OS lus :', seg.filter((l) => /^OS /.test(l)).slice(0, 10).map((l) => l.split(' ')[1]).join(' ') || '(aucun)');
+}
+page.close();
diff --git a/test/library-cram.mjs b/test/library-cram.mjs
@@ -1,13 +1,15 @@
-// Volume Exelmémoire choisi dans la bibliothèque : clic sur l'entrée, la page bascule sur la machine
-// visée (EXELTEL) avec le volume dans le localStorage et un message d'état. Capture après redémarrage.
-// node test/library-cram.mjs [bout de titre] [base-url] [capture.png]
+// Volume Exelmémoire choisi dans la bibliothèque (EXL 100) : clic sur l'entrée, le volume part dans le
+// localStorage, la machine redémarre sur ExelBASIC avec l'Exelmémoire branchée et « CALL EXEC(n) » est tapé
+// automatiquement. Captures à 12 s (BASIC + frappe) et 30 s (programme lancé).
+// node test/library-cram.mjs [bout de titre] [base-url] [préfixe des captures]
import { open, sleep } from './cdp.mjs';
-const want = (process.argv[2] ?? 'kong').toLowerCase();
+const want = (process.argv[2] ?? 'kamicar').toLowerCase();
const base = process.argv[3] ?? 'http://127.0.0.1:8195';
-const shot = process.argv[4] ?? '/tmp/lib-cram.png';
+const prefix = process.argv[4] ?? '/tmp/lib-cram';
const page = await open(base + '/', { width: 900, height: 1100 });
-await page.evaluate(`localStorage.removeItem('exl100.cram.exeltel')`);
+await page.evaluate(`localStorage.removeItem('exl100.cram.exl100'); localStorage.removeItem('exl100.exelmem')`);
+await page.evaluate('location.reload()');
await sleep(7000);
const hit = await page.evaluate(`(() => {
const b = [...document.querySelectorAll('.pg')].find((b) => b.dataset.type === 'cram' && b.dataset.search.includes(${JSON.stringify(want)}));
@@ -16,9 +18,12 @@ const hit = await page.evaluate(`(() => {
})()`);
console.log('entrée cliquée :', hit);
await sleep(12000);
-const info = await page.evaluate(`({ href: location.href, status: document.getElementById('status').textContent,
- cram: (() => { try { return Object.keys(localStorage).filter((k) => k.startsWith('exl100.cram')).map((k) => k + '=' + localStorage.getItem(k).length).join(' '); } catch (e) { return ''; } })() })`);
-console.log('après :', JSON.stringify(info));
-await page.screenshot(shot);
-console.log('Capture :', shot);
+const state = () => page.evaluate(`({ href: location.href, status: document.getElementById('status').textContent,
+ cram: (() => { try { return Object.keys(localStorage).filter((k) => k.startsWith('exl100.')).map((k) => k + '=' + localStorage.getItem(k).length).join(' '); } catch (e) { return ''; } })() })`);
+console.log('à 12 s :', JSON.stringify(await state()));
+await page.screenshot(prefix + '-12s.png');
+await sleep(18000);
+console.log('à 30 s :', JSON.stringify(await state()));
+await page.screenshot(prefix + '-30s.png');
+console.log('Captures :', prefix + '-12s.png', prefix + '-30s.png');
page.close();
diff --git a/tools/get_cram.py b/tools/get_cram.py
@@ -1,11 +1,14 @@
#!/usr/bin/env python3
-"""Volumes Exelmémoire pour la bibliothèque : les 11 sauvegardes .BKP de webmo.fr (programmes EXELTEL
-diffusés par le serveur Exelvision, section « EXELMEMOIRES (CRAM) » de la page restauration-de-medias2)
+"""Volumes Exelmémoire pour la bibliothèque : les 11 sauvegardes .BKP de webmo.fr (programmes EXL 100 en code
+machine, qui appellent ExelBASIC et le moniteur de l'EXL 100, diffusés par le serveur télématique Exelvision ;
+section « EXELMEMOIRES (CRAM) » de la page restauration-de-medias2 ; sur l'EXL 100 : ExelBASIC puis CALL EXEC(n))
et les deux volumes formatés vides de DCExel. Chaque volume devient roms/cram/<slug>.cram (16 Ko bruts,
$8000-$BFFF) + roms/cram/titles.json ; non versionnés (roms/cram/ est ignoré), embarqués dans dsp.data.
-Un .BKP = 514 octets d'en-tête puis la CRAM à l'envers ($BFFF d'abord). L'amorce d'autostart EXELTEL
-(Périphériques -> Autostart : $BFE8 = $AA puis BR @$BFE9) est posée sur l'entrée indiquée par webmo
+Un .BKP = 512 octets d'en-tête, un bloc de contrôle, puis 64 blocs de 258 octets (marqueur $01 $00 + 256 octets)
+qui donnent la CRAM à l'envers ($BFFF d'abord) ; sans retirer les marqueurs, l'image glisse de 2 octets par
+bloc. L'amorce d'autostart EXELTEL (Périphériques -> Autostart : $BFE8 = $AA puis BR @$BFE9) est posée sur
+l'entrée indiquée par webmo
(« CALL EXEC(n) » : * 32768, ** 32769, *** 32772, **** 40843, ***** 32784).
Usage : python3 -I tools/get_cram.py [--dry-run]
@@ -52,14 +55,27 @@ def slug_of(title):
def bkp_to_cram(data):
- """En-tête de 514 octets (vérifié par le bloc de contrôle : $BFE1 = $AA, zone libre $8004) puis 16 Ko inversés."""
- for h in (514, *range(0, 1100)):
- img = data[h:h + 16384][::-1]
+ """64 blocs de 258 octets ($01 $00 + 256 octets) à partir de l'offset $200, CRAM à l'envers. Vérifié par le
+ bloc de contrôle du volume : $BFE1 = $AA, zone libre $8004."""
+ blocks = [data[o + 2:o + 258] for o in range(0x200, 0x200 + 64 * 258, 258)]
+ if len(data) >= 0x200 + 64 * 258 and all(data[o:o + 2] == b"\x01\x00" for o in range(0x200, 0x200 + 64 * 258, 258)):
+ img = b"".join(blocks)[::-1]
if len(img) == 16384 and img[0x3FE1] == 0xAA and img[0x3FE3:0x3FE5] == b"\x80\x04":
return bytearray(img)
raise ValueError("pas de volume CROS64 reconnu dans le .BKP")
+def entry_of(img, legend):
+ """Entrée réelle d'après l'image : BR @ en $8000 = 32768 (Burgertime, Kong, Descente, Slalom, Sphinx) ;
+ $AA puis BR @ vers la CRAM en $8000 (amorce d'autostart EXL 100, on saute le $AA) = 32769 quand webmo
+ n'indique rien de plus précis (Pindo) ; sinon la valeur des étoiles de webmo."""
+ if img[0] == 0x8C:
+ return 32768
+ if img[0] == 0xAA and img[1] == 0x8C and 0x80 <= img[2] <= 0xBF and legend in (None, 32768):
+ return 32769
+ return legend
+
+
def with_autostart(img, entry):
if entry:
img[0x3FE8:0x3FEC] = bytes([0xAA, 0x8C, entry >> 8, entry & 0xFF])
@@ -72,12 +88,14 @@ def main():
titles = {}
for fn, title, by, year, entry in WEBMO_VOLUMES:
try:
- img = with_autostart(bkp_to_cram(get(WEBMO + fn)), entry)
+ img = bkp_to_cram(get(WEBMO + fn))
+ entry = entry_of(img, entry)
+ img = with_autostart(img, entry)
except Exception as e:
print(f" ! {fn}: {e}", file=sys.stderr)
continue
out = slug_of(title) + ".cram"
- titles[out] = {"t": title, "by": by, "y": year, "cat": "jeu", "src": "webmo.fr", "machine": "exeltel",
+ titles[out] = {"t": title, "by": by, "y": year, "cat": "jeu", "src": "webmo.fr", "machine": "exl100",
"entry": entry}
print(f" + {out:36s} {title} — {by} (entrée {'$%04X' % entry if entry else '?'})", file=sys.stderr)
if not dry:
diff --git a/web/shell.html.in b/web/shell.html.in
@@ -456,22 +456,34 @@
args.push('--tape', '/roms/cartridges/exelbasic.bin', '--tape', '/roms/tapes/' + value.slice(5));
cassette = true;
} else if (value.startsWith('cram:')) {
- // Volume Exelmémoire : chargé dans le localStorage de la machine visée (EXELTEL pour les
- // programmes webmo), puis démarrage sur cette machine ; le volume est restauré au lancement.
+ // Volume Exelmémoire de la bibliothèque. Programmes EXL 100 (ceux de webmo : code machine qui
+ // appelle ExelBASIC et le moniteur de l'EXL 100) : volume dans l'Exelmémoire, ExelBASIC, puis
+ // « CALL EXEC(entrée) » tapé automatiquement. Volumes EXELTEL : chargés puis machine redémarrée
+ // (Périphériques → Exelmémoire ou → Autostart).
const file = value.slice(5);
let data, meta = {};
try {
data = fs().readFile('/roms/cram/' + file);
meta = JSON.parse(fs().readFile('/roms/cram/titles.json', { encoding: 'utf8' }))[file] || {};
} catch (e) { statusEl.textContent = 'Volume introuvable : ' + file; return; }
- const machine = meta.machine || 'exeltel';
+ const machine = meta.machine || 'exl100';
let s = '';
for (let i = 0; i < data.length; i += 4096) s += String.fromCharCode.apply(null, data.subarray(i, i + 4096));
try {
localStorage.setItem('exl100.cram.' + machine, btoa(s));
if (machine === 'exl100') localStorage.setItem('exl100.exelmem', '1');
- sessionStorage.setItem('exl100.msg', 'Volume « ' + (meta.t || file) + ' » chargé dans l’Exelmémoire' +
- (machine === 'exeltel' ? ' : Périphériques → Exelmémoire (CROS64) ou → Autostart pour le lancer.' : ' : il démarre sur CROS.'));
+ } catch (e) {}
+ if (machine === 'exl100' && !onExeltel && exists('/roms/cros64_exl100.rom')) {
+ // Même page : l'Exelmémoire est encore branchée, on relance la machine directement.
+ args.push('--tape', '/roms/cartridges/exelbasic.bin', biosZip);
+ launch(args);
+ if (meta.entry) autoExec(token, meta.t || file, meta.entry);
+ else statusEl.textContent = 'Volume « ' + (meta.t || file) + ' » chargé dans l’Exelmémoire (entrée inconnue : voir CROS, commande DIR).';
+ return;
+ }
+ try {
+ if (machine === 'exl100') sessionStorage.setItem('exl100.exec', value);
+ else sessionStorage.setItem('exl100.msg', 'Volume « ' + (meta.t || file) + ' » chargé dans l’Exelmémoire : Périphériques → Exelmémoire (CROS64) ou → Autostart pour le lancer.');
} catch (e) {}
if ((machine === 'exeltel') !== onExeltel) {
location.href = machine === 'exeltel' ? location.pathname + '?game=exeltel&rom=/roms/exeltel.zip' : location.pathname;
@@ -485,10 +497,10 @@
if (cassette) autoLoadCassette(token);
}
- // Tape LOAD"1" ⏎, Échap, puis RUN ⏎ dans ExelBASIC, en événements clavier synthétiques sur le
- // canevas (SDL écoute #canvas). Le " de l'EXL 100 = Shift+3 ; les modificateurs sont de vraies
- // touches Shift maintenues. S'annule si un autre programme est lancé entre-temps.
- async function autoLoadCassette(token) {
+ // Frappe synthétique dans ExelBASIC : événements clavier sur le canevas (SDL écoute #canvas). Le " de
+ // l'EXL 100 = Shift+3, ( = Shift+5, ) = Shift+- ; les modificateurs sont de vraies touches Shift
+ // maintenues. S'annule si un autre programme est lancé entre-temps (jeton).
+ function typist(token) {
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const alive = () => token === bootToken;
const send = (type, code, keyName, shift) =>
@@ -500,17 +512,43 @@
send('keydown', code, shifted, true); await sleep(70); send('keyup', code, shifted, true); await sleep(80);
send('keyup', 'ShiftLeft', 'Shift', false); await sleep(120);
};
+ const text = async (str) => {
+ for (const ch of str) {
+ if (ch === '"') await sh('Digit3', '"');
+ else if (ch === '(') await sh('Digit5', '(');
+ else if (ch === ')') await sh('Minus', ')');
+ else if (ch === ' ') await tap('Space', ' ');
+ else if (/[0-9]/.test(ch)) await tap('Digit' + ch, ch);
+ else await tap('Key' + ch.toUpperCase(), ch.toLowerCase());
+ }
+ };
+ return { sleep, alive, tap, sh, text };
+ }
+
+ // Tape LOAD"1" ⏎, Échap, puis RUN ⏎ dans ExelBASIC.
+ async function autoLoadCassette(token) {
+ const { sleep, alive, tap, text } = typist(token);
statusEl.textContent = 'ExelBASIC — chargement automatique de la cassette…';
await sleep(4500); if (!alive()) return; // ExelBASIC prêt
canvas.focus();
- for (const [c, k] of [['KeyL', 'l'], ['KeyO', 'o'], ['KeyA', 'a'], ['KeyD', 'd']]) await tap(c, k);
- await sh('Digit3', '"'); await tap('Digit1', '1'); await sh('Digit3', '"');
+ await text('LOAD"1"');
await tap('Enter', 'Enter'); await sleep(1500);
await tap('Escape', 'Escape'); await sleep(7000); if (!alive()) return; // lecture (chargement rapide)
- for (const [c, k] of [['KeyR', 'r'], ['KeyU', 'u'], ['KeyN', 'n']]) await tap(c, k);
+ await text('RUN');
await tap('Enter', 'Enter');
if (alive()) statusEl.textContent = 'Si rien ne démarre : au curseur, tapez LOAD "1" (Entrée), Échap, puis RUN. Certains programmes mettent jusqu’à une minute à s’initialiser.';
}
+
+ // Programme en Exelmémoire : CALL EXEC(entrée) ⏎ dans ExelBASIC.
+ async function autoExec(token, title, entry) {
+ const { sleep, alive, tap, text } = typist(token);
+ statusEl.textContent = 'ExelBASIC — lancement de « ' + title + ' » depuis l’Exelmémoire…';
+ await sleep(4500); if (!alive()) return; // ExelBASIC prêt
+ canvas.focus();
+ await text('CALL EXEC(' + entry + ')');
+ await tap('Enter', 'Enter');
+ if (alive()) statusEl.textContent = 'Si rien ne démarre : au curseur, tapez CALL EXEC(' + entry + ') puis Entrée.';
+ }
function sourceOf(m) {
const p = [];
if (m.by) p.push(m.by);
@@ -636,8 +674,9 @@
};
// Fichier de l'utilisateur : une seule zone (clic ou dépôt, aussi sur l'écran), type détecté par
// l'extension puis le contenu, puis lancé comme il faut : cassette = ExelBASIC + auto-chargement,
- // cartouche = démarrage direct, image Exelmémoire (.cram 16 Ko, image 64 Ko DCExel, .bkp webmo =
- // 514 octets d'en-tête + 16 Ko à l'envers) = volume chargé puis rechargement, disquette = étape à venir.
+ // cartouche = démarrage direct, image Exelmémoire (.cram 16 Ko, image 64 Ko DCExel, .bkp webmo = en-tête
+ // de 512 octets puis 64 blocs de 258 octets, 16 Ko à l'envers) = volume chargé puis rechargement,
+ // disquette = étape à venir.
const CART_SIZES = new Set([8192, 16384, 32256, 32768]);
function detectMedia(name, data) {
const ext = (name.toLowerCase().match(/\.([a-z0-9]+)$/) || [])[1] || '';
@@ -675,7 +714,12 @@
}
if (d.kind === 'exelmemoire') {
let vol = data;
- if (d.bkp) { vol = new Uint8Array(16384); for (let i = 0; i < 16384; i++) vol[i] = data[514 + 16383 - i]; }
+ if (d.bkp) {
+ // 64 blocs de 258 octets ($01 $00 + 256 octets) à partir de $200, CRAM à l'envers ($BFFF d'abord).
+ const fwd = new Uint8Array(16384);
+ for (let b = 0; b < 64; b++) fwd.set(data.subarray(0x200 + b * 258 + 2, 0x200 + b * 258 + 258), b * 256);
+ vol = new Uint8Array(16384); for (let i = 0; i < 16384; i++) vol[i] = fwd[16383 - i];
+ }
else if (data.length === 65536) vol = data.subarray(0x8000, 0xc000);
if (vol.length !== 16384) { statusEl.textContent = 'Image Exelmémoire inattendue (' + data.length + ' octets).'; return; }
let s = '';
@@ -816,7 +860,10 @@
statusEl.textContent = drivers.length + ' drivers';
populatePrograms();
markActive(''); // le démarrage par défaut, c'est le logo
- if (args.length > 0) launch(args);
+ let pending = null;
+ try { pending = sessionStorage.getItem('exl100.exec'); if (pending) sessionStorage.removeItem('exl100.exec'); } catch (e) {}
+ if (pending && !onExeltel) bootProgram(pending); // programme Exelmémoire choisi avant le rechargement
+ else if (args.length > 0) launch(args);
else if (Module.ccall('dsp_web_has_embedded', 'number', [], [])) launch([]); // media built in
},
};