SameBoy | Accurate GB/GBC emulator |
| download: https://git.y1.nz/archives/sameboy.tar.gz | |
| README | Files | Log | Refs | LICENSE |
commit 2a29570f17757ab7fb93feb2bbf4c7e8c644634d parent de10338962d37b5cc69fe5bee8b148f5db476298 Author: Lior Halphon <LIJI32@gmail.com> Date: Sat, 23 Jul 2022 13:41:35 +0300 Merge remote-tracking branch 'origin/master' into openal Diffstat:
| D | Cocoa/AppDelegate.h | 25 | ------------------------- |
| D | Cocoa/AppDelegate.m | 583 | ------------------------------------------------------------------------------- |
| M | Cocoa/Document.h | 10 | +++++----- |
| M | Cocoa/Document.m | 2 | +- |
| A | Cocoa/GBApp.h | 26 | ++++++++++++++++++++++++++ |
| A | Cocoa/GBApp.m | 646 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | Cocoa/GBButtons.h | 10 | ++++++++-- |
| M | Cocoa/GBButtons.m | 2 | +- |
| M | Cocoa/GBPreferencesWindow.h | 2 | ++ |
| M | Cocoa/GBPreferencesWindow.m | 59 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- |
| M | Cocoa/GBView.h | 1 | + |
| M | Cocoa/Info.plist | 2 | +- |
| M | Cocoa/MainMenu.xib | 13 | ++++++------- |
| M | Cocoa/Preferences.xib | 164 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- |
| M | Cocoa/UpdateWindow.xib | 2 | +- |
| M | Core/camera.c | 35 | +++++++++++++++++++++++++---------- |
| M | Core/debugger.c | 2 | +- |
| M | Core/display.c | 37 | +++++++++++++++++++++++++++++++------ |
| M | Core/gb.h | 2 | ++ |
| M | Core/memory.c | 18 | +++++++++++++++--- |
| M | Core/save_state.h | 7 | ++++++- |
| M | Core/sm83_cpu.c | 3 | +++ |
| M | Core/timing.c | 24 | ++++++++++++++++++++++++ |
| M | SDL/configuration.c | 3 | +++ |
| M | SDL/configuration.h | 34 | +++++++++++++++++++++++++++++++++- |
| M | SDL/gui.c | 55 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | SDL/main.c | 35 | +++++++++++++++++++++++++++++++++++ |
27 files changed, 1142 insertions(+), 660 deletions(-)
diff --git a/Cocoa/AppDelegate.h b/Cocoa/AppDelegate.h @@ -1,25 +0,0 @@ -#import <Cocoa/Cocoa.h> -#import <WebKit/WebKit.h> - -@interface AppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate, NSMenuDelegate, WebUIDelegate, WebPolicyDelegate, WebFrameLoadDelegate> - -@property (nonatomic, strong) IBOutlet NSWindow *preferencesWindow; -@property (nonatomic, strong) IBOutlet NSView *graphicsTab; -@property (nonatomic, strong) IBOutlet NSView *emulationTab; -@property (nonatomic, strong) IBOutlet NSView *audioTab; -@property (nonatomic, strong) IBOutlet NSView *controlsTab; -@property (nonatomic, strong) IBOutlet NSView *updatesTab; -- (IBAction)showPreferences: (id) sender; -- (IBAction)toggleDeveloperMode:(id)sender; -- (IBAction)switchPreferencesTab:(id)sender; -@property (nonatomic, weak) IBOutlet NSMenuItem *linkCableMenuItem; -@property (nonatomic, strong) IBOutlet NSWindow *updateWindow; -@property (nonatomic, strong) IBOutlet WebView *updateChanges; -@property (nonatomic, strong) IBOutlet NSProgressIndicator *updatesSpinner; -@property (strong) IBOutlet NSButton *updatesButton; -@property (strong) IBOutlet NSTextField *updateProgressLabel; -@property (strong) IBOutlet NSButton *updateProgressButton; -@property (strong) IBOutlet NSWindow *updateProgressWindow; -@property (strong) IBOutlet NSProgressIndicator *updateProgressSpinner; -@end - diff --git a/Cocoa/AppDelegate.m b/Cocoa/AppDelegate.m @@ -1,583 +0,0 @@ -#import "AppDelegate.h" -#include "GBButtons.h" -#include "GBView.h" -#include <Core/gb.h> -#import <Carbon/Carbon.h> -#import <JoyKit/JoyKit.h> -#import <WebKit/WebKit.h> -#import <mach-o/dyld.h> - -#define UPDATE_SERVER "https://sameboy.github.io" - -static uint32_t color_to_int(NSColor *color) -{ - color = [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; - return (((unsigned)(color.redComponent * 0xFF)) << 16) | - (((unsigned)(color.greenComponent * 0xFF)) << 8) | - ((unsigned)(color.blueComponent * 0xFF)); -} - -@implementation AppDelegate -{ - NSWindow *preferences_window; - NSArray<NSView *> *preferences_tabs; - NSString *_lastVersion; - NSString *_updateURL; - NSURLSessionDownloadTask *_updateTask; - enum { - UPDATE_DOWNLOADING, - UPDATE_EXTRACTING, - UPDATE_WAIT_INSTALL, - UPDATE_INSTALLING, - UPDATE_FAILED, - } _updateState; - NSString *_downloadDirectory; - AuthorizationRef _auth; -} - -- (void) applicationDidFinishLaunching:(NSNotification *)notification -{ - // Refresh icon if launched via a software update - [NSApplication sharedApplication].applicationIconImage = [NSImage imageNamed:@"AppIcon"]; - - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - for (unsigned i = 0; i < GBButtonCount; i++) { - if ([[defaults objectForKey:button_to_preference_name(i, 0)] isKindOfClass:[NSString class]]) { - [defaults removeObjectForKey:button_to_preference_name(i, 0)]; - } - } - [[NSUserDefaults standardUserDefaults] registerDefaults:@{ - @"GBRight": @(kVK_RightArrow), - @"GBLeft": @(kVK_LeftArrow), - @"GBUp": @(kVK_UpArrow), - @"GBDown": @(kVK_DownArrow), - - @"GBA": @(kVK_ANSI_X), - @"GBB": @(kVK_ANSI_Z), - @"GBSelect": @(kVK_Delete), - @"GBStart": @(kVK_Return), - - @"GBTurbo": @(kVK_Space), - @"GBRewind": @(kVK_Tab), - @"GBSlow-Motion": @(kVK_Shift), - - @"GBFilter": @"NearestNeighbor", - @"GBColorCorrection": @(GB_COLOR_CORRECTION_EMULATE_HARDWARE), - @"GBHighpassFilter": @(GB_HIGHPASS_REMOVE_DC_OFFSET), - @"GBRewindLength": @(10), - @"GBFrameBlendingMode": @([defaults boolForKey:@"DisableFrameBlending"]? GB_FRAME_BLENDING_MODE_DISABLED : GB_FRAME_BLENDING_MODE_ACCURATE), - - @"GBDMGModel": @(GB_MODEL_DMG_B), - @"GBCGBModel": @(GB_MODEL_CGB_E), - @"GBSGBModel": @(GB_MODEL_SGB2), - @"GBRumbleMode": @(GB_RUMBLE_CARTRIDGE_ONLY), - - @"GBVolume": @(1.0), - - @"GBMBC7JoystickOverride": @NO, - @"GBMBC7AllowMouse": @YES, - - // Default themes - @"GBThemes": @{ - @"Desert": @{ - @"BrightnessBias": @0.0, - @"Colors": @[@0xff302f3e, @0xff576674, @0xff839ba4, @0xffb1d0d2, @0xffb7d7d8], - @"DisabledLCDColor": @YES, - @"HueBias": @0.10087773904382469, - @"HueBiasStrength": @0.062142056772908363, - @"Manual": @NO, - }, - @"Evening": @{ - @"BrightnessBias": @-0.10168700106441975, - @"Colors": @[@0xff362601, @0xff695518, @0xff899853, @0xffa6e4ae, @0xffa9eebb], - @"DisabledLCDColor": @YES, - @"HueBias": @0.60027079191058874, - @"HueBiasStrength": @0.33816297305747867, - @"Manual": @NO, - }, - @"Fog": @{ - @"BrightnessBias": @0.0, - @"Colors": @[@0xff373c34, @0xff737256, @0xff9da386, @0xffc3d2bf, @0xffc7d8c6], - @"DisabledLCDColor": @YES, - @"HueBias": @0.55750435756972117, - @"HueBiasStrength": @0.18424738545816732, - @"Manual": @NO, - }, - @"Magic Eggplant": @{ - @"BrightnessBias": @0.0, - @"Colors": @[@0xff3c2136, @0xff942e84, @0xffc7699d, @0xfff1e4b0, @0xfff6f9b2], - @"DisabledLCDColor": @YES, - @"HueBias": @0.87717878486055778, - @"HueBiasStrength": @0.65018052788844627, - @"Manual": @NO, - }, - @"Radioactive Pea": @{ - @"BrightnessBias": @-0.48079556772908372, - @"Colors": @[@0xff215200, @0xff1f7306, @0xff169e34, @0xff03ceb8, @0xff00d4d1], - @"DisabledLCDColor": @YES, - @"HueBias": @0.3795131972111554, - @"HueBiasStrength": @0.34337649402390436, - @"Manual": @NO, - }, - @"Seaweed": @{ - @"BrightnessBias": @-0.28532744023904377, - @"Colors": @[@0xff3f0015, @0xff426532, @0xff58a778, @0xff95e0df, @0xffa0e7ee], - @"DisabledLCDColor": @YES, - @"HueBias": @0.2694067480079681, - @"HueBiasStrength": @0.51565612549800799, - @"Manual": @NO, - }, - @"Twilight": @{ - @"BrightnessBias": @-0.091789093625498031, - @"Colors": @[@0xff3f0015, @0xff461286, @0xff6254bd, @0xff97d3e9, @0xffa0e7ee], - @"DisabledLCDColor": @YES, - @"HueBias": @0.0, - @"HueBiasStrength": @0.49710532868525897, - @"Manual": @NO, - }, - }, - - @"NSToolbarItemForcesStandardSize": @YES, // Forces Monterey to resepect toolbar item sizes - }]; - - [JOYController startOnRunLoop:[NSRunLoop currentRunLoop] withOptions:@{ - JOYAxes2DEmulateButtonsKey: @YES, - JOYHatsEmulateButtonsKey: @YES, - }]; - - if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { - [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; - } - - [self askAutoUpdates]; - - if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBAutoUpdatesEnabled"]) { - [self checkForUpdates]; - } - - if ([[NSProcessInfo processInfo].arguments containsObject:@"--update-launch"]) { - [NSApp activateIgnoringOtherApps:true]; - } -} - -- (IBAction)toggleDeveloperMode:(id)sender -{ - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - [defaults setBool:![defaults boolForKey:@"DeveloperMode"] forKey:@"DeveloperMode"]; -} - -- (IBAction)switchPreferencesTab:(id)sender -{ - for (NSView *view in preferences_tabs) { - [view removeFromSuperview]; - } - NSView *tab = preferences_tabs[[sender tag]]; - NSRect old = [_preferencesWindow frame]; - NSRect new = [_preferencesWindow frameRectForContentRect:tab.frame]; - new.origin.x = old.origin.x; - new.origin.y = old.origin.y + (old.size.height - new.size.height); - [_preferencesWindow setFrame:new display:true animate:_preferencesWindow.visible]; - [_preferencesWindow.contentView addSubview:tab]; -} - -- (BOOL)validateMenuItem:(NSMenuItem *)anItem -{ - if ([anItem action] == @selector(toggleDeveloperMode:)) { - [(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]]; - } - - if (anItem == self.linkCableMenuItem) { - return [[NSDocumentController sharedDocumentController] documents].count > 1; - } - return true; -} - -- (void)menuNeedsUpdate:(NSMenu *)menu -{ - NSMutableArray *items = [NSMutableArray array]; - NSDocument *currentDocument = [[NSDocumentController sharedDocumentController] currentDocument]; - - for (NSDocument *document in [[NSDocumentController sharedDocumentController] documents]) { - if (document == currentDocument) continue; - NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:document.displayName action:@selector(connectLinkCable:) keyEquivalent:@""]; - item.representedObject = document; - item.image = [[NSWorkspace sharedWorkspace] iconForFile:document.fileURL.path]; - [item.image setSize:NSMakeSize(16, 16)]; - [items addObject:item]; - } - menu.itemArray = items; -} - -- (IBAction) showPreferences: (id) sender -{ - NSArray *objects; - if (!_preferencesWindow) { - [[NSBundle mainBundle] loadNibNamed:@"Preferences" owner:self topLevelObjects:&objects]; - NSToolbarItem *first_toolbar_item = [_preferencesWindow.toolbar.items firstObject]; - _preferencesWindow.toolbar.selectedItemIdentifier = [first_toolbar_item itemIdentifier]; - preferences_tabs = @[self.emulationTab, self.graphicsTab, self.audioTab, self.controlsTab, self.updatesTab]; - [self switchPreferencesTab:first_toolbar_item]; - [_preferencesWindow center]; -#ifndef UPDATE_SUPPORT - [_preferencesWindow.toolbar removeItemAtIndex:4]; -#endif - if (@available(macOS 11.0, *)) { - [_preferencesWindow.toolbar insertItemWithItemIdentifier:NSToolbarFlexibleSpaceItemIdentifier atIndex:0]; - [_preferencesWindow.toolbar insertItemWithItemIdentifier:NSToolbarFlexibleSpaceItemIdentifier atIndex:_preferencesWindow.toolbar.items.count]; - } - } - [_preferencesWindow makeKeyAndOrderFront:self]; -} - -- (BOOL)applicationOpenUntitledFile:(NSApplication *)sender -{ - [self askAutoUpdates]; - /* Bring an existing panel to the foreground */ - for (NSWindow *window in [[NSApplication sharedApplication] windows]) { - if ([window isKindOfClass:[NSOpenPanel class]]) { - [(NSOpenPanel *)window makeKeyAndOrderFront:nil]; - return true; - } - } - [[NSDocumentController sharedDocumentController] openDocument:self]; - return true; -} - -- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification -{ - [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:true]; -} - -- (void)updateFound -{ - [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/raw_changes"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - - NSColor *linkColor = [NSColor colorWithRed:0.125 green:0.325 blue:1.0 alpha:1.0]; - if (@available(macOS 10.10, *)) { - linkColor = [NSColor linkColor]; - } - - NSString *changes = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - NSRange cutoffRange = [changes rangeOfString:@"<!--(" GB_VERSION ")-->"]; - if (cutoffRange.location != NSNotFound) { - changes = [changes substringToIndex:cutoffRange.location]; - } - - NSString *html = [NSString stringWithFormat:@"<!DOCTYPE html><html><head><title></title>" - "<style>html {background-color:transparent; color: #%06x; line-height:1.5} a:link, a:visited{color:#%06x; text-decoration:none}</style>" - "</head><body>%@</body></html>", - color_to_int([NSColor textColor]), - color_to_int(linkColor), - changes]; - - if ([(NSHTTPURLResponse *)response statusCode] == 200) { - dispatch_async(dispatch_get_main_queue(), ^{ - NSArray *objects; - [[NSBundle mainBundle] loadNibNamed:@"UpdateWindow" owner:self topLevelObjects:&objects]; - self.updateChanges.preferences.standardFontFamily = [NSFont systemFontOfSize:0].familyName; - self.updateChanges.preferences.fixedFontFamily = @"Menlo"; - self.updateChanges.drawsBackground = false; - [self.updateChanges.mainFrame loadHTMLString:html baseURL:nil]; - }); - } - }] resume]; -} - -- (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems -{ - // Disable reload context menu - if ([defaultMenuItems count] <= 2) { - return nil; - } - return defaultMenuItems; -} - -- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame -{ - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - sender.mainFrame.frameView.documentView.enclosingScrollView.drawsBackground = true; - sender.mainFrame.frameView.documentView.enclosingScrollView.backgroundColor = [NSColor textBackgroundColor]; - sender.policyDelegate = self; - [self.updateWindow center]; - [self.updateWindow makeKeyAndOrderFront:nil]; - }); -} - -- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener -{ - [listener ignore]; - [[NSWorkspace sharedWorkspace] openURL:[request URL]]; -} - -- (void)checkForUpdates -{ -#ifdef UPDATE_SUPPORT - [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/latest_version"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self.updatesSpinner stopAnimation:nil]; - [self.updatesButton setEnabled:true]; - }); - if ([(NSHTTPURLResponse *)response statusCode] == 200) { - NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - NSArray <NSString *> *components = [string componentsSeparatedByString:@"|"]; - if (components.count != 2) return; - _lastVersion = components[0]; - _updateURL = components[1]; - if (![@GB_VERSION isEqualToString:_lastVersion] && - ![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBSkippedVersion"] isEqualToString:_lastVersion]) { - [self updateFound]; - } - } - }] resume]; -#endif -} - -- (IBAction)userCheckForUpdates:(id)sender -{ - if (self.updateWindow) { - [self.updateWindow makeKeyAndOrderFront:sender]; - } - else { - [[NSUserDefaults standardUserDefaults] setObject:nil forKey:@"GBSkippedVersion"]; - [self checkForUpdates]; - [sender setEnabled:false]; - [self.updatesSpinner startAnimation:sender]; - } -} - -- (void)askAutoUpdates -{ -#ifdef UPDATE_SUPPORT - if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAskedAutoUpdates"]) { - NSAlert *alert = [[NSAlert alloc] init]; - alert.messageText = @"Should SameBoy check for updates when launched?"; - alert.informativeText = @"SameBoy is frequently updated with new features, accuracy improvements, and bug fixes. This setting can always be changed in the preferences window."; - [alert addButtonWithTitle:@"Check on Launch"]; - [alert addButtonWithTitle:@"Don't Check on Launch"]; - - [[NSUserDefaults standardUserDefaults] setBool:[alert runModal] == NSAlertFirstButtonReturn forKey:@"GBAutoUpdatesEnabled"]; - [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBAskedAutoUpdates"]; - } -#endif -} - -- (IBAction)skipVersion:(id)sender -{ - [[NSUserDefaults standardUserDefaults] setObject:_lastVersion forKey:@"GBSkippedVersion"]; - [self.updateWindow performClose:sender]; -} - -- (bool)executePath:(NSString *)path withArguments:(NSArray <NSString *> *)arguments -{ - if (!_auth) { - NSTask *task = [[NSTask alloc] init]; - task.launchPath = path; - task.arguments = arguments; - [task launch]; - [task waitUntilExit]; - return task.terminationStatus == 0 && task.terminationReason == NSTaskTerminationReasonExit; - } - - char *argv[arguments.count + 1]; - argv[arguments.count] = NULL; - for (unsigned i = 0; i < arguments.count; i++) { - argv[i] = (char *)arguments[i].UTF8String; - } - - return AuthorizationExecuteWithPrivileges(_auth, path.UTF8String, kAuthorizationFlagDefaults, argv, NULL) == errAuthorizationSuccess; -} - -- (void)deauthorize -{ - if (_auth) { - AuthorizationFree(_auth, kAuthorizationFlagDefaults); - _auth = nil; - } -} - -- (IBAction)installUpdate:(id)sender -{ - bool needsAuthorization = false; - if ([self executePath:@"/usr/sbin/spctl" withArguments:@[@"--status"]]) { // Succeeds when GateKeeper is on - // GateKeeper is on, we need to --add ourselves as root, else we might get a GateKeeper crash - needsAuthorization = true; - } - else if (access(_dyld_get_image_name(0), W_OK)) { - // We don't have write access, so we need authorization to update as root - needsAuthorization = true; - } - - if (needsAuthorization && !_auth) { - AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagPreAuthorize | kAuthorizationFlagInteractionAllowed, &_auth); - // Make sure we can modify the bundle - if (![self executePath:@"/usr/sbin/chown" withArguments:@[@"-R", [NSString stringWithFormat:@"%d:%d", getuid(), getgid()], [NSBundle mainBundle].bundlePath]]) { - [self deauthorize]; - return; - } - } - - [self.updateProgressSpinner startAnimation:nil]; - self.updateProgressButton.title = @"Cancel"; - self.updateProgressButton.enabled = true; - self.updateProgressLabel.stringValue = @"Downloading update..."; - _updateState = UPDATE_DOWNLOADING; - _updateTask = [[NSURLSession sharedSession] downloadTaskWithURL: [NSURL URLWithString:_updateURL] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { - _updateTask = nil; - dispatch_sync(dispatch_get_main_queue(), ^{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Extracting update..."; - _updateState = UPDATE_EXTRACTING; - }); - - _downloadDirectory = [[[NSFileManager defaultManager] URLForDirectory:NSItemReplacementDirectory - inDomain:NSUserDomainMask - appropriateForURL:[[NSBundle mainBundle] bundleURL] - create:true - error:nil] path]; - if (!_downloadDirectory) { - [self deauthorize]; - dispatch_sync(dispatch_get_main_queue(), ^{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Failed to extract update."; - _updateState = UPDATE_FAILED; - self.updateProgressButton.title = @"Close"; - self.updateProgressButton.enabled = true; - [self.updateProgressSpinner stopAnimation:nil]; - }); - return; - } - - NSTask *unzipTask; - unzipTask = [[NSTask alloc] init]; - unzipTask.launchPath = @"/usr/bin/unzip"; - unzipTask.arguments = @[location.path, @"-d", _downloadDirectory]; - [unzipTask launch]; - [unzipTask waitUntilExit]; - if (unzipTask.terminationStatus != 0 || unzipTask.terminationReason != NSTaskTerminationReasonExit) { - [self deauthorize]; - [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; - dispatch_sync(dispatch_get_main_queue(), ^{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Failed to extract update."; - _updateState = UPDATE_FAILED; - self.updateProgressButton.title = @"Close"; - self.updateProgressButton.enabled = true; - [self.updateProgressSpinner stopAnimation:nil]; - }); - return; - } - - dispatch_sync(dispatch_get_main_queue(), ^{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Update ready, save your game progress and click Install."; - _updateState = UPDATE_WAIT_INSTALL; - self.updateProgressButton.title = @"Install"; - self.updateProgressButton.enabled = true; - [self.updateProgressSpinner stopAnimation:nil]; - }); - }]; - [_updateTask resume]; - - self.updateProgressWindow.preventsApplicationTerminationWhenModal = false; - [self.updateWindow beginSheet:self.updateProgressWindow completionHandler:^(NSModalResponse returnCode) { - [self.updateWindow close]; - }]; -} - -- (void)performUpgrade -{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Instaling update..."; - _updateState = UPDATE_INSTALLING; - self.updateProgressButton.enabled = false; - [self.updateProgressSpinner startAnimation:nil]; - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - NSString *executablePath = [[NSBundle mainBundle] executablePath]; - NSString *contentsPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Contents"]; - NSString *contentsTempPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"TempContents"]; - NSString *updateContentsPath = [_downloadDirectory stringByAppendingPathComponent:@"SameBoy.app/Contents"]; - NSError *error = nil; - [[NSFileManager defaultManager] moveItemAtPath:contentsPath toPath:contentsTempPath error:&error]; - if (error) { - [self deauthorize]; - [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; - _downloadDirectory = nil; - dispatch_sync(dispatch_get_main_queue(), ^{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Failed to install update."; - _updateState = UPDATE_FAILED; - self.updateProgressButton.title = @"Close"; - self.updateProgressButton.enabled = true; - [self.updateProgressSpinner stopAnimation:nil]; - }); - return; - } - [[NSFileManager defaultManager] moveItemAtPath:updateContentsPath toPath:contentsPath error:&error]; - if (error) { - [self deauthorize]; - [[NSFileManager defaultManager] moveItemAtPath:contentsTempPath toPath:contentsPath error:nil]; - [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; - _downloadDirectory = nil; - dispatch_sync(dispatch_get_main_queue(), ^{ - self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Failed to install update."; - _updateState = UPDATE_FAILED; - self.updateProgressButton.title = @"Close"; - self.updateProgressButton.enabled = true; - [self.updateProgressSpinner stopAnimation:nil]; - }); - return; - } - [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; - [[NSFileManager defaultManager] removeItemAtPath:contentsTempPath error:nil]; - _downloadDirectory = nil; - atexit_b(^{ - execl(executablePath.UTF8String, executablePath.UTF8String, "--update-launch", NULL); - }); - - dispatch_async(dispatch_get_main_queue(), ^{ - [NSApp terminate:nil]; - }); - }); -} - -- (IBAction)updateAction:(id)sender -{ - switch (_updateState) { - case UPDATE_DOWNLOADING: - [_updateTask cancelByProducingResumeData:nil]; - _updateTask = nil; - [self.updateProgressWindow close]; - break; - case UPDATE_WAIT_INSTALL: - [self performUpgrade]; - break; - case UPDATE_EXTRACTING: - case UPDATE_INSTALLING: - break; - case UPDATE_FAILED: - [self.updateProgressWindow close]; - break; - } -} - -- (void)orderFrontAboutPanel:(id)sender -{ - // NSAboutPanelOptionApplicationIcon is not available prior to 10.13, but the key is still there and working. - [[NSApplication sharedApplication] orderFrontStandardAboutPanelWithOptions:@{ - @"ApplicationIcon": [NSImage imageNamed:@"Icon"] - }]; -} - -- (void)dealloc -{ - if (_downloadDirectory) { - [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; - } -} - -- (IBAction)nop:(id)sender -{ -} -@end diff --git a/Cocoa/Document.h b/Cocoa/Document.h @@ -1,9 +1,9 @@ #import <Cocoa/Cocoa.h> -#include "GBView.h" -#include "GBImageView.h" -#include "GBSplitView.h" -#include "GBVisualizerView.h" -#include "GBOSDView.h" +#import "GBView.h" +#import "GBImageView.h" +#import "GBSplitView.h" +#import "GBVisualizerView.h" +#import "GBOSDView.h" @class GBCheatWindowController; @class GBPaletteView; diff --git a/Cocoa/Document.m b/Cocoa/Document.m @@ -3,7 +3,7 @@ #import <Core/gb.h> #import "GBAudioClient.h" #import "Document.h" -#import "AppDelegate.h" +#import "GBApp.h" #import "HexFiend/HexFiend.h" #import "GBMemoryByteArray.h" #import "GBWarningPopover.h" diff --git a/Cocoa/GBApp.h b/Cocoa/GBApp.h @@ -0,0 +1,26 @@ +#import <Cocoa/Cocoa.h> +#import <WebKit/WebKit.h> +#import <JoyKit/JoyKit.h> + +@interface GBApp : NSApplication <NSApplicationDelegate, NSUserNotificationCenterDelegate, NSMenuDelegate, WebUIDelegate, WebPolicyDelegate, WebFrameLoadDelegate, JOYListener> + +@property (nonatomic, strong) IBOutlet NSWindow *preferencesWindow; +@property (nonatomic, strong) IBOutlet NSView *graphicsTab; +@property (nonatomic, strong) IBOutlet NSView *emulationTab; +@property (nonatomic, strong) IBOutlet NSView *audioTab; +@property (nonatomic, strong) IBOutlet NSView *controlsTab; +@property (nonatomic, strong) IBOutlet NSView *updatesTab; +- (IBAction)showPreferences: (id) sender; +- (IBAction)toggleDeveloperMode:(id)sender; +- (IBAction)switchPreferencesTab:(id)sender; +@property (nonatomic, weak) IBOutlet NSMenuItem *linkCableMenuItem; +@property (nonatomic, strong) IBOutlet NSWindow *updateWindow; +@property (nonatomic, strong) IBOutlet WebView *updateChanges; +@property (nonatomic, strong) IBOutlet NSProgressIndicator *updatesSpinner; +@property (strong) IBOutlet NSButton *updatesButton; +@property (strong) IBOutlet NSTextField *updateProgressLabel; +@property (strong) IBOutlet NSButton *updateProgressButton; +@property (strong) IBOutlet NSWindow *updateProgressWindow; +@property (strong) IBOutlet NSProgressIndicator *updateProgressSpinner; +@end + diff --git a/Cocoa/GBApp.m b/Cocoa/GBApp.m @@ -0,0 +1,646 @@ +#import "GBApp.h" +#include "GBButtons.h" +#include "GBView.h" +#include "Document.h" +#include <Core/gb.h> +#import <Carbon/Carbon.h> +#import <JoyKit/JoyKit.h> +#import <WebKit/WebKit.h> +#import <mach-o/dyld.h> + +#define UPDATE_SERVER "https://sameboy.github.io" + +static uint32_t color_to_int(NSColor *color) +{ + color = [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + return (((unsigned)(color.redComponent * 0xFF)) << 16) | + (((unsigned)(color.greenComponent * 0xFF)) << 8) | + ((unsigned)(color.blueComponent * 0xFF)); +} + +@implementation GBApp +{ + NSArray<NSView *> *_preferencesTabs; + NSString *_lastVersion; + NSString *_updateURL; + NSURLSessionDownloadTask *_updateTask; + enum { + UPDATE_DOWNLOADING, + UPDATE_EXTRACTING, + UPDATE_WAIT_INSTALL, + UPDATE_INSTALLING, + UPDATE_FAILED, + } _updateState; + NSString *_downloadDirectory; + AuthorizationRef _auth; + bool _simulatingMenuEvent; +} + +- (void) applicationDidFinishLaunching:(NSNotification *)notification +{ + // Refresh icon if launched via a software update + [NSApplication sharedApplication].applicationIconImage = [NSImage imageNamed:@"AppIcon"]; + + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + for (unsigned i = 0; i < GBButtonCount; i++) { + if ([[defaults objectForKey:button_to_preference_name(i, 0)] isKindOfClass:[NSString class]]) { + [defaults removeObjectForKey:button_to_preference_name(i, 0)]; + } + } + [[NSUserDefaults standardUserDefaults] registerDefaults:@{ + @"GBRight": @(kVK_RightArrow), + @"GBLeft": @(kVK_LeftArrow), + @"GBUp": @(kVK_UpArrow), + @"GBDown": @(kVK_DownArrow), + + @"GBA": @(kVK_ANSI_X), + @"GBB": @(kVK_ANSI_Z), + @"GBSelect": @(kVK_Delete), + @"GBStart": @(kVK_Return), + + @"GBTurbo": @(kVK_Space), + @"GBRewind": @(kVK_Tab), + @"GBSlow-Motion": @(kVK_Shift), + + @"GBFilter": @"NearestNeighbor", + @"GBColorCorrection": @(GB_COLOR_CORRECTION_EMULATE_HARDWARE), + @"GBHighpassFilter": @(GB_HIGHPASS_REMOVE_DC_OFFSET), + @"GBRewindLength": @(10), + @"GBFrameBlendingMode": @([defaults boolForKey:@"DisableFrameBlending"]? GB_FRAME_BLENDING_MODE_DISABLED : GB_FRAME_BLENDING_MODE_ACCURATE), + + @"GBDMGModel": @(GB_MODEL_DMG_B), + @"GBCGBModel": @(GB_MODEL_CGB_E), + @"GBSGBModel": @(GB_MODEL_SGB2), + @"GBRumbleMode": @(GB_RUMBLE_CARTRIDGE_ONLY), + + @"GBVolume": @(1.0), + + @"GBMBC7JoystickOverride": @NO, + @"GBMBC7AllowMouse": @YES, + + // Default themes + @"GBThemes": @{ + @"Desert": @{ + @"BrightnessBias": @0.0, + @"Colors": @[@0xff302f3e, @0xff576674, @0xff839ba4, @0xffb1d0d2, @0xffb7d7d8], + @"DisabledLCDColor": @YES, + @"HueBias": @0.10087773904382469, + @"HueBiasStrength": @0.062142056772908363, + @"Manual": @NO, + }, + @"Evening": @{ + @"BrightnessBias": @-0.10168700106441975, + @"Colors": @[@0xff362601, @0xff695518, @0xff899853, @0xffa6e4ae, @0xffa9eebb], + @"DisabledLCDColor": @YES, + @"HueBias": @0.60027079191058874, + @"HueBiasStrength": @0.33816297305747867, + @"Manual": @NO, + }, + @"Fog": @{ + @"BrightnessBias": @0.0, + @"Colors": @[@0xff373c34, @0xff737256, @0xff9da386, @0xffc3d2bf, @0xffc7d8c6], + @"DisabledLCDColor": @YES, + @"HueBias": @0.55750435756972117, + @"HueBiasStrength": @0.18424738545816732, + @"Manual": @NO, + }, + @"Magic Eggplant": @{ + @"BrightnessBias": @0.0, + @"Colors": @[@0xff3c2136, @0xff942e84, @0xffc7699d, @0xfff1e4b0, @0xfff6f9b2], + @"DisabledLCDColor": @YES, + @"HueBias": @0.87717878486055778, + @"HueBiasStrength": @0.65018052788844627, + @"Manual": @NO, + }, + @"Radioactive Pea": @{ + @"BrightnessBias": @-0.48079556772908372, + @"Colors": @[@0xff215200, @0xff1f7306, @0xff169e34, @0xff03ceb8, @0xff00d4d1], + @"DisabledLCDColor": @YES, + @"HueBias": @0.3795131972111554, + @"HueBiasStrength": @0.34337649402390436, + @"Manual": @NO, + }, + @"Seaweed": @{ + @"BrightnessBias": @-0.28532744023904377, + @"Colors": @[@0xff3f0015, @0xff426532, @0xff58a778, @0xff95e0df, @0xffa0e7ee], + @"DisabledLCDColor": @YES, + @"HueBias": @0.2694067480079681, + @"HueBiasStrength": @0.51565612549800799, + @"Manual": @NO, + }, + @"Twilight": @{ + @"BrightnessBias": @-0.091789093625498031, + @"Colors": @[@0xff3f0015, @0xff461286, @0xff6254bd, @0xff97d3e9, @0xffa0e7ee], + @"DisabledLCDColor": @YES, + @"HueBias": @0.0, + @"HueBiasStrength": @0.49710532868525897, + @"Manual": @NO, + }, + }, + + @"NSToolbarItemForcesStandardSize": @YES, // Forces Monterey to resepect toolbar item sizes + }]; + + [JOYController startOnRunLoop:[NSRunLoop currentRunLoop] withOptions:@{ + JOYAxes2DEmulateButtonsKey: @YES, + JOYHatsEmulateButtonsKey: @YES, + }]; + + [JOYController registerListener:self]; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { + [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; + } + + [self askAutoUpdates]; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBAutoUpdatesEnabled"]) { + [self checkForUpdates]; + } + + if ([[NSProcessInfo processInfo].arguments containsObject:@"--update-launch"]) { + [NSApp activateIgnoringOtherApps:true]; + } +} + +- (IBAction)toggleDeveloperMode:(id)sender +{ + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + [defaults setBool:![defaults boolForKey:@"DeveloperMode"] forKey:@"DeveloperMode"]; +} + +- (IBAction)switchPreferencesTab:(id)sender +{ + for (NSView *view in _preferencesTabs) { + [view removeFromSuperview]; + } + NSView *tab = _preferencesTabs[[sender tag]]; + NSRect old = [_preferencesWindow frame]; + NSRect new = [_preferencesWindow frameRectForContentRect:tab.frame]; + new.origin.x = old.origin.x; + new.origin.y = old.origin.y + (old.size.height - new.size.height); + [_preferencesWindow setFrame:new display:true animate:_preferencesWindow.visible]; + [_preferencesWindow.contentView addSubview:tab]; +} + +- (BOOL)validateMenuItem:(NSMenuItem *)anItem +{ + if ([anItem action] == @selector(toggleDeveloperMode:)) { + [(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]]; + } + + if (anItem == self.linkCableMenuItem) { + return [[NSDocumentController sharedDocumentController] documents].count > 1; + } + return true; +} + +- (void)menuNeedsUpdate:(NSMenu *)menu +{ + NSMutableArray *items = [NSMutableArray array]; + NSDocument *currentDocument = [[NSDocumentController sharedDocumentController] currentDocument]; + + for (NSDocument *document in [[NSDocumentController sharedDocumentController] documents]) { + if (document == currentDocument) continue; + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:document.displayName action:@selector(connectLinkCable:) keyEquivalent:@""]; + item.representedObject = document; + item.image = [[NSWorkspace sharedWorkspace] iconForFile:document.fileURL.path]; + [item.image setSize:NSMakeSize(16, 16)]; + [items addObject:item]; + } + menu.itemArray = items; +} + +- (IBAction) showPreferences: (id) sender +{ + NSArray *objects; + if (!_preferencesWindow) { + [[NSBundle mainBundle] loadNibNamed:@"Preferences" owner:self topLevelObjects:&objects]; + NSToolbarItem *first_toolbar_item = [_preferencesWindow.toolbar.items firstObject]; + _preferencesWindow.toolbar.selectedItemIdentifier = [first_toolbar_item itemIdentifier]; + _preferencesTabs = @[self.emulationTab, self.graphicsTab, self.audioTab, self.controlsTab, self.updatesTab]; + [self switchPreferencesTab:first_toolbar_item]; + [_preferencesWindow center]; +#ifndef UPDATE_SUPPORT + [_preferencesWindow.toolbar removeItemAtIndex:4]; +#endif + if (@available(macOS 11.0, *)) { + [_preferencesWindow.toolbar insertItemWithItemIdentifier:NSToolbarFlexibleSpaceItemIdentifier atIndex:0]; + [_preferencesWindow.toolbar insertItemWithItemIdentifier:NSToolbarFlexibleSpaceItemIdentifier atIndex:_preferencesWindow.toolbar.items.count]; + } + } + [_preferencesWindow makeKeyAndOrderFront:self]; +} + +- (BOOL)applicationOpenUntitledFile:(NSApplication *)sender +{ + [self askAutoUpdates]; + /* Bring an existing panel to the foreground */ + for (NSWindow *window in [[NSApplication sharedApplication] windows]) { + if ([window isKindOfClass:[NSOpenPanel class]]) { + [(NSOpenPanel *)window makeKeyAndOrderFront:nil]; + return true; + } + } + [[NSDocumentController sharedDocumentController] openDocument:self]; + return true; +} + +- (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification +{ + [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:true]; +} + +- (void)updateFound +{ + [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/raw_changes"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + + NSColor *linkColor = [NSColor colorWithRed:0.125 green:0.325 blue:1.0 alpha:1.0]; + if (@available(macOS 10.10, *)) { + linkColor = [NSColor linkColor]; + } + + NSString *changes = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSRange cutoffRange = [changes rangeOfString:@"<!--(" GB_VERSION ")-->"]; + if (cutoffRange.location != NSNotFound) { + changes = [changes substringToIndex:cutoffRange.location]; + } + + NSString *html = [NSString stringWithFormat:@"<!DOCTYPE html><html><head><title></title>" + "<style>html {background-color:transparent; color: #%06x; line-height:1.5} a:link, a:visited{color:#%06x; text-decoration:none}</style>" + "</head><body>%@</body></html>", + color_to_int([NSColor textColor]), + color_to_int(linkColor), + changes]; + + if ([(NSHTTPURLResponse *)response statusCode] == 200) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSArray *objects; + [[NSBundle mainBundle] loadNibNamed:@"UpdateWindow" owner:self topLevelObjects:&objects]; + self.updateChanges.preferences.standardFontFamily = [NSFont systemFontOfSize:0].familyName; + self.updateChanges.preferences.fixedFontFamily = @"Menlo"; + self.updateChanges.drawsBackground = false; + [self.updateChanges.mainFrame loadHTMLString:html baseURL:nil]; + }); + } + }] resume]; +} + +- (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems +{ + // Disable reload context menu + if ([defaultMenuItems count] <= 2) { + return nil; + } + return defaultMenuItems; +} + +- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sender.mainFrame.frameView.documentView.enclosingScrollView.drawsBackground = true; + sender.mainFrame.frameView.documentView.enclosingScrollView.backgroundColor = [NSColor textBackgroundColor]; + sender.policyDelegate = self; + [self.updateWindow center]; + [self.updateWindow makeKeyAndOrderFront:nil]; + }); +} + +- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener +{ + [listener ignore]; + [[NSWorkspace sharedWorkspace] openURL:[request URL]]; +} + +- (void)checkForUpdates +{ +#ifdef UPDATE_SUPPORT + [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/latest_version"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.updatesSpinner stopAnimation:nil]; + [self.updatesButton setEnabled:true]; + }); + if ([(NSHTTPURLResponse *)response statusCode] == 200) { + NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSArray <NSString *> *components = [string componentsSeparatedByString:@"|"]; + if (components.count != 2) return; + _lastVersion = components[0]; + _updateURL = components[1]; + if (![@GB_VERSION isEqualToString:_lastVersion] && + ![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBSkippedVersion"] isEqualToString:_lastVersion]) { + [self updateFound]; + } + } + }] resume]; +#endif +} + +- (IBAction)userCheckForUpdates:(id)sender +{ + if (self.updateWindow) { + [self.updateWindow makeKeyAndOrderFront:sender]; + } + else { + [[NSUserDefaults standardUserDefaults] setObject:nil forKey:@"GBSkippedVersion"]; + [self checkForUpdates]; + [sender setEnabled:false]; + [self.updatesSpinner startAnimation:sender]; + } +} + +- (void)askAutoUpdates +{ +#ifdef UPDATE_SUPPORT + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAskedAutoUpdates"]) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.messageText = @"Should SameBoy check for updates when launched?"; + alert.informativeText = @"SameBoy is frequently updated with new features, accuracy improvements, and bug fixes. This setting can always be changed in the preferences window."; + [alert addButtonWithTitle:@"Check on Launch"]; + [alert addButtonWithTitle:@"Don't Check on Launch"]; + + [[NSUserDefaults standardUserDefaults] setBool:[alert runModal] == NSAlertFirstButtonReturn forKey:@"GBAutoUpdatesEnabled"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBAskedAutoUpdates"]; + } +#endif +} + +- (IBAction)skipVersion:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:_lastVersion forKey:@"GBSkippedVersion"]; + [self.updateWindow performClose:sender]; +} + +- (bool)executePath:(NSString *)path withArguments:(NSArray <NSString *> *)arguments +{ + if (!_auth) { + NSTask *task = [[NSTask alloc] init]; + task.launchPath = path; + task.arguments = arguments; + [task launch]; + [task waitUntilExit]; + return task.terminationStatus == 0 && task.terminationReason == NSTaskTerminationReasonExit; + } + + char *argv[arguments.count + 1]; + argv[arguments.count] = NULL; + for (unsigned i = 0; i < arguments.count; i++) { + argv[i] = (char *)arguments[i].UTF8String; + } + + return AuthorizationExecuteWithPrivileges(_auth, path.UTF8String, kAuthorizationFlagDefaults, argv, NULL) == errAuthorizationSuccess; +} + +- (void)deauthorize +{ + if (_auth) { + AuthorizationFree(_auth, kAuthorizationFlagDefaults); + _auth = nil; + } +} + +- (IBAction)installUpdate:(id)sender +{ + bool needsAuthorization = false; + if ([self executePath:@"/usr/sbin/spctl" withArguments:@[@"--status"]]) { // Succeeds when GateKeeper is on + // GateKeeper is on, we need to --add ourselves as root, else we might get a GateKeeper crash + needsAuthorization = true; + } + else if (access(_dyld_get_image_name(0), W_OK)) { + // We don't have write access, so we need authorization to update as root + needsAuthorization = true; + } + + if (needsAuthorization && !_auth) { + AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagPreAuthorize | kAuthorizationFlagInteractionAllowed, &_auth); + // Make sure we can modify the bundle + if (![self executePath:@"/usr/sbin/chown" withArguments:@[@"-R", [NSString stringWithFormat:@"%d:%d", getuid(), getgid()], [NSBundle mainBundle].bundlePath]]) { + [self deauthorize]; + return; + } + } + + [self.updateProgressSpinner startAnimation:nil]; + self.updateProgressButton.title = @"Cancel"; + self.updateProgressButton.enabled = true; + self.updateProgressLabel.stringValue = @"Downloading update..."; + _updateState = UPDATE_DOWNLOADING; + _updateTask = [[NSURLSession sharedSession] downloadTaskWithURL: [NSURL URLWithString:_updateURL] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { + _updateTask = nil; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Extracting update..."; + _updateState = UPDATE_EXTRACTING; + }); + + _downloadDirectory = [[[NSFileManager defaultManager] URLForDirectory:NSItemReplacementDirectory + inDomain:NSUserDomainMask + appropriateForURL:[[NSBundle mainBundle] bundleURL] + create:true + error:nil] path]; + if (!_downloadDirectory) { + [self deauthorize]; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Failed to extract update."; + _updateState = UPDATE_FAILED; + self.updateProgressButton.title = @"Close"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + return; + } + + NSTask *unzipTask; + unzipTask = [[NSTask alloc] init]; + unzipTask.launchPath = @"/usr/bin/unzip"; + unzipTask.arguments = @[location.path, @"-d", _downloadDirectory]; + [unzipTask launch]; + [unzipTask waitUntilExit]; + if (unzipTask.terminationStatus != 0 || unzipTask.terminationReason != NSTaskTerminationReasonExit) { + [self deauthorize]; + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Failed to extract update."; + _updateState = UPDATE_FAILED; + self.updateProgressButton.title = @"Close"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + return; + } + + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Update ready, save your game progress and click Install."; + _updateState = UPDATE_WAIT_INSTALL; + self.updateProgressButton.title = @"Install"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + }]; + [_updateTask resume]; + + self.updateProgressWindow.preventsApplicationTerminationWhenModal = false; + [self.updateWindow beginSheet:self.updateProgressWindow completionHandler:^(NSModalResponse returnCode) { + [self.updateWindow close]; + }]; +} + +- (void)performUpgrade +{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Instaling update..."; + _updateState = UPDATE_INSTALLING; + self.updateProgressButton.enabled = false; + [self.updateProgressSpinner startAnimation:nil]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *executablePath = [[NSBundle mainBundle] executablePath]; + NSString *contentsPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Contents"]; + NSString *contentsTempPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"TempContents"]; + NSString *updateContentsPath = [_downloadDirectory stringByAppendingPathComponent:@"SameBoy.app/Contents"]; + NSError *error = nil; + [[NSFileManager defaultManager] moveItemAtPath:contentsPath toPath:contentsTempPath error:&error]; + if (error) { + [self deauthorize]; + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + _downloadDirectory = nil; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Failed to install update."; + _updateState = UPDATE_FAILED; + self.updateProgressButton.title = @"Close"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + return; + } + [[NSFileManager defaultManager] moveItemAtPath:updateContentsPath toPath:contentsPath error:&error]; + if (error) { + [self deauthorize]; + [[NSFileManager defaultManager] moveItemAtPath:contentsTempPath toPath:contentsPath error:nil]; + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + _downloadDirectory = nil; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Failed to install update."; + _updateState = UPDATE_FAILED; + self.updateProgressButton.title = @"Close"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + return; + } + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + [[NSFileManager defaultManager] removeItemAtPath:contentsTempPath error:nil]; + _downloadDirectory = nil; + atexit_b(^{ + execl(executablePath.UTF8String, executablePath.UTF8String, "--update-launch", NULL); + }); + + dispatch_async(dispatch_get_main_queue(), ^{ + [NSApp terminate:nil]; + }); + }); +} + +- (IBAction)updateAction:(id)sender +{ + switch (_updateState) { + case UPDATE_DOWNLOADING: + [_updateTask cancelByProducingResumeData:nil]; + _updateTask = nil; + [self.updateProgressWindow close]; + break; + case UPDATE_WAIT_INSTALL: + [self performUpgrade]; + break; + case UPDATE_EXTRACTING: + case UPDATE_INSTALLING: + break; + case UPDATE_FAILED: + [self.updateProgressWindow close]; + break; + } +} + +- (void)orderFrontAboutPanel:(id)sender +{ + // NSAboutPanelOptionApplicationIcon is not available prior to 10.13, but the key is still there and working. + [[NSApplication sharedApplication] orderFrontStandardAboutPanelWithOptions:@{ + @"ApplicationIcon": [NSImage imageNamed:@"Icon"] + }]; +} + +- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button +{ + if (!button.isPressed) return; + NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; + if (!mapping) { + mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName]; + } + + JOYButtonUsage usage = ((JOYButtonUsage)[mapping[n2s(button.uniqueID)] unsignedIntValue]) ?: -1; + if (!mapping && usage >= JOYButtonUsageGeneric0) { + usage = (const JOYButtonUsage[]){JOYButtonUsageY, JOYButtonUsageA, JOYButtonUsageB, JOYButtonUsageX}[(usage - JOYButtonUsageGeneric0) & 3]; + } + + if (usage == GBJoyKitHotkey1 || usage == GBJoyKitHotkey2) { + if (_preferencesWindow && self.keyWindow == _preferencesWindow) { + return; + } + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAllowBackgroundControllers"] && !self.keyWindow) { + return; + } + + NSString *keyEquivalent = [[NSUserDefaults standardUserDefaults] stringForKey:usage == GBJoyKitHotkey1? @"GBJoypadHotkey1" : @"GBJoypadHotkey2"]; + NSEventModifierFlags flags = NSEventModifierFlagCommand; + if ([keyEquivalent hasPrefix:@"^"]) { + flags |= NSEventModifierFlagShift; + [keyEquivalent substringFromIndex:1]; + } + _simulatingMenuEvent = true; + [[NSApplication sharedApplication] sendEvent:[NSEvent keyEventWithType:NSEventTypeKeyDown + location:(NSPoint){0,} + modifierFlags:flags + timestamp:0 + windowNumber:0 + context:NULL + characters:keyEquivalent + charactersIgnoringModifiers:keyEquivalent + isARepeat:false + keyCode:0]]; + _simulatingMenuEvent = false; + } +} + +- (NSWindow *)keyWindow +{ + NSWindow *ret = [super keyWindow]; + if (!ret && _simulatingMenuEvent) { + ret = [(Document *)self.orderedDocuments.firstObject mainWindow]; + } + return ret; +} + +- (NSWindow *)mainWindow +{ + NSWindow *ret = [super mainWindow]; + if (!ret && _simulatingMenuEvent) { + ret = [(Document *)self.orderedDocuments.firstObject mainWindow]; + } + return ret; +} + +- (void)dealloc +{ + if (_downloadDirectory) { + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + } +} + +- (IBAction)nop:(id)sender +{ +} +@end diff --git a/Cocoa/GBButtons.h b/Cocoa/GBButtons.h @@ -13,11 +13,17 @@ typedef enum : NSUInteger { GBTurbo, GBRewind, GBUnderclock, - GBButtonCount, + GBHotkey1, + GBHotkey2, + GBJoypadButtonCount, + GBButtonCount = GBUnderclock + 1, GBGameBoyButtonCount = GBStart + 1, } GBButton; -extern NSString const *GBButtonNames[GBButtonCount]; +#define GBJoyKitHotkey1 JOYButtonUsageGeneric0 + 0x100 +#define GBJoyKitHotkey2 JOYButtonUsageGeneric0 + 0x101 + +extern NSString const *GBButtonNames[GBJoypadButtonCount]; static inline NSString *n2s(uint64_t number) { diff --git a/Cocoa/GBButtons.m b/Cocoa/GBButtons.m @@ -1,4 +1,4 @@ #import <Foundation/Foundation.h> #import "GBButtons.h" -NSString const *GBButtonNames[] = {@"Right", @"Left", @"Up", @"Down", @"A", @"B", @"Select", @"Start", @"Turbo", @"Rewind", @"Slow-Motion"}; +NSString const *GBButtonNames[] = {@"Right", @"Left", @"Up", @"Down", @"A", @"B", @"Select", @"Start", @"Turbo", @"Rewind", @"Slow-Motion", @"Hotkey 1", @"Hotkey 2"}; diff --git a/Cocoa/GBPreferencesWindow.h b/Cocoa/GBPreferencesWindow.h @@ -20,6 +20,8 @@ @property IBOutlet NSMenuItem *bootROMsFolderItem; @property IBOutlet NSPopUpButtonCell *bootROMsButton; @property IBOutlet NSPopUpButton *rumbleModePopupButton; +@property IBOutlet NSPopUpButton *hotkey1PopupButton; +@property IBOutlet NSPopUpButton *hotkey2PopupButton; @property IBOutlet NSSlider *temperatureSlider; @property IBOutlet NSSlider *interferenceSlider; @property IBOutlet NSPopUpButton *dmgPopupButton; diff --git a/Cocoa/GBPreferencesWindow.m b/Cocoa/GBPreferencesWindow.m @@ -29,6 +29,8 @@ NSPopUpButton *_dmgPopupButton, *_sgbPopupButton, *_cgbPopupButton; NSPopUpButton *_preferredJoypadButton; NSPopUpButton *_rumbleModePopupButton; + NSPopUpButton *_hotkey1PopupButton; + NSPopUpButton *_hotkey2PopupButton; NSSlider *_temperatureSlider; NSSlider *_interferenceSlider; NSSlider *_volumeSlider; @@ -195,6 +197,45 @@ return _rumbleModePopupButton; } +static inline NSString *keyEquivalentString(NSMenuItem *item) +{ + return [NSString stringWithFormat:@"%s%@", (item.keyEquivalentModifierMask & NSEventModifierFlagShift)? "^":"", item.keyEquivalent]; +} + +- (void)setHotkey1PopupButton:(NSPopUpButton *)hotkey1PopupButton +{ + _hotkey1PopupButton = hotkey1PopupButton; + NSString *keyEquivalent = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBJoypadHotkey1"]; + for (NSMenuItem *item in _hotkey1PopupButton.menu.itemArray) { + if ([keyEquivalent isEqualToString:keyEquivalentString(item)]) { + [_hotkey1PopupButton selectItem:item]; + break; + } + } +} + +- (NSPopUpButton *)hotkey1PopupButton +{ + return _hotkey1PopupButton; +} + +- (void)setHotkey2PopupButton:(NSPopUpButton *)hotkey2PopupButton +{ + _hotkey2PopupButton = hotkey2PopupButton; + NSString *keyEquivalent = [[NSUserDefaults standardUserDefaults] stringForKey:@"GBJoypadHotkey2"]; + for (NSMenuItem *item in _hotkey2PopupButton.menu.itemArray) { + if ([keyEquivalent isEqualToString:keyEquivalentString(item)]) { + [_hotkey2PopupButton selectItem:item]; + break; + } + } +} + +- (NSPopUpButton *)hotkey2PopupButton +{ + return _hotkey2PopupButton; +} + - (void)setRewindPopupButton:(NSPopUpButton *)rewindPopupButton { _rewindPopupButton = rewindPopupButton; @@ -457,6 +498,18 @@ [[NSNotificationCenter defaultCenter] postNotificationName:@"GBRumbleModeChanged" object:nil]; } +- (IBAction)hotkey1Changed:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:keyEquivalentString([sender selectedItem]) + forKey:@"GBJoypadHotkey1"]; +} + +- (IBAction)hotkey2Changed:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:keyEquivalentString([sender selectedItem]) + forKey:@"GBJoypadHotkey2"]; +} + - (IBAction)rewindLengthChanged:(id)sender { [[NSUserDefaults standardUserDefaults] setObject:@([sender selectedTag]) @@ -497,7 +550,7 @@ if (joystick_configuration_state == GBUnderclock) { [self.configureJoypadButton setTitle:@"Press Button for Slo-Mo"]; // Full name is too long :< } - else if (joystick_configuration_state < GBButtonCount) { + else if (joystick_configuration_state < GBJoypadButtonCount) { [self.configureJoypadButton setTitle:[NSString stringWithFormat:@"Press Button for %@", GBButtonNames[joystick_configuration_state]]]; } else { @@ -519,7 +572,7 @@ if (!button.isPressed) return; if (joystick_configuration_state == -1) return; - if (joystick_configuration_state == GBButtonCount) return; + if (joystick_configuration_state == GBJoypadButtonCount) return; if (!joystick_being_configured) { joystick_being_configured = controller.uniqueID; } @@ -561,6 +614,8 @@ [GBTurbo] = JOYButtonUsageL1, [GBRewind] = JOYButtonUsageL2, [GBUnderclock] = JOYButtonUsageR1, + [GBHotkey1] = GBJoyKitHotkey1, + [GBHotkey2] = GBJoyKitHotkey2, }; if (joystick_configuration_state == GBUnderclock) { diff --git a/Cocoa/GBView.h b/Cocoa/GBView.h @@ -2,6 +2,7 @@ #include <Core/gb.h> #import <JoyKit/JoyKit.h> #import "GBOSDView.h" + @class Document; typedef enum { diff --git a/Cocoa/Info.plist b/Cocoa/Info.plist @@ -134,7 +134,7 @@ <key>NSMainNibFile</key> <string>MainMenu</string> <key>NSPrincipalClass</key> - <string>NSApplication</string> + <string>GBApp</string> <key>UTExportedTypeDeclarations</key> <array> <dict> diff --git a/Cocoa/MainMenu.xib b/Cocoa/MainMenu.xib @@ -7,12 +7,11 @@ <objects> <customObject id="-2" userLabel="File's Owner" customClass="NSApplication"> <connections> - <outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/> + <outlet property="delegate" destination="-3" id="GzC-gU-4Uq"/> </connections> </customObject> <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> - <customObject id="-3" userLabel="Application" customClass="NSObject"/> - <customObject id="Voe-Tx-rLC" customClass="AppDelegate"> + <customObject id="-3" userLabel="Application" customClass="GBApp"> <connections> <outlet property="linkCableMenuItem" destination="V4S-Fo-xJK" id="KL9-3K-64i"/> </connections> @@ -33,7 +32,7 @@ <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/> <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"> <connections> - <action selector="showPreferences:" target="Voe-Tx-rLC" id="RcX-51-nzq"/> + <action selector="showPreferences:" target="-3" id="RcX-51-nzq"/> </connections> </menuItem> <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/> @@ -415,11 +414,11 @@ <modifierMask key="keyEquivalentModifierMask"/> <menu key="submenu" title="Game Link Cable & Infrared" id="6sJ-Wz-QLj"> <connections> - <outlet property="delegate" destination="Voe-Tx-rLC" id="PMY-5j-25T"/> + <outlet property="delegate" destination="-3" id="PMY-5j-25T"/> </connections> </menu> <connections> - <action selector="nop:" target="Voe-Tx-rLC" id="Bpa-0C-lkN"/> + <action selector="nop:" target="-3" id="Bpa-0C-lkN"/> </connections> </menuItem> <menuItem title="Game Boy Printer" id="zHR-Ha-pOR"> @@ -444,7 +443,7 @@ <menuItem title="Developer Mode" id="Qx6-Tt-zZR"> <modifierMask key="keyEquivalentModifierMask"/> <connections> - <action selector="toggleDeveloperMode:" target="Voe-Tx-rLC" id="PIc-o3-bzb"/> + <action selector="toggleDeveloperMode:" target="-3" id="PIc-o3-bzb"/> </connections> </menuItem> <menuItem isSeparatorItem="YES" id="66c-T0-8pW"/> diff --git a/Cocoa/Preferences.xib b/Cocoa/Preferences.xib @@ -6,7 +6,7 @@ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> <objects> - <customObject id="-2" userLabel="File's Owner" customClass="AppDelegate"> + <customObject id="-2" userLabel="File's Owner" customClass="GBApp"> <connections> <outlet property="audioTab" destination="Zn1-Y5-RbR" id="PCn-a5-RzH"/> <outlet property="controlsTab" destination="8TU-6J-NCg" id="BFd-im-2Pw"/> @@ -85,6 +85,8 @@ <outlet property="frameBlendingModePopupButton" destination="lxk-db-Sxv" id="wzt-uo-TE6"/> <outlet property="graphicsFilterPopupButton" destination="6pP-kK-EEC" id="LS7-HY-kHC"/> <outlet property="highpassFilterPopupButton" destination="T69-6N-dhT" id="0p6-4m-hb1"/> + <outlet property="hotkey1PopupButton" destination="qsH-w5-Rja" id="TFr-p8-IYo"/> + <outlet property="hotkey2PopupButton" destination="Ryk-mD-c0y" id="m2J-O5-dng"/> <outlet property="interferenceSlider" destination="FpE-5i-j5L" id="hfH-e8-7cx"/> <outlet property="joystickMBC7Checkbox" destination="i7F-1r-NkQ" id="msM-WX-w9H"/> <outlet property="mouseMBC7Checkbox" destination="wUE-aB-ub1" id="mXX-mZ-sKJ"/> @@ -612,11 +614,11 @@ <point key="canvasLocation" x="-854" y="627"/> </customView> <customView id="8TU-6J-NCg"> - <rect key="frame" x="0.0" y="0.0" width="320" height="560"/> + <rect key="frame" x="0.0" y="0.0" width="320" height="667"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <subviews> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Utu-t4-cLx"> - <rect key="frame" x="18" y="523" width="122" height="17"/> + <rect key="frame" x="18" y="630" width="122" height="17"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Control settings for" id="YqW-Ds-VIC"> <font key="font" metaFont="system"/> @@ -625,7 +627,7 @@ </textFieldCell> </textField> <scrollView focusRingType="none" fixedFrame="YES" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" hasHorizontalScroller="NO" hasVerticalScroller="NO" usesPredominantAxisScrolling="NO" horizontalScrollElasticity="none" verticalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="PBp-dj-EIa"> - <rect key="frame" x="32" y="301" width="257" height="211"/> + <rect key="frame" x="32" y="408" width="257" height="211"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <clipView key="contentView" focusRingType="none" ambiguous="YES" drawsBackground="NO" id="AMs-PO-nid"> <rect key="frame" x="1" y="1" width="255" height="209"/> @@ -683,7 +685,7 @@ </scroller> </scrollView> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fcF-wc-KwM"> - <rect key="frame" x="30" y="276" width="231" height="17"/> + <rect key="frame" x="30" y="383" width="231" height="17"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Controller for multiplayer games:" id="AJA-9b-VKI"> <font key="font" metaFont="system"/> @@ -692,7 +694,7 @@ </textFieldCell> </textField> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0Az-0R-oNw"> - <rect key="frame" x="42" y="243" width="250" height="26"/> + <rect key="frame" x="42" y="350" width="250" height="26"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> <popUpButtonCell key="cell" type="push" title="None" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingMiddle" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="hy8-cr-RrE" id="uEC-vN-8Jq"> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> @@ -708,11 +710,11 @@ </connections> </popUpButton> <box verticalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="VEc-Ed-Z6f"> - <rect key="frame" x="12" y="232" width="296" height="5"/> + <rect key="frame" x="12" y="339" width="296" height="5"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> </box> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ReM-uo-H0r"> - <rect key="frame" x="227" y="524" width="8" height="17"/> + <rect key="frame" x="227" y="631" width="8" height="17"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title=":" id="VhO-3T-glt"> <font key="font" metaFont="system"/> @@ -721,7 +723,7 @@ </textFieldCell> </textField> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gWx-7h-0xq"> - <rect key="frame" x="140" y="518" width="87" height="26"/> + <rect key="frame" x="140" y="625" width="87" height="26"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <popUpButtonCell key="cell" type="push" title="Player 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="TO3-R7-9HN" id="pbt-Lr-bU1"> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> @@ -801,6 +803,15 @@ <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> </textFieldCell> </textField> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2SB-Bq-Exy"> + <rect key="frame" x="18" y="315" width="129" height="17"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Hotkey 1 Action:" id="GTq-aw-0cU"> + <font key="font" metaFont="system"/> + <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ogs-xG-b4b"> <rect key="frame" x="30" y="58" width="262" height="26"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> @@ -819,6 +830,139 @@ <action selector="rumbleModeChanged:" target="QvC-M9-y7g" id="AQe-vQ-mSl"/> </connections> </popUpButton> + <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qsH-w5-Rja"> + <rect key="frame" x="30" y="286" width="262" height="26"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> + <popUpButtonCell key="cell" type="push" title="None" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="uFa-fO-lTm" id="x4X-Uv-vXf"> + <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="menu"/> + <menu key="menu" id="hHe-Vn-UE4"> + <items> + <menuItem title="None" state="on" id="uFa-fO-lTm"/> + <menuItem isSeparatorItem="YES" id="UoV-yr-2Eq"/> + <menuItem title="Toggle Pause" keyEquivalent="p" id="WxT-Gp-rKn"/> + <menuItem title="Reset" keyEquivalent="r" id="vVL-1l-e3f"/> + <menuItem title="Toggle Mute" keyEquivalent="m" id="bPj-M7-WNV"/> + <menuItem isSeparatorItem="YES" id="ewP-Y6-2Fx"/> + <menuItem title="Save State to Slot 1" keyEquivalent="1" id="349-ay-RSZ"/> + <menuItem title="Save State to Slot 2" keyEquivalent="2" id="sHb-Cv-8i8"/> + <menuItem title="Save State to Slot 3" keyEquivalent="3" id="YVf-94-KpR"/> + <menuItem title="Save State to Slot 4" keyEquivalent="4" id="KDs-fC-8ad"/> + <menuItem title="Save State to Slot 5" keyEquivalent="5" id="iCo-oT-5Mo"/> + <menuItem title="Save State to Slot 6" keyEquivalent="6" id="LTP-nk-QLs"/> + <menuItem title="Save State to Slot 7" keyEquivalent="7" id="SOE-dW-uEV"/> + <menuItem title="Save State to Slot 8" keyEquivalent="8" id="IRO-ko-INM"/> + <menuItem title="Save State to Slot 9" keyEquivalent="9" id="nJz-iY-YQ9"/> + <menuItem title="Save State to Slot 10" keyEquivalent="0" id="XIh-A2-QAg"/> + <menuItem isSeparatorItem="YES" id="Nfr-cm-L2W"/> + <menuItem title="Load State from Slot 1" keyEquivalent="1" id="bEy-D8-ckq"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 2" keyEquivalent="2" id="46a-Gt-mIa"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 3" keyEquivalent="3" id="9tn-pS-BDn"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 4" keyEquivalent="4" id="xsE-oe-cTS"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 5" keyEquivalent="5" id="lso-J8-dJO"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 6" keyEquivalent="6" id="nO5-pj-ba2"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 7" keyEquivalent="7" id="BhF-xL-MhS"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 8" keyEquivalent="8" id="2Qv-vX-7Qm"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 9" keyEquivalent="9" id="CBy-h9-gm8"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 10" keyEquivalent="0" id="UuH-sh-D9c"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + </items> + </menu> + </popUpButtonCell> + <connections> + <action selector="hotkey1Changed:" target="QvC-M9-y7g" id="XMJ-vu-L6C"/> + </connections> + </popUpButton> + <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8py-43-vaO"> + <rect key="frame" x="18" y="264" width="125" height="17"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> + <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Hotkey 2 Action:" id="Gdp-S8-V3q"> + <font key="font" metaFont="system"/> + <color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + </textField> + <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ryk-mD-c0y"> + <rect key="frame" x="30" y="231" width="262" height="26"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/> + <popUpButtonCell key="cell" type="push" title="None" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="wmV-YY-dHY" id="fS9-cS-iTD"> + <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> + <font key="font" metaFont="menu"/> + <menu key="menu" id="vPS-8b-MAH"> + <items> + <menuItem title="None" state="on" id="wmV-YY-dHY"/> + <menuItem isSeparatorItem="YES" id="qSd-QD-gLA"/> + <menuItem title="Toggle Pause" keyEquivalent="p" id="13d-2H-Cpq"/> + <menuItem title="Reset" keyEquivalent="r" id="5vx-zH-TsX"/> + <menuItem title="Toggle Mute" keyEquivalent="m" id="zFL-4A-plp"/> + <menuItem isSeparatorItem="YES" id="8e3-z2-8gY"/> + <menuItem title="Save State to Slot 1" keyEquivalent="1" id="4s2-s5-xa7"/> + <menuItem title="Save State to Slot 2" keyEquivalent="2" id="sRd-sU-DHW"/> + <menuItem title="Save State to Slot 3" keyEquivalent="3" id="Yqr-1y-H2o"/> + <menuItem title="Save State to Slot 4" keyEquivalent="4" id="4AM-Ki-x4l"/> + <menuItem title="Save State to Slot 5" keyEquivalent="5" id="q4A-0K-4py"/> + <menuItem title="Save State to Slot 6" keyEquivalent="6" id="NA3-rP-ni2"/> + <menuItem title="Save State to Slot 7" keyEquivalent="7" id="XF9-Ig-ZPU"/> + <menuItem title="Save State to Slot 8" keyEquivalent="8" id="3cM-DC-NC9"/> + <menuItem title="Save State to Slot 9" keyEquivalent="9" id="h9i-lA-TGi"/> + <menuItem title="Save State to Slot 10" keyEquivalent="0" id="5eE-XE-E76"/> + <menuItem isSeparatorItem="YES" id="Q57-67-3h1"/> + <menuItem title="Load State from Slot 1" keyEquivalent="1" id="fdH-LH-gl1"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 2" keyEquivalent="2" id="77e-oa-0qg"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 3" keyEquivalent="3" id="LKg-oc-egx"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 4" keyEquivalent="4" id="CAi-yh-rg4"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 5" keyEquivalent="5" id="smK-o2-ZI6"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 6" keyEquivalent="6" id="SoT-kd-9M1"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 7" keyEquivalent="7" id="BCT-R6-O4Y"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 8" keyEquivalent="8" id="U64-kB-gnC"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 9" keyEquivalent="9" id="sDw-Wr-Od1"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + <menuItem title="Load State from Slot 10" keyEquivalent="0" id="XZl-tl-CcJ"> + <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/> + </menuItem> + </items> + </menu> + </popUpButtonCell> + <connections> + <action selector="hotkey2Changed:" target="QvC-M9-y7g" id="Cul-EW-jNz"/> + </connections> + </popUpButton> <button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Qa7-Z7-yfO"> <rect key="frame" x="26" y="13" width="204" height="32"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES"/> @@ -842,7 +986,7 @@ </connections> </button> </subviews> - <point key="canvasLocation" x="-854" y="213"/> + <point key="canvasLocation" x="-854" y="139.5"/> </customView> <customView id="ffn-ie-9C3"> <rect key="frame" x="0.0" y="0.0" width="320" height="95"/> diff --git a/Cocoa/UpdateWindow.xib b/Cocoa/UpdateWindow.xib @@ -7,7 +7,7 @@ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> <objects> - <customObject id="-2" userLabel="File's Owner" customClass="AppDelegate"> + <customObject id="-2" userLabel="File's Owner" customClass="GBApp"> <connections> <outlet property="updateChanges" destination="ATt-VM-cb5" id="hJj-Nd-FBv"/> <outlet property="updateProgressButton" destination="7nO-AA-WmG" id="wTa-9l-cOG"/> diff --git a/Core/camera.c b/Core/camera.c @@ -3,7 +3,7 @@ static uint32_t noise_seed = 0; /* This is not a complete emulation of the camera chip. Only the features used by the Game Boy Camera ROMs are supported. - We also do not emulate the timing of the real cart, as it might be actually faster than the webcam. */ + We also do not emulate the timing of the real cart when a webcam is used, as it might be actually faster than the webcam. */ static uint8_t generate_noise(uint8_t x, uint8_t y) { @@ -55,10 +55,6 @@ static long get_processed_color(GB_gameboy_t *gb, uint8_t x, uint8_t y) uint8_t GB_camera_read_image(GB_gameboy_t *gb, uint16_t addr) { - if (gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1) { - /* Forbid reading the image while the camera is busy. */ - return 0xFF; - } uint8_t tile_x = addr / 0x10 % 0x10; uint8_t tile_y = addr / 0x10 / 0x10; @@ -112,6 +108,12 @@ void GB_set_camera_get_pixel_callback(GB_gameboy_t *gb, GB_camera_get_pixel_call void GB_set_camera_update_request_callback(GB_gameboy_t *gb, GB_camera_update_request_callback_t callback) { + if (gb->camera_countdown > 0 && callback) { + GB_log(gb, "Camera update request callback set while camera was proccessing, clearing camera countdown.\n"); + gb->camera_countdown = 0; + GB_camera_updated(gb); + } + gb->camera_update_request_callback = callback; } @@ -125,12 +127,25 @@ void GB_camera_write_register(GB_gameboy_t *gb, uint16_t addr, uint8_t value) addr &= 0x7F; if (addr == GB_CAMERA_SHOOT_AND_1D_FLAGS) { value &= 0x7; - noise_seed = rand(); - if ((value & 1) && !(gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1) && gb->camera_update_request_callback) { - /* If no callback is set, ignore the write as if the camera is instantly done */ - gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] |= 1; - gb->camera_update_request_callback(gb); + noise_seed = GB_random(); + if ((value & 1) && !(gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1)) { + if (gb->camera_update_request_callback) { + gb->camera_update_request_callback(gb); + } + else { + /* If no callback is set, wait the amount of time the real camera would take before clearing the busy bit */ + uint16_t exposure = (gb->camera_registers[GB_CAMERA_EXPOSURE_HIGH] << 8) | gb->camera_registers[GB_CAMERA_EXPOSURE_LOW]; + gb->camera_countdown = 129792 + ((gb->camera_registers[GB_CAMERA_GAIN_AND_EDGE_ENHACEMENT_FLAGS] & 0x80)? 0 : 2048) + (exposure * 64) + (gb->camera_alignment & 4); + } } + + if (!(value & 1) && (gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1)) { + /* We don't support cancelling a camera shoot */ + GB_log(gb, "ROM attempted to cancel camera shoot, which is currently not supported. The camera shoot will not be cancelled.\n"); + value |= 1; + } + + gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] = value; } else { if (addr >= 0x36) { diff --git a/Core/debugger.c b/Core/debugger.c @@ -1995,7 +1995,7 @@ static const debugger_command_t commands[] = { .argument_completer = symbol_completer, .modifiers_completer = rw_completer }, {"unwatch", 3, unwatch, "Delete a watchpoint by its address, or all watchpoints", "[<expression>]", .argument_completer = symbol_completer}, - {"softbreak", 2, softbreak, "Enable or disable software breakpoints", "(on|off)", .argument_completer = on_off_completer}, + {"softbreak", 2, softbreak, "Enable or disable software breakpoints ('ld b, b' opcodes)", "(on|off)", .argument_completer = on_off_completer}, {"list", 1, list, "List all set breakpoints and watchpoints"}, {"ticks", 2, ticks, "Display the number of CPU ticks since the last time 'ticks' was" HELP_NEWLINE "used"}, diff --git a/Core/display.c b/Core/display.c @@ -908,6 +908,21 @@ static void advance_fetcher_state_machine(GB_gameboy_t *gb, unsigned *cycles) } if (fifo_size(&gb->bg_fifo) > 0) break; + if (unlikely(gb->wy_triggered && !(gb->io_registers[GB_IO_LCDC] & 0x20) && !GB_is_cgb(gb))) { + /* See https://github.com/LIJI32/SameBoy/issues/278 for documentation */ + uint8_t logical_position = gb->position_in_line + 7; + if (logical_position > 167) { + logical_position = 0; + } + if (gb->io_registers[GB_IO_WX] == logical_position) { + gb->bg_fifo.read_end--; + gb->bg_fifo.read_end &= GB_FIFO_LENGTH - 1; + gb->bg_fifo.fifo[gb->bg_fifo.read_end] = (GB_fifo_item_t){0,}; + gb->bg_fifo.size = 1; + break; + } + } + fifo_push_bg_row(&gb->bg_fifo, gb->current_tile_data[0], gb->current_tile_data[1], gb->current_tile_attributes & 7, gb->current_tile_attributes & 0x80, gb->current_tile_attributes & 0x20); gb->fetcher_state = 0; @@ -1294,8 +1309,17 @@ static inline uint16_t mode3_batching_length(GB_gameboy_t *gb) if (gb->hdma_on) return 0; if (gb->stopped) return 0; if (GB_is_dma_active(gb)) return 0; - if (gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20) && (gb->io_registers[GB_IO_WX] < 8 || gb->io_registers[GB_IO_WX] == 166)) { - return 0; + if (gb->wy_triggered) { + if (gb->io_registers[GB_IO_LCDC] & 0x20) { + if ((gb->io_registers[GB_IO_WX] < 8 || gb->io_registers[GB_IO_WX] == 166)) { + return 0; + } + } + else { + if (gb->io_registers[GB_IO_WX] < 167 && !GB_is_cgb(gb)) { + return 0; + } + } } // No objects or window, timing is trivial @@ -1638,13 +1662,13 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) if (!gb->wx_triggered && gb->wy_triggered && (gb->io_registers[GB_IO_LCDC] & 0x20)) { bool should_activate_window = false; if (gb->io_registers[GB_IO_WX] == 0) { - static const uint8_t scx_to_wx0_comparisons[] = {-7, -9, -10, -11, -12, -13, -14, -14}; + static const uint8_t scx_to_wx0_comparisons[] = {-7, -1, -2, -3, -4, -5, -6, -6}; if (gb->position_in_line == scx_to_wx0_comparisons[gb->io_registers[GB_IO_SCX] & 7]) { should_activate_window = true; } } else if (gb->wx166_glitch) { - static const uint8_t scx_to_wx166_comparisons[] = {-8, -9, -10, -11, -12, -13, -14, -15}; + static const uint8_t scx_to_wx166_comparisons[] = {-16, -1, -2, -3, -4, -5, -6, -7}; if (gb->position_in_line == scx_to_wx166_comparisons[gb->io_registers[GB_IO_SCX] & 7]) { should_activate_window = true; } @@ -1683,13 +1707,14 @@ void GB_display_run(GB_gameboy_t *gb, unsigned cycles, bool force) } } - /* TODO: What happens when WX=0? */ + /* TODO: What happens when WX=0? When the fifo is full? */ if (!GB_is_cgb(gb) && gb->wx_triggered && !gb->window_is_being_fetched && - gb->fetcher_state == 0 && gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 7) ) { + gb->fetcher_state == 0 && gb->io_registers[GB_IO_WX] == (uint8_t) (gb->position_in_line + 7) && gb->bg_fifo.size == 8) { // Insert a pixel right at the FIFO's end gb->bg_fifo.read_end--; gb->bg_fifo.read_end &= GB_FIFO_LENGTH - 1; gb->bg_fifo.fifo[gb->bg_fifo.read_end] = (GB_fifo_item_t){0,}; + gb->bg_fifo.size++; gb->window_is_being_fetched = false; } diff --git a/Core/gb.h b/Core/gb.h @@ -531,6 +531,8 @@ struct GB_gameboy_internal_s { uint8_t camera_registers[0x36]; uint8_t rumble_strength; bool cart_ir; + uint8_t camera_alignment; + int32_t camera_countdown; ) /* HRAM and HW Registers */ diff --git a/Core/memory.c b/Core/memory.c @@ -414,8 +414,15 @@ static uint8_t read_mbc_ram(GB_gameboy_t *gb, uint16_t addr) return 0xFF; } - if (gb->cartridge_type->mbc_type == GB_CAMERA && gb->mbc_ram_bank == 0 && addr >= 0xA100 && addr < 0xAF00) { - return GB_camera_read_image(gb, addr - 0xA100); + if (gb->cartridge_type->mbc_type == GB_CAMERA) { + /* Forbid reading RAM while the camera is busy. */ + if (gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1) { + return 0; + } + + if (gb->mbc_ram_bank == 0 && addr >= 0xA100 && addr < 0xAF00) { + return GB_camera_read_image(gb, addr - 0xA100); + } } uint8_t effective_bank = gb->mbc_ram_bank; @@ -1219,7 +1226,12 @@ static void write_mbc_ram(GB_gameboy_t *gb, uint16_t addr, uint8_t value) if (!gb->mbc_ram || !gb->mbc_ram_size) { return; } - + + if (gb->cartridge_type->mbc_type == GB_CAMERA && (gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1)) { + /* Forbid writing to RAM while the camera is busy. */ + return; + } + uint8_t effective_bank = gb->mbc_ram_bank; if (gb->cartridge_type->mbc_type == GB_MBC3 && !gb->is_mbc30) { if (gb->cartridge_type->has_rtc) { diff --git a/Core/save_state.h b/Core/save_state.h @@ -8,7 +8,12 @@ #ifdef __cplusplus /* For bsnes integration. C++ code does not need section information, and throws a fit over certain types such as anonymous enums inside unions */ -#define GB_SECTION(name, ...) struct __attribute__ ((aligned (8))) { __VA_ARGS__} +#if __clang__ +#define GB_SECTION(name, ...) __attribute__ ((aligned (8))) __VA_ARGS__ +#else +// GCC's handling of attributes is so awfully bad, that it is alone a good enough reason to never use that compiler +#define GB_SECTION(name, ...) _Pragma("GCC diagnostic push") _Pragma("GCC diagnostic ignored \"-Wpedantic\"") alignas(8) char _align_##name[0]; __VA_ARGS__ _Pragma("GCC diagnostic pop") +#endif #else #define GB_SECTION(name, ...) union __attribute__ ((aligned (8))) {uint8_t name##_section_start; struct {__VA_ARGS__};}; uint8_t name##_section_end[0]; #define GB_SECTION_OFFSET(name) (offsetof(GB_gameboy_t, name##_section_start)) diff --git a/Core/sm83_cpu.c b/Core/sm83_cpu.c @@ -416,6 +416,9 @@ static void stop(GB_gameboy_t *gb, uint8_t opcode) if (gb->apu.global_enable && gb->cgb_double_speed) { GB_log(gb, "ROM triggered an APU odd mode, which is currently not tested.\n"); } + if (gb->cartridge_type->mbc_type == GB_CAMERA && (gb->camera_registers[GB_CAMERA_SHOOT_AND_1D_FLAGS] & 1) && !gb->cgb_double_speed) { + GB_log(gb, "ROM entered double speed mode with a camera cartridge, this could damage a real cartridge's camera.\n"); + } if (gb->cgb_double_speed) { gb->cgb_double_speed = false; diff --git a/Core/timing.c b/Core/timing.c @@ -365,6 +365,29 @@ static void rtc_run(GB_gameboy_t *gb, uint8_t cycles) } } +static void camera_run(GB_gameboy_t *gb, uint8_t cycles) +{ + /* Do we have a camera? */ + if (likely(gb->cartridge_type->mbc_type != GB_CAMERA)) return; + + /* The camera mapper uses the PHI pin to clock itself */ + + /* PHI does not run in halt nor stop mode */ + if (unlikely(gb->halted || gb->stopped)) return; + + /* Only every other PHI is used (as the camera wants a 512KiHz clock) */ + gb->camera_alignment += cycles; + + /* Is the camera processing an image? */ + if (likely(gb->camera_countdown == 0)) return; + + gb->camera_countdown -= cycles; + if (gb->camera_countdown <= 0) { + gb->camera_countdown = 0; + GB_camera_updated(gb); + } +} + void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles) { @@ -389,6 +412,7 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles) gb->dma_cycles = cycles; timers_run(gb, cycles); + camera_run(gb, cycles); if (unlikely(gb->speed_switch_halt_countdown)) { gb->speed_switch_halt_countdown -= cycles; diff --git a/SDL/configuration.c b/SDL/configuration.c @@ -30,6 +30,9 @@ configuration_t configuration = 4, -1, 5, + // The rest are unmapped by default + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, }, .joypad_axises = { 0, diff --git a/SDL/configuration.h b/SDL/configuration.h @@ -25,6 +25,8 @@ typedef enum { JOYPAD_BUTTON_TURBO, JOYPAD_BUTTON_REWIND, JOYPAD_BUTTON_SLOW_MOTION, + JOYPAD_BUTTON_HOTKEY_1, + JOYPAD_BUTTON_HOTKEY_2, JOYPAD_BUTTONS_MAX } joypad_button_t; @@ -34,6 +36,35 @@ typedef enum { JOYPAD_AXISES_MAX } joypad_axis_t; +typedef enum { + HOTKEY_NONE, + HOTKEY_PAUSE, + HOTKEY_MUTE, + HOTKEY_RESET, + HOTKEY_QUIT, + HOTKEY_SAVE_STATE_1, + HOTKEY_LOAD_STATE_1, + HOTKEY_SAVE_STATE_2, + HOTKEY_LOAD_STATE_2, + HOTKEY_SAVE_STATE_3, + HOTKEY_LOAD_STATE_3, + HOTKEY_SAVE_STATE_4, + HOTKEY_LOAD_STATE_4, + HOTKEY_SAVE_STATE_5, + HOTKEY_LOAD_STATE_5, + HOTKEY_SAVE_STATE_6, + HOTKEY_LOAD_STATE_6, + HOTKEY_SAVE_STATE_7, + HOTKEY_LOAD_STATE_7, + HOTKEY_SAVE_STATE_8, + HOTKEY_LOAD_STATE_8, + HOTKEY_SAVE_STATE_9, + HOTKEY_LOAD_STATE_9, + HOTKEY_SAVE_STATE_10, + HOTKEY_LOAD_STATE_10, + HOTKEY_MAX = HOTKEY_LOAD_STATE_10, +} hotkey_action_t; + typedef struct { SDL_Scancode keys[9]; GB_color_correction_mode_t color_correction_mode; @@ -59,7 +90,7 @@ typedef struct { /* v0.11 */ uint32_t rewind_length; SDL_Scancode keys_2[32]; /* Rewind and underclock, + padding for the future */ - uint8_t joypad_configuration[32]; /* 12 Keys + padding for the future*/; + uint8_t joypad_configuration[32]; /* 14 Keys + padding for the future*/; uint8_t joypad_axises[JOYPAD_AXISES_MAX]; /* v0.12 */ @@ -99,6 +130,7 @@ typedef struct { bool allow_background_controllers; bool gui_pallete_enabled; // Change the GUI palette only once the user changed the DMG palette char dmg_palette_name[25]; + hotkey_action_t hotkey_actions[2]; }; } configuration_t; diff --git a/SDL/gui.c b/SDL/gui.c @@ -1283,10 +1283,63 @@ static const char *current_background_control_mode(unsigned index) return configuration.allow_background_controllers? "Always" : "During Window Focus Only"; } +static void cycle_hotkey(unsigned index) +{ + if (configuration.hotkey_actions[index - 2] == HOTKEY_MAX) { + configuration.hotkey_actions[index - 2] = 0; + } + else { + configuration.hotkey_actions[index - 2]++; + } +} + +static void cycle_hotkey_backwards(unsigned index) +{ + if (configuration.hotkey_actions[index - 2] == 0) { + configuration.hotkey_actions[index - 2] = HOTKEY_MAX; + } + else { + configuration.hotkey_actions[index - 2]--; + } +} + +static const char *current_hotkey(unsigned index) +{ + return (const char *[]){ + "None", + "Toggle Pause", + "Toggle Mute", + "Reset", + "Quit SameBoy", + "Save State Slot 1", + "Load State Slot 1", + "Save State Slot 2", + "Load State Slot 2", + "Save State Slot 3", + "Load State Slot 3", + "Save State Slot 4", + "Load State Slot 4", + "Save State Slot 5", + "Load State Slot 5", + "Save State Slot 6", + "Load State Slot 6", + "Save State Slot 7", + "Load State Slot 7", + "Save State Slot 8", + "Load State Slot 8", + "Save State Slot 9", + "Load State Slot 9", + "Save State Slot 10", + "Load State Slot 10", + } + [configuration.hotkey_actions[index - 2]]; +} static const struct menu_item joypad_menu[] = { {"Joypad:", cycle_joypads, current_joypad_name, cycle_joypads_backwards}, {"Configure layout", detect_joypad_layout}, + {"Hotkey 1 Action:", cycle_hotkey, current_hotkey, cycle_hotkey_backwards}, + {"Hotkey 2 Action:", cycle_hotkey, current_hotkey, cycle_hotkey_backwards}, {"Rumble Mode:", cycle_rumble_mode, current_rumble_mode, cycle_rumble_mode_backwards}, {"Enable Control:", toggle_allow_background_controllers, current_background_control_mode, toggle_allow_background_controllers}, {"Back", enter_controls_menu}, @@ -2015,6 +2068,8 @@ void run_gui(bool is_running) "Turbo", "Rewind", "Slow-Motion", + "Hotkey 1", + "Hotkey 2", "", } [joypad_configuration_progress], gui_palette_native[3], gui_palette_native[0], DECORATION_NONE); diff --git a/SDL/main.c b/SDL/main.c @@ -271,6 +271,37 @@ static void handle_events(GB_gameboy_t *gb) else if (button == JOYPAD_BUTTON_MENU && event.type == SDL_JOYBUTTONDOWN) { open_menu(); } + else if ((button == JOYPAD_BUTTON_HOTKEY_1 || button == JOYPAD_BUTTON_HOTKEY_2) && event.type == SDL_JOYBUTTONDOWN) { + hotkey_action_t action = configuration.hotkey_actions[button - JOYPAD_BUTTON_HOTKEY_1]; + switch (action) { + case HOTKEY_NONE: + break; + case HOTKEY_PAUSE: + paused = !paused; + break; + case HOTKEY_MUTE: + GB_audio_set_paused(GB_audio_is_playing()); + break; + case HOTKEY_RESET: + pending_command = GB_SDL_RESET_COMMAND; + break; + case HOTKEY_QUIT: + pending_command = GB_SDL_QUIT_COMMAND; + break; + default: + command_parameter = (action - HOTKEY_SAVE_STATE_1) / 2 + 1; + pending_command = ((action - HOTKEY_SAVE_STATE_1) % 2)? GB_SDL_LOAD_STATE_COMMAND:GB_SDL_SAVE_STATE_COMMAND; + break; + case HOTKEY_SAVE_STATE_10: + command_parameter = 0; + pending_command = GB_SDL_SAVE_STATE_COMMAND; + break; + case HOTKEY_LOAD_STATE_10: + command_parameter = 0; + pending_command = GB_SDL_LOAD_STATE_COMMAND; + break; + } + } } break; @@ -862,6 +893,10 @@ int main(int argc, char **argv) configuration.cgb_revision %= GB_MODEL_CGB_E - GB_MODEL_CGB_0 + 1; configuration.audio_driver[15] = 0; configuration.dmg_palette_name[24] = 0; + // Fix broken defaults, should keys 12-31 should be unmapped by default + if (configuration.joypad_configuration[31] == 0) { + memset(configuration.joypad_configuration + 12 , -1, 32 - 12); + } } if (configuration.model >= MODEL_MAX) {
This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.