Working Python version for Commodore.
This commit is contained in:
commit
2a48f52979
51 changed files with 3095 additions and 0 deletions
145
c64view/diskimage.py
Normal file
145
c64view/diskimage.py
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
"""Build .d64/.d71/.d81 Commodore disk images with VICE's `c1541`.
|
||||
|
||||
We shell out to `c1541` rather than re-implementing CBM-DOS: it is battle-tested
|
||||
and handles BAM/directory layout for all three formats correctly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
FORMATS = {
|
||||
"d64": ("d64", 174848), # 35 tracks, 664 blocks free
|
||||
"d71": ("d71", 349696), # double sided
|
||||
"d81": ("d81", 819200), # 3.5" 800K
|
||||
}
|
||||
|
||||
# Per-format usable data budget (blocks * 254), conservative.
|
||||
BLOCKS_FREE = {"d64": 664, "d71": 1328, "d81": 3160}
|
||||
|
||||
|
||||
class DiskError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def have_c1541() -> bool:
|
||||
return shutil.which("c1541") is not None
|
||||
|
||||
|
||||
# x64sc is the accurate C64 emulator; x64 is the faster fallback.
|
||||
VICE_EMULATORS = ["x64sc", "x64"]
|
||||
|
||||
|
||||
def vice_emulator() -> str | None:
|
||||
for exe in VICE_EMULATORS:
|
||||
path = shutil.which(exe)
|
||||
if path:
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def have_vice() -> bool:
|
||||
return vice_emulator() is not None
|
||||
|
||||
|
||||
def launch_in_vice(disk_path: str, warp: bool = True, standard: str = "pal"):
|
||||
"""Open VICE on ``disk_path`` (drive 8), list the directory, then run the viewer.
|
||||
|
||||
Types ``LOAD"$",8`` / ``LIST`` / ``LOAD"*",8,1`` / ``RUN`` via the keyboard
|
||||
buffer. BASIC commands must be *lower* case here: VICE maps lower-case ASCII
|
||||
to the PETSCII keyword range, and "\\n" is the RETURN key. Runs detached so
|
||||
the GUI stays responsive. ``warp`` should be False for the interlace mode,
|
||||
whose 50 Hz field-flip flickers too fast under warp.
|
||||
"""
|
||||
exe = vice_emulator()
|
||||
if not exe:
|
||||
raise DiskError(
|
||||
"VICE (x64sc) was not found on PATH.\n"
|
||||
"Install it with: sudo apt install vice (Debian/Ubuntu)")
|
||||
keys = 'load"$",8\nlist\nload"*",8,1\nrun\n'
|
||||
# -default keeps the device config predictable so LOAD"*" reads the attached
|
||||
# image rather than a host-filesystem virtual device; -warp runs full speed.
|
||||
cmd = [exe, "-default", "-ntsc" if standard == "ntsc" else "-pal"]
|
||||
if warp:
|
||||
cmd.append("-warp")
|
||||
cmd += ["-8", os.path.abspath(disk_path), "-keybuf", keys]
|
||||
return subprocess.Popen(cmd, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL, start_new_session=True)
|
||||
|
||||
|
||||
def petscii_name(text: str, maxlen: int = 16) -> str:
|
||||
"""Sanitise a host string into a legal CBM filename.
|
||||
|
||||
Letters are forced to lower case: c1541 maps lower-case ASCII into the
|
||||
standard PETSCII letter range ($41-$5A), which renders as clean letters in a
|
||||
C64 directory listing, whereas upper-case ASCII lands in the shifted range
|
||||
that displays oddly.
|
||||
"""
|
||||
out = []
|
||||
for ch in text.lower():
|
||||
if ch.isalnum() or ch in " -+.":
|
||||
out.append(ch)
|
||||
name = "".join(out).strip() or "c64view"
|
||||
return name[:maxlen]
|
||||
|
||||
|
||||
def fmt_from_path(path: str, override: str | None) -> str:
|
||||
if override:
|
||||
if override not in FORMATS:
|
||||
raise DiskError(f"unknown disk format: {override}")
|
||||
return override
|
||||
ext = os.path.splitext(path)[1].lower().lstrip(".")
|
||||
return ext if ext in FORMATS else "d64"
|
||||
|
||||
|
||||
def build_disk(path: str, disk_format: str, disk_name: str, disk_id: str,
|
||||
files: list[tuple[str, bytes]]) -> str:
|
||||
"""Create ``path`` of ``disk_format`` containing ``files`` (cbm_name, prg_bytes).
|
||||
|
||||
The first file in the list lands first in the directory, so ``LOAD"*",8,1``
|
||||
loads it -- make that the viewer.
|
||||
"""
|
||||
if not have_c1541():
|
||||
raise DiskError(
|
||||
"VICE's 'c1541' tool was not found on PATH.\n"
|
||||
"Install it with: sudo apt install vice (Debian/Ubuntu)\n"
|
||||
"or build from https://vice-emu.sourceforge.io/")
|
||||
|
||||
total = sum(len(b) for _, b in files)
|
||||
budget = BLOCKS_FREE[disk_format] * 254
|
||||
if total > budget:
|
||||
raise DiskError(
|
||||
f"data ({total} bytes) exceeds {disk_format} capacity (~{budget} bytes); "
|
||||
f"use a larger disk format")
|
||||
|
||||
vtype = FORMATS[disk_format][0]
|
||||
name = petscii_name(disk_name)
|
||||
did = petscii_name(disk_id, 2) or "01"
|
||||
path = os.path.abspath(path)
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
cmd = ["c1541", "-format", f"{name},{did}", vtype, path]
|
||||
for i, (cbm, data) in enumerate(files):
|
||||
host = os.path.join(td, f"f{i}.prg")
|
||||
with open(host, "wb") as f:
|
||||
f.write(data)
|
||||
cmd += ["-write", host, petscii_name(cbm)]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True,
|
||||
errors="replace")
|
||||
# c1541 prints a harmless "OPENCBM ... libopencbm.so" warning; only fail
|
||||
# if the image was not actually produced.
|
||||
if not os.path.exists(path):
|
||||
raise DiskError(f"c1541 failed:\n{proc.stdout}{proc.stderr}")
|
||||
return path
|
||||
|
||||
|
||||
def directory(path: str) -> str:
|
||||
"""Return the disk directory listing (for verification)."""
|
||||
proc = subprocess.run(["c1541", "-attach", path, "-dir"],
|
||||
capture_output=True, text=True, errors="replace")
|
||||
return proc.stdout
|
||||
Loading…
Add table
Add a link
Reference in a new issue