mirror of https://github.com/LIJI32/SameBoy.git
Add ZIP import support to the iOS version
This commit is contained in:
parent
fb8508ea20
commit
2b2bb3569b
2
Makefile
2
Makefile
|
@ -273,7 +273,7 @@ LDFLAGS += -arch arm64
|
|||
OCFLAGS += -x objective-c -fobjc-arc -Wno-deprecated-declarations -isysroot $(SYSROOT)
|
||||
LDFLAGS += -miphoneos-version-min=$(IOS_MIN) -isysroot $(SYSROOT)
|
||||
IOS_INSTALLER_LDFLAGS := $(LDFLAGS) -lobjc -framework CoreServices -framework Foundation
|
||||
LDFLAGS += -lobjc -framework UIKit -framework Foundation -framework CoreGraphics -framework Metal -framework MetalKit -framework AudioToolbox -framework AVFoundation -framework QuartzCore -framework CoreMotion -framework CoreVideo -framework CoreMedia -framework CoreImage -framework UserNotifications -framework GameController -weak_framework CoreHaptics -framework MobileCoreServices
|
||||
LDFLAGS += -lobjc -framework UIKit -framework Foundation -framework CoreGraphics -framework Metal -framework MetalKit -framework AudioToolbox -framework AVFoundation -framework QuartzCore -framework CoreMotion -framework CoreVideo -framework CoreMedia -framework CoreImage -framework UserNotifications -framework GameController -weak_framework CoreHaptics -framework MobileCoreServices -lcompression
|
||||
CODESIGN := codesign -fs -
|
||||
else
|
||||
ifeq ($(PLATFORM),Darwin)
|
||||
|
|
|
@ -122,6 +122,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
[extensions addObject:@"zip"];
|
||||
|
||||
[self.presentingViewController dismissViewControllerAnimated:true completion:^{
|
||||
UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"com.github.liji32.sameboy.gb",
|
||||
@"com.github.liji32.sameboy.gbc",
|
||||
|
@ -161,61 +163,7 @@
|
|||
|
||||
- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray <NSURL *>*)urls
|
||||
{
|
||||
NSMutableArray<NSURL *> *validURLs = [NSMutableArray array];
|
||||
NSMutableArray<NSString *> *skippedBasenames = [NSMutableArray array];
|
||||
|
||||
for (NSURL *url in urls) {
|
||||
if ([@[@"gb", @"gbc", @"isx"] containsObject:url.pathExtension.lowercaseString]) {
|
||||
[validURLs addObject:url];
|
||||
}
|
||||
else {
|
||||
[skippedBasenames addObject:url.lastPathComponent];
|
||||
}
|
||||
}
|
||||
|
||||
if (skippedBasenames.count) {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Unsupported Files"
|
||||
message:[NSString stringWithFormat:@"Could not import the following files because they're not supported:\n%@",
|
||||
[skippedBasenames componentsJoinedByString:@"\n"]]
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Close"
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *action) {
|
||||
[[NSUserDefaults standardUserDefaults] setBool:false forKey:@"GBShownUTIWarning"]; // Somebody might need a reminder
|
||||
}]];
|
||||
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:true completion:nil];
|
||||
urls = validURLs;
|
||||
}
|
||||
|
||||
if (urls.count == 1) {
|
||||
NSURL *url = urls.firstObject;
|
||||
NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
|
||||
if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
|
||||
[GBROMManager sharedManager].currentROM = potentialROM;
|
||||
}
|
||||
else {
|
||||
[url startAccessingSecurityScopedResource];
|
||||
[GBROMManager sharedManager].currentROM =
|
||||
[[GBROMManager sharedManager] importROM:url.path
|
||||
keepOriginal:true];
|
||||
[url stopAccessingSecurityScopedResource];
|
||||
}
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"GBROMChanged" object:nil];
|
||||
}
|
||||
else {
|
||||
for (NSURL *url in urls) {
|
||||
NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
|
||||
if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
|
||||
// That's an already imported ROM
|
||||
continue;
|
||||
}
|
||||
[url startAccessingSecurityScopedResource];
|
||||
[[GBROMManager sharedManager] importROM:url.path
|
||||
keepOriginal:true];
|
||||
[url stopAccessingSecurityScopedResource];
|
||||
}
|
||||
[(GBViewController *)[UIApplication sharedApplication].keyWindow.rootViewController openLibrary];
|
||||
}
|
||||
[(GBViewController *)[UIApplication sharedApplication] handleOpenURLs:urls openInPlace:false];
|
||||
}
|
||||
|
||||
- (UIModalPresentationStyle)modalPresentationStyle
|
||||
|
|
|
@ -24,5 +24,6 @@ typedef enum {
|
|||
- (void)showAbout;
|
||||
- (void)saveStateToFile:(NSString *)file;
|
||||
- (bool)loadStateFromFile:(NSString *)file;
|
||||
- (bool)handleOpenURLs:(NSArray <NSURL *> *)urls openInPlace:(bool)inPlace;
|
||||
@property (nonatomic) GBRunMode runMode;
|
||||
@end
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
#import "GBSettingsViewController.h"
|
||||
#import "GBStatesViewController.h"
|
||||
#import "GCExtendedGamepad+AllElements.h"
|
||||
#import "GBZipReader.h"
|
||||
#import <sys/stat.h>
|
||||
#import <CoreMotion/CoreMotion.h>
|
||||
#import <dlfcn.h>
|
||||
|
||||
|
@ -1147,23 +1149,117 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
|
|||
GB_set_palette(&_gb, &_palette);
|
||||
}
|
||||
|
||||
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
|
||||
- (bool)handleOpenURLs:(NSArray <NSURL *> *)urls openInPlace:(bool)inPlace
|
||||
{
|
||||
[self stop];
|
||||
NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
|
||||
if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
|
||||
[GBROMManager sharedManager].currentROM = potentialROM;
|
||||
NSMutableArray<NSURL *> *validURLs = [NSMutableArray array];
|
||||
NSMutableArray<NSString *> *skippedBasenames = [NSMutableArray array];
|
||||
NSMutableArray<NSString *> *unusedZips = [NSMutableArray array];
|
||||
NSString *tempDir = NSTemporaryDirectory();
|
||||
|
||||
__block unsigned tempIndex = 0;
|
||||
for (NSURL *url in urls) {
|
||||
if ([url.pathExtension.lowercaseString isEqualToString:@"zip"]) {
|
||||
GBZipReader *reader = [[GBZipReader alloc] initWithPath:url.path];
|
||||
if (!reader) {
|
||||
[skippedBasenames addObject:url.lastPathComponent];
|
||||
continue;
|
||||
}
|
||||
__block bool used = false;
|
||||
[reader iterateFiles:^bool(NSString *filename, size_t uncompressedSize, bool (^getData)(void *), bool (^writeToPath)(NSString *)) {
|
||||
if ([@[@"gb", @"gbc", @"isx"] containsObject:filename.pathExtension.lowercaseString] && uncompressedSize <= 32 * 1024 * 1024) {
|
||||
tempIndex++;
|
||||
NSString *subDir = [tempDir stringByAppendingFormat:@"/%u", tempIndex];
|
||||
mkdir(subDir.UTF8String, 0755);
|
||||
NSString *dest = [subDir stringByAppendingPathComponent:filename.lastPathComponent];
|
||||
if (writeToPath(dest)) {
|
||||
[validURLs addObject:[NSURL fileURLWithPath:dest]];
|
||||
used = true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}];
|
||||
if (!used) {
|
||||
[unusedZips addObject:url.lastPathComponent];
|
||||
}
|
||||
}
|
||||
else if ([@[@"gb", @"gbc", @"isx"] containsObject:url.pathExtension.lowercaseString]) {
|
||||
[validURLs addObject:url];
|
||||
}
|
||||
else {
|
||||
[skippedBasenames addObject:url.lastPathComponent];
|
||||
}
|
||||
}
|
||||
else {
|
||||
[url startAccessingSecurityScopedResource];
|
||||
[GBROMManager sharedManager].currentROM =
|
||||
|
||||
if (unusedZips.count) {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"No ROMs in archive"
|
||||
message:[NSString stringWithFormat:@"Could not find any Game Boy ROM files in the following archives:\n%@",
|
||||
[unusedZips componentsJoinedByString:@"\n"]]
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Close"
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:nil]];
|
||||
[self stop];
|
||||
[self presentViewController:alert animated:true completion:nil];
|
||||
}
|
||||
|
||||
if (skippedBasenames.count) {
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Unsupported Files"
|
||||
message:[NSString stringWithFormat:@"Could not import the following files because they're not supported:\n%@",
|
||||
[skippedBasenames componentsJoinedByString:@"\n"]]
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"Close"
|
||||
style:UIAlertActionStyleCancel
|
||||
handler:^(UIAlertAction *action) {
|
||||
[[NSUserDefaults standardUserDefaults] setBool:false forKey:@"GBShownUTIWarning"]; // Somebody might need a reminder
|
||||
}]];
|
||||
[self stop];
|
||||
[self presentViewController:alert animated:true completion:nil];
|
||||
}
|
||||
|
||||
if (validURLs.count == 1 && urls.count == 1) {
|
||||
NSURL *url = validURLs.firstObject;
|
||||
NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
|
||||
if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
|
||||
[GBROMManager sharedManager].currentROM = potentialROM;
|
||||
}
|
||||
else {
|
||||
[url startAccessingSecurityScopedResource];
|
||||
[GBROMManager sharedManager].currentROM =
|
||||
[[GBROMManager sharedManager] importROM:url.path
|
||||
keepOriginal:[options[UIApplicationOpenURLOptionsOpenInPlaceKey] boolValue]];
|
||||
keepOriginal:![url.path hasPrefix:tempDir] && !inPlace];
|
||||
[url stopAccessingSecurityScopedResource];
|
||||
}
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"GBROMChanged" object:nil];
|
||||
return true;
|
||||
}
|
||||
for (NSURL *url in validURLs) {
|
||||
NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
|
||||
if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
|
||||
// That's an already imported ROM
|
||||
continue;
|
||||
}
|
||||
[url startAccessingSecurityScopedResource];
|
||||
[[GBROMManager sharedManager] importROM:url.path
|
||||
keepOriginal:![url.path hasPrefix:tempDir] && !inPlace];
|
||||
[url stopAccessingSecurityScopedResource];
|
||||
}
|
||||
[self loadROM];
|
||||
[self start];
|
||||
return [GBROMManager sharedManager].currentROM != nil;
|
||||
[self stop];
|
||||
[self openLibrary];
|
||||
|
||||
return validURLs.count;
|
||||
}
|
||||
|
||||
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
|
||||
{
|
||||
NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
|
||||
if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
|
||||
[self stop];
|
||||
[GBROMManager sharedManager].currentROM = potentialROM;
|
||||
[self loadROM];
|
||||
[self start];
|
||||
return [GBROMManager sharedManager].currentROM != nil;
|
||||
}
|
||||
return [self handleOpenURLs:@[url] openInPlace:[options[UIApplicationOpenURLOptionsOpenInPlaceKey] boolValue]];
|
||||
}
|
||||
|
||||
- (void)setRunMode:(GBRunMode)runMode ignoreDynamicSpeed:(bool)ignoreDynamicSpeed
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface GBZipReader : NSObject
|
||||
- (instancetype)initWithBuffer:(const void *)buffer size:(size_t)size free:(void (^)(void))callback;
|
||||
- (instancetype)initWithPath:(NSString *)path;
|
||||
- (void)iterateFiles:(bool (^)(NSString *filename, size_t uncompressedSize, bool (^getData)(void *), bool (^writeToPath)(NSString *)))callback;
|
||||
@end
|
|
@ -0,0 +1,179 @@
|
|||
#import "GBZipReader.h"
|
||||
#import <compression.h>
|
||||
#import <sys/mman.h>
|
||||
#import <mach/vm_region.h>
|
||||
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
||||
|
||||
@implementation GBZipReader
|
||||
{
|
||||
void (^_freeCallback)(void);
|
||||
const void *_buffer;
|
||||
size_t _size;
|
||||
|
||||
const struct __attribute__((packed)) {
|
||||
uint32_t magic;
|
||||
uint8_t skip[6];
|
||||
uint16_t fileCount;
|
||||
uint32_t cdSize;
|
||||
uint32_t cdOffset;
|
||||
uint16_t commentSize;
|
||||
} *_eocd;
|
||||
}
|
||||
|
||||
- (instancetype)initWithBuffer:(const void *)buffer size:(size_t)size free:(void (^)(void))callback
|
||||
{
|
||||
self = [super init];
|
||||
if (!self) return nil;
|
||||
|
||||
_buffer = buffer;
|
||||
_size = size;
|
||||
_freeCallback = callback;
|
||||
|
||||
if (_size < sizeof(*_eocd)) return nil;
|
||||
|
||||
for (unsigned i = 0; i < 0x10000; i++) {
|
||||
_eocd = (void *)((uint8_t *)buffer + size - sizeof(*_eocd) - i);
|
||||
if ((void *)_eocd < buffer) return nil;
|
||||
if (_eocd->magic == htonl('PK\05\06')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path
|
||||
{
|
||||
int fd = open(path.UTF8String, O_RDONLY);
|
||||
if (fd < 0) return nil;
|
||||
size_t size = lseek(fd, 0, SEEK_END);
|
||||
size_t alignedSize = (size + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1);
|
||||
void *mapping = mmap(NULL, alignedSize, PROT_READ, MAP_FILE | MAP_PRIVATE, fd, 0);
|
||||
close(fd);
|
||||
if (!mapping) return nil;
|
||||
|
||||
return [self initWithBuffer:mapping size:size free:^{
|
||||
munmap(mapping, alignedSize);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)iterateFiles:(bool (^)(NSString *filename, size_t uncompressedSize, bool (^getData)(void *), bool (^writeToPath)(NSString *)))callback {
|
||||
const struct __attribute__((packed)) {
|
||||
uint32_t magic;
|
||||
uint8_t skip[6];
|
||||
uint16_t compressionMethod;
|
||||
uint8_t skip2[8];
|
||||
uint32_t compressedSize;
|
||||
uint32_t uncompressedSize;
|
||||
uint16_t nameLength;
|
||||
uint16_t extraLength;
|
||||
uint16_t commentLength;
|
||||
uint8_t skip3[8];
|
||||
uint32_t localHeaderOffset;
|
||||
char name[0];
|
||||
} *entry = (void *)((uint8_t *)_buffer + _eocd->cdOffset);
|
||||
for (unsigned i = _eocd->fileCount; i--;) {
|
||||
if ((uint8_t *)entry + sizeof(*entry) - (uint8_t *)_buffer >= _size) return;
|
||||
if (entry->magic != htonl('PK\01\02')) return;
|
||||
|
||||
typeof(entry) next = (void *)((uint8_t *)entry + sizeof(*entry) +
|
||||
entry->nameLength + entry->extraLength + entry->commentLength);
|
||||
if ((uint8_t *)next - (uint8_t *)_buffer >= _size) return;
|
||||
|
||||
|
||||
bool (^getData)(void *) = ^bool(void *output) {
|
||||
// Directory, no data
|
||||
if (entry->name[entry->nameLength - 1] == '/') return false;
|
||||
|
||||
if (entry->uncompressedSize == 0xffffffff || entry->compressedSize == 0xffffffff) {
|
||||
// ZIP64
|
||||
return false;
|
||||
}
|
||||
|
||||
const struct __attribute__((packed)) {
|
||||
uint32_t magic;
|
||||
uint8_t skip[4];
|
||||
uint16_t compressionMethod;
|
||||
uint8_t skip2[8];
|
||||
uint32_t compressedSize;
|
||||
uint32_t uncompressedSize;
|
||||
uint16_t nameLength;
|
||||
uint16_t extraLength;
|
||||
char name[0];
|
||||
} *localEntry = (void *)((uint8_t *)_buffer + entry->localHeaderOffset);
|
||||
|
||||
if ((uint8_t *)localEntry + sizeof(*localEntry) - (uint8_t *)_buffer >= _size) return nil;
|
||||
if ((uint8_t *)localEntry + sizeof(*localEntry) +
|
||||
localEntry->nameLength + localEntry->extraLength +
|
||||
entry->compressedSize - (uint8_t *)_buffer >= _size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (localEntry->magic != htonl('PK\03\04')) return nil;
|
||||
if (entry->uncompressedSize != localEntry->uncompressedSize) return nil;
|
||||
|
||||
const void *dataStart = &localEntry->name[localEntry->nameLength + localEntry->extraLength];
|
||||
if (localEntry->compressionMethod == 0) {
|
||||
if (localEntry->uncompressedSize != entry->compressedSize) return false;
|
||||
memcpy(output, dataStart, localEntry->uncompressedSize);
|
||||
return true;
|
||||
}
|
||||
else if (localEntry->compressionMethod != 8) {
|
||||
// Unsupported compression
|
||||
return false;
|
||||
}
|
||||
if (compression_decode_buffer(output, localEntry->uncompressedSize,
|
||||
dataStart, entry->compressedSize,
|
||||
NULL, COMPRESSION_ZLIB) != localEntry->uncompressedSize) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
bool (^writeToPath)(NSString *) = ^bool(NSString *path) {
|
||||
int fd = open(path.UTF8String, O_CREAT | O_RDWR, 0644);
|
||||
if (!fd) return false;
|
||||
if (ftruncate(fd, entry->uncompressedSize) != 0) {
|
||||
close(fd);
|
||||
unlink(path.UTF8String);
|
||||
return false;
|
||||
}
|
||||
size_t alignedSize = (entry->uncompressedSize + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1);
|
||||
void *mapping = mmap(NULL, alignedSize, PROT_READ | PROT_WRITE, MAP_FILE | MAP_SHARED, fd, 0);
|
||||
close(fd);
|
||||
if (!mapping) {
|
||||
unlink(path.UTF8String);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ret = getData(mapping);
|
||||
if (!ret) {
|
||||
unlink(path.UTF8String);
|
||||
}
|
||||
munmap(mapping, alignedSize);
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
if (!callback([[NSString alloc] initWithBytes:entry->name
|
||||
length:entry->nameLength
|
||||
encoding:NSUTF8StringEncoding],
|
||||
entry->uncompressedSize,
|
||||
getData,
|
||||
writeToPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry = next;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
if (_freeCallback) {
|
||||
_freeCallback();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -114,6 +114,14 @@
|
|||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>zip</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.pkware.zip-archive</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
|
@ -174,24 +182,24 @@
|
|||
<integer>1</integer>
|
||||
<integer>2</integer>
|
||||
</array>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>GCSupportedGameControllers</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>ProfileName</key>
|
||||
<string>ExtendedGamepad</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>GCSupportsControllerUserInteraction</key>
|
||||
<true/>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>iPhoneOS</string>
|
||||
</array>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UIRequiresFullScreen</key>
|
||||
<true/>
|
||||
<key>GCSupportedGameControllers</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>ProfileName</key>
|
||||
<string>ExtendedGamepad</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>GCSupportsControllerUserInteraction</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
Loading…
Reference in New Issue