diff --git a/Machines/AmstradCPC/AmstradCPC.cpp b/Machines/AmstradCPC/AmstradCPC.cpp index 9698aa3b1..c570b341a 100644 --- a/Machines/AmstradCPC/AmstradCPC.cpp +++ b/Machines/AmstradCPC/AmstradCPC.cpp @@ -781,27 +781,37 @@ template class ConcreteMachine: ay_.ay().set_port_handler(&key_state_); // construct the list of necessary ROMs - std::vector required_roms = {"amsdos.rom"}; + const std::string machine_name = "AmstradCPC"; + std::vector required_roms = { + ROMMachine::ROM(machine_name, "the Amstrad Disk Operating System", "amsdos.rom", 16*1024, 0x1fe22ecd) + }; std::string model_number; + uint32_t crcs[2]; switch(target.model) { default: model_number = "6128"; has_128k_ = true; + crcs[0] = 0x0219bb74; + crcs[1] = 0xca6af63d; break; case Analyser::Static::AmstradCPC::Target::Model::CPC464: model_number = "464"; has_128k_ = false; + crcs[0] = 0x815752df; + crcs[1] = 0x7d9a3bac; break; case Analyser::Static::AmstradCPC::Target::Model::CPC664: model_number = "664"; has_128k_ = false; + crcs[0] = 0x3f5a6dc4; + crcs[1] = 0x32fee492; break; } - required_roms.push_back("os" + model_number + ".rom"); - required_roms.push_back("basic" + model_number + ".rom"); + required_roms.emplace_back(machine_name, "the CPC " + model_number + " firmware", "os" + model_number + ".rom", 16*1024, crcs[0]); + required_roms.emplace_back(machine_name, "the CPC " + model_number + " BASIC ROM", "basic" + model_number + ".rom", 16*1024, crcs[1]); // fetch and verify the ROMs - const auto roms = rom_fetcher("AmstradCPC", required_roms); + const auto roms = rom_fetcher(required_roms); for(std::size_t index = 0; index < roms.size(); ++index) { auto &data = roms[index]; diff --git a/Machines/Apple/AppleII/AppleII.cpp b/Machines/Apple/AppleII/AppleII.cpp index f36e93103..8a857c6ef 100644 --- a/Machines/Apple/AppleII/AppleII.cpp +++ b/Machines/Apple/AppleII/AppleII.cpp @@ -347,30 +347,39 @@ template class ConcreteMachine: // Pick the required ROMs. using Target = Analyser::Static::AppleII::Target; - std::vector rom_names; + const std::string machine_name = "AppleII"; + std::vector rom_descriptions; size_t rom_size = 12*1024; switch(target.model) { default: - rom_names.push_back("apple2-character.rom"); - rom_names.push_back("apple2o.rom"); + rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6); + rom_descriptions.emplace_back(machine_name, "the original Apple II ROM", "apple2o.rom", 12*1024, 0xba210588); break; case Target::Model::IIplus: - rom_names.push_back("apple2-character.rom"); - rom_names.push_back("apple2.rom"); + rom_descriptions.emplace_back(machine_name, "the basic Apple II character ROM", "apple2-character.rom", 2*1024, 0x64f415c6); + rom_descriptions.emplace_back(machine_name, "the Apple II+ ROM", "apple2.rom", 12*1024, 0xf66f9c26); break; case Target::Model::IIe: rom_size += 3840; - rom_names.push_back("apple2eu-character.rom"); - rom_names.push_back("apple2eu.rom"); + rom_descriptions.emplace_back(machine_name, "the Apple IIe character ROM", "apple2eu-character.rom", 4*1024, 0x816a86f1); + rom_descriptions.emplace_back(machine_name, "the Apple IIe ROM", "apple2eu.rom", 32*1024, 0xe12be18d); break; case Target::Model::EnhancedIIe: rom_size += 3840; - rom_names.push_back("apple2e-character.rom"); - rom_names.push_back("apple2e.rom"); + rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe character ROM", "apple2e-character.rom", 4*1024, 0x2651014d); + rom_descriptions.emplace_back(machine_name, "the Enhanced Apple IIe ROM", "apple2e.rom", 32*1024, 0x65989942); break; } - const auto roms = rom_fetcher("AppleII", rom_names); + const auto roms = rom_fetcher(rom_descriptions); + // Try to install a Disk II card now, before checking the ROM list, + // to make sure that Disk II dependencies have been communicated. + if(target.disk_controller != Target::DiskController::None) { + // Apple recommended slot 6 for the (first) Disk II. + install_card(6, new Apple::II::DiskIICard(rom_fetcher, target.disk_controller == Target::DiskController::SixteenSector)); + } + + // Now, check and move the ROMs. if(!roms[0] || !roms[1]) { throw ROMMachine::Error::MissingROMs; } @@ -382,11 +391,6 @@ template class ConcreteMachine: video_.set_character_rom(*roms[0]); - if(target.disk_controller != Target::DiskController::None) { - // Apple recommended slot 6 for the (first) Disk II. - install_card(6, new Apple::II::DiskIICard(rom_fetcher, target.disk_controller == Target::DiskController::SixteenSector)); - } - // Set up the default memory blocks. On a II or II+ these values will never change. // On a IIe they'll be affected by selection of auxiliary RAM. set_main_paging(); diff --git a/Machines/Apple/AppleII/DiskIICard.cpp b/Machines/Apple/AppleII/DiskIICard.cpp index 2235cff6a..11349e1fc 100644 --- a/Machines/Apple/AppleII/DiskIICard.cpp +++ b/Machines/Apple/AppleII/DiskIICard.cpp @@ -11,12 +11,22 @@ using namespace Apple::II; DiskIICard::DiskIICard(const ROMMachine::ROMFetcher &rom_fetcher, bool is_16_sector) : diskii_(2045454) { - const auto roms = rom_fetcher( - "DiskII", - { - is_16_sector ? "boot-16.rom" : "boot-13.rom", - is_16_sector ? "state-machine-16.rom" : "state-machine-13.rom" + std::vector>> roms; + if(is_16_sector) { + roms = rom_fetcher({ + {"DiskII", "the Disk II 16-sector boot ROM", "boot-16.rom", 256, 0xce7144f6}, + {"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 } } }); + } else { + roms = rom_fetcher({ + {"DiskII", "the Disk II 13-sector boot ROM", "boot-13.rom", 256, 0xd34eb2ff}, + {"DiskII", "the Disk II 13-sector state machine ROM", "state-machine-13.rom", 256, 0x62e22620 } + }); + } + if(!roms[0] || !roms[1]) { + throw ROMMachine::Error::MissingROMs; + } + boot_ = std::move(*roms[0]); diskii_.set_state_machine(*roms[1]); set_select_constraints(None); diff --git a/Machines/Apple/Macintosh/Macintosh.cpp b/Machines/Apple/Macintosh/Macintosh.cpp index 46730e890..32f979528 100644 --- a/Machines/Apple/Macintosh/Macintosh.cpp +++ b/Machines/Apple/Macintosh/Macintosh.cpp @@ -70,33 +70,35 @@ template class ConcreteMachin // Select a ROM name and determine the proper ROM and RAM sizes // based on the machine model. using Model = Analyser::Static::Macintosh::Target::Model; - std::string rom_name; + const std::string machine_name = "Macintosh"; uint32_t ram_size, rom_size; + std::vector rom_descriptions; switch(model) { default: case Model::Mac128k: ram_size = 128*1024; rom_size = 64*1024; - rom_name = "mac128k.rom"; + rom_descriptions.emplace_back(machine_name, "the Macintosh 128k ROM", "mac128k.rom", 64*1024, 0x6d0c8a28); break; case Model::Mac512k: ram_size = 512*1024; rom_size = 64*1024; - rom_name = "mac512k.rom"; + rom_descriptions.emplace_back(machine_name, "the Macintosh 512k ROM", "mac512k.rom", 64*1024, 0xcf759e0d); break; case Model::Mac512ke: - case Model::MacPlus: + case Model::MacPlus: { ram_size = 512*1024; rom_size = 128*1024; - rom_name = "macplus.rom"; - break; + const std::initializer_list crc32s = { 0x4fa5b399, 0x7cacd18f, 0xb2102e8e }; + rom_descriptions.emplace_back(machine_name, "the Macintosh Plus ROM", "macplus.rom", 128*1024, crc32s); + } break; } ram_mask_ = (ram_size >> 1) - 1; rom_mask_ = (rom_size >> 1) - 1; video_.set_ram_mask(ram_mask_); // Grab a copy of the ROM and convert it into big-endian data. - const auto roms = rom_fetcher("Macintosh", { rom_name }); + const auto roms = rom_fetcher(rom_descriptions); if(!roms[0]) { throw ROMMachine::Error::MissingROMs; } diff --git a/Machines/ColecoVision/ColecoVision.cpp b/Machines/ColecoVision/ColecoVision.cpp index 722a33c9b..d699a4016 100644 --- a/Machines/ColecoVision/ColecoVision.cpp +++ b/Machines/ColecoVision/ColecoVision.cpp @@ -131,8 +131,7 @@ class ConcreteMachine: joysticks_.emplace_back(new Joystick); const auto roms = rom_fetcher( - "ColecoVision", - { "coleco.rom" }); + { {"ColecoVision", "the ColecoVision BIOS", "coleco.rom", 8*1024, 0x3aa93ef3} }); if(!roms[0]) { throw ROMMachine::Error::MissingROMs; diff --git a/Machines/Commodore/1540/Implementation/C1540.cpp b/Machines/Commodore/1540/Implementation/C1540.cpp index 8fa73ee31..515dee334 100644 --- a/Machines/Commodore/1540/Implementation/C1540.cpp +++ b/Machines/Commodore/1540/Implementation/C1540.cpp @@ -39,13 +39,20 @@ MachineBase::MachineBase(Personality personality, const ROMMachine::ROMFetcher & // attach the only drive there is set_drive(drive_); - std::string rom_name; + std::string device_name; + uint32_t crc = 0; switch(personality) { - case Personality::C1540: rom_name = "1540.bin"; break; - case Personality::C1541: rom_name = "1541.bin"; break; + case Personality::C1540: + device_name = "1540"; + crc = 0x718d42b1; + break; + case Personality::C1541: + device_name = "1541"; + crc = 0xfb760019; + break; } - auto roms = rom_fetcher("Commodore1540", {rom_name}); + auto roms = rom_fetcher({ {"Commodore1540", "the " + device_name + " ROM", device_name + ".bin", 16*1024, crc} }); if(!roms[0]) { throw ROMMachine::Error::MissingROMs; } diff --git a/Machines/Commodore/Vic-20/Vic20.cpp b/Machines/Commodore/Vic-20/Vic20.cpp index 1d82bc909..2ac58a2e5 100644 --- a/Machines/Commodore/Vic-20/Vic20.cpp +++ b/Machines/Commodore/Vic-20/Vic20.cpp @@ -323,31 +323,34 @@ class ConcreteMachine: // install a joystick joysticks_.emplace_back(new Joystick(*user_port_via_port_handler_, *keyboard_via_port_handler_)); - std::vector rom_names = { "basic.bin" }; + const std::string machine_name = "Vic20"; + std::vector rom_names = { + {machine_name, "the VIC-20 BASIC ROM", "basic.bin", 8*1024, 0xdb4c43c1} + }; switch(target.region) { default: - rom_names.push_back("characters-english.bin"); - rom_names.push_back("kernel-pal.bin"); + rom_names.emplace_back(machine_name, "the English-language VIC-20 character ROM", "characters-english.bin", 4*1024, 0x83e032a6); + rom_names.emplace_back(machine_name, "the English-language PAL VIC-20 kernel ROM", "kernel-pal.bin", 8*1024, 0x4be07cb4); break; case Analyser::Static::Commodore::Target::Region::American: - rom_names.push_back("characters-english.bin"); - rom_names.push_back("kernel-ntsc.bin"); + rom_names.emplace_back(machine_name, "the English-language VIC-20 character ROM", "characters-english.bin", 4*1024, 0x83e032a6); + rom_names.emplace_back(machine_name, "the English-language NTSC VIC-20 kernel ROM", "kernel-ntsc.bin", 8*1024, 0xe5e7c174); break; case Analyser::Static::Commodore::Target::Region::Danish: - rom_names.push_back("characters-danish.bin"); - rom_names.push_back("kernel-danish.bin"); + rom_names.emplace_back(machine_name, "the Danish VIC-20 character ROM", "characters-danish.bin", 4*1024, 0x7fc11454); + rom_names.emplace_back(machine_name, "the Danish VIC-20 kernel ROM", "kernel-danish.bin", 8*1024, 0x02adaf16); break; case Analyser::Static::Commodore::Target::Region::Japanese: - rom_names.push_back("characters-japanese.bin"); - rom_names.push_back("kernel-japanese.bin"); + rom_names.emplace_back(machine_name, "the Japanese VIC-20 character ROM", "characters-japanese.bin", 4*1024, 0xfcfd8a4b); + rom_names.emplace_back(machine_name, "the Japanese VIC-20 kernel ROM", "kernel-japanese.bin", 8*1024, 0x336900d7); break; case Analyser::Static::Commodore::Target::Region::Swedish: - rom_names.push_back("characters-swedish.bin"); - rom_names.push_back("kernel-japanese.bin"); + rom_names.emplace_back(machine_name, "the Swedish VIC-20 character ROM", "characters-swedish.bin", 4*1024, 0xd808551d); + rom_names.emplace_back(machine_name, "the Swedish VIC-20 kernel ROM", "kernel-swedish.bin", 8*1024, 0xb2a60662); break; } - const auto roms = rom_fetcher("Vic20", rom_names); + const auto roms = rom_fetcher(rom_names); for(const auto &rom: roms) { if(!rom) { diff --git a/Machines/Electron/Electron.cpp b/Machines/Electron/Electron.cpp index 383793397..f0a31815e 100644 --- a/Machines/Electron/Electron.cpp +++ b/Machines/Electron/Electron.cpp @@ -64,16 +64,20 @@ class ConcreteMachine: speaker_.set_input_rate(2000000 / SoundGenerator::clock_rate_divider); speaker_.set_high_frequency_cutoff(7000); - std::vector rom_names = {"basic.rom", "os.rom"}; + const std::string machine_name = "Electron"; + std::vector required_roms = { + {machine_name, "the Acorn BASIC II ROM", "basic.rom", 16*1024, 0x79434781}, + {machine_name, "the Electron MOS ROM", "os.rom", 16*1024, 0xbf63fb1f} + }; if(target.has_adfs) { - rom_names.push_back("ADFS-E00_1.rom"); - rom_names.push_back("ADFS-E00_2.rom"); + required_roms.emplace_back(machine_name, "the E00 ADFS ROM, first slot", "ADFS-E00_1.rom", 16*1024, 0x51523993); + required_roms.emplace_back(machine_name, "the E00 ADFS ROM, second slot", "ADFS-E00_2.rom", 16*1024, 0x8d17de0e); } - const size_t dfs_rom_position = rom_names.size(); + const size_t dfs_rom_position = required_roms.size(); if(target.has_dfs) { - rom_names.push_back("DFS-1770-2.20.rom"); + required_roms.emplace_back(machine_name, "the 1770 DFS ROM", "DFS-1770-2.20.rom", 16*1024, 0xf3dc9bc5); } - const auto roms = rom_fetcher("Electron", rom_names); + const auto roms = rom_fetcher(required_roms); for(const auto &rom: roms) { if(!rom) { diff --git a/Machines/MSX/MSX.cpp b/Machines/MSX/MSX.cpp index b2e57dcdb..a4b563d01 100644 --- a/Machines/MSX/MSX.cpp +++ b/Machines/MSX/MSX.cpp @@ -173,16 +173,20 @@ class ConcreteMachine: mixer_.set_relative_volumes({0.5f, 0.1f, 0.4f}); // Install the proper TV standard and select an ideal BIOS name. - std::vector rom_names = {"msx.rom"}; + const std::string machine_name = "MSX"; + std::vector required_roms = { + {machine_name, "any MSX BIOS", "msx.rom", 32*1024, 0x94ee12f3} + }; bool is_ntsc = true; uint8_t character_generator = 1; /* 0 = Japan, 1 = USA, etc, 2 = USSR */ uint8_t date_format = 1; /* 0 = Y/M/D, 1 = M/D/Y, 2 = D/M/Y */ uint8_t keyboard = 1; /* 0 = Japan, 1 = USA, 2 = France, 3 = UK, 4 = Germany, 5 = USSR, 6 = Spain */ + // TODO: CRCs below are incomplete, at best. switch(target.region) { case Target::Region::Japan: - rom_names.push_back("msx-japanese.rom"); + required_roms.emplace_back(machine_name, "a Japanese MSX BIOS", "msx-japanese.rom", 32*1024, 0xee229390); vdp_.set_tv_standard(TI::TMS::TVStandard::NTSC); is_ntsc = true; @@ -190,7 +194,7 @@ class ConcreteMachine: date_format = 0; break; case Target::Region::USA: - rom_names.push_back("msx-american.rom"); + required_roms.emplace_back(machine_name, "an American MSX BIOS", "msx-american.rom", 32*1024, 0); vdp_.set_tv_standard(TI::TMS::TVStandard::NTSC); is_ntsc = true; @@ -198,7 +202,7 @@ class ConcreteMachine: date_format = 1; break; case Target::Region::Europe: - rom_names.push_back("msx-european.rom"); + required_roms.emplace_back(machine_name, "a European MSX BIOS", "msx-european.rom", 32*1024, 0); vdp_.set_tv_standard(TI::TMS::TVStandard::PAL); is_ntsc = false; @@ -211,10 +215,10 @@ class ConcreteMachine: // but failing that fall back on patching the main one. size_t disk_index = 0; if(target.has_disk_drive) { - disk_index = rom_names.size(); - rom_names.push_back("disk.rom"); + disk_index = required_roms.size(); + required_roms.emplace_back(machine_name, "the MSX-DOS ROM", "disk.rom", 16*1024, 0x721f61df); } - const auto roms = rom_fetcher("MSX", rom_names); + const auto roms = rom_fetcher(required_roms); if((!roms[0] && !roms[1]) || (target.has_disk_drive && !roms[2])) { throw ROMMachine::Error::MissingROMs; diff --git a/Machines/MasterSystem/MasterSystem.cpp b/Machines/MasterSystem/MasterSystem.cpp index 7400c6dae..0f5f89eda 100644 --- a/Machines/MasterSystem/MasterSystem.cpp +++ b/Machines/MasterSystem/MasterSystem.cpp @@ -132,7 +132,12 @@ class ConcreteMachine: // Load the BIOS if relevant. if(has_bios()) { - const auto roms = rom_fetcher("MasterSystem", {"bios.sms"}); + // TODO: there's probably a million other versions of the Master System BIOS; try to build a + // CRC32 catalogue of those. So far: + // + // 0072ed54 = US/European BIOS 1.3 + // 48d44a13 = Japanese BIOS 2.1 + const auto roms = rom_fetcher({ {"MasterSystem", "the Master System BIOS", "bios.sms", 8*1024, { 0x0072ed54, 0x48d44a13 } } }); if(!roms[0]) { // No BIOS found; attempt to boot as though it has already disabled itself. memory_control_ |= 0x08; diff --git a/Machines/Oric/Oric.cpp b/Machines/Oric/Oric.cpp index 58ebdd907..b90ce9fd8 100644 --- a/Machines/Oric/Oric.cpp +++ b/Machines/Oric/Oric.cpp @@ -228,19 +228,34 @@ template class Co diskii_.set_clocking_hint_observer(this); } - std::vector rom_names = {"colour.rom"}; + const std::string machine_name = "Oric"; + std::vector rom_names = { {machine_name, "the Oric colour ROM", "colour.rom", 128, 0xd50fca65} }; switch(target.rom) { - case Analyser::Static::Oric::Target::ROM::BASIC10: rom_names.push_back("basic10.rom"); break; - case Analyser::Static::Oric::Target::ROM::BASIC11: rom_names.push_back("basic11.rom"); break; - case Analyser::Static::Oric::Target::ROM::Pravetz: rom_names.push_back("pravetz.rom"); break; + case Analyser::Static::Oric::Target::ROM::BASIC10: + rom_names.emplace_back(machine_name, "Oric BASIC 1.0", "basic10.rom", 16*1024, 0xf18710b4); + break; + case Analyser::Static::Oric::Target::ROM::BASIC11: + rom_names.emplace_back(machine_name, "Oric BASIC 1.1", "basic11.rom", 16*1024, 0xc3a92bef); + break; + case Analyser::Static::Oric::Target::ROM::Pravetz: + rom_names.emplace_back(machine_name, "Pravetz BASIC", "pravetz.rom", 16*1024, 0x58079502); + break; } + size_t diskii_state_machine_index = 0; switch(disk_interface) { default: break; - case Analyser::Static::Oric::Target::DiskInterface::Microdisc: rom_names.push_back("microdisc.rom"); break; - case Analyser::Static::Oric::Target::DiskInterface::Pravetz: rom_names.push_back("8dos.rom"); break; + case Analyser::Static::Oric::Target::DiskInterface::Microdisc: + rom_names.emplace_back(machine_name, "the ORIC Microdisc ROM", "microdisc.rom", 8*1024, 0xa9664a9c); + break; + case Analyser::Static::Oric::Target::DiskInterface::Pravetz: + rom_names.emplace_back(machine_name, "the 8DOS boot ROM", "8dos.rom", 512, 0x49a74c06); + // These ROM details are coupled with those in the DiskIICard. + diskii_state_machine_index = rom_names.size(); + rom_names.push_back({"DiskII", "the Disk II 16-sector state machine ROM", "state-machine-16.rom", 256, { 0x9796a238, 0xb72a2c70 }}); + break; } - const auto roms = rom_fetcher("Oric", rom_names); + const auto roms = rom_fetcher(rom_names); for(std::size_t index = 0; index < roms.size(); ++index) { if(!roms[index]) { @@ -261,11 +276,7 @@ template class Co pravetz_rom_ = std::move(*roms[2]); pravetz_rom_.resize(512); - auto state_machine_rom = rom_fetcher("DiskII", {"state-machine-16.rom"}); - if(!state_machine_rom[0]) { - throw ROMMachine::Error::MissingROMs; - } - diskii_.set_state_machine(*state_machine_rom[0]); + diskii_.set_state_machine(*roms[diskii_state_machine_index]); } break; } diff --git a/Machines/ROMMachine.hpp b/Machines/ROMMachine.hpp index a42942563..b1b9cb818 100644 --- a/Machines/ROMMachine.hpp +++ b/Machines/ROMMachine.hpp @@ -16,15 +16,41 @@ namespace ROMMachine { +/*! + Describes a ROM image; this term is used in this emulator strictly in the sense of firmware — + system software that is an inherent part of a machine. +*/ +struct ROM { + /// The machine with which this ROM is associated, in a form that is safe for using as + /// part of a file name. + std::string machine_name; + /// A descriptive name for this ROM, suitable for use in a bullet-point list, a bracket + /// clause, etc, e.g. "the Electron MOS 1.0". + std::string descriptive_name; + /// An idiomatic file name for this ROM, e.g. "os10.rom". + std::string file_name; + /// The expected size of this ROM in bytes, e.g. 32768. + size_t size = 0; + /// CRC32s for all known acceptable copies of this ROM; intended to allow a host platform + /// to test user-provided ROMs of unknown provenance. **Not** intended to be used + /// to exclude ROMs where the user's intent is otherwise clear. + std::vector crc32s; + + ROM(std::string machine_name, std::string descriptive_name, std::string file_name, size_t size, uint32_t crc32) : + machine_name(machine_name), descriptive_name(descriptive_name), file_name(file_name), size(size), crc32s({crc32}) {} + ROM(std::string machine_name, std::string descriptive_name, std::string file_name, size_t size, std::initializer_list crc32s) : + machine_name(machine_name), descriptive_name(descriptive_name), file_name(file_name), size(size), crc32s(crc32s) {} +}; + /*! Defines the signature for a function that must be supplied by the host environment in order to give machines a route for fetching any system ROMs they might need. - The caller will supply the idiomatic name of the machine plus a vector of the names of ROM files that it expects - to be present. The recevier should return a vector of unique_ptrs that either contain the contents of the - ROM from @c names that corresponds by index, or else are the nullptr + The caller will supply a vector of the names of ROM files that it would like to inspect. The recevier should + return a vector of unique_ptrs that either contain the contents of the ROM from @c names that corresponds by + index, or else are @c nullptr. */ -typedef std::function>>(const std::string &machine, const std::vector &names)> ROMFetcher; +typedef std::function>>(const std::vector &roms)> ROMFetcher; enum class Error { MissingROMs diff --git a/Machines/ZX8081/ZX8081.cpp b/Machines/ZX8081/ZX8081.cpp index ab9818006..024601e41 100644 --- a/Machines/ZX8081/ZX8081.cpp +++ b/Machines/ZX8081/ZX8081.cpp @@ -76,7 +76,11 @@ template class ConcreteMachine: clear_all_keys(); const bool use_zx81_rom = target.is_ZX81 || target.ZX80_uses_ZX81_ROM; - const auto roms = rom_fetcher("ZX8081", { use_zx81_rom ? "zx81.rom" : "zx80.rom" }); + const auto roms = + use_zx81_rom ? + rom_fetcher({ {"ZX8081", "the ZX81 BASIC ROM", "zx81.rom", 8 * 1024, 0x4b1dd6eb} }) : + rom_fetcher({ {"ZX8081", "the ZX80 BASIC ROM", "zx80.rom", 4 * 1024, 0x4c7fc597} }); + if(!roms[0]) throw ROMMachine::Error::MissingROMs; rom_ = std::move(*roms[0]); diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj index cb265c8f6..0c8c3b434 100644 --- a/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj +++ b/OSBindings/Mac/Clock Signal.xcodeproj/project.pbxproj @@ -145,7 +145,6 @@ 4B1EDB451E39A0AC009D6819 /* chip.png in Resources */ = {isa = PBXBuildFile; fileRef = 4B1EDB431E39A0AC009D6819 /* chip.png */; }; 4B2A332D1DB86821002876E3 /* OricOptions.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B2A332B1DB86821002876E3 /* OricOptions.xib */; }; 4B2A539F1D117D36003C6002 /* CSAudioQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A53911D117D36003C6002 /* CSAudioQueue.m */; }; - 4B2A53A01D117D36003C6002 /* CSMachine.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A53961D117D36003C6002 /* CSMachine.mm */; }; 4B2B3A4B1F9B8FA70062DABF /* Typer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A471F9B8FA70062DABF /* Typer.cpp */; }; 4B2B3A4C1F9B8FA70062DABF /* MemoryFuzzer.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2B3A481F9B8FA70062DABF /* MemoryFuzzer.cpp */; }; 4B2BFC5F1D613E0200BA3AA9 /* TapePRG.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4B2BFC5D1D613E0200BA3AA9 /* TapePRG.cpp */; }; @@ -633,7 +632,6 @@ 4BC76E691C98E31700E6EF73 /* FIRFilter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC76E671C98E31700E6EF73 /* FIRFilter.cpp */; }; 4BC76E6B1C98F43700E6EF73 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BC76E6A1C98F43700E6EF73 /* Accelerate.framework */; }; 4BC91B831D1F160E00884B76 /* CommodoreTAP.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC91B811D1F160E00884B76 /* CommodoreTAP.cpp */; }; - 4BC9DF451D044FCA00F44158 /* ROMImages in Resources */ = {isa = PBXBuildFile; fileRef = 4BC9DF441D044FCA00F44158 /* ROMImages */; }; 4BC9DF4F1D04691600F44158 /* 6560.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BC9DF4D1D04691600F44158 /* 6560.cpp */; }; 4BC9E1EE1D23449A003FCEE4 /* 6502InterruptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC9E1ED1D23449A003FCEE4 /* 6502InterruptTests.swift */; }; 4BCA6CC81D9DD9F000C2D7B2 /* CommodoreROM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BCA6CC61D9DD9F000C2D7B2 /* CommodoreROM.cpp */; }; @@ -666,6 +664,11 @@ 4BD67DCC209BE4D700AB2146 /* StaticAnalyser.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD67DCA209BE4D600AB2146 /* StaticAnalyser.cpp */; }; 4BD67DD0209BF27B00AB2146 /* Encoder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD67DCE209BF27B00AB2146 /* Encoder.cpp */; }; 4BD67DD1209BF27B00AB2146 /* Encoder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 4BD67DCE209BF27B00AB2146 /* Encoder.cpp */; }; + 4BDA00DA22E60EE300AC3CD0 /* ROMRequester.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BDA00D922E60EE300AC3CD0 /* ROMRequester.xib */; }; + 4BDA00DD22E622C200AC3CD0 /* ROMImages in Resources */ = {isa = PBXBuildFile; fileRef = 4BC9DF441D044FCA00F44158 /* ROMImages */; }; + 4BDA00E022E644AF00AC3CD0 /* CSROMReceiverView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BDA00DF22E644AF00AC3CD0 /* CSROMReceiverView.m */; }; + 4BDA00E422E663B900AC3CD0 /* NSData+CRC32.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BDA00E222E663B900AC3CD0 /* NSData+CRC32.m */; }; + 4BDA00E622E699B000AC3CD0 /* CSMachine.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A53961D117D36003C6002 /* CSMachine.mm */; }; 4BDB61EB2032806E0048AF91 /* CSAtari2600.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4B2A539A1D117D36003C6002 /* CSAtari2600.mm */; }; 4BDB61EC203285AE0048AF91 /* Atari2600OptionsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8FE21F1DA19D7C0090D3CE /* Atari2600OptionsPanel.swift */; }; 4BDDBA991EF3451200347E61 /* Z80MachineCycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDDBA981EF3451200347E61 /* Z80MachineCycleTests.swift */; }; @@ -1473,6 +1476,11 @@ 4BD67DCE209BF27B00AB2146 /* Encoder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = Encoder.cpp; sourceTree = ""; }; 4BD67DCF209BF27B00AB2146 /* Encoder.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = Encoder.hpp; sourceTree = ""; }; 4BD9137D1F311BC5009BCF85 /* i8255.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = i8255.hpp; path = 8255/i8255.hpp; sourceTree = ""; }; + 4BDA00D922E60EE300AC3CD0 /* ROMRequester.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ROMRequester.xib; sourceTree = ""; }; + 4BDA00DE22E644AF00AC3CD0 /* CSROMReceiverView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CSROMReceiverView.h; sourceTree = ""; }; + 4BDA00DF22E644AF00AC3CD0 /* CSROMReceiverView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CSROMReceiverView.m; sourceTree = ""; }; + 4BDA00E222E663B900AC3CD0 /* NSData+CRC32.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+CRC32.m"; sourceTree = ""; }; + 4BDA00E322E663B900AC3CD0 /* NSData+CRC32.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+CRC32.h"; sourceTree = ""; }; 4BDB3D8522833321002D3CEE /* Keyboard.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Keyboard.hpp; sourceTree = ""; }; 4BDCC5F81FB27A5E001220C5 /* ROMMachine.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = ROMMachine.hpp; sourceTree = ""; }; 4BDDBA981EF3451200347E61 /* Z80MachineCycleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Z80MachineCycleTests.swift; sourceTree = ""; }; @@ -1736,13 +1744,15 @@ 4BBC34241D2208B100FFC9DF /* CSFastLoading.h */, 4B2A53951D117D36003C6002 /* CSMachine.h */, 4B643F3C1D77AE5C00D431D6 /* CSMachine+Target.h */, - 4B98A05C1FFAD3F600ADF63B /* CSROMFetcher.hpp */, 4B2A53971D117D36003C6002 /* KeyCodes.h */, 4B8FE2251DA1DE2D0090D3CE /* NSBundle+DataResource.h */, + 4BDA00E322E663B900AC3CD0 /* NSData+CRC32.h */, 4BA61EAE1D91515900B3C876 /* NSData+StdVector.h */, - 4B98A05D1FFAD3F600ADF63B /* CSROMFetcher.mm */, + 4B98A05C1FFAD3F600ADF63B /* CSROMFetcher.hpp */, 4B8FE2261DA1DE2D0090D3CE /* NSBundle+DataResource.m */, + 4BDA00E222E663B900AC3CD0 /* NSData+CRC32.m */, 4B2A53961D117D36003C6002 /* CSMachine.mm */, + 4B98A05D1FFAD3F600ADF63B /* CSROMFetcher.mm */, 4BA61EAF1D91515900B3C876 /* NSData+StdVector.mm */, 4B643F3B1D77AD6D00D431D6 /* StaticAnalyser */, 4B2A53981D117D36003C6002 /* Wrappers */, @@ -2944,6 +2954,7 @@ 4BE5F85A1C3E1C2500C43F01 /* Resources */, 4BD5F1961D1352A000631CD1 /* Updater */, 4B55CE5A1C3B7D6F0093A61B /* Views */, + 4BDA00DB22E60EE900AC3CD0 /* ROMRequester */, ); path = "Clock Signal"; sourceTree = ""; @@ -3297,6 +3308,16 @@ name = 8255; sourceTree = ""; }; + 4BDA00DB22E60EE900AC3CD0 /* ROMRequester */ = { + isa = PBXGroup; + children = ( + 4BDA00D922E60EE300AC3CD0 /* ROMRequester.xib */, + 4BDA00DE22E644AF00AC3CD0 /* CSROMReceiverView.h */, + 4BDA00DF22E644AF00AC3CD0 /* CSROMReceiverView.m */, + ); + path = ROMRequester; + sourceTree = ""; + }; 4BE5F85A1C3E1C2500C43F01 /* Resources */ = { isa = PBXGroup; children = ( @@ -3544,6 +3565,7 @@ 4BB73EA91B587A5100552FC2 /* Assets.xcassets in Resources */, 4B79E4451E3AF38600141F11 /* floppy35.png in Resources */, 4B55DD8420DF06680043F2E5 /* MachinePicker.xib in Resources */, + 4BDA00DA22E60EE300AC3CD0 /* ROMRequester.xib in Resources */, 4BC5FC3020CDDDEF00410AA0 /* AppleIIOptions.xib in Resources */, 4B1EDB451E39A0AC009D6819 /* chip.png in Resources */, 4B2A332D1DB86821002876E3 /* OricOptions.xib in Resources */, @@ -3555,7 +3577,6 @@ 4BB73EAC1B587A5100552FC2 /* MainMenu.xib in Resources */, 4B8FE21D1DA19D5F0090D3CE /* QuickLoadCompositeOptions.xib in Resources */, 4B79E4461E3AF38600141F11 /* floppy525.png in Resources */, - 4BC9DF451D044FCA00F44158 /* ROMImages in Resources */, 4BEEE6BD20DC72EB003723BF /* CompositeOptions.xib in Resources */, 4B1497981EE4B97F00CE2596 /* ZX8081Options.xib in Resources */, ); @@ -3610,6 +3631,7 @@ 4BB299B91B587D8400A49093 /* rorz in Resources */, 4BB299F61B587D8400A49093 /* tsxn in Resources */, 4BB298F11B587D8400A49093 /* start in Resources */, + 4BDA00DD22E622C200AC3CD0 /* ROMImages in Resources */, 4BB299061B587D8400A49093 /* asla in Resources */, 4BB299901B587D8400A49093 /* lsrn in Resources */, 4BB298FE1B587D8400A49093 /* anday in Resources */, @@ -4067,6 +4089,7 @@ 4B595FAD2086DFBA0083CAA8 /* AudioToggle.cpp in Sources */, 4B1497921EE4B5A800CE2596 /* ZX8081.cpp in Sources */, 4B643F3F1D77B88000D431D6 /* DocumentController.swift in Sources */, + 4BDA00E422E663B900AC3CD0 /* NSData+CRC32.m in Sources */, 4BB4BFB022A42F290069048D /* MacintoshIMG.cpp in Sources */, 4B05401E219D1618001BF69C /* ScanTarget.cpp in Sources */, 4B4518861F75E91A00926311 /* MFMDiskController.cpp in Sources */, @@ -4099,6 +4122,7 @@ 4B55CE5F1C3B7D960093A61B /* MachineDocument.swift in Sources */, 4B2B3A4C1F9B8FA70062DABF /* MemoryFuzzer.cpp in Sources */, 4B7913CC1DFCD80E00175A82 /* Video.cpp in Sources */, + 4BDA00E622E699B000AC3CD0 /* CSMachine.mm in Sources */, 4B4518831F75E91A00926311 /* PCMTrack.cpp in Sources */, 4B45189F1F75FD1C00926311 /* AcornADF.cpp in Sources */, 4B7136911F789C93008B8ED9 /* SegmentParser.cpp in Sources */, @@ -4120,6 +4144,7 @@ 4B4518851F75E91A00926311 /* DiskController.cpp in Sources */, 4B8334841F5DA0360097E338 /* Z80Storage.cpp in Sources */, 4BA61EB01D91515900B3C876 /* NSData+StdVector.mm in Sources */, + 4BDA00E022E644AF00AC3CD0 /* CSROMReceiverView.m in Sources */, 4BD191F42191180E0042E144 /* ScanTarget.cpp in Sources */, 4BCD634922D6756400F567F1 /* MacintoshDoubleDensityDrive.cpp in Sources */, 4B0F94FE208C1A1600FE41D9 /* NIB.cpp in Sources */, @@ -4193,7 +4218,6 @@ 4BF437EE209D0F7E008CBD6B /* SegmentParser.cpp in Sources */, 4B8334861F5DA3780097E338 /* 6502Storage.cpp in Sources */, 4B8FE2271DA1DE2D0090D3CE /* NSBundle+DataResource.m in Sources */, - 4B2A53A01D117D36003C6002 /* CSMachine.mm in Sources */, 4BC91B831D1F160E00884B76 /* CommodoreTAP.cpp in Sources */, 4B55DD8320DF06680043F2E5 /* MachinePicker.swift in Sources */, 4B2A539F1D117D36003C6002 /* CSAudioQueue.m in Sources */, diff --git a/OSBindings/Mac/Clock Signal/Base.lproj/MachineDocument.xib b/OSBindings/Mac/Clock Signal/Base.lproj/MachineDocument.xib index b5225926c..0ed43edf8 100644 --- a/OSBindings/Mac/Clock Signal/Base.lproj/MachineDocument.xib +++ b/OSBindings/Mac/Clock Signal/Base.lproj/MachineDocument.xib @@ -1,8 +1,8 @@ - + - + @@ -14,7 +14,7 @@ - + @@ -25,7 +25,7 @@ - @@ -36,6 +36,11 @@ + + + + + diff --git a/OSBindings/Mac/Clock Signal/ClockSignal-Bridging-Header.h b/OSBindings/Mac/Clock Signal/ClockSignal-Bridging-Header.h index 314208601..5b21dd3fc 100644 --- a/OSBindings/Mac/Clock Signal/ClockSignal-Bridging-Header.h +++ b/OSBindings/Mac/Clock Signal/ClockSignal-Bridging-Header.h @@ -10,10 +10,13 @@ #import "CSStaticAnalyser.h" -#import "CSOpenGLView.h" #import "CSAudioQueue.h" +#import "CSOpenGLView.h" +#import "CSROMReceiverView.h" #import "CSBestEffortUpdater.h" #import "CSJoystickManager.h" +#import "NSData+CRC32.h" + #include "KeyCodes.h" diff --git a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift index c4b42aa0a..b36369ffa 100644 --- a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift +++ b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift @@ -16,7 +16,8 @@ class MachineDocument: CSOpenGLViewDelegate, CSOpenGLViewResponderDelegate, CSBestEffortUpdaterDelegate, - CSAudioQueueDelegate + CSAudioQueueDelegate, + CSROMReciverViewDelegate { fileprivate let actionLock = NSLock() fileprivate let drawLock = NSLock() @@ -152,12 +153,27 @@ class MachineDocument: } // MARK: configuring + fileprivate var missingROMs: [CSMissingROM] = [] + fileprivate var selectedMachine: CSStaticAnalyser? + func configureAs(_ analysis: CSStaticAnalyser) { - if let machine = CSMachine(analyser: analysis) { + let missingROMs = NSMutableArray() + if let machine = CSMachine(analyser: analysis, missingROMs: missingROMs) { + self.selectedMachine = nil self.machine = machine self.optionsPanelNibName = analysis.optionsPanelNibName setupMachineOutput() setupActivityDisplay() + } else { + // Store the selected machine and list of missing ROMs, and + // show the missing ROMs dialogue. + self.missingROMs = [] + for untypedMissingROM in missingROMs { + self.missingROMs.append(untypedMissingROM as! CSMissingROM) + } + + self.selectedMachine = analysis + requestRoms() } } @@ -303,19 +319,173 @@ class MachineDocument: } } - // MARK: New machine creation + // MARK: New machine creation. @IBOutlet var machinePicker: MachinePicker? @IBOutlet var machinePickerPanel: NSWindow? @IBAction func createMachine(_ sender: NSButton?) { - self.configureAs(machinePicker!.selectedMachine()) - machinePicker = nil + let selectedMachine = machinePicker!.selectedMachine() self.windowControllers[0].window?.endSheet(self.machinePickerPanel!) + machinePicker = nil + self.configureAs(selectedMachine) } @IBAction func cancelCreateMachine(_ sender: NSButton?) { close() } + // MARK: User ROM provision. + @IBOutlet var romRequesterPanel: NSWindow? + @IBOutlet var romRequesterText: NSTextField? + @IBOutlet var romReceiverErrorField: NSTextField? + @IBOutlet var romReceiverView: CSROMReceiverView? + private var romRequestBaseText = "" + func requestRoms() { + // Load the ROM requester dialogue. + Bundle.main.loadNibNamed("ROMRequester", owner: self, topLevelObjects: nil) + self.romReceiverView!.delegate = self + self.romRequestBaseText = romRequesterText!.stringValue + romReceiverErrorField?.alphaValue = 0.0 + + // Populate the current absentee list. + populateMissingRomList() + + // Show the thing. + self.windowControllers[0].window?.beginSheet(self.romRequesterPanel!, completionHandler: nil) + } + + @IBAction func cancelRequestROMs(_ sender: NSButton?) { + close() + } + + func populateMissingRomList() { + // Fill in the missing details; first build a list of all the individual + // line items. + var requestLines: [String] = [] + for missingROM in self.missingROMs { + if let descriptiveName = missingROM.descriptiveName { + requestLines.append("• " + descriptiveName) + } else { + requestLines.append("• " + missingROM.fileName) + } + } + + // Suffix everything up to the penultimate line with a semicolon; + // the penultimate line with a semicolon and a conjunctive; the final + // line with a full stop. + for x in 0 ..< requestLines.count { + if x < requestLines.count - 2 { + requestLines[x].append(";") + } else if x < requestLines.count - 1 { + requestLines[x].append("; and") + } else { + requestLines[x].append(".") + } + } + romRequesterText!.stringValue = self.romRequestBaseText + requestLines.joined(separator: "\n") + } + + func romReceiverView(_ view: CSROMReceiverView, didReceiveFileAt URL: URL) { + // Test whether the file identified matches any of the currently missing ROMs. + // If so then remove that ROM from the missing list and update the request screen. + // If no ROMs are still missing, start the machine. + do { + let fileData = try Data(contentsOf: URL) + var didInstallRom = false + + // Try to match by size first, CRC second. Accept that some ROMs may have + // some additional appended data. Arbitrarily allow them to be up to 10kb + // too large. + var index = 0 + for missingROM in self.missingROMs { + if fileData.count >= missingROM.size && fileData.count < missingROM.size + 10*1024 { + // Trim to size. + let trimmedData = fileData[0 ..< missingROM.size] + + // Get CRC. + if missingROM.crc32s.contains( (trimmedData as NSData).crc32 ) { + // This ROM matches; copy it into the application library, + // strike it from the missing ROM list and decide how to + // proceed. + let fileManager = FileManager.default + let targetPath = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + .appendingPathComponent("ROMImages") + .appendingPathComponent(missingROM.machineName) + let targetFile = targetPath + .appendingPathComponent(missingROM.fileName) + + do { + try fileManager.createDirectory(atPath: targetPath.path, withIntermediateDirectories: true, attributes: nil) + try trimmedData.write(to: targetFile) + } catch let error { + showRomReceiverError(error: "Couldn't write to application support directory: \(error)") + } + + self.missingROMs.remove(at: index) + didInstallRom = true + break + } + } + + index = index + 1 + } + + if didInstallRom { + if self.missingROMs.count == 0 { + self.windowControllers[0].window?.endSheet(self.romRequesterPanel!) + configureAs(self.selectedMachine!) + } else { + populateMissingRomList() + } + } else { + showRomReceiverError(error: "Didn't recognise contents of \(URL.lastPathComponent)") + } + } catch let error { + showRomReceiverError(error: "Couldn't read file at \(URL.absoluteString): \(error)") + } + } + + // Yucky ugliness follows; my experience as an iOS developer intersects poorly with + // NSAnimationContext hence the various stateful diplications below. isShowingError + // should be essentially a duplicate of the current alphaValue, and animationCount + // is to resolve my inability to figure out how to cancel scheduled animations. + private var errorText = "" + private var isShowingError = false + private var animationCount = 0 + private func showRomReceiverError(error: String) { + // Set or append the new error. + if self.errorText.count > 0 { + self.errorText = self.errorText + "\n" + error + } else { + self.errorText = error + } + + // Apply the new complete text. + romReceiverErrorField!.stringValue = self.errorText + + if !isShowingError { + // Schedule the box's appearance. + NSAnimationContext.beginGrouping() + NSAnimationContext.current.duration = 0.1 + romReceiverErrorField?.animator().alphaValue = 1.0 + NSAnimationContext.endGrouping() + isShowingError = true + } + + // Schedule the box to disappear. + self.animationCount = self.animationCount + 1 + let capturedAnimationCount = animationCount + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(2)) { + if self.animationCount == capturedAnimationCount { + NSAnimationContext.beginGrouping() + NSAnimationContext.current.duration = 1.0 + self.romReceiverErrorField?.animator().alphaValue = 0.0 + NSAnimationContext.endGrouping() + self.isShowingError = false + self.errorText = "" + } + } + } + // MARK: Joystick-via-the-keyboard selection @IBAction func useKeyboardAsKeyboard(_ sender: NSMenuItem?) { machine.inputMode = .keyboard diff --git a/OSBindings/Mac/Clock Signal/Info.plist b/OSBindings/Mac/Clock Signal/Info.plist index f9f308fcd..07ade7517 100644 --- a/OSBindings/Mac/Clock Signal/Info.plist +++ b/OSBindings/Mac/Clock Signal/Info.plist @@ -509,6 +509,10 @@ CFBundleTypeIconFile floppy35 + CFBundleTypeMIMETypes + + application/x-apple-diskimage + CFBundleTypeName DiskCopy 4.2 Disk Image CFBundleTypeOSTypes diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h index 5f1bd5da0..7a14bf721 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h @@ -33,6 +33,14 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) { CSMachineKeyboardInputModeJoystick }; +@interface CSMissingROM: NSObject +@property (nonatomic, readonly, nonnull) NSString *machineName; +@property (nonatomic, readonly, nonnull) NSString *fileName; +@property (nonatomic, readonly, nullable) NSString *descriptiveName; +@property (nonatomic, readonly) NSUInteger size; +@property (nonatomic, readonly, nonnull) NSArray *crc32s; +@end + // Deliberately low; to ensure CSMachine has been declared as an @class already. #import "CSAtari2600.h" #import "CSZX8081.h" @@ -45,8 +53,10 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) { Initialises an instance of CSMachine. @param result The CSStaticAnalyser result that describes the machine needed. + @param missingROMs An array that is filled with a list of ROMs that the machine requested but which + were not found; populated only if this `init` has failed. */ -- (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSMutableArray *)missingROMs NS_DESIGNATED_INITIALIZER; - (void)runForInterval:(NSTimeInterval)interval; diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index c2077baef..28e755a8e 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -74,6 +74,68 @@ struct ActivityObserver: public Activity::Observer { __unsafe_unretained CSMachine *machine; }; +@interface CSMissingROM (Mutability) +@property (nonatomic, nonnull, copy) NSString *machineName; +@property (nonatomic, nonnull, copy) NSString *fileName; +@property (nonatomic, nullable, copy) NSString *descriptiveName; +@property (nonatomic, readwrite) NSUInteger size; +@property (nonatomic, copy) NSArray *crc32s; +@end + +@implementation CSMissingROM { + NSString *_machineName; + NSString *_fileName; + NSString *_descriptiveName; + NSUInteger _size; + NSArray *_crc32s; +} + +- (NSString *)machineName { + return _machineName; +} + +- (void)setMachineName:(NSString *)machineName { + _machineName = [machineName copy]; +} + +- (NSString *)fileName { + return _fileName; +} + +- (void)setFileName:(NSString *)fileName { + _fileName = [fileName copy]; +} + +- (NSString *)descriptiveName { + return _descriptiveName; +} + +- (void)setDescriptiveName:(NSString *)descriptiveName { + _descriptiveName = [descriptiveName copy]; +} + +- (NSUInteger)size { + return _size; +} + +- (void)setSize:(NSUInteger)size { + _size = size; +} + +- (NSArray *)crc32s { + return _crc32s; +} + +- (void)setCrc32s:(NSArray *)crc32s { + _crc32s = [crc32s copy]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"%@/%@, %@ bytes, CRCs: %@", _fileName, _descriptiveName, @(_size), _crc32s]; +} + +@end + @implementation CSMachine { SpeakerDelegate _speakerDelegate; ActivityObserver _activityObserver; @@ -90,14 +152,37 @@ struct ActivityObserver: public Activity::Observer { std::unique_ptr _scanTarget; } -- (instancetype)initWithAnalyser:(CSStaticAnalyser *)result { +- (instancetype)initWithAnalyser:(CSStaticAnalyser *)result missingROMs:(inout NSMutableArray *)missingROMs { self = [super init]; if(self) { _analyser = result; Machine::Error error; - _machine.reset(Machine::MachineForTargets(_analyser.targets, CSROMFetcher(), error)); - if(!_machine) return nil; + std::vector missing_roms; + _machine.reset(Machine::MachineForTargets(_analyser.targets, CSROMFetcher(&missing_roms), error)); + if(!_machine) { + for(const auto &missing_rom : missing_roms) { + CSMissingROM *rom = [[CSMissingROM alloc] init]; + + // Copy/convert the primitive fields. + rom.machineName = [NSString stringWithUTF8String:missing_rom.machine_name.c_str()]; + rom.fileName = [NSString stringWithUTF8String:missing_rom.file_name.c_str()]; + rom.descriptiveName = missing_rom.descriptive_name.empty() ? nil : [NSString stringWithUTF8String:missing_rom.descriptive_name.c_str()]; + rom.size = missing_rom.size; + + // Convert the CRC list. + NSMutableArray *crc32s = [[NSMutableArray alloc] init]; + for(const auto &crc : missing_rom.crc32s) { + [crc32s addObject:@(crc)]; + } + rom.crc32s = [crc32s copy]; + + // Add to the missing list. + [missingROMs addObject:rom]; + } + + return nil; + } _inputMode = (_machine->keyboard_machine() && _machine->keyboard_machine()->get_keyboard().is_exclusive()) diff --git a/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.hpp b/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.hpp index 28958b535..f1d039ade 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.hpp +++ b/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.hpp @@ -8,4 +8,4 @@ #include "ROMMachine.hpp" -ROMMachine::ROMFetcher CSROMFetcher(); +ROMMachine::ROMFetcher CSROMFetcher(std::vector *missing_roms = nullptr); diff --git a/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.mm b/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.mm index 0ab615bbf..474a36b41 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.mm @@ -14,15 +14,39 @@ #include -ROMMachine::ROMFetcher CSROMFetcher() { - return [] (const std::string &machine, const std::vector &names) -> std::vector>> { - NSString *subDirectory = [@"ROMImages/" stringByAppendingString:[NSString stringWithUTF8String:machine.c_str()]]; - std::vector>> results; - for(const auto &name: names) { - NSData *fileData = [[NSBundle mainBundle] dataForResource:[NSString stringWithUTF8String:name.c_str()] withExtension:nil subdirectory:subDirectory]; +ROMMachine::ROMFetcher CSROMFetcher(std::vector *missing_roms) { + return [missing_roms] (const std::vector &roms) -> std::vector>> { + NSArray *const supportURLs = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask]; - if(!fileData) + std::vector>> results; + for(const auto &rom: roms) { + NSData *fileData; + NSString *const subdirectory = [@"ROMImages/" stringByAppendingString:[NSString stringWithUTF8String:rom.machine_name.c_str()]]; + + // Check for this file first within the application support directories. + for(NSURL *supportURL in supportURLs) { + NSURL *const fullURL = [[supportURL URLByAppendingPathComponent:subdirectory] + URLByAppendingPathComponent:[NSString stringWithUTF8String:rom.file_name.c_str()]]; + fileData = [NSData dataWithContentsOfURL:fullURL]; + if(fileData) break; + } + + // Failing that, check inside the application bundle. + if(!fileData) { + fileData = [[NSBundle mainBundle] + dataForResource:[NSString stringWithUTF8String:rom.file_name.c_str()] + withExtension:nil + subdirectory:subdirectory]; + } + + // Store an appropriate result, accumulating a list of the missing if requested. + if(!fileData) { results.emplace_back(nullptr); + + if(missing_roms) { + missing_roms->push_back(rom); + } + } else { std::unique_ptr> data(new std::vector); *data = fileData.stdVector8; diff --git a/OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.h b/OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.h new file mode 100644 index 000000000..c62f5ac97 --- /dev/null +++ b/OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.h @@ -0,0 +1,17 @@ +// +// NSData+CRC32.m +// Clock Signal +// +// Created by Thomas Harte on 22/07/2019. +// Copyright 2019 Thomas Harte. All rights reserved. +// + +#import + +#include + +@interface NSData (CRC32) + +@property(nonnull, nonatomic, readonly) NSNumber *crc32; + +@end diff --git a/OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.m b/OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.m new file mode 100644 index 000000000..e31513f92 --- /dev/null +++ b/OSBindings/Mac/Clock Signal/Machine/NSData+CRC32.m @@ -0,0 +1,19 @@ +// +// NSData+CRC32.m +// Clock Signal +// +// Created by Thomas Harte on 22/07/2019. +// Copyright 2019 Thomas Harte. All rights reserved. +// + +#import "NSData+CRC32.h" + +#include + +@implementation NSData (StdVector) + +- (NSNumber *)crc32 { + return @(crc32(crc32(0, Z_NULL, 0), self.bytes, (uInt)self.length)); +} + +@end diff --git a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib index b51807442..241a2f2c1 100644 --- a/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib +++ b/OSBindings/Mac/Clock Signal/MachinePicker/Base.lproj/MachinePicker.xib @@ -64,7 +64,7 @@ Gw - + @@ -168,7 +168,7 @@ Gw - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OSBindings/SDL/main.cpp b/OSBindings/SDL/main.cpp index 31721e5d7..ffa356b30 100644 --- a/OSBindings/SDL/main.cpp +++ b/OSBindings/SDL/main.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -339,21 +340,21 @@ int main(int argc, char *argv[]) { } std::cout << std::endl; } - return 0; + return EXIT_SUCCESS; } // Perform a sanity check on arguments. if(arguments.file_name.empty()) { std::cerr << "Usage: " << final_path_component(argv[0]) << usage_suffix << std::endl; std::cerr << "Use --help to learn more about available options." << std::endl; - return -1; + return EXIT_FAILURE; } // Determine the machine for the supplied file. Analyser::Static::TargetList targets = Analyser::Static::GetTargets(arguments.file_name); if(targets.empty()) { std::cerr << "Cannot open " << arguments.file_name << "; no target machine found" << std::endl; - return -1; + return EXIT_FAILURE; } Concurrency::BestEffortUpdater updater; @@ -365,12 +366,10 @@ int main(int argc, char *argv[]) { // /usr/local/share/CLK/[system]; // /usr/share/CLK/[system]; or // [user-supplied path]/[system] - std::vector rom_names; - std::string machine_name; - ROMMachine::ROMFetcher rom_fetcher = [&rom_names, &machine_name, &arguments] - (const std::string &machine, const std::vector &names) -> std::vector>> { - rom_names.insert(rom_names.end(), names.begin(), names.end()); - machine_name = machine; + std::vector requested_roms; + ROMMachine::ROMFetcher rom_fetcher = [&requested_roms, &arguments] + (const std::vector &roms) -> std::vector>> { + requested_roms.insert(requested_roms.end(), roms.begin(), roms.end()); std::vector paths = { "/usr/local/share/CLK/", @@ -386,10 +385,10 @@ int main(int argc, char *argv[]) { } std::vector>> results; - for(const auto &name: names) { + for(const auto &rom: roms) { FILE *file = nullptr; for(const auto &path: paths) { - std::string local_path = path + machine + "/" + name; + std::string local_path = path + rom.machine_name + "/" + rom.file_name; file = std::fopen(local_path.c_str(), "rb"); if(file) break; } @@ -425,13 +424,17 @@ int main(int argc, char *argv[]) { case ::Machine::Error::MissingROM: std::cerr << "Could not find system ROMs; please install to /usr/local/share/CLK/ or /usr/share/CLK/, or provide a --rompath." << std::endl; std::cerr << "One or more of the following was needed but not found:" << std::endl; - for(const auto &name: rom_names) { - std::cerr << machine_name << '/' << name << std::endl; + for(const auto &rom: requested_roms) { + std::cerr << rom.machine_name << '/' << rom.file_name; + if(!rom.descriptive_name.empty()) { + std::cerr << " (" << rom.descriptive_name << ")"; + } + std::cerr << std::endl; } break; } - return -1; + return EXIT_FAILURE; } best_effort_updater_delegate.machine = machine.get(); @@ -441,7 +444,7 @@ int main(int argc, char *argv[]) { // Attempt to set up video and audio. if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) { std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl; - return -1; + return EXIT_FAILURE; } // Ask for no depth buffer, a core profile and vsync-aligned rendering. @@ -463,7 +466,7 @@ int main(int argc, char *argv[]) { if(!window || !gl_context) { std::cerr << "Could not create " << (window ? "OpenGL context" : "window"); std::cerr << "; reported error: \"" << SDL_GetError() << "\"" << std::endl; - return -1; + return EXIT_FAILURE; } SDL_GL_MakeCurrent(window, gl_context); @@ -816,5 +819,5 @@ int main(int argc, char *argv[]) { SDL_DestroyWindow( window ); SDL_Quit(); - return 0; + return EXIT_SUCCESS; }