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.