"""Platform abstraction so the GUI (and anything else) can treat C64 and Atari uniformly: which modes/palettes/formats exist, and how to convert/export/run. """ from __future__ import annotations import os import tempfile from . import crt, imageprep, mame from .apple.convert import MODES as APPLE_MODES, convert_image as _apple_convert from .apple.exporter import export_dsk from .atari.convert import MODES as ATARI_MODES, convert_image as _atari_convert from .atari.exporter import export_atr, export_car from .atari.palette import nearest_hue from .bbc.convert import MODES as BBC_MODES, convert_image as _bbc_convert from .bbc.exporter import export_ssd from .coco.convert import MODES as COCO_MODES, convert_image as _coco_convert from .coco.exporter import export_ccc from .coleco.convert import MODES as COLECO_MODES, convert_image as _coleco_convert from .coleco.exporter import export_col from .a2600.convert import MODES as A2600_MODES, convert_image as _a2600_convert from .a2600.exporter import export_a26 from .convert import MODES as C64_MODES, convert_image as _c64_convert from .diskimage import FORMATS from .exporter import export_cart, export_disk from .palette import COLOR_NAMES, get_palette from .ti99.convert import MODES as TI99_MODES, convert_image as _ti99_convert from .ti99.exporter import export_rpk from .intv.convert import MODES as INTV_MODES, convert_image as _intv_convert from .intv.exporter import export_int from .vic20.convert import MODES as VIC20_MODES, convert_image as _vic20_convert from .vic20.exporter import export_a0 from .spectrum.convert import MODES as SPECTRUM_MODES, convert_image as _spectrum_convert from .spectrum.exporter import export_sna from .a5200.convert import MODES as A5200_MODES, convert_image as _a5200_convert from .a5200.exporter import export_a52 from .a7800.convert import MODES as A7800_MODES, convert_image as _a7800_convert from .a7800.exporter import export_a78 from .c128.convert import MODES as C128_MODES, convert_image as _c128_convert from .c128.exporter import export_d64 as _c128_export_d64 from .c16.convert import MODES as C16_MODES, convert_image as _c16_convert from .c16.exporter import export_prg as _c16_export_prg from .plus4.convert import MODES as PLUS4_MODES, convert_image as _plus4_convert from .plus4.exporter import export_prg as _plus4_export_prg from .cpc.convert import MODES as CPC_MODES, convert_image as _cpc_convert from .cpc.exporter import export_sna as _cpc_export_sna from .coco3.convert import MODES as COCO3_MODES, convert_image as _coco3_convert from .coco3.exporter import export_ccc as _coco3_export_ccc from .nes.convert import MODES as NES_MODES, convert_image as _nes_convert from .nes.exporter import export_nes as _nes_export_nes from .iigs.convert import MODES as IIGS_MODES, convert_image as _iigs_convert from .iigs.exporter import export_dsk as _iigs_export_dsk from .pet.convert import convert_image as _pet_convert from .pet.exporter import export_prg as _pet_export_prg from .sms.convert import MODES as SMS_MODES, convert_image as _sms_convert from .sms.exporter import export_sms as _sms_export_sms from .amiga.convert import MODES as AMIGA_MODES, convert_image as _amiga_convert from .amiga.exporter import export_adf as _amiga_export_adf from .ansi.convert import MODES as ANSI_MODES, convert_image as _ansi_convert from .ansi.exporter import export_ans as _ansi_export_ans # PET/CBM families -> (MAME machine, screen columns). 40-col models share the # 80x50 quadrant-block encoder; 80-col models share 160x50. PET_CFG = {"pet2001": ("pet2001n", 40), "pet4032": ("pet4032", 40), "pet8032": ("pet8032", 80), "superpet": ("cbm8032", 80)} # (MAME's `superpet` driver is incomplete; the SuperPET's display is identical to # the CBM 8032 it is built on, so the working cbm8032 machine is used.) PLATFORMS = ["c64", "atari", "apple", "ti99", "coco", "bbc", "coleco", "a2600", "intv", "vic20", "spectrum", "a5200", "a7800", "c128", "c16", "plus4", "cpc", "coco3", "nes", "iigs", "pet2001", "pet4032", "pet8032", "superpet", "sms", "amiga", "ansi"] # Apple modes needing a //e (auxiliary memory) rather than a II+. _APPLE_IIE_MODES = {"dhgr"} def modes(platform: str) -> list[str]: if platform == "c64": return ["auto", *C64_MODES] if platform == "atari": return list(ATARI_MODES) if platform == "ti99": return list(TI99_MODES) if platform == "coco": return list(COCO_MODES) if platform == "bbc": return list(BBC_MODES) if platform == "coleco": return list(COLECO_MODES) if platform == "a2600": return list(A2600_MODES) if platform == "intv": return list(INTV_MODES) if platform == "vic20": return list(VIC20_MODES) if platform == "spectrum": return list(SPECTRUM_MODES) if platform == "a5200": return list(A5200_MODES) if platform == "a7800": return list(A7800_MODES) if platform == "c128": return list(C128_MODES) if platform == "c16": return list(C16_MODES) if platform == "plus4": return list(PLUS4_MODES) if platform == "cpc": return list(CPC_MODES) if platform == "coco3": return list(COCO3_MODES) if platform == "nes": return list(NES_MODES) if platform == "iigs": return list(IIGS_MODES) if platform in PET_CFG: return ["mono"] if platform == "sms": return list(SMS_MODES) if platform == "amiga": return list(AMIGA_MODES) if platform == "ansi": return list(ANSI_MODES) return list(APPLE_MODES) def palettes(platform: str) -> list[str]: if platform == "c64": return ["colodore", "pepto"] if platform == "atari": return ["ntsc"] if platform == "ti99": return ["tms9918"] if platform == "coco": return ["mc6847"] if platform == "bbc": return ["bbc"] if platform == "coleco": return ["tms9918"] if platform == "a2600": return ["tia"] if platform == "intv": return ["stic"] if platform == "vic20": return ["vic"] if platform == "spectrum": return ["spectrum"] if platform == "a5200": return ["ntsc"] if platform == "a7800": return ["ntsc"] if platform == "c128": return ["vdc"] if platform in ("c16", "plus4"): return ["ted"] if platform == "cpc": return ["cpc"] if platform == "coco3": return ["gime"] if platform == "nes": return ["nes"] if platform == "iigs": return ["iigs"] if platform in PET_CFG: return ["pet"] if platform == "sms": return ["sms"] if platform == "amiga": return ["amiga"] if platform == "ansi": return ["vga"] return ["mono"] def disk_formats(platform: str) -> list[str]: if platform == "c64": return [*FORMATS.keys(), "crt"] # disks + 16K cartridge if platform == "atari": return ["atr", "car"] # disk + 16K cartridge if platform == "ti99": return ["rpk"] if platform == "coco": return ["ccc"] # Program Pak cartridge if platform == "bbc": return ["ssd"] # DFS disk if platform == "coleco": return ["col"] # ColecoVision cartridge (runs on Adam too) if platform == "a2600": return ["a26"] # Atari 2600 cartridge if platform == "intv": return ["int"] # Intellivision cartridge (.int/.bin/.rom) if platform == "vic20": return ["a0"] # VIC-20 autostart cartridge ($A000) if platform == "spectrum": return ["sna"] # 48K snapshot (also drops a .scr) if platform == "a5200": return ["a52"] # Atari 5200 cartridge if platform == "a7800": return ["a78"] # Atari 7800 cartridge if platform == "c128": return ["d64"] # C128 autobooting disk (VDC 80-col) if platform in ("c16", "plus4"): return ["prg"] # TED program (quickload / LOAD+RUN) if platform == "cpc": return ["sna"] # Amstrad CPC snapshot if platform == "coco3": return ["ccc"] # CoCo 3 cartridge if platform == "nes": return ["nes"] # iNES cartridge if platform == "iigs": return ["dsk"] # bootable 5.25" SHR disk if platform in PET_CFG: return ["prg"] # PET program (quickload / LOAD+RUN) if platform == "sms": return ["sms"] # Master System cartridge if platform == "amiga": return ["adf"] # Amiga bootable floppy if platform == "ansi": return ["ans"] # ANSI/CP437 BBS art (plain text file) return ["dsk"] def _base_color(platform: str, base_name: str): if base_name == "grayscale": return None idx = COLOR_NAMES.index(base_name) if platform == "atari": return nearest_hue(get_palette("colodore")[idx]) return idx def convert(platform, path, mode, palette, dither, intensive, prep_opt, base_name): base = _base_color(platform, base_name) if platform == "ti99": if mode not in TI99_MODES: mode = "gm2" return _ti99_convert(path, mode=mode, palette_name="tms9918", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "apple": if mode not in APPLE_MODES: mode = "hgr_mono" return _apple_convert(path, mode=mode, dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "atari": if mode not in ATARI_MODES: mode = "gr15" return _atari_convert(path, mode=mode, palette_name="ntsc", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "coco": if mode not in COCO_MODES: mode = "pmode4" return _coco_convert(path, mode=mode, palette_name="mc6847", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "bbc": if mode not in BBC_MODES: mode = "mode2" return _bbc_convert(path, mode=mode, palette_name="bbc", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "coleco": if mode not in COLECO_MODES: mode = "gm2" return _coleco_convert(path, mode=mode, palette_name="tms9918", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "a2600": if mode not in A2600_MODES: mode = "pf" return _a2600_convert(path, mode=mode, palette_name="tia", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "intv": if mode not in INTV_MODES: mode = "stic" return _intv_convert(path, mode=mode, palette_name="stic", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "vic20": if mode not in VIC20_MODES: mode = "multicolor" return _vic20_convert(path, mode=mode, palette_name="vic", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "spectrum": if mode not in SPECTRUM_MODES: mode = "hires" return _spectrum_convert(path, mode=mode, palette_name="spectrum", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "a5200": if mode not in A5200_MODES: mode = "gr15" return _a5200_convert(path, mode=mode, palette_name="ntsc", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "a7800": if mode not in A7800_MODES: mode = "c160" return _a7800_convert(path, mode=mode, palette_name="ntsc", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "c128": c128_mode = mode if mode in C128_MODES else "mono" return _c128_convert(path, mode=c128_mode, palette_name="vdc", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "c16": c16_mode = mode if mode in C16_MODES else "hires" return _c16_convert(path, mode=c16_mode, palette_name="ted", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "plus4": p4_mode = mode if mode in PLUS4_MODES else "hires" return _plus4_convert(path, mode=p4_mode, palette_name="ted", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "cpc": cpc_mode = mode if mode in CPC_MODES else "mode0" return _cpc_convert(path, mode=cpc_mode, palette_name="cpc", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "coco3": c3_mode = mode if mode in COCO3_MODES else "gr16" return _coco3_convert(path, mode=c3_mode, palette_name="gime", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "nes": nes_mode = mode if mode in NES_MODES else "bg" return _nes_convert(path, mode=nes_mode, palette_name="nes", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "iigs": gs_mode = mode if mode in IIGS_MODES else "shr" return _iigs_convert(path, mode=gs_mode, palette_name="iigs", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform in PET_CFG: return _pet_convert(path, cols=PET_CFG[platform][1], dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "sms": sms_mode = mode if mode in SMS_MODES else "bg" return _sms_convert(path, mode=sms_mode, palette_name="sms", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "amiga": am_mode = mode if mode in AMIGA_MODES else "lowres" return _amiga_convert(path, mode=am_mode, palette_name="amiga", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) if platform == "ansi": ansi_mode = mode if mode in ANSI_MODES else "80x25" return _ansi_convert(path, mode=ansi_mode, palette_name="vga", dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) return _c64_convert(path, mode=mode, palette_name=palette, dither_mode=dither, intensive=intensive, prep_opt=prep_opt, base_color=base) def export(platform, conv, path, fmt, source_path, video, display="key", seconds=0, layout="unified"): if platform == "ti99": return export_rpk(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "coco": return export_ccc(conv, path, source_path=source_path, display=display, seconds=seconds) if platform == "bbc": return export_ssd(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "coleco": return export_col(conv, path, source_path=source_path, display=display, seconds=seconds) if platform == "a2600": return export_a26(conv, path, source_path=source_path, display=display, seconds=seconds) if platform == "intv": return export_int(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "vic20": return export_a0(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "spectrum": return export_sna(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "a5200": return export_a52(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "a7800": return export_a78(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "c128": return _c128_export_d64(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "c16": return _c16_export_prg(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "plus4": return _plus4_export_prg(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "cpc": return _cpc_export_sna(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "coco3": return _coco3_export_ccc(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "nes": return _nes_export_nes(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "iigs": return _iigs_export_dsk(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform in PET_CFG: return _pet_export_prg(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "sms": return _sms_export_sms(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "amiga": return _amiga_export_adf(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "ansi": return _ansi_export_ans(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if platform == "apple": return export_dsk(conv, path, source_path=source_path, display=display, seconds=seconds) if platform == "atari": if fmt == "car" or str(path).lower().endswith(".car"): return export_car(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) return export_atr(conv, path, source_path=source_path, display=display, seconds=seconds, video=video) if fmt == "crt" or str(path).lower().endswith(".crt"): return export_cart(conv, path, source_path=source_path, video=video, display=display, seconds=seconds) return export_disk(conv, path, disk_format=fmt, source_path=source_path, video=video, display=display, seconds=seconds, layout=layout) def has_emulator(platform: str) -> bool: if platform == "ansi": return False # a text file has nothing to "run" return mame.have_mame() def run_emulator(platform, path, mode, standard): """Launch the generated image under MAME (every platform uses MAME).""" pal = standard != "ntsc" if platform == "ti99": # cartridge auto-listed on the TI menu as item 2. return mame.launch("ti99_4a", [("cart", path)], autoboot="2", autoboot_delay=5) if platform == "apple": machine = "apple2ee" if mode in _APPLE_IIE_MODES else "apple2p" return mame.launch(machine, [("flop1", path)], throttle=False) if platform == "coco": # Color Computer 2; the Program Pak autostarts at $C000. return mame.launch("coco2b", [("cart", path)]) if platform == "bbc": # BBC Model B; a Lua script types *RUN PIC once DFS has booted. return mame.launch("bbcb", [("flop1", path)], autoboot_script_text=mame.bbc_autorun_lua()) if platform == "coleco": # ColecoVision; the cartridge autostarts (the same .col runs on the Adam). return mame.launch("coleco", [("cart", path)]) if platform == "a2600": # interlace flickers two banks at 60Hz -> needs real-time, not warp. return mame.launch("a2600", [("cart", path)], throttle=(mode == "pf_il")) if platform == "intv": return mame.launch("intv", [("cart", path)]) if platform == "vic20": # The autostart cartridge programs the VIC itself. MAME raises an # "imperfect emulation" warning the user dismisses with a key. return mame.launch("vic20p" if pal else "vic20", [("cart", path)]) if platform == "spectrum": # The .sna snapshot bakes the picture into screen RAM + an idle loop, so # it appears instantly; the ULA holds it with no further CPU work. return mame.launch("spectrum", [("snapshot", path)]) if platform == "a5200": # The cartridge programs ANTIC/GTIA directly; the BIOS jumps straight in. return mame.launch("a5200", [("cart", path)]) if platform == "a7800": # The cartridge programs MARIA directly; the BIOS validates + jumps in. return mame.launch("a7800", [("cart", path)]) if platform == "c128": # Disk holds a viewer that drives the 80-column VDC; RUN"PIC" loads + runs # it. The image appears on the VDC (80-col) screen. -nothrottle for the load. return mame.launch("c128", [("flop", path)], autoboot='run"pic"\\n', autoboot_delay=5, throttle=False) if platform == "c16": # quickload loads the PRG into RAM; a Lua script SYSes the viewer (MAME's # c16 quickload doesn't set BASIC pointers, so RUN won't, but SYS does). return mame.launch("c16", [("quik", path)], autoboot_script_text=mame.c16_autorun_lua(), autoboot_delay=4, throttle=False) if platform == "plus4": # Same TED + BASIC 3.5 as the C16, so the identical .prg + SYS autorun runs # on the Plus/4 (just a different MAME machine, with 64K RAM). return mame.launch("plus4", [("quik", path)], autoboot_script_text=mame.c16_autorun_lua(), autoboot_delay=4, throttle=False) if platform == "cpc": # The .sna snapshot bakes the screen, palette, mode and an idle CPU, so # the picture appears instantly with no loader (like the ZX Spectrum). return mame.launch("cpc6128", [("snapshot", path)]) if platform == "coco3": # The cartridge autostarts a 6809 viewer that programs the GIME; a Lua # script forces the RGB monitor (MAME defaults to the composite palette). return mame.launch("coco3", [("cart", path)], autoboot_script_text=mame.coco3_rgb_lua()) if platform == "nes": # The cartridge resets into a 6502 viewer that programs the PPU; MAME # shows an "imperfect graphics" warning the user dismisses with a key. return mame.launch("nes", [("cart", path)]) if platform == "iigs": # Boots a 5.25" disk (slot 6) into a loader that fills the Super Hi-Res # screen ($E1) and turns SHR on. The 32K load takes ~30s; -nothrottle # makes it appear as fast as possible. return mame.launch("apple2gs", [("flop1", path)], throttle=False) if platform in PET_CFG: # quickload the .prg; a Lua script SYSes the viewer, which pokes the # quadrant-block screen codes into the PET screen RAM at $8000. return mame.launch(PET_CFG[platform][0], [("quik", path)], autoboot_script_text=mame.pet_autorun_lua(), autoboot_delay=3) if platform == "sms": # The cartridge resets into a Z80 viewer that programs the VDP and # uploads tiles/name table/palette, then idles. return mame.launch("sms", [("cart", path)]) if platform == "amiga": # Bootable floppy: a 68000 boot block loads the bitplanes into chip RAM # and points the Copper at them. MAME shows an "imperfect graphics" # warning the user dismisses with a key. return mame.launch("a500", [("flop", path)]) if platform == "atari": # a800xl(p): the XL ROMs only raise MAME's soft "imperfect" notice, not the # bad-dump notice the plain Atari 800 OS-B ROM triggers. The .atr self-boots. machine = "a800xlp" if pal else "a800xl" if str(path).lower().endswith(".car"): return mame.launch(machine, [("cart", path)], throttle=False) return mame.launch(machine, [("flop1", path)], throttle=False) # C64: attach the .d64 to the 1541, then LOAD"*",8,1 + RUN. Run unthrottled so # the (cycle-accurate, slow) 1541 load finishes fast; keep real-time for the # interlace mode whose 50 Hz field-flip must not run faster than the display. machine = "c64p" if pal else "c64" if str(path).lower().endswith(".crt"): # The exported .crt is one 16K CHIP (VICE's format); MAME needs two 8K # CHIPs, so re-pack the ROM into a temp .crt just for the preview. rom = crt.read_rom(path) fd, mpath = tempfile.mkstemp(suffix=".crt", prefix="lenser_mame_") os.close(fd) crt.write_crt(rom, mpath, split=True) return mame.launch(machine, [("cart", mpath)], # cartridge autostarts throttle=(mode == "interlace")) # Disk: a Lua script types LOAD, waits for the stock-1541 load to finish, # then RUN (a single autoboot command loses the RUN during the slow load). return mame.launch(machine, [("flop", path)], autoboot_script_text=mame.c64_autorun_lua(), throttle=(mode == "interlace"))