git.y1.nz

SameBoy

Accurate GB/GBC emulator
download: https://git.y1.nz/archives/sameboy.tar.gz
README | Files | Log | Refs | LICENSE

Cocoa/GBApp.m

      1 #import "GBApp.h"
      2 #import "GBButtons.h"
      3 #import "GBView.h"
      4 #import "Document.h"
      5 #import "GBJoyConManager.h"
      6 #import <Core/gb.h>
      7 #import <Carbon/Carbon.h>
      8 #import <JoyKit/JoyKit.h>
      9 #import <WebKit/WebKit.h>
     10 #import <mach-o/dyld.h>
     11 #include <sys/mount.h>
     12 #include <sys/xattr.h>
     13 
     14 #define UPDATE_SERVER "https://sameboy.github.io"
     15 
     16 @interface NSToolbarItem(private)
     17 - (NSButton *)_view;
     18 @end
     19 
     20 static uint32_t color_to_int(NSColor *color)
     21 {
     22     color = [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]];
     23     return (((unsigned)(color.redComponent * 0xFF)) << 16) |
     24            (((unsigned)(color.greenComponent * 0xFF)) << 8) |
     25            ((unsigned)(color.blueComponent * 0xFF));
     26 }
     27 
     28 @implementation GBApp
     29 {
     30     NSArray<NSView *> *_preferencesTabs;
     31     NSString *_lastVersion;
     32     NSString *_updateURL;
     33     NSURLSessionDownloadTask *_updateTask;
     34     enum {
     35         UPDATE_DOWNLOADING,
     36         UPDATE_EXTRACTING,
     37         UPDATE_WAIT_INSTALL,
     38         UPDATE_INSTALLING,
     39         UPDATE_FAILED,
     40     } _updateState;
     41     NSString *_downloadDirectory;
     42     AuthorizationRef _auth;
     43     bool _simulatingMenuEvent;
     44 }
     45 
     46 - (void) applicationDidFinishLaunching:(NSNotification *)notification
     47 {
     48     // Refresh icon if launched via a software update
     49     if (@available(macOS 26.0, *)) {
     50         // Severely broken on macOS 26
     51     }
     52     else {
     53         NSImage *icon = [[NSWorkspace sharedWorkspace] iconForFile:[[NSBundle mainBundle] bundlePath]];
     54         icon.size = [NSApplication sharedApplication].applicationIconImage.size;
     55         [NSApplication sharedApplication].applicationIconImage = icon;
     56     }
     57     
     58     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
     59     for (unsigned i = 0; i < GBKeyboardButtonCount; i++) {
     60         if ([[defaults objectForKey:button_to_preference_name(i, 0)] isKindOfClass:[NSString class]]) {
     61             [defaults removeObjectForKey:button_to_preference_name(i, 0)];
     62         }
     63     }
     64     
     65     bool hasSFMono = false;
     66     if (@available(macOS 10.15, *)) {
     67         hasSFMono = [[NSFont monospacedSystemFontOfSize:12 weight:NSFontWeightRegular].displayName containsString:@"SF"];
     68     }
     69     [[NSUserDefaults standardUserDefaults] registerDefaults:@{
     70                                                               @"GBRight": @(kVK_RightArrow),
     71                                                               @"GBLeft": @(kVK_LeftArrow),
     72                                                               @"GBUp": @(kVK_UpArrow),
     73                                                               @"GBDown": @(kVK_DownArrow),
     74 
     75                                                               @"GBA": @(kVK_ANSI_X),
     76                                                               @"GBB": @(kVK_ANSI_Z),
     77                                                               @"GBSelect": @(kVK_Delete),
     78                                                               @"GBStart": @(kVK_Return),
     79 
     80                                                               @"GBTurbo": @(kVK_Space),
     81                                                               @"GBRewind": @(kVK_Tab),
     82                                                               @"GBSlow-Motion": @(kVK_Shift),
     83 
     84                                                               @"GBFilter": @"NearestNeighbor",
     85                                                               @"GBColorCorrection": @(GB_COLOR_CORRECTION_MODERN_BALANCED),
     86                                                               @"GBHighpassFilter": @(GB_HIGHPASS_ACCURATE),
     87                                                               @"GBRewindLength": @(120),
     88                                                               @"GBFrameBlendingMode": @([defaults boolForKey:@"DisableFrameBlending"]? GB_FRAME_BLENDING_MODE_DISABLED : GB_FRAME_BLENDING_MODE_ACCURATE),
     89                                                               
     90                                                               @"GBDMGModel": @(GB_MODEL_DMG_B),
     91                                                               @"GBCGBModel": @(GB_MODEL_CGB_E),
     92                                                               @"GBAGBModel": @(GB_MODEL_AGB_A),
     93                                                               @"GBSGBModel": @(GB_MODEL_SGB2),
     94                                                               @"GBRumbleMode": @(GB_RUMBLE_CARTRIDGE_ONLY),
     95                                                               
     96                                                               @"GBVolume": @(1.0),
     97                                                               
     98                                                               @"GBMBC7JoystickOverride": @NO,
     99                                                               @"GBMBC7AllowMouse": @YES,
    100                                                               
    101                                                               @"GBJoyConAutoPair": @YES,
    102                                                               @"GBJoyConsDefaultsToHorizontal": @YES,
    103                                                               
    104                                                               @"GBEmulatedModel": @(MODEL_AUTO),
    105                                                               
    106                                                               @"GBDebuggerFont": hasSFMono? @"SF Mono" : @"Menlo",
    107                                                               @"GBDebuggerFontSize": @12,
    108                                                               
    109                                                               @"GBColorPalette": @1,
    110                                                               @"GBTurboCap": @0,
    111                                                               @"GBRumbleStrength" : @1,
    112                                                               
    113                                                               // Default themes
    114                                                               @"GBThemes": @{
    115                                                                       @"Canyon": @{
    116                                                                               @"BrightnessBias": @0.1227009965823247,
    117                                                                               @"Colors": @[@0xff0c1e20, @0xff122b91, @0xff466aa2, @0xfff1efae, @0xfff1efae],
    118                                                                               @"DisabledLCDColor": @NO,
    119                                                                               @"HueBias": @0.01782661816105247,
    120                                                                               @"HueBiasStrength": @1,
    121                                                                               @"Manual": @NO,
    122                                                                       },
    123                                                                       @"Desert": @{
    124                                                                               @"BrightnessBias": @0.0,
    125                                                                               @"Colors": @[@0xff302f3e, @0xff576674, @0xff839ba4, @0xffb1d0d2, @0xffb7d7d8],
    126                                                                               @"DisabledLCDColor": @YES,
    127                                                                               @"HueBias": @0.10087773904382469,
    128                                                                               @"HueBiasStrength": @0.062142056772908363,
    129                                                                               @"Manual": @NO,
    130                                                                       },
    131                                                                       @"Evening": @{
    132                                                                               @"BrightnessBias": @-0.10168700106441975,
    133                                                                               @"Colors": @[@0xff362601, @0xff695518, @0xff899853, @0xffa6e4ae, @0xffa9eebb],
    134                                                                               @"DisabledLCDColor": @YES,
    135                                                                               @"HueBias": @0.60027079191058874,
    136                                                                               @"HueBiasStrength": @0.33816297305747867,
    137                                                                               @"Manual": @NO,
    138                                                                       },
    139                                                                       @"Fog": @{
    140                                                                               @"BrightnessBias": @0.0,
    141                                                                               @"Colors": @[@0xff373c34, @0xff737256, @0xff9da386, @0xffc3d2bf, @0xffc7d8c6],
    142                                                                               @"DisabledLCDColor": @YES,
    143                                                                               @"HueBias": @0.55750435756972117,
    144                                                                               @"HueBiasStrength": @0.18424738545816732,
    145                                                                               @"Manual": @NO,
    146                                                                       },
    147                                                                       @"Green Slate": @{
    148                                                                               @"BrightnessBias": @0.2210012227296829,
    149                                                                               @"Colors": @[@0xff343117, @0xff6a876f, @0xff98b4a1, @0xffc3daca, @0xffc8decf],
    150                                                                               @"DisabledLCDColor": @YES,
    151                                                                               @"HueBias": @0.1887667975388467,
    152                                                                               @"HueBiasStrength": @0.1272283345460892,
    153                                                                               @"Manual": @NO,
    154                                                                       },
    155                                                                       @"Green Tea": @{
    156                                                                               @"BrightnessBias": @-0.4946326622596153,
    157                                                                               @"Colors": @[@0xff1a1d08, @0xff1d5231, @0xff3b9774, @0xff97e4c6, @0xffa9eed1],
    158                                                                               @"DisabledLCDColor": @YES,
    159                                                                               @"HueBias": @0.1912955007245464,
    160                                                                               @"HueBiasStrength": @0.3621708039314516,
    161                                                                               @"Manual": @NO,
    162                                                                       },
    163                                                                       @"Lavender": @{
    164                                                                               @"BrightnessBias": @0.10072476038566,
    165                                                                               @"Colors": @[@0xff2b2a3a, @0xff8c507c, @0xffbf82a8, @0xffe9bcce, @0xffeec3d3],
    166                                                                               @"DisabledLCDColor": @YES,
    167                                                                               @"HueBias": @0.7914529587142169,
    168                                                                               @"HueBiasStrength": @0.2498168498277664,
    169                                                                               @"Manual": @NO,
    170                                                                       },
    171                                                                       @"Magic Eggplant": @{
    172                                                                               @"BrightnessBias": @0.0,
    173                                                                               @"Colors": @[@0xff3c2136, @0xff942e84, @0xffc7699d, @0xfff1e4b0, @0xfff6f9b2],
    174                                                                               @"DisabledLCDColor": @YES,
    175                                                                               @"HueBias": @0.87717878486055778,
    176                                                                               @"HueBiasStrength": @0.65018052788844627,
    177                                                                               @"Manual": @NO,
    178                                                                       },
    179                                                                       @"Mystic Blue": @{
    180                                                                               @"BrightnessBias": @-0.3291049897670746,
    181                                                                               @"Colors": @[@0xff3b2306, @0xffa27807, @0xffd1b523, @0xfff6ebbe, @0xfffaf1e4],
    182                                                                               @"DisabledLCDColor": @YES,
    183                                                                               @"HueBias": @0.5282051088288426,
    184                                                                               @"HueBiasStrength": @0.7699633836746216,
    185                                                                               @"Manual": @NO,
    186                                                                       },
    187                                                                       @"Pink Pop": @{
    188                                                                               @"BrightnessBias": @0.624908447265625,
    189                                                                               @"Colors": @[@0xff28140a, @0xff7c42cb, @0xffaa83de, @0xffd1ceeb, @0xffd5d8ec],
    190                                                                               @"DisabledLCDColor": @YES,
    191                                                                               @"HueBias": @0.9477411056868732,
    192                                                                               @"HueBiasStrength": @0.80024421215057373,
    193                                                                               @"Manual": @NO,
    194                                                                       },
    195                                                                       @"Radioactive Pea": @{
    196                                                                               @"BrightnessBias": @-0.48079556772908372,
    197                                                                               @"Colors": @[@0xff215200, @0xff1f7306, @0xff169e34, @0xff03ceb8, @0xff00d4d1],
    198                                                                               @"DisabledLCDColor": @YES,
    199                                                                               @"HueBias": @0.3795131972111554,
    200                                                                               @"HueBiasStrength": @0.34337649402390436,
    201                                                                               @"Manual": @NO,
    202                                                                       },
    203                                                                       @"Rose": @{
    204                                                                               @"BrightnessBias": @0.2727272808551788,
    205                                                                               @"Colors": @[@0xff001500, @0xff4e1fae, @0xff865ac4, @0xffb7e6d3, @0xffbdffd4],
    206                                                                               @"DisabledLCDColor": @YES,
    207                                                                               @"HueBias": @0.9238900924101472,
    208                                                                               @"HueBiasStrength": @0.9957716464996338,
    209                                                                               @"Manual": @NO,
    210                                                                       },
    211                                                                       @"Seaweed": @{
    212                                                                               @"BrightnessBias": @-0.28532744023904377,
    213                                                                               @"Colors": @[@0xff3f0015, @0xff426532, @0xff58a778, @0xff95e0df, @0xffa0e7ee],
    214                                                                               @"DisabledLCDColor": @YES,
    215                                                                               @"HueBias": @0.2694067480079681,
    216                                                                               @"HueBiasStrength": @0.51565612549800799,
    217                                                                               @"Manual": @NO,
    218                                                                       },
    219                                                                       @"Twilight": @{
    220                                                                               @"BrightnessBias": @-0.091789093625498031,
    221                                                                               @"Colors": @[@0xff3f0015, @0xff461286, @0xff6254bd, @0xff97d3e9, @0xffa0e7ee],
    222                                                                               @"DisabledLCDColor": @YES,
    223                                                                               @"HueBias": @0.0,
    224                                                                               @"HueBiasStrength": @0.49710532868525897,
    225                                                                               @"Manual": @NO,
    226                                                                       },
    227                                                               },
    228                                                               
    229                                                               @"NSToolbarItemForcesStandardSize": @YES, // Forces Monterey to resepect toolbar item sizes
    230                                                               @"NSToolbarItemWarnOnMinMaxSize": @NO, // Not going to use Constraints, Apple
    231                                                               }];
    232     
    233     [JOYController startOnRunLoop:[NSRunLoop currentRunLoop] withOptions:@{
    234         JOYAxes2DEmulateButtonsKey: @YES,
    235         JOYHatsEmulateButtonsKey: @YES,
    236     }];
    237     
    238     [GBJoyConManager sharedInstance]; // Starts handling Joy-Cons
    239     
    240     [JOYController registerListener:self];
    241     
    242     if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) {
    243         [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self;
    244     }
    245     
    246     [self askAutoUpdates];
    247     
    248     if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBAutoUpdatesEnabled"]) {
    249         [self checkForUpdates];
    250     }
    251     
    252     if ([[NSProcessInfo processInfo].arguments containsObject:@"--update-launch"]) {
    253         [NSApp activateIgnoringOtherApps:true];
    254     }
    255     
    256     if (![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBThemesVersion"] isEqualToString:@(GB_VERSION)]) {
    257         [self updateThemesDefault:false];
    258         [[NSUserDefaults standardUserDefaults] setObject:@(GB_VERSION) forKey:@"GBThemesVersion"];
    259     }
    260 }
    261 
    262 - (void)updateThemesDefault:(bool)overwrite
    263 {
    264     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    265     NSMutableDictionary *currentThemes = [defaults dictionaryForKey:@"GBThemes"].mutableCopy;
    266     [defaults removeObjectForKey:@"GBThemes"];
    267     NSMutableDictionary *defaultThemes = [defaults dictionaryForKey:@"GBThemes"].mutableCopy;
    268     if (![[NSUserDefaults standardUserDefaults] stringForKey:@"GBThemesVersion"]) {
    269         // Force update the Pink Pop theme, it was glitchy in 1.0
    270         [currentThemes removeObjectForKey:@"Pink Pop"];
    271     }
    272     if (overwrite) {
    273         [currentThemes addEntriesFromDictionary:defaultThemes];
    274         [defaults setObject:currentThemes forKey:@"GBThemes"];
    275     }
    276     else {
    277         [defaultThemes addEntriesFromDictionary:currentThemes];
    278         [defaults setObject:defaultThemes forKey:@"GBThemes"];
    279     }
    280 }
    281 
    282 - (IBAction)toggleDeveloperMode:(id)sender
    283 {
    284     NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    285     [defaults setBool:![defaults boolForKey:@"DeveloperMode"] forKey:@"DeveloperMode"];
    286 }
    287 
    288 - (IBAction)switchPreferencesTab:(id)sender
    289 {
    290     for (NSView *view in _preferencesTabs) {
    291         [view removeFromSuperview];
    292     }
    293     NSView *tab = _preferencesTabs[[sender tag]];
    294     NSRect old = [_preferencesWindow frame];
    295     NSRect new = [_preferencesWindow frameRectForContentRect:tab.frame];
    296     new.origin.x = old.origin.x;
    297     new.origin.y = old.origin.y + (old.size.height - new.size.height);
    298     [_preferencesWindow setFrame:new display:true animate:_preferencesWindow.visible];
    299     [_preferencesWindow.contentView addSubview:tab];
    300 }
    301 
    302 - (BOOL)validateMenuItem:(NSMenuItem *)anItem
    303 {
    304     if ([anItem action] == @selector(toggleDeveloperMode:)) {
    305         [(NSMenuItem *)anItem setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"DeveloperMode"]];
    306     }
    307     
    308     if (anItem == self.linkCableMenuItem) {
    309         return [[NSDocumentController sharedDocumentController] documents].count > 1;
    310     }
    311     return true;
    312 }
    313 
    314 - (void)menuNeedsUpdate:(NSMenu *)menu
    315 {
    316     NSMutableArray *items = [NSMutableArray array];
    317     NSDocument *currentDocument = [[NSDocumentController sharedDocumentController] currentDocument];
    318     
    319     for (NSDocument *document in [[NSDocumentController sharedDocumentController] documents]) {
    320         if (document == currentDocument) continue;
    321         NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:document.displayName action:@selector(connectLinkCable:) keyEquivalent:@""];
    322         item.representedObject = document;
    323         item.image = [[NSWorkspace sharedWorkspace] iconForFile:document.fileURL.path];
    324         [item.image setSize:NSMakeSize(16, 16)];
    325         [items addObject:item];
    326     }
    327     [menu removeAllItems];
    328     for (NSMenuItem *item in items) {
    329         [menu addItem:item];
    330     }
    331 }
    332 
    333 - (IBAction) showPreferences: (id) sender
    334 {
    335     NSArray *objects;
    336     if (!_preferencesWindow) {
    337         [[NSBundle mainBundle] loadNibNamed:@"Preferences" owner:self topLevelObjects:&objects];
    338         NSToolbarItem *first_toolbar_item = [_preferencesWindow.toolbar.items firstObject];
    339         _preferencesWindow.toolbar.selectedItemIdentifier = [first_toolbar_item itemIdentifier];
    340         _preferencesTabs = @[self.emulationTab, self.graphicsTab, self.audioTab, self.controlsTab, self.updatesTab];
    341         [self switchPreferencesTab:first_toolbar_item];
    342         [_preferencesWindow center];
    343 #ifndef UPDATE_SUPPORT
    344         [_preferencesWindow.toolbar removeItemAtIndex:4];
    345 #endif
    346         for (unsigned i = _preferencesWindow.toolbar.items.count; i--;) {
    347             [_preferencesWindow.toolbar.items[i] _view].imageScaling = NSImageScaleNone;
    348         }
    349     }
    350     [_preferencesWindow makeKeyAndOrderFront:self];
    351 }
    352 
    353 - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender
    354 {
    355     [self askAutoUpdates];
    356     /* Bring an existing panel to the foreground */
    357     for (NSWindow *window in [[NSApplication sharedApplication] windows]) {
    358         if ([window isKindOfClass:[NSOpenPanel class]]) {
    359             [(NSOpenPanel *)window makeKeyAndOrderFront:nil];
    360             return true;
    361         }
    362     }
    363     [[NSDocumentController sharedDocumentController] openDocument:self];
    364     return true;
    365 }
    366 
    367 - (void)userNotificationCenter:(NSUserNotificationCenter *)center didActivateNotification:(NSUserNotification *)notification
    368 {
    369     [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:true];
    370 }
    371 
    372 - (void)updateFound
    373 {
    374     [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/raw_changes"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    375         
    376         NSColor *linkColor = [NSColor colorWithRed:0.125 green:0.325 blue:1.0 alpha:1.0];
    377         if (@available(macOS 10.10, *)) {
    378             linkColor = [NSColor linkColor];
    379         }
    380         
    381         NSString *changes = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    382         NSRange cutoffRange = [changes rangeOfString:@"<!--(" GB_VERSION ")-->"];
    383         if (cutoffRange.location != NSNotFound) {
    384             changes = [changes substringToIndex:cutoffRange.location];
    385         }
    386         
    387         NSString *html = [NSString stringWithFormat:@"<!DOCTYPE html><html><head><title></title>"
    388                           "<style>html {background-color:transparent; color: #%06x; line-height:1.5} a:link, a:visited{color:#%06x; text-decoration:none}</style>"
    389                           "</head><body>%@</body></html>",
    390                           color_to_int([NSColor textColor]),
    391                           color_to_int(linkColor),
    392                           changes];
    393         
    394         if ([(NSHTTPURLResponse *)response statusCode] == 200) {
    395             dispatch_async(dispatch_get_main_queue(), ^{
    396                 NSArray *objects;
    397                 [[NSBundle mainBundle] loadNibNamed:@"UpdateWindow" owner:self topLevelObjects:&objects];
    398                 if (@available(macOS 10.11, *)) {
    399                     self.updateChanges.preferences.standardFontFamily = @"-apple-system";
    400                 }
    401                 else if (@available(macOS 10.10, *)) {
    402                     self.updateChanges.preferences.standardFontFamily = @"Helvetica Neue";
    403                 }
    404                 else {
    405                     self.updateChanges.preferences.standardFontFamily = @"Lucida Grande";
    406                 }
    407                 if (@available(macOS 10.15, *)) {
    408                     self.updateChanges.preferences.fixedFontFamily = [NSFont monospacedSystemFontOfSize:12 weight:NSFontWeightRegular].displayName;
    409                 }
    410                 else {
    411                     self.updateChanges.preferences.fixedFontFamily = @"Menlo";
    412                 }
    413                 self.updateChanges.drawsBackground = false;
    414                 [self.updateChanges.mainFrame loadHTMLString:html baseURL:nil];
    415             });
    416         }
    417     }] resume];
    418 }
    419 
    420 - (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
    421 {
    422     // Disable reload context menu
    423     if ([defaultMenuItems count] <= 2) {
    424         return nil;
    425     }
    426     return defaultMenuItems;
    427 }
    428 
    429 - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
    430 {
    431     static dispatch_once_t onceToken;
    432     dispatch_once(&onceToken, ^{
    433         sender.mainFrame.frameView.documentView.enclosingScrollView.drawsBackground = true;
    434         sender.mainFrame.frameView.documentView.enclosingScrollView.backgroundColor = [NSColor textBackgroundColor];
    435         sender.policyDelegate = self;
    436         [self.updateWindow center];
    437         [self.updateWindow makeKeyAndOrderFront:nil];
    438     });
    439 }
    440 
    441 - (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener
    442 {
    443     [listener ignore];
    444     [[NSWorkspace sharedWorkspace] openURL:[request URL]];
    445 }
    446 
    447 - (void)checkForUpdates
    448 {
    449 #ifdef UPDATE_SUPPORT
    450     [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/latest_version"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    451         dispatch_async(dispatch_get_main_queue(), ^{
    452             [self.updatesSpinner stopAnimation:nil];
    453             [self.updatesButton setEnabled:true];
    454         });
    455         if ([(NSHTTPURLResponse *)response statusCode] == 200) {
    456             NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    457             NSArray <NSString *> *components = [string componentsSeparatedByString:@"|"];
    458             if (components.count != 2) return;
    459             _lastVersion = components[0];
    460             _updateURL = components[1];
    461             if (![@GB_VERSION isEqualToString:_lastVersion] &&
    462                 ![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBSkippedVersion"] isEqualToString:_lastVersion]) {
    463                 [self updateFound];
    464             }
    465         }
    466     }] resume];
    467 #endif
    468 }
    469 
    470 - (IBAction)userCheckForUpdates:(id)sender
    471 {
    472     if (self.updateWindow) {
    473         [self.updateWindow makeKeyAndOrderFront:sender];
    474     }
    475     else {
    476         [[NSUserDefaults standardUserDefaults] setObject:nil forKey:@"GBSkippedVersion"];
    477         [self checkForUpdates];
    478         [sender setEnabled:false];
    479         [self.updatesSpinner startAnimation:sender];
    480     }
    481 }
    482 
    483 - (void)askAutoUpdates
    484 {
    485 #ifdef UPDATE_SUPPORT
    486     if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAskedAutoUpdates"]) {
    487         NSAlert *alert = [[NSAlert alloc] init];
    488         alert.messageText = @"Should SameBoy check for updates when launched?";
    489         alert.informativeText = @"SameBoy is frequently updated with new features, accuracy improvements, and bug fixes. This setting can always be changed in the preferences window.";
    490         [alert addButtonWithTitle:@"Check on Launch"];
    491         [alert addButtonWithTitle:@"Don't Check on Launch"];
    492         
    493         [[NSUserDefaults standardUserDefaults] setBool:[alert runModal] == NSAlertFirstButtonReturn forKey:@"GBAutoUpdatesEnabled"];
    494         [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBAskedAutoUpdates"];
    495     }
    496 #endif
    497 }
    498 
    499 - (IBAction)skipVersion:(id)sender
    500 {
    501     [[NSUserDefaults standardUserDefaults] setObject:_lastVersion forKey:@"GBSkippedVersion"];
    502     [self.updateWindow performClose:sender];
    503 }
    504 
    505 - (bool)executePath:(NSString *)path withArguments:(NSArray <NSString *> *)arguments
    506 {
    507     if (!_auth) {
    508         NSTask *task = [[NSTask alloc] init];
    509         task.launchPath = path;
    510         task.arguments = arguments;
    511         [task launch];
    512         [task waitUntilExit];
    513         return task.terminationStatus == 0 && task.terminationReason == NSTaskTerminationReasonExit;
    514     }
    515     
    516     char *argv[arguments.count + 1];
    517     argv[arguments.count] = NULL;
    518     for (unsigned i = 0; i < arguments.count; i++) {
    519         argv[i] = (char *)arguments[i].UTF8String;
    520     }
    521     
    522     return AuthorizationExecuteWithPrivileges(_auth, path.UTF8String, kAuthorizationFlagDefaults, argv, NULL) == errAuthorizationSuccess;
    523 }
    524 
    525 - (void)deauthorize
    526 {
    527     if (_auth) {
    528         AuthorizationFree(_auth, kAuthorizationFlagDefaults);
    529         _auth = nil;
    530     }
    531 }
    532 
    533 - (IBAction)installUpdate:(id)sender
    534 {
    535     bool needsAuthorization = false;
    536     if ([self executePath:@"/usr/sbin/spctl" withArguments:@[@"--status"]]) { // Succeeds when GateKeeper is on
    537         // GateKeeper is on, we need to --add ourselves as root, else we might get a GateKeeper crash
    538         needsAuthorization = true;
    539     }
    540     else if (access(_dyld_get_image_name(0), W_OK)) {
    541         // We don't have write access, so we need authorization to update as root
    542         needsAuthorization = true;
    543     }
    544     
    545     if (needsAuthorization && !_auth) {
    546         AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagPreAuthorize | kAuthorizationFlagInteractionAllowed, &_auth);
    547         // Make sure we can modify the bundle
    548         if (![self executePath:@"/usr/sbin/chown" withArguments:@[@"-R", [NSString stringWithFormat:@"%d:%d", getuid(), getgid()], [NSBundle mainBundle].bundlePath]]) {
    549             [self deauthorize];
    550             return;
    551         }
    552     }
    553     
    554     [self.updateProgressSpinner startAnimation:nil];
    555     self.updateProgressButton.title = @"Cancel";
    556     self.updateProgressButton.enabled = true;
    557     self.updateProgressLabel.stringValue = @"Downloading update…";
    558     _updateState = UPDATE_DOWNLOADING;
    559     _updateTask = [[NSURLSession sharedSession] downloadTaskWithURL: [NSURL URLWithString:_updateURL] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
    560         _updateTask = nil;
    561         dispatch_sync(dispatch_get_main_queue(), ^{
    562             self.updateProgressButton.enabled = false;
    563             self.updateProgressLabel.stringValue = @"Extracting update…";
    564             _updateState = UPDATE_EXTRACTING;
    565         });
    566         
    567         _downloadDirectory = [[[NSFileManager defaultManager] URLForDirectory:NSItemReplacementDirectory
    568                                                                      inDomain:NSUserDomainMask
    569                                                             appropriateForURL:[[NSBundle mainBundle] bundleURL]
    570                                                                        create:true
    571                                                                         error:nil] path];
    572         if (!_downloadDirectory) {
    573             [self deauthorize];
    574             dispatch_sync(dispatch_get_main_queue(), ^{
    575                 self.updateProgressButton.enabled = false;
    576                 self.updateProgressLabel.stringValue = @"Failed to extract update.";
    577                 _updateState = UPDATE_FAILED;
    578                 self.updateProgressButton.title = @"Close";
    579                 self.updateProgressButton.enabled = true;
    580                 [self.updateProgressSpinner stopAnimation:nil];
    581             });
    582             return;
    583         }
    584         
    585         NSTask *unzipTask;
    586         unzipTask = [[NSTask alloc] init];
    587         unzipTask.launchPath = @"/usr/bin/unzip";
    588         unzipTask.arguments = @[location.path, @"-d", _downloadDirectory];
    589         [unzipTask launch];
    590         [unzipTask waitUntilExit];
    591         if (unzipTask.terminationStatus != 0 || unzipTask.terminationReason != NSTaskTerminationReasonExit) {
    592             [self deauthorize];
    593             [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
    594             dispatch_sync(dispatch_get_main_queue(), ^{
    595                 self.updateProgressButton.enabled = false;
    596                 self.updateProgressLabel.stringValue = @"Failed to extract update.";
    597                 _updateState = UPDATE_FAILED;
    598                 self.updateProgressButton.title = @"Close";
    599                 self.updateProgressButton.enabled = true;
    600                 [self.updateProgressSpinner stopAnimation:nil];
    601             });
    602             return;
    603         }
    604         
    605         dispatch_sync(dispatch_get_main_queue(), ^{
    606             self.updateProgressButton.enabled = false;
    607             self.updateProgressLabel.stringValue = @"Update ready, save your game progress and click Install.";
    608             _updateState = UPDATE_WAIT_INSTALL;
    609             self.updateProgressButton.title = @"Install";
    610             self.updateProgressButton.enabled = true;
    611             [self.updateProgressSpinner stopAnimation:nil];
    612         });
    613     }];
    614     [_updateTask resume];
    615     
    616     self.updateProgressWindow.preventsApplicationTerminationWhenModal = false;
    617     [self.updateWindow beginSheet:self.updateProgressWindow completionHandler:^(NSModalResponse returnCode) {
    618         [self.updateWindow close];
    619     }];
    620 }
    621 
    622 - (void)performUpgrade
    623 {
    624     self.updateProgressButton.enabled = false;
    625     self.updateProgressLabel.stringValue = @"Instaling update…";
    626     _updateState = UPDATE_INSTALLING;
    627     self.updateProgressButton.enabled = false;
    628     [self.updateProgressSpinner startAnimation:nil];
    629     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    630         NSString *executablePath = [[NSBundle mainBundle] executablePath];
    631         NSString *contentsPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Contents"];
    632         NSString *contentsTempPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"TempContents"];
    633         NSString *updateContentsPath = [_downloadDirectory stringByAppendingPathComponent:@"SameBoy.app/Contents"];
    634         NSError *error = nil;
    635         [[NSFileManager defaultManager] moveItemAtPath:contentsPath toPath:contentsTempPath error:&error];
    636         if (error) {
    637             [self deauthorize];
    638             [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
    639             _downloadDirectory = nil;
    640             dispatch_sync(dispatch_get_main_queue(), ^{
    641                 self.updateProgressButton.enabled = false;
    642                 self.updateProgressLabel.stringValue = @"Failed to install update.";
    643                 _updateState = UPDATE_FAILED;
    644                 self.updateProgressButton.title = @"Close";
    645                 self.updateProgressButton.enabled = true;
    646                 [self.updateProgressSpinner stopAnimation:nil];
    647             });
    648             return;
    649         }
    650         [[NSFileManager defaultManager] moveItemAtPath:updateContentsPath toPath:contentsPath error:&error];
    651         if (error) {
    652             [self deauthorize];
    653             [[NSFileManager defaultManager] moveItemAtPath:contentsTempPath toPath:contentsPath error:nil];
    654             [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
    655             _downloadDirectory = nil;
    656             dispatch_sync(dispatch_get_main_queue(), ^{
    657                 self.updateProgressButton.enabled = false;
    658                 self.updateProgressLabel.stringValue = @"Failed to install update.";
    659                 _updateState = UPDATE_FAILED;
    660                 self.updateProgressButton.title = @"Close";
    661                 self.updateProgressButton.enabled = true;
    662                 [self.updateProgressSpinner stopAnimation:nil];
    663             });
    664             return;
    665         }
    666         [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
    667         [[NSFileManager defaultManager] removeItemAtPath:contentsTempPath error:nil];
    668         
    669         // Remove the quarantine flag so we don't have to escape translocation
    670         NSString *bundlePath = [NSBundle mainBundle].bundlePath;
    671         removexattr(bundlePath.UTF8String, "com.apple.quarantine", 0);
    672         for (NSString *path in [[NSFileManager defaultManager] enumeratorAtPath:bundlePath]) {
    673             removexattr([bundlePath stringByAppendingPathComponent:path].UTF8String, "com.apple.quarantine", 0);
    674         };
    675         
    676         _downloadDirectory = nil;
    677         atexit_b(^{
    678             execl(executablePath.UTF8String, executablePath.UTF8String, "--update-launch", NULL);
    679         });
    680         
    681         dispatch_async(dispatch_get_main_queue(), ^{
    682             [NSApp terminate:nil];
    683         });
    684     });
    685 }
    686 
    687 - (IBAction)updateAction:(id)sender
    688 {
    689     switch (_updateState) {
    690         case UPDATE_DOWNLOADING:
    691             [_updateTask cancelByProducingResumeData:nil];
    692             _updateTask = nil;
    693             [self.updateProgressWindow close];
    694             break;
    695         case UPDATE_WAIT_INSTALL:
    696             [self performUpgrade];
    697             break;
    698         case UPDATE_EXTRACTING:
    699         case UPDATE_INSTALLING:
    700             break;
    701         case UPDATE_FAILED:
    702             [self.updateProgressWindow close];
    703             break;
    704     }
    705 }
    706 
    707 - (void)orderFrontAboutPanel:(id)sender
    708 {
    709     // NSAboutPanelOptionApplicationIcon is not available prior to 10.13, but the key is still there and working.
    710     [[NSApplication sharedApplication] orderFrontStandardAboutPanelWithOptions:@{
    711         @"ApplicationIcon": [NSImage imageNamed:@"Icon"]
    712     }];
    713 }
    714 
    715 - (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button
    716 {
    717     if (!button.isPressed) return;
    718     NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID];
    719     if (!mapping) {
    720         mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName];
    721     }
    722     
    723     JOYButtonUsage usage = ((JOYButtonUsage)[mapping[n2s(button.uniqueID)] unsignedIntValue]) ?: -1;
    724     if (!mapping && usage >= JOYButtonUsageGeneric0) {
    725         usage = GB_inline_const(JOYButtonUsage[], {JOYButtonUsageY, JOYButtonUsageA, JOYButtonUsageB, JOYButtonUsageX})[(usage - JOYButtonUsageGeneric0) & 3];
    726     }
    727     
    728     if (usage == GBJoyKitHotkey1 || usage == GBJoyKitHotkey2) {
    729         if (_preferencesWindow && self.keyWindow == _preferencesWindow) {
    730             return;
    731         }
    732         if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAllowBackgroundControllers"] && !self.keyWindow) {
    733             return;
    734         }
    735 
    736         NSString *keyEquivalent = [[NSUserDefaults standardUserDefaults] stringForKey:usage == GBJoyKitHotkey1? @"GBJoypadHotkey1" : @"GBJoypadHotkey2"];
    737         NSEventModifierFlags flags = NSEventModifierFlagCommand;
    738         if ([keyEquivalent hasPrefix:@"^"]) {
    739             flags |= NSEventModifierFlagShift;
    740             [keyEquivalent substringFromIndex:1];
    741         }
    742         _simulatingMenuEvent = true;
    743         [[NSApplication sharedApplication] sendEvent:[NSEvent keyEventWithType:NSEventTypeKeyDown
    744                                                                                  location:(NSPoint){0,}
    745                                                                             modifierFlags:flags
    746                                                                                 timestamp:0
    747                                                                              windowNumber:0
    748                                                                                   context:NULL
    749                                                                                characters:keyEquivalent
    750                                                               charactersIgnoringModifiers:keyEquivalent
    751                                                                                 isARepeat:false
    752                                                                                   keyCode:0]];
    753         _simulatingMenuEvent = false;
    754     }
    755 }
    756 
    757 - (NSWindow *)keyWindow
    758 {
    759     NSWindow *ret = [super keyWindow];
    760     if (!ret && _simulatingMenuEvent) {
    761         ret = [(Document *)self.orderedDocuments.firstObject mainWindow];
    762     }
    763     return ret;
    764 }
    765 
    766 - (NSWindow *)mainWindow
    767 {
    768     NSWindow *ret = [super mainWindow];
    769     if (!ret && _simulatingMenuEvent) {
    770         ret = [(Document *)self.orderedDocuments.firstObject mainWindow];
    771     }
    772     return ret;
    773 }
    774 
    775 - (IBAction)openDebuggerHelp:(id)sender
    776 {
    777     [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://sameboy.github.io/debugger/"]];
    778 }
    779 
    780 - (IBAction)openSponsor:(id)sender
    781 {
    782     [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://github.com/sponsors/LIJI32"]];
    783 }
    784 
    785 - (void)dealloc
    786 {
    787     if (_downloadDirectory) {
    788         [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil];
    789     }
    790 }
    791 
    792 - (IBAction)nop:(id)sender
    793 {
    794 }
    795 
    796 /* This runs before C constructors. If we need to escape translocation, we should
    797    do it ASAP to minimize our launch time. */
    798 
    799 + (void)load
    800 {
    801     if (@available(macOS 10.12, *)) {
    802         /* Detect and escape translocation so we can safely update ourselves */
    803         if ([[[NSBundle mainBundle] bundlePath] containsString:@"/AppTranslocation/"]) {
    804             const char *mountPath = [[[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent].UTF8String;
    805             struct statfs *mntbuf;
    806             int mntsize = getmntinfo(&mntbuf, MNT_NOWAIT);
    807             for (unsigned i = 0; i < mntsize; i++) {
    808                 if (strcmp(mntbuf[i].f_mntonname, mountPath) == 0) {
    809                     NSBundle *origBundle = [NSBundle bundleWithPath:@(mntbuf[i].f_mntfromname)];
    810                     
    811                     execl(origBundle.executablePath.UTF8String, origBundle.executablePath.UTF8String, NULL);
    812                     break;
    813                 }
    814             }
    815         }
    816     }
    817 }
    818 @end

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.