diff --git a/Machines/Utility/ROMCatalogue.cpp b/Machines/Utility/ROMCatalogue.cpp index 5a1181bfd..45b6bb9ae 100644 --- a/Machines/Utility/ROMCatalogue.cpp +++ b/Machines/Utility/ROMCatalogue.cpp @@ -13,6 +13,10 @@ using namespace ROM; +namespace { +constexpr Name MaxName = Name::SpectrumPlus3; +} + Request::Request(Name name, bool optional) { node.name = name; node.is_optional = optional; @@ -173,8 +177,16 @@ void Request::Node::visit( } } +std::vector ROM::all_descriptions() { + std::vector result; + for(int name = 1; name <= MaxName; name++) { + result.push_back(Description(ROM::Name(name))); + } + return result; +} + std::optional Description::from_crc(uint32_t crc32) { - for(int name = 1; name <= SpectrumPlus3; name++) { + for(int name = 1; name <= MaxName; name++) { const Description candidate = Description(ROM::Name(name)); const auto found_crc = std::find(candidate.crc32s.begin(), candidate.crc32s.end(), crc32); @@ -232,8 +244,8 @@ Description::Description(Name name) { case Name::Macintosh128k: *this = Description(name, "Macintosh", "the Macintosh 128k ROM", "mac128k.rom", 64*1024, 0x6d0c8a28u); break; case Name::Macintosh512k: *this = Description(name, "Macintosh", "the Macintosh 512k ROM", "mac512k.rom", 64*1024, 0xcf759e0d); break; case Name::MacintoshPlus: { - const std::initializer_list crc32s = { 0x4fa5b399, 0x7cacd18f, 0xb2102e8e }; - *this = Description(name, "Macintosh", "the Macintosh Plus ROM", "macplus.rom", 128*1024, crc32s); + const std::initializer_list crcs = { 0x4fa5b399, 0x7cacd18f, 0xb2102e8e }; + *this = Description(name, "Macintosh", "the Macintosh Plus ROM", "macplus.rom", 128*1024, crcs); } break; case Name::AtariSTTOS100: *this = Description(name, "AtariST", "the UK TOS 1.00 ROM", "tos100.img", 192*1024, 0x1a586c64u); break; @@ -250,8 +262,8 @@ Description::Description(Name name) { case Name::Spectrum128k: *this = Description(name, "ZXSpectrum", "the 128kb ROM", "128.rom", 32 * 1024, 0x2cbe8995u); break; case Name::SpecrumPlus2: *this = Description(name, "ZXSpectrum", "the +2 ROM", "plus2.rom", 32 * 1024, 0xe7a517dcu); break; case Name::SpectrumPlus3: { - const std::initializer_list crc32s = { 0x96e3c17a, 0xbe0d9ec4 }; - *this = Description(name, "ZXSpectrum", "the +2a/+3 ROM", "plus3.rom", 64 * 1024, crc32s); + const std::initializer_list crcs = { 0x96e3c17a, 0xbe0d9ec4 }; + *this = Description(name, "ZXSpectrum", "the +2a/+3 ROM", "plus3.rom", 64 * 1024, crcs); } break; case Name::AcornBASICII: *this = Description(name, "Electron", "the Acorn BASIC II ROM", "basic.rom", 16*1024, 0x79434781u); break; diff --git a/Machines/Utility/ROMCatalogue.hpp b/Machines/Utility/ROMCatalogue.hpp index 0cb759507..86bc5d9df 100644 --- a/Machines/Utility/ROMCatalogue.hpp +++ b/Machines/Utility/ROMCatalogue.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -135,7 +136,7 @@ struct Description { /// 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; + std::set crc32s; /// Constructs the @c Description that correlates to @c name. Description(Name name); @@ -145,14 +146,19 @@ struct Description { private: template Description( - Name name, std::string machine_name, std::string descriptive_name, FileNameT file_names, size_t size, CRC32T crc32s = CRC32T(0) + Name name, std::string machine_name, std::string descriptive_name, FileNameT file_names, size_t size, CRC32T crc32s = uint32_t(0) ) : name{name}, machine_name{machine_name}, descriptive_name{descriptive_name}, file_names{file_names}, size{size}, crc32s{crc32s} { - if(this->crc32s.size() == 1 && !this->crc32s[0]) { + // Slightly lazy: deal with the case where the constructor wasn't provided with any + // CRCs by spotting that the set has exactly one member, which has value 0. The alternative + // would be to provide a partial specialisation that never put anything into the set. + if(this->crc32s.size() == 1 && !*this->crc32s.begin()) { this->crc32s.clear(); } } }; +std::vector all_descriptions(); + struct Request { Request(Name name, bool optional = false); Request() {} diff --git a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift index 726d6196b..71a02ced9 100644 --- a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift +++ b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift @@ -137,22 +137,18 @@ class MachineDocument: volumeSlider.floatValue = userDefaultsVolume() } - private var missingROMs: [CSMissingROM] = [] + private var missingROMs: String = "" func configureAs(_ analysis: CSStaticAnalyser) { self.machineDescription = analysis actionLock.lock() drawLock.lock() - let missingROMs = NSMutableArray() if let machine = CSMachine(analyser: analysis, missingROMs: missingROMs) { self.machine = machine machine.setVolume(userDefaultsVolume()) setupMachineOutput() } else { - // Store the selected machine and list of missing ROMs, and - // show the missing ROMs dialogue. - self.missingROMs = missingROMs.map({$0 as! CSMissingROM}) requestRoms() } @@ -437,30 +433,7 @@ class MachineDocument: } 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") + romRequesterText!.stringValue = self.romRequestBaseText + self.missingROMs } func romReceiverView(_ view: CSROMReceiverView, didReceiveFileAt URL: URL) { @@ -474,7 +447,7 @@ class MachineDocument: // 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 +/* var index = 0 for missingROM in self.missingROMs { if fileData.count >= missingROM.size && fileData.count < missingROM.size + 10*1024 { // Trim to size. @@ -506,7 +479,7 @@ class MachineDocument: } index = index + 1 - } + }*/ if didInstallRom { if self.missingROMs.count == 0 { diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h index 50ac394f6..9bc37dfad 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h @@ -33,20 +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" @interface CSMachine : NSObject ++ (BOOL)attemptInstallROM:(NSURL *)url; + - (nonnull instancetype)init NS_UNAVAILABLE; /*! @@ -56,7 +50,7 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) { @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 missingROMs:(nullable inout NSMutableArray *)missingROMs NS_DESIGNATED_INITIALIZER; +- (nullable instancetype)initWithAnalyser:(nonnull CSStaticAnalyser *)result missingROMs:(nullable inout NSString *)missingROMs NS_DESIGNATED_INITIALIZER; - (float)idealSamplingRateFromRange:(NSRange)range; @property (readonly, getter=isStereo) BOOL stereo; diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index 0d33f1ea1..e81d63451 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -74,28 +74,6 @@ 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 - -@synthesize machineName=_machineName; -@synthesize fileName=_fileName; -@synthesize descriptiveName=_descriptiveName; -@synthesize size=_size; -@synthesize crc32s=_crc32s; - -- (NSString *)description { - return [NSString stringWithFormat:@"%@/%@, %lu bytes, CRCs: %@", _fileName, _descriptiveName, (unsigned long)_size, _crc32s]; -} - -@end - @implementation CSMachine { SpeakerDelegate _speakerDelegate; ActivityObserver _activityObserver; @@ -126,7 +104,7 @@ struct ActivityObserver: public Activity::Observer { NSMutableArray *_inputEvents; } -- (instancetype)initWithAnalyser:(CSStaticAnalyser *)result missingROMs:(inout NSMutableArray *)missingROMs { +- (instancetype)initWithAnalyser:(CSStaticAnalyser *)result missingROMs:(inout NSString *)missingROMs { self = [super init]; if(self) { _analyser = result; @@ -783,4 +761,8 @@ struct ActivityObserver: public Activity::Observer { _timer = nil; } ++ (BOOL)attemptInstallROM:(NSURL *)url { + return CSInstallROM(url); +} + @end diff --git a/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.hpp b/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.hpp index bd00e601e..dede1a869 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.hpp +++ b/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.hpp @@ -9,3 +9,4 @@ #include "ROMMachine.hpp" ROMMachine::ROMFetcher CSROMFetcher(ROM::Request *missing = nullptr); +BOOL CSInstallROM(NSURL *); diff --git a/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.mm b/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.mm index 0dce4fdb2..a48561a6b 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSROMFetcher.mm @@ -11,24 +11,75 @@ #import "NSBundle+DataResource.h" #import "NSData+StdVector.h" +#import "NSData+CRC32.h" #include +namespace { + +NSString *directoryFor(const ROM::Description &description) { + return [@"ROMImages/" stringByAppendingString:[NSString stringWithUTF8String:description.machine_name.c_str()]]; +} + +NSArray *urlsFor(const ROM::Description &description, const std::string &file_name) { + NSMutableArray *const urls = [[NSMutableArray alloc] init]; + NSArray *const supportURLs = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask]; + NSString *const subdirectory = directoryFor(description); + + for(NSURL *supportURL in supportURLs) { + [urls addObject:[[supportURL URLByAppendingPathComponent:subdirectory] + URLByAppendingPathComponent:[NSString stringWithUTF8String:file_name.c_str()]]]; + } + + return urls; +} + +} + +BOOL CSInstallROM(NSURL *url) { + NSData *const data = [NSData dataWithContentsOfURL:url]; + if(!data) return NO; + + // Try for a direct CRC match. + std::optional target_description; + target_description = ROM::Description::from_crc(uint32_t(data.crc32.integerValue)); + + // See whether there's an acceptable trimming that creates a CRC match. + if(!target_description) { + const std::vector descriptions = ROM::all_descriptions(); + for(const auto &description: descriptions) { + if(description.size > data.length) continue; + + NSData *const trimmedData = [data subdataWithRange:NSMakeRange(0, description.size)]; + if(description.crc32s.find(uint32_t(trimmedData.crc32.unsignedIntValue)) != description.crc32s.end()) { + target_description = description; + break; + } + } + } + + // If no destination was found, stop. + if(!target_description) { + return NO; + } + + // Copy the data to its destination and report success. + NSURL *const targetURL = [urlsFor(*target_description, target_description->file_names[0]) firstObject]; + [data writeToURL:targetURL atomically:YES]; + + return YES; +} + ROMMachine::ROMFetcher CSROMFetcher(ROM::Request *missing) { return [missing] (const ROM::Request &roms) -> ROM::Map { - NSArray *const supportURLs = [[NSFileManager defaultManager] URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask]; - ROM::Map results; for(const auto &description: roms.all_descriptions()) { for(const auto &file_name: description.file_names) { NSData *fileData; - NSString *const subdirectory = [@"ROMImages/" stringByAppendingString:[NSString stringWithUTF8String:description.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:file_name.c_str()]]; - fileData = [NSData dataWithContentsOfURL:fullURL]; + for(NSURL *fileURL in urlsFor(description, file_name)) { + fileData = [NSData dataWithContentsOfURL:fileURL]; if(fileData) break; } @@ -37,7 +88,7 @@ ROMMachine::ROMFetcher CSROMFetcher(ROM::Request *missing) { fileData = [[NSBundle mainBundle] dataForResource:[NSString stringWithUTF8String:file_name.c_str()] withExtension:nil - subdirectory:subdirectory]; + subdirectory:directoryFor(description)]; } // Store an appropriate result. @@ -47,7 +98,9 @@ ROMMachine::ROMFetcher CSROMFetcher(ROM::Request *missing) { } } - // TODO: sever all found ROMs from roms and store to missing, if provided. + if(missing) { + *missing = roms.subtract(results); + } return results; };