get_cram.py (6001B)
1 #!/usr/bin/env python3 2 """Volumes Exelmémoire pour la bibliothèque : les 11 sauvegardes .BKP de webmo.fr (programmes EXL 100 en code 3 machine, qui appellent ExelBASIC et le moniteur de l'EXL 100, diffusés par le serveur télématique Exelvision ; 4 section « EXELMEMOIRES (CRAM) » de la page restauration-de-medias2 ; sur l'EXL 100 : ExelBASIC puis CALL EXEC(n)) 5 et les deux volumes formatés vides de DCExel. Chaque volume devient roms/cram/<slug>.cram (16 Ko bruts, 6 $8000-$BFFF) + roms/cram/titles.json ; non versionnés (roms/cram/ est ignoré), embarqués dans dsp.data. 7 8 Un .BKP = 512 octets d'en-tête, un bloc de contrôle, puis 64 blocs de 258 octets (marqueur $01 $00 + 256 octets) 9 qui donnent la CRAM à l'envers ($BFFF d'abord) ; sans retirer les marqueurs, l'image glisse de 2 octets par 10 bloc. L'amorce d'autostart EXELTEL (Périphériques -> Autostart : $BFE8 = $AA puis BR @$BFE9) est posée sur 11 l'entrée indiquée par webmo 12 (« CALL EXEC(n) » : * 32768, ** 32769, *** 32772, **** 40843, ***** 32784). 13 14 Usage : python3 -I tools/get_cram.py [--dry-run] 15 """ 16 import io 17 import json 18 import os 19 import re 20 import sys 21 import unicodedata 22 import urllib.request 23 import zipfile 24 25 ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") 26 OUT = os.path.join(ROOT, "roms", "cram") 27 WEBMO = "https://p7398.webmo.fr/httpdocs/exelvision/website/telechargement/" 28 DCEXEL = "http://dcexel.free.fr/programmes/exelmemoire/cram_formatee_cram.zip" 29 UA = {"User-Agent": "Mozilla/5.0 (exl100.schurger.org)"} 30 31 # fichier webmo, titre, éditeur, année, entrée (CALL EXEC) d'après les étoiles de la page webmo 32 WEBMO_VOLUMES = [ 33 ("BURGER.BKP", "Burgertime", "JMR Diffusion", "", 32769), 34 ("CB5QUAD.BKP", "CB5", "Exelvision", "", 40843), 35 ("DESC.BKP", "Descente - Albertville 92", "Éditions Parallèles", "", 32769), 36 ("KONG.BKP", "Kong", "JMR Diffusion", "1987", 32769), 37 ("KAMICAR.BKP", "Kamicar", "Charles Arosio", "", 32769), 38 ("PINDO.BKP", "Pindo", "Minipuce", "", 32768), 39 ("PYROX.BKP", "Pyrox", "Éditions Parallèles", "", 32772), 40 ("QUIZZY.BKP", "Quizzy", "Exelvision", "", 32784), 41 ("SLALOM.BKP", "Slalom géant - Albertville 92", "Éditions Parallèles", "", 32769), 42 ("SPHINX.BKP", "Le Sphinx d'Or", "Éditions Parallèles", "", None), 43 ("AIGLES.BKP", "Les Aigles", "P. Gérard", "", 32772), 44 ] 45 46 47 def get(url): 48 with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=60) as r: 49 return r.read() 50 51 52 def slug_of(title): 53 s = unicodedata.normalize("NFKD", title).encode("ascii", "ignore").decode().lower() 54 return re.sub(r"-+", "-", re.sub(r"[^a-z0-9]+", "-", s)).strip("-") 55 56 57 def bkp_to_cram(data): 58 """64 blocs de 258 octets ($01 $00 + 256 octets) à partir de l'offset $200, CRAM à l'envers. Vérifié par le 59 bloc de contrôle du volume : $BFE1 = $AA, zone libre $8004.""" 60 blocks = [data[o + 2:o + 258] for o in range(0x200, 0x200 + 64 * 258, 258)] 61 if len(data) >= 0x200 + 64 * 258 and all(data[o:o + 2] == b"\x01\x00" for o in range(0x200, 0x200 + 64 * 258, 258)): 62 img = b"".join(blocks)[::-1] 63 if len(img) == 16384 and img[0x3FE1] == 0xAA and img[0x3FE3:0x3FE5] == b"\x80\x04": 64 return bytearray(img) 65 raise ValueError("pas de volume CROS64 reconnu dans le .BKP") 66 67 68 def entry_of(img, legend): 69 """Entrée réelle d'après l'image : BR @ en $8000 = 32768 (Burgertime, Kong, Descente, Slalom, Sphinx) ; 70 $AA puis BR @ vers la CRAM en $8000 (amorce d'autostart EXL 100, on saute le $AA) = 32769 quand webmo 71 n'indique rien de plus précis (Pindo) ; sinon la valeur des étoiles de webmo.""" 72 if img[0] == 0x8C: 73 return 32768 74 if img[0] == 0xAA and img[1] == 0x8C and 0x80 <= img[2] <= 0xBF and legend in (None, 32768): 75 return 32769 76 return legend 77 78 79 def with_autostart(img, entry): 80 if entry: 81 img[0x3FE8:0x3FEC] = bytes([0xAA, 0x8C, entry >> 8, entry & 0xFF]) 82 return img 83 84 85 def main(): 86 dry = "--dry-run" in sys.argv 87 os.makedirs(OUT, exist_ok=True) 88 titles = {} 89 for fn, title, by, year, entry in WEBMO_VOLUMES: 90 try: 91 img = bkp_to_cram(get(WEBMO + fn)) 92 entry = entry_of(img, entry) 93 img = with_autostart(img, entry) 94 except Exception as e: 95 print(f" ! {fn}: {e}", file=sys.stderr) 96 continue 97 out = slug_of(title) + ".cram" 98 titles[out] = {"t": title, "by": by, "y": year, "cat": "jeu", "src": "webmo.fr", "machine": "exl100", 99 "entry": entry} 100 print(f" + {out:36s} {title} — {by} (entrée {'$%04X' % entry if entry else '?'})", file=sys.stderr) 101 if not dry: 102 open(os.path.join(OUT, out), "wb").write(img) 103 try: 104 z = zipfile.ZipFile(io.BytesIO(get(DCEXEL))) 105 for name in z.namelist(): 106 data = z.read(name) 107 if len(data) == 65536: 108 out, machine, title = "volume-vierge-64k.cram", "exeltel", "Volume vierge formaté (EXELTEL)" 109 img = data[0x8000:0xC000] 110 elif len(data) == 16384: 111 out, machine, title = "volume-vierge-16k.cram", "exl100", "Volume vierge formaté (EXL 100)" 112 img = data 113 else: 114 continue 115 titles[out] = {"t": title, "by": "DCExel", "y": "1985", "cat": "application", "src": "DCExel", 116 "machine": machine} 117 print(f" + {out:36s} {title}", file=sys.stderr) 118 if not dry: 119 open(os.path.join(OUT, out), "wb").write(img) 120 except Exception as e: 121 print(f" ! DCExel : {e}", file=sys.stderr) 122 if not dry: 123 with open(os.path.join(OUT, "titles.json"), "w", encoding="utf-8") as f: 124 json.dump(dict(sorted(titles.items(), key=lambda kv: kv[1]["t"].lower())), f, ensure_ascii=False, indent=1) 125 print(f"{len(titles)} volumes Exelmémoire {'(simulation)' if dry else 'dans ' + OUT}", file=sys.stderr) 126 127 128 if __name__ == "__main__": 129 main()