exl100

Émulateur Exelvision EXL 100 (1984) dans le navigateur : dsp-cpp en WebAssembly, cartouches et cassettes BASIC
git clone https://git.schurger.org/exl100.git
Log | Files | Refs | README

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()