SameBoy | Accurate GB/GBC emulator |
| download: https://git.y1.nz/archives/sameboy.tar.gz | |
| README | Files | Log | Refs | LICENSE |
iOS/GBViewController.m
1 #import "GBViewController.h"
2 #import "GBHorizontalLayout.h"
3 #import "GBVerticalLayout.h"
4 #import "GBViewMetal.h"
5 #import "GBAudioClient.h"
6 #import "GBROMManager.h"
7 #import "GBLibraryViewController.h"
8 #import "GBBackgroundView.h"
9 #import "GBHapticManager.h"
10 #import "GBMenuViewController.h"
11 #import "GBOptionViewController.h"
12 #import "GBAboutController.h"
13 #import "GBSettingsViewController.h"
14 #import "GBPalettePicker.h"
15 #import "GBStatesViewController.h"
16 #import "GBCheckableAlertController.h"
17 #import "GBPrinterFeedController.h"
18 #import "GBCheatsController.h"
19 #import "UILabel+LockFonts.h"
20 #import <CommonCrypto/CommonCrypto.h>
21 #include <sys/xattr.h>
22 #import "GCControllerGetElements.h"
23 #import "GBZipReader.h"
24 #import <sys/stat.h>
25 #import <CoreMotion/CoreMotion.h>
26 #import <dlfcn.h>
27 #import <objc/runtime.h>
28
29 #if !__has_include(<UIKit/UISliderTrackConfiguration.h>)
30 /* Building with older SDKs */
31
32 typedef NS_ENUM(NSInteger, UIMenuSystemElementGroupPreference) {
33 UIMenuSystemElementGroupPreferenceAutomatic = 0,
34 UIMenuSystemElementGroupPreferenceRemoved,
35 UIMenuSystemElementGroupPreferenceIncluded,
36 };
37
38 API_AVAILABLE(ios(19.0))
39 @interface UIMainMenuSystemConfiguration : NSObject <NSCopying>
40 @property (nonatomic, assign) UIMenuSystemElementGroupPreference newScenePreference;
41 @property (nonatomic, assign) UIMenuSystemElementGroupPreference documentPreference;
42 @property (nonatomic, assign) UIMenuSystemElementGroupPreference printingPreference;
43 @property (nonatomic, assign) UIMenuSystemElementGroupPreference findingPreference;
44 @property (nonatomic, assign) UIMenuSystemElementGroupPreference toolbarPreference;
45 @property (nonatomic, assign) UIMenuSystemElementGroupPreference sidebarPreference;
46 @property (nonatomic, assign) UIMenuSystemElementGroupPreference inspectorPreference;
47 @property (nonatomic, assign) UIMenuSystemElementGroupPreference textFormattingPreference;
48 @end
49
50 API_AVAILABLE(ios(19.0))
51 @interface UIMainMenuSystem : UIMenuSystem
52 @property (class, nonatomic, readonly) UIMainMenuSystem *sharedSystem;
53 - (void)setBuildConfiguration:(UIMainMenuSystemConfiguration *)configuration buildHandler:(void(^)(NSObject<UIMenuBuilder> *builder))buildHandler;
54 @end
55
56 API_AVAILABLE(ios(19.0))
57 @interface NSObject(UIMenuBuilder)
58 - (void)insertElements:(NSArray<UIMenuElement *> *)childElements atStartOfMenuForIdentifier:(UIMenuIdentifier)parentIdentifier;
59 @end
60
61 #endif
62
63
64 static UIImage *CreateMenuImage(NSString *name)
65 {
66 static const unsigned size = 20;
67 UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(size, size)];
68 CGRect destRect = {0,};
69 UIImage *source = [UIImage imageNamed:name];
70 CGSize sourceSize = source.size;
71 if (sourceSize.width > sourceSize.height) {
72 destRect.size.width = size;
73 destRect.size.height = sourceSize.height * size / sourceSize.width;
74 destRect.origin.y = (size - destRect.size.height) / 2;
75 }
76 else {
77 destRect.size.height = size;
78 destRect.size.width = sourceSize.width * size / sourceSize.height;
79 destRect.origin.x = (size - destRect.size.width) / 2;
80 }
81 UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *myContext) {
82 [source drawInRect:destRect];
83 }];
84 return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
85 }
86
87 API_AVAILABLE(ios(13.0))
88 @implementation UIKeyCommand (KeyCommandWithImage)
89
90 + (instancetype)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action title:(NSString *)title image:(UIImage *)image
91 {
92 UIKeyCommand *ret = [self keyCommandWithInput:input modifierFlags:modifierFlags action:action];
93 ret.title = title;
94 ret.image = image;
95 return ret;
96 }
97
98 @end
99
100 @implementation GBViewController
101 {
102 GB_gameboy_t _gb;
103 GBView *_gbView;
104 dispatch_queue_t _runQueue;
105
106 volatile bool _running;
107 volatile bool _stopping;
108 bool _rewind;
109 bool _rewindOver;
110 bool _romLoaded;
111 bool _swappingROM;
112 bool _skipAutoLoad;
113
114 bool _rapidA, _rapidB;
115 uint8_t _rapidACount, _rapidBCount;
116
117 UIInterfaceOrientation _orientation;
118 GBHorizontalLayout *_horizontalLayoutLeft;
119 GBHorizontalLayout *_horizontalLayoutRight;
120 GBVerticalLayout *_verticalLayout;
121 GBBackgroundView *_backgroundView;
122
123 NSCondition *_audioLock;
124 GB_sample_t *_audioBuffer;
125 size_t _audioBufferSize;
126 size_t _audioBufferPosition;
127 size_t _audioBufferNeeded;
128 GBAudioClient *_audioClient;
129
130 NSMutableSet *_defaultsObservers;
131 GB_palette_t _palette;
132 CMMotionManager *_motionManager;
133
134 CVImageBufferRef _cameraImage;
135 AVCaptureSession *_cameraSession;
136 AVCaptureConnection *_cameraConnection;
137 AVCaptureVideoDataOutput *_cameraOutput;
138 bool _cameraNeedsUpdate;
139 NSTimer *_disableCameraTimer;
140 AVCaptureDevicePosition _cameraPosition;
141 UIButton *_cameraPositionButton;
142 UIButton *_changeCameraButton;
143 AVCaptureDevice *_frontCaptureDevice;
144 AVCaptureDevice *_backCaptureDevice;
145 NSMutableArray<NSNumber *> *_zoomLevels;
146 unsigned _currentZoomIndex;
147
148 __weak GCController *_lastController;
149
150 dispatch_queue_t _cameraQueue;
151
152 bool _runModeFromController;
153
154 UIWindow *_mirrorWindow;
155 GBView *_mirrorView;
156
157 bool _printerConnected;
158 UIButton *_printerButton;
159 UIActivityIndicatorView *_printerSpinner;
160 NSMutableData *_currentPrinterImageData;
161
162 NSString *_lastSavedROM;
163 NSDate *_saveDate;
164
165 unsigned _autosaveCountdown;
166 }
167
168 static void loadBootROM(GB_gameboy_t *gb, GB_boot_rom_t type)
169 {
170 GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
171 [self loadBootROM:type];
172 }
173
174 static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type)
175 {
176 GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
177 [self vblankWithType:type];
178 }
179
180
181 static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height,
182 uint8_t top_margin, uint8_t bottom_margin, uint8_t exposure)
183 {
184 GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
185 [self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure];
186 }
187
188 static void printDone(GB_gameboy_t *gb)
189 {
190 GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
191 [self printDone];
192 }
193
194
195 static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes)
196 {
197 static NSString *buffer = @"";
198 buffer = [buffer stringByAppendingString:@(string)];
199 if ([buffer containsString:@"\n"]) {
200 NSLog(@"%@", buffer);
201 buffer = @"";
202 }
203 }
204
205 static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b)
206 {
207 return (r << 0) | (g << 8) | (b << 16) | 0xFF000000;
208 }
209
210 static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample)
211 {
212 GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
213 [self gotNewSample:sample];
214 }
215
216 static void cameraRequestUpdate(GB_gameboy_t *gb)
217 {
218 GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
219 [self cameraRequestUpdate];
220 }
221
222 static uint8_t cameraGetPixel(GB_gameboy_t *gb, uint8_t x, uint8_t y)
223 {
224 GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
225 return [self cameraGetPixelAtX:x andY:y];
226 }
227
228
229 static void rumbleCallback(GB_gameboy_t *gb, double amp)
230 {
231 GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
232 double strength = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBRumbleStrength"];
233 if (strength != 1) {
234 amp = pow(amp, strength) * strength;
235 }
236 [self rumbleChanged:amp];
237 }
238
239 - (void)initGameBoy
240 {
241 GB_gameboy_t *gb = &_gb;
242 GB_init(gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]);
243 GB_set_user_data(gb, (__bridge void *)(self));
244 GB_set_boot_rom_load_callback(gb, (GB_boot_rom_load_callback_t)loadBootROM);
245 GB_set_vblank_callback(gb, (GB_vblank_callback_t) vblank);
246 GB_set_log_callback(gb, (GB_log_callback_t) consoleLog);
247 GB_set_camera_get_pixel_callback(gb, cameraGetPixel);
248 GB_set_camera_update_request_callback(gb, cameraRequestUpdate);
249 [self addDefaultObserver:^(id newValue) {
250 GB_set_color_correction_mode(gb, (GB_color_correction_mode_t)[newValue integerValue]);
251 } forKey:@"GBColorCorrection"];
252 [self addDefaultObserver:^(id newValue) {
253 GB_set_light_temperature(gb, [newValue doubleValue]);
254 } forKey:@"GBLightTemperature"];
255 GB_set_border_mode(gb, GB_BORDER_NEVER);
256 __weak typeof(self) weakSelf = self;
257 [self addDefaultObserver:^(id newValue) {
258 [weakSelf updatePalette];
259 } forKey:@"GBCurrentTheme"];
260 GB_set_rgb_encode_callback(gb, rgbEncode);
261 [self addDefaultObserver:^(id newValue) {
262 GB_set_highpass_filter_mode(gb, (GB_highpass_mode_t)[newValue integerValue]);
263 } forKey:@"GBHighpassFilter"];
264 [self addDefaultObserver:^(id newValue) {
265 GB_set_rtc_mode(gb, [newValue integerValue]);
266 } forKey:@"GBRTCMode"];
267 GB_apu_set_sample_callback(gb, audioCallback);
268 GB_set_rumble_callback(gb, rumbleCallback);
269 [self addDefaultObserver:^(id newValue) {
270 GB_set_rumble_mode(gb, [newValue integerValue]);
271 } forKey:@"GBRumbleMode"];
272 [self addDefaultObserver:^(id newValue) {
273 GB_set_interference_volume(gb, [newValue doubleValue]);
274 } forKey:@"GBInterferenceVolume"];
275 [self addDefaultObserver:^(id newValue) {
276 GB_set_rewind_length(gb, [newValue unsignedIntValue]);
277 } forKey:@"GBRewindLength"];
278 [self addDefaultObserver:^(id newValue) {
279 GB_set_turbo_cap(gb, [newValue doubleValue]);
280 } forKey:@"GBTurboCap"];
281 [self addDefaultObserver:^(id newValue) {
282 [[AVAudioSession sharedInstance] setCategory:[newValue isEqual:@"on"]? AVAudioSessionCategoryPlayback : AVAudioSessionCategorySoloAmbient
283 mode:AVAudioSessionModeDefault
284 routeSharingPolicy:AVAudioSessionRouteSharingPolicyDefault
285 options:0
286 error:nil];
287 } forKey:@"GBAudioMode"];
288 }
289
290 - (void)addDefaultObserver:(void(^)(id newValue))block forKey:(NSString *)key
291 {
292 if (!_defaultsObservers) {
293 _defaultsObservers = [NSMutableSet set];
294 }
295 block = [block copy];
296 [_defaultsObservers addObject:block];
297 [[NSUserDefaults standardUserDefaults] addObserver:self
298 forKeyPath:key
299 options:NSKeyValueObservingOptionNew
300 context:(void *)block];
301 block([[NSUserDefaults standardUserDefaults] objectForKey:key]);
302 }
303
304 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
305 {
306 ((__bridge void(^)(id))context)(change[NSKeyValueChangeNewKey]);
307 }
308
309 - (NSArray<NSNumber *> *)zoomFactorsForDevice:(AVCaptureDevice *)device
310 {
311 if (@available(iOS 13.0, *)) {
312 return device.virtualDeviceSwitchOverVideoZoomFactors;
313 }
314 double factor = device.dualCameraSwitchOverVideoZoomFactor;
315 if (factor == 1.0) {
316 return @[];
317 }
318 return @[@(factor)];
319 }
320
321 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
322 {
323 _window = [[UIWindow alloc] init];
324 _window.rootViewController = self;
325 [_window makeKeyAndVisible];
326
327 _runQueue = dispatch_queue_create("SameBoy Emulation Queue", NULL);
328
329 #pragma clang diagnostic push
330 #pragma clang diagnostic ignored "-Warc-retain-cycles"
331 [self addDefaultObserver:^(id newValue) {
332 GBTheme *theme = [GBSettingsViewController themeNamed:newValue];
333 _horizontalLayoutLeft = [[GBHorizontalLayout alloc] initWithTheme:theme cutoutOnRight:false];
334 _horizontalLayoutRight = _horizontalLayoutLeft.cutout?
335 [[GBHorizontalLayout alloc] initWithTheme:theme cutoutOnRight:true] :
336 _horizontalLayoutLeft;
337 _verticalLayout = [[GBVerticalLayout alloc] initWithTheme:theme];
338 _printerSpinner.color = theme.buttonColor;
339
340 [self willRotateToInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation
341 duration:0];
342 [_backgroundView reloadThemeImages];
343
344 [self setNeedsStatusBarAppearanceUpdate];
345 } forKey:@"GBInterfaceTheme"];
346 #pragma clang diagnostic pop
347
348 _backgroundView = [[GBBackgroundView alloc] initWithLayout:_verticalLayout];
349 [_window addSubview:_backgroundView];
350 self.view = _backgroundView;
351
352
353 [self initGameBoy];
354 _gbView = _backgroundView.gbView;
355 _gbView.hidden = true;
356 _gbView.gb = &_gb;
357 [_gbView screenSizeChanged];
358
359 [self addDefaultObserver:^(id newValue) {
360 [[NSNotificationCenter defaultCenter] postNotificationName:@"GBFilterChanged" object:nil];
361 } forKey:@"GBFilter"];
362
363 __weak GBView *gbview = _gbView;
364 [self addDefaultObserver:^(id newValue) {
365 gbview.frameBlendingMode = [newValue integerValue];
366 } forKey:@"GBFrameBlendingMode"];
367
368 __weak GBBackgroundView *backgroundView = _backgroundView;
369 [self addDefaultObserver:^(id newValue) {
370 backgroundView.usesSwipePad = [newValue boolValue];
371 } forKey:@"GBSwipeDpad"];
372
373
374 [self willRotateToInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation
375 duration:0];
376
377
378 _audioLock = [[NSCondition alloc] init];
379
380 [[NSNotificationCenter defaultCenter] addObserverForName:@"GBROMChanged"
381 object:nil
382 queue:nil
383 usingBlock:^(NSNotification *note) {
384 _swappingROM = true;
385 [self stop];
386 [self start];
387 }];
388
389 _motionManager = [[CMMotionManager alloc] init];
390 _cameraPosition = AVCaptureDevicePositionBack;
391
392 // Back camera setup
393 NSArray *deviceTypes = @[AVCaptureDeviceTypeBuiltInWideAngleCamera,
394 AVCaptureDeviceTypeBuiltInTelephotoCamera,
395 AVCaptureDeviceTypeBuiltInDualCamera];
396 if (@available(iOS 13.0, *)) {
397 // AVCaptureDeviceTypeBuiltInUltraWideCamera is only available in iOS 13+
398 deviceTypes = @[AVCaptureDeviceTypeBuiltInWideAngleCamera,
399 AVCaptureDeviceTypeBuiltInUltraWideCamera,
400 AVCaptureDeviceTypeBuiltInTelephotoCamera,
401 AVCaptureDeviceTypeBuiltInTripleCamera,
402 AVCaptureDeviceTypeBuiltInDualWideCamera,
403 AVCaptureDeviceTypeBuiltInDualCamera];
404 }
405
406 // Use a discovery session to gather the capture devices (all back cameras as well as the front camera)
407 AVCaptureDeviceDiscoverySession *cameraDiscoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
408 mediaType:AVMediaTypeVideo
409 position:AVCaptureDevicePositionUnspecified];
410 for (AVCaptureDevice *device in cameraDiscoverySession.devices) {
411 if ([device position] == AVCaptureDevicePositionBack) {
412 if (!_backCaptureDevice ||
413 [self zoomFactorsForDevice:_backCaptureDevice].count < [self zoomFactorsForDevice:device].count) {
414 _backCaptureDevice = device;
415 }
416 }
417 else if ([device position] == AVCaptureDevicePositionFront) {
418 _frontCaptureDevice = device;
419 }
420 }
421
422 _zoomLevels = [self zoomFactorsForDevice:_backCaptureDevice].mutableCopy;
423 [_zoomLevels insertObject:@1 atIndex:0];
424 if (_zoomLevels.count == 3 && _zoomLevels[2].doubleValue > 5.5 && _zoomLevels[1].doubleValue < 3.5) {
425 [_zoomLevels insertObject:@4 atIndex:2];
426 }
427
428 _cameraPositionButton = [[UIButton alloc] init];
429 [self didRotateFromInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation];
430 if (@available(iOS 13.0, *)) {
431 [_cameraPositionButton setImage:[UIImage systemImageNamed:@"camera.rotate"
432 withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]]
433 forState:UIControlStateNormal];
434 _cameraPositionButton.backgroundColor = [UIColor systemBackgroundColor];
435
436 // Configure the change camera button stacked on top of the camera position button
437 _changeCameraButton = [[UIButton alloc] init];
438 [_changeCameraButton setImage:[UIImage systemImageNamed:@"camera.aperture"
439 withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]]
440 forState:UIControlStateNormal];
441 _changeCameraButton.backgroundColor = [UIColor systemBackgroundColor];
442 _changeCameraButton.layer.cornerRadius = 6;
443 _changeCameraButton.alpha = 0;
444 [_changeCameraButton addTarget:self
445 action:@selector(changeCamera)
446 forControlEvents:UIControlEventTouchUpInside];
447 // Only show the change camera button if we have more than one back camera to swap between.
448 if (_zoomLevels.count > 1) {
449 [_backgroundView addSubview:_changeCameraButton];
450 }
451 }
452 else {
453 UIImage *rotateImage = [[UIImage imageNamed:@"CameraRotateTemplate"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
454 [_cameraPositionButton setImage:rotateImage
455 forState:UIControlStateNormal];
456 _cameraPositionButton.backgroundColor = [UIColor whiteColor];
457 }
458
459 _cameraPositionButton.layer.cornerRadius = 6;
460 _cameraPositionButton.alpha = 0;
461 [_cameraPositionButton addTarget:self
462 action:@selector(rotateCamera)
463 forControlEvents:UIControlEventTouchUpInside];
464
465 [_backgroundView addSubview:_cameraPositionButton];
466
467 _cameraQueue = dispatch_queue_create("SameBoy Camera Queue", NULL);
468
469 [UNUserNotificationCenter currentNotificationCenter].delegate = self;
470 [self verifyEntitlements];
471
472 [self setControllerHandlers];
473 [[NSNotificationCenter defaultCenter] addObserver:self
474 selector:@selector(setControllerHandlers)
475 name:GCControllerDidConnectNotification
476 object:nil];
477
478 [[NSNotificationCenter defaultCenter] addObserver:self
479 selector:@selector(controllerDisconnected:)
480 name:GCControllerDidDisconnectNotification
481 object:nil];
482
483 for (NSString *name in @[UIScreenDidConnectNotification,
484 UIScreenDidDisconnectNotification,
485 UIScreenModeDidChangeNotification]) {
486 [[NSNotificationCenter defaultCenter] addObserver:self
487 selector:@selector(updateMirrorWindow)
488 name:name
489 object:nil];
490 }
491
492 _printerButton = [[UIButton alloc] init];
493 _printerSpinner = [[UIActivityIndicatorView alloc] init];
494 _printerSpinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite;
495 _printerSpinner.color = _verticalLayout.theme.buttonColor;
496 [self didRotateFromInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation];
497
498 if (@available(iOS 13.0, *)) {
499 [_printerButton setImage:[UIImage systemImageNamed:@"printer"
500 withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]]
501 forState:UIControlStateNormal];
502 _printerButton.backgroundColor = [UIColor systemBackgroundColor];
503 }
504 else {
505 UIImage *rotateImage = [[UIImage imageNamed:@"PrinterTemplate"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
506 [_printerButton setImage:rotateImage
507 forState:UIControlStateNormal];
508 _printerButton.backgroundColor = [UIColor whiteColor];
509 }
510
511 _printerButton.layer.cornerRadius = 6;
512 _printerButton.alpha = 0;
513 [_printerButton addTarget:self
514 action:@selector(showPrinterFeed)
515 forControlEvents:UIControlEventTouchUpInside];
516
517
518 [_backgroundView addSubview:_printerButton];
519 [_backgroundView addSubview:_printerSpinner];
520
521 [self updateMirrorWindow];
522
523 if (@available(iOS 26.0, *)) {
524 UIMainMenuSystemConfiguration *conf = [[objc_getClass("UIMainMenuSystemConfiguration") alloc] init];
525 conf.newScenePreference = UIMenuSystemElementGroupPreferenceRemoved;
526 conf.documentPreference = UIMenuSystemElementGroupPreferenceRemoved;
527 conf.printingPreference = UIMenuSystemElementGroupPreferenceRemoved;
528 conf.findingPreference = UIMenuSystemElementGroupPreferenceRemoved;
529 conf.toolbarPreference = UIMenuSystemElementGroupPreferenceRemoved;
530 conf.sidebarPreference = UIMenuSystemElementGroupPreferenceRemoved;
531 conf.inspectorPreference = UIMenuSystemElementGroupPreferenceRemoved;
532 conf.textFormattingPreference = UIMenuSystemElementGroupPreferenceRemoved;
533
534 UIMainMenuSystem *system = (id)[objc_getClass("UIMainMenuSystem") sharedSystem];
535 [system setBuildConfiguration:conf
536 buildHandler:^(id<UIMenuBuilder> builder) {
537 [builder removeMenuForIdentifier:UIMenuView]; // This menu's always empty
538 [builder removeMenuForIdentifier:UIMenuOpenRecent]; // This will list files to re-import, bad
539
540 [(id)builder insertElements:@[[UICommand commandWithTitle:@"About SameBoy"
541 image:nil
542 action:@selector(showAbout)
543 propertyList:nil]]
544 atStartOfMenuForIdentifier:UIMenuApplication];
545
546 UIMenu *emulationMenu = [UIMenu menuWithTitle:@"Emulation" children:@[
547 [UIKeyCommand keyCommandWithInput:@"r" modifierFlags:UIKeyModifierCommand action:@selector(reset) title:@"Reset" image:[UIImage systemImageNamed:@"arrow.2.circlepath"]],
548 [UICommand commandWithTitle:@"Change Model…" image:CreateMenuImage(@"ModelTemplate") action:@selector(changeModel) propertyList:nil],
549 [UICommand commandWithTitle:@"Save States…" image:[UIImage systemImageNamed:@"square.stack"] action:@selector(openStates) propertyList:nil],
550 [UIMenu menuWithTitle:@"Cheats" image:CreateMenuImage(@"CheatsTemplate") identifier:nil options:0 children:@[
551 [UIKeyCommand keyCommandWithInput:@"c" modifierFlags:UIKeyModifierCommand | UIKeyModifierShift action:@selector(toggleCheats) title:@"Enable Cheats" image:nil],
552 [UICommand commandWithTitle:@"Show Cheats…" image:nil action:@selector(openCheats) propertyList:nil],
553 ]],
554 [UIMenu menuWithTitle:@"Connect" image:CreateMenuImage(@"LinkCableTemplate") identifier:nil options:0 children:@[
555 [UICommand commandWithTitle:@"None" image:nil action:@selector(disconnectCable) propertyList:nil],
556 [UICommand commandWithTitle:@"Printer" image:[UIImage systemImageNamed:@"printer"] action:@selector(connectPrinter) propertyList:nil],
557 ]],
558 ]];
559 [builder insertSiblingMenu:emulationMenu beforeMenuForIdentifier:UIMenuWindow];
560 }];
561 }
562
563 return true;
564 }
565
566 - (BOOL)canPerformAction:(SEL)action withSender:(id)sender
567 {
568 if (action == @selector(reset)) {
569 if (self.presentedViewController || ![GBROMManager sharedManager].currentROM) return false;
570 }
571 if (action == @selector(openStates) || action == @selector(changeModel) || action == @selector(openCheats) ||
572 action == @selector(toggleCheats) || action == @selector(disconnectCable) || action == @selector(connectPrinter)) {
573 if (![GBROMManager sharedManager].currentROM) return false;
574 }
575 return [super canPerformAction:action withSender:sender];
576 }
577
578 - (void)validateCommand:(UICommand *)command
579 {
580 if (command.action == @selector(toggleCheats)) {
581 command.state = GB_is_inited(&_gb) && GB_cheats_enabled(&_gb);
582 }
583
584 if (command.action == @selector(connectPrinter)) {
585 command.state = _printerConnected;
586 }
587
588 if (command.action == @selector(disconnectCable)) {
589 command.state = !_printerConnected;
590 }
591
592 [super validateCommand:command];
593 }
594
595 - (void)toggleCheats
596 {
597 GB_set_cheats_enabled(&_gb, !GB_cheats_enabled(&_gb));
598 }
599
600 - (void)orderFrontPreferencesPanel:(id)sender
601 {
602 [self openSettings];
603 }
604
605 - (void)open:(id)sender
606 {
607 [self openLibrary];
608 }
609
610 - (void)updateMirrorWindow
611 {
612 if ([UIScreen screens].count == 1) {
613 _mirrorWindow = nil;
614 _mirrorView = nil;
615 return;
616 }
617 if (_mirrorWindow && ![[UIScreen screens] containsObject:_mirrorWindow.screen]) {
618 _mirrorWindow = nil;
619 _mirrorView = nil;
620 }
621 for (UIScreen *screen in [UIScreen screens]) {
622 if (screen == UIScreen.mainScreen) continue;
623 CGRect rect = screen.bounds;
624 rect.size.height = floor(rect.size.height / 144) * 144;
625 rect.size.width = rect.size.height / 144 * 160;
626 rect.origin.x = (screen.bounds.size.width - rect.size.width) / 2;
627 rect.origin.y = (screen.bounds.size.height - rect.size.height) / 2;
628 _mirrorWindow = [[UIWindow alloc] initWithFrame:screen.bounds];
629 _mirrorWindow.screen = screen;
630 _mirrorView = [_gbView mirroredView];
631 _mirrorView.frame = rect;
632 _mirrorWindow.backgroundColor = [UIColor blackColor];
633 [_mirrorWindow addSubview:_mirrorView];
634 [_mirrorWindow setHidden:false];
635 break;
636 }
637 }
638
639 - (void)controllerDisconnected:(NSNotification *)notification
640 {
641 if (notification.object == _lastController) {
642 _backgroundView.fullScreenMode = GBControllerFocusOff;
643 }
644 }
645
646 - (void)setControllerHandlers
647 {
648 for (GCController *controller in [GCController controllers]) {
649 __weak GCController *weakController = controller;
650 NSDictionary <NSNumber *, GCControllerElement *> *elements = GCControllerGetElements(controller);
651 if (!elements) continue; // Controller not supported
652 [elements enumerateKeysAndObjectsUsingBlock:^(NSNumber *usage, GCControllerElement *element, BOOL *stop) {
653 if ([element isKindOfClass:[GCControllerButtonInput class]]) {
654 [(GCControllerButtonInput *)element setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) {
655 [self controller:weakController buttonChanged:button usage:usage.unsignedIntValue];
656 }];
657 }
658 else if ([element isKindOfClass:[GCControllerDirectionPad class]]) {
659 NSMutableSet *childrenUsages = [NSMutableSet set];
660 [elements enumerateKeysAndObjectsUsingBlock:^(NSNumber *childUsage, GCControllerElement *child, BOOL *stop) {
661 if (child.collection == element) {
662 [childrenUsages addObject:childUsage];
663 }
664 }];
665 [(GCControllerDirectionPad *)element setValueChangedHandler:^(GCControllerDirectionPad *dpad, float xValue, float yValue) {
666 [self controller:weakController axisChanged:dpad usage:usage.unsignedIntValue childrenUsages:childrenUsages];
667 }];
668 }
669 }];
670
671 if (controller.motion) {
672 [controller.motion setValueChangedHandler:^(GCMotion *motion) {
673 [self controller:weakController motionChanged:motion];
674 }];
675 }
676 }
677 }
678
679 - (void)updateLastController:(GCController *)controller
680 {
681 if (_lastController == controller) return;
682 _lastController = controller;
683 [GBHapticManager sharedManager].controller = controller;
684 }
685
686 - (void)controller:(GCController *)controller buttonChanged:(GCControllerButtonInput *)button usage:(GBControllerUsage)usage
687 {
688 [self updateLastController:controller];
689 if (_running && button.value > 0.25) {
690 _backgroundView.fullScreenMode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBControllersHideInterface"];
691 }
692
693 GBButton gbButton = [GBSettingsViewController controller:controller convertUsageToButton:usage];
694 static const double analogThreshold = 0.0625;
695 switch (gbButton) {
696 case GBRight:
697 case GBLeft:
698 case GBUp:
699 case GBDown:
700 GB_set_use_faux_analog_inputs(&_gb, 0, false);
701 case GBA:
702 case GBB:
703 case GBSelect:
704 case GBStart:
705 GB_set_key_state(&_gb, (GB_key_t)gbButton, button.value > 0.25);
706 if (_runMode == GBRunModeRewind || _runMode == GBRunModePaused) {
707 self.runMode = GBRunModeNormal;
708 [_backgroundView fadeOverlayOut];
709 }
710 break;
711 case GBRapidA:
712 _rapidA = button.value > 0.25;
713 _rapidACount = 0;
714 break;
715 case GBRapidB:
716 _rapidB = button.value > 0.25;
717 _rapidBCount = 0;
718 break;
719 case GBTurbo:
720 if (button.value > analogThreshold) {
721 [self setRunMode:GBRunModeTurbo ignoreDynamicSpeed:!button.isAnalog];
722 if (button.isAnalog && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
723 GB_set_clock_multiplier(&_gb, (button.value - analogThreshold) / (1 - analogThreshold) * 3 + 1);
724 }
725 _runModeFromController = true;
726 [_backgroundView fadeOverlayOut];
727 }
728 else {
729 if (self.runMode == GBRunModeTurbo && _runModeFromController) {
730 [self setRunMode:GBRunModeNormal];
731 _runModeFromController = false;
732 }
733 }
734 break;
735 case GBRewind:
736 if (button.value > analogThreshold) {
737 [self setRunMode:GBRunModeRewind ignoreDynamicSpeed:!button.isAnalog];
738 if (button.isAnalog && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
739 GB_set_clock_multiplier(&_gb, (button.value - analogThreshold) / (1 - analogThreshold) * 4);
740 }
741 _runModeFromController = true;
742 [_backgroundView fadeOverlayOut];
743 }
744 else {
745 if ((self.runMode == GBRunModeRewind || self.runMode == GBRunModePaused) && _runModeFromController) {
746 [self setRunMode:GBRunModeNormal];
747 _runModeFromController = false;
748 }
749 }
750 break;
751 case GBUnderclock:
752 if (button.value > analogThreshold) {
753 [self setRunMode:GBRunModeUnderclock ignoreDynamicSpeed:!button.isAnalog];
754 if (button.isAnalog && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
755 GB_set_clock_multiplier(&_gb, 1 - ((button.value - analogThreshold) / (1 - analogThreshold) * 0.75));
756 }
757 _runModeFromController = true;
758 [_backgroundView fadeOverlayOut];
759 }
760 else {
761 if (self.runMode == GBRunModeUnderclock && _runModeFromController) {
762 [self setRunMode:GBRunModeNormal];
763 _runModeFromController = false;
764 }
765 }
766 break;
767 case GBSaveState1:
768 if (_romLoaded) {
769 [_backgroundView saveSwipeFromController:true];
770 }
771 break;
772 case GBLoadState1:
773 if (_romLoaded) {
774 [_backgroundView loadSwipeFromController:true];
775 }
776 break;
777 case GBReset:
778 if (_romLoaded) {
779 [self stop];
780 _skipAutoLoad = true;
781 GB_reset(&_gb);
782 [self start];
783 }
784 break;
785 case GBOpenMenu:
786 self.window.backgroundColor = nil;
787 [self presentViewController:[GBMenuViewController menu] animated:true completion:nil];
788 break;
789 default: break;
790 }
791 }
792
793 - (void)controller:(GCController *)controller axisChanged:(GCControllerDirectionPad *)axis usage:(GBControllerUsage)usage childrenUsages:(NSSet *)childrenUsages
794 {
795 [self updateLastController:controller];
796 bool left = axis.left.value > 0.5;
797 bool right = axis.right.value > 0.5;
798 bool up = axis.up.value > 0.5;
799 bool down = axis.down.value > 0.5;
800
801 bool hasUnmappedChild = false;
802 for (NSNumber *childUsage in childrenUsages) {
803 if ([GBSettingsViewController controller:controller convertUsageToButton:childUsage.unsignedIntValue] != GBUnusedButton) {
804 GCControllerButtonInput *child = (id)(GCControllerGetElements(controller)[childUsage]);
805 if ([child isKindOfClass:[GCControllerButtonInput class]] && child.pressed) {
806 left = right = up = down = false;
807 }
808 }
809 else {
810 hasUnmappedChild = true;
811 }
812 }
813
814 if (!hasUnmappedChild) return;
815
816 if (_running && (left || right || up || down)) {
817 _backgroundView.fullScreenMode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBControllersHideInterface"];
818 }
819
820 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBFauxAnalogInputs"]) {
821 GB_set_use_faux_analog_inputs(&_gb, 0, true);
822 GB_set_faux_analog_inputs(&_gb, 0, axis.right.value - axis.left.value, axis.down.value - axis.up.value);
823 }
824
825 GB_set_key_state(&_gb, GB_KEY_LEFT, left);
826 GB_set_key_state(&_gb, GB_KEY_RIGHT, right);
827 GB_set_key_state(&_gb, GB_KEY_UP, up);
828 GB_set_key_state(&_gb, GB_KEY_DOWN, down);
829 if (_runMode == GBRunModeRewind || _runMode == GBRunModePaused) {
830 self.runMode = GBRunModeNormal;
831 [_backgroundView fadeOverlayOut];
832 }
833 }
834
835 - (void)controller:(GCController *)controller motionChanged:(GCMotion *)motion
836 {
837 if (controller != _lastController) return;
838 GCAcceleration gravity = {0,};
839 GCAcceleration userAccel = {0,};
840 if (@available(iOS 14.0, *)) {
841 if (motion.hasGravityAndUserAcceleration) {
842 gravity = motion.gravity;
843 userAccel = motion.userAcceleration;
844 }
845 else {
846 gravity = motion.acceleration;
847 }
848 }
849 else {
850 gravity = motion.gravity;
851 userAccel = motion.userAcceleration;
852 }
853 GB_set_accelerometer_values(&_gb, -(gravity.x + userAccel.x), gravity.y + userAccel.y);
854 }
855
856
857 - (void)verifyEntitlements
858 {
859 /*
860 Make sure SameBoy is properly signed. If the bundle identifier the Info.plist file does not match the bundle
861 identifier in the application-identifier entitlement, iOS will not allow SameBoy to open files.
862 */
863 typedef void *xpc_object_t;
864 void *libxpc = dlopen("/usr/lib/system/libxpc.dylib", RTLD_NOW);
865
866 extern xpc_object_t xpc_copy_entitlements_for_self$(void);
867 extern const char *xpc_dictionary_get_string$(xpc_object_t *object, const char *key);
868
869 typeof(xpc_copy_entitlements_for_self$) *xpc_copy_entitlements_for_self = dlsym(libxpc, "xpc_copy_entitlements_for_self");
870 typeof(xpc_dictionary_get_string$) *xpc_dictionary_get_string = dlsym(libxpc, "xpc_dictionary_get_string");
871
872 if (!xpc_copy_entitlements_for_self || !xpc_dictionary_get_string) return;
873
874 xpc_object_t entitlements = xpc_copy_entitlements_for_self();
875 if (!entitlements) return;
876 const char *_entIdentifier = xpc_dictionary_get_string(entitlements, "application-identifier");
877 NSString *entIdentifier = _entIdentifier? @(_entIdentifier) : nil;
878
879 const char *_teamIdentifier = xpc_dictionary_get_string(entitlements, "com.apple.developer.team-identifier");
880 NSString *teamIdentifier = _teamIdentifier? @(_teamIdentifier) : nil;
881
882 if (!entIdentifier) { // No identifier. Installed using a jailbreak, we're fine.
883 return;
884 }
885
886 if (teamIdentifier && [entIdentifier hasPrefix:[teamIdentifier stringByAppendingString:@"."]]) {
887 entIdentifier = [entIdentifier substringFromIndex:teamIdentifier.length + 1];
888 }
889
890 NSString *plistIdentifier = [NSBundle mainBundle].bundleIdentifier;
891
892 if (![entIdentifier isEqual:plistIdentifier]) {
893 UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"SameBoy is not properly signed and might not be able to open ROMs"
894 message:[NSString stringWithFormat:@"The bundle identifier in the Info.plist file (“%@”) does not match the one in the entitlements (“%@”)", plistIdentifier, entIdentifier]
895 preferredStyle:UIAlertControllerStyleAlert];
896 [alert addAction:[UIAlertAction actionWithTitle:@"Close"
897 style:UIAlertActionStyleCancel
898 handler:nil]];
899 [self presentViewController:alert animated:true completion:nil];
900 }
901 }
902
903 - (void)saveStateToFile:(NSString *)file
904 {
905 NSString *tempPath = [file stringByAppendingPathExtension:@"tmp"];
906 int error = GB_save_state(&_gb, tempPath.UTF8String);
907 if (!error) {
908 rename(tempPath.UTF8String, file.UTF8String);
909 NSData *data = [NSData dataWithBytes:_gbView.previousBuffer
910 length:GB_get_screen_width(&_gb) *
911 GB_get_screen_height(&_gb) *
912 sizeof(*_gbView.previousBuffer)];
913 UIImage *screenshot = [self imageFromData:data width:GB_get_screen_width(&_gb) height:GB_get_screen_height(&_gb)];
914 [UIImagePNGRepresentation(screenshot) writeToFile:[file stringByAppendingPathExtension:@"png"] atomically:false];
915 }
916 else {
917 dispatch_async(dispatch_get_main_queue(), ^{
918 UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Could not Save State"
919 message:[NSString stringWithFormat:@"An error occured while attempting to save: %s", strerror(error)]
920 preferredStyle:UIAlertControllerStyleAlert];
921 [alert addAction:[UIAlertAction actionWithTitle:@"Close"
922 style:UIAlertActionStyleCancel
923 handler:nil]];
924 UIViewController *top = self;
925 while (top.presentedViewController) {
926 top = top.presentedViewController;
927 }
928 [top presentViewController:alert animated:true completion:nil];
929 });
930 }
931 }
932
933 - (bool)loadStateFromFile:(NSString *)file
934 {
935 _skipAutoLoad = true;
936 GB_model_t model;
937 if (!GB_get_state_model(file.fileSystemRepresentation, &model)) {
938 if (GB_get_model(&_gb) != model) {
939 GB_switch_model_and_reset(&_gb, model);
940 }
941 return GB_load_state(&_gb, file.fileSystemRepresentation) == 0;
942 }
943
944 return false;
945 }
946
947 - (void)loadROM
948 {
949 GBROMManager *romManager = [GBROMManager sharedManager];
950 if (romManager.romFile) {
951 if (!_skipAutoLoad) {
952 // Todo: display errors and warnings
953 bool needsStateLoad = false;
954 if (![_lastSavedROM isEqual:[GBROMManager sharedManager].currentROM]) {
955 if ([romManager.romFile.pathExtension.lowercaseString isEqualToString:@"isx"]) {
956 _romLoaded = GB_load_isx(&_gb, romManager.romFile.fileSystemRepresentation) == 0;
957 }
958 else {
959 _romLoaded = GB_load_rom(&_gb, romManager.romFile.fileSystemRepresentation) == 0;
960 }
961 needsStateLoad = true;
962 if (@available(iOS 16.0, *)) {
963 dispatch_async(dispatch_get_main_queue(), ^{
964 [super setNeedsUpdateOfSupportedInterfaceOrientations];
965 });
966 }
967 }
968 else if (access(romManager.romFile.fileSystemRepresentation, R_OK)) {
969 _romLoaded = false;
970 }
971 if (!_romLoaded) {
972 dispatch_async(dispatch_get_main_queue(), ^{
973 romManager.currentROM = nil;
974 });
975 }
976
977 if (!needsStateLoad) {
978 NSDate *date = nil;
979 [[NSURL fileURLWithPath:[GBROMManager sharedManager].autosaveStateFile] getResourceValue:&date
980 forKey:NSURLContentModificationDateKey
981 error:nil];
982 if (![_saveDate isEqual:date]) {
983 needsStateLoad = true;
984 }
985 }
986
987 if (_romLoaded && needsStateLoad) {
988 GB_reset(&_gb);
989 GB_load_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation);
990 GB_remove_all_cheats(&_gb);
991 GB_load_cheats(&_gb, [GBROMManager sharedManager].cheatsFile.UTF8String, false);
992 if (![self loadStateFromFile:[GBROMManager sharedManager].autosaveStateFile]) {
993 if ([_lastSavedROM isEqual:[GBROMManager sharedManager].currentROM]) {
994 /* Something weird just happened: we didn't change a ROM, but we failed to load the
995 latest save state. Save over the existing file, it's probably corrupt in some
996 way. */
997 [self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile];
998 }
999 else {
1000 // Newly played ROM, pick the best model
1001 uint8_t *rom = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, NULL);
1002
1003 if ((rom[0x143] & 0x80)) {
1004 if (!GB_is_cgb(&_gb)) {
1005 GB_switch_model_and_reset(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]);
1006 }
1007 }
1008 else if ((rom[0x146] == 3) && !GB_is_sgb(&_gb)) {
1009 GB_switch_model_and_reset(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"]);
1010 }
1011 }
1012 }
1013 GB_rewind_reset(&_gb);
1014 }
1015 }
1016 }
1017 else {
1018 _romLoaded = false;
1019 }
1020 dispatch_async(dispatch_get_main_queue(), ^{
1021 _gbView.hidden = !_romLoaded;
1022 });
1023 _swappingROM = false;
1024 _skipAutoLoad = false;
1025 }
1026
1027 - (void)applicationDidBecomeActive:(UIApplication *)application
1028 {
1029 [self start];
1030 }
1031
1032 -(void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion
1033 {
1034 [self stop];
1035 [super presentViewController:viewControllerToPresent
1036 animated:flag
1037 completion:completion];
1038 }
1039
1040 - (void)reset
1041 {
1042 UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad?
1043 UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet;
1044 UIAlertController *menu = [UIAlertController alertControllerWithTitle:@"Reset Emulation?"
1045 message:@"Unsaved progress will be lost."
1046 preferredStyle:style];
1047 [menu addAction:[UIAlertAction actionWithTitle:@"Reset"
1048 style:UIAlertActionStyleDestructive
1049 handler:^(UIAlertAction *action) {
1050 [self stop];
1051 _skipAutoLoad = true;
1052 GB_reset(&_gb);
1053 [self start];
1054 }]];
1055 [menu addAction:[UIAlertAction actionWithTitle:@"Cancel"
1056 style:UIAlertActionStyleCancel
1057 handler:nil]];
1058 [self presentViewController:menu animated:true completion:nil];
1059 }
1060
1061 - (void)openLibrary
1062 {
1063 static __weak UIViewController *presentedController;
1064 if (presentedController && self.presentedViewController == presentedController) return;
1065 if (![self dismissViewControllerIfSafe]) return;
1066
1067 UIViewController *controller = [[GBLibraryViewController alloc] init];
1068 presentedController = controller;
1069
1070 [self presentViewController:controller
1071 animated:true
1072 completion:nil];
1073 }
1074
1075 - (void)changeModel
1076 {
1077 static __weak UIViewController *presentedController;
1078 if (presentedController && self.presentedViewController == presentedController) return;
1079 if (![self dismissViewControllerIfSafe]) return;
1080
1081 GBOptionViewController *controller = [[GBOptionViewController alloc] initWithHeader:@"Select a model to emulate"];
1082 controller.footer = @"Changing the emulated model will reset the emulator";
1083 presentedController = controller;
1084
1085 GB_model_t currentModel = GB_get_model(&_gb);
1086 struct {
1087 NSString *title;
1088 NSString *settingKey;
1089 bool checked;
1090 } items[] = {
1091 {@"Game Boy", @"GBDMGModel", currentModel < GB_MODEL_SGB},
1092 {@"Game Boy Pocket/Light", nil, currentModel == GB_MODEL_MGB},
1093 {@"Super Game Boy", @"GBSGBModel", GB_is_sgb(&_gb)},
1094 {@"Game Boy Color", @"GBCGBModel", GB_is_cgb(&_gb) && currentModel <= GB_MODEL_CGB_E},
1095 {@"Game Boy Advance", @"GBAGBModel", currentModel > GB_MODEL_CGB_E},
1096 };
1097
1098 for (unsigned i = 0; i < sizeof(items) / sizeof(items[0]); i++) {
1099 GB_model_t model = GB_MODEL_MGB;
1100 if (items[i].settingKey) {
1101 model = [[NSUserDefaults standardUserDefaults] integerForKey:items[i].settingKey];
1102 }
1103 [controller addOption:items[i].title withCheckmark:items[i].checked action:^{
1104 [self stop];
1105 _skipAutoLoad = true;
1106 GB_switch_model_and_reset(&_gb, model);
1107 if (model > GB_MODEL_CGB_E && ![[NSUserDefaults standardUserDefaults] boolForKey:@"GBShownGBAWarning"]) {
1108 UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"SameBoy is not a Game Boy Advance Emulator"
1109 message:@"SameBoy cannot play GBA games. Changing the model to Game Boy Advance lets you play Game Boy games as if on a Game Boy Advance in Game Boy Color mode."
1110 preferredStyle:UIAlertControllerStyleAlert];
1111 [alert addAction:[UIAlertAction actionWithTitle:@"Close"
1112 style:UIAlertActionStyleCancel
1113 handler:^(UIAlertAction *action) {
1114 [self start];
1115 [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBShownGBAWarning"];
1116 }]];
1117 [self presentViewController:alert animated:true completion:nil];
1118 }
1119 else {
1120 [self start];
1121 }
1122 }];
1123 }
1124 controller.title = @"Change Model";
1125
1126 UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:controller];
1127 UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
1128 style:UIBarButtonItemStylePlain
1129 target:self
1130 action:@selector(dismissViewController)];
1131 [navController.visibleViewController.navigationItem setLeftBarButtonItem:close];
1132
1133 [self presentViewController:navController animated:true completion:nil];
1134 }
1135
1136 - (void)openStates
1137 {
1138 static __weak UIViewController *presentedController;
1139 if (presentedController && self.presentedViewController == presentedController) return;
1140 if (![self dismissViewControllerIfSafe]) return;
1141
1142 UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBStatesViewController alloc] init]];
1143 presentedController = controller;
1144 UIVisualEffect *effect = [UIBlurEffect effectWithStyle:(UIBlurEffectStyle)UIBlurEffectStyleProminent];
1145 UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
1146 effectView.frame = controller.view.bounds;
1147 effectView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
1148 [controller.view insertSubview:effectView atIndex:0];
1149 UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
1150 style:UIBarButtonItemStylePlain
1151 target:self
1152 action:@selector(dismissViewController)];
1153 [controller.visibleViewController.navigationItem setLeftBarButtonItem:close];
1154 [self presentViewController:controller
1155 animated:true
1156 completion:nil];
1157 }
1158
1159 - (void)openSettings
1160 {
1161 static __weak UIViewController *presentedController;
1162 if (presentedController && self.presentedViewController == presentedController) return;
1163 if (![self dismissViewControllerIfSafe]) return;
1164
1165 UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
1166 style:UIBarButtonItemStylePlain
1167 target:self
1168 action:@selector(dismissViewController)];
1169 UIViewController *controller = [GBSettingsViewController settingsViewControllerWithLeftButton:close];
1170 presentedController = controller;
1171 [self presentViewController:controller
1172 animated:true
1173 completion:nil];
1174 }
1175
1176 - (void)showAbout
1177 {
1178 static __weak UIViewController *presentedController;
1179 if (presentedController && self.presentedViewController == presentedController) return;
1180 if (![self dismissViewControllerIfSafe]) return;
1181
1182 UIViewController *controller = [[GBAboutController alloc] init];
1183 presentedController = controller;
1184
1185 [self presentViewController:controller animated:true completion:nil];
1186 }
1187
1188 - (void)openCheats
1189 {
1190 static __weak UIViewController *presentedController;
1191 if (presentedController && self.presentedViewController == presentedController) return;
1192 if (![self dismissViewControllerIfSafe]) return;
1193
1194 UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBCheatsController alloc] initWithGameBoy:&_gb]];
1195 presentedController = controller;
1196 UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
1197 style:UIBarButtonItemStylePlain
1198 target:self
1199 action:@selector(dismissViewController)];
1200 [controller.visibleViewController.navigationItem setLeftBarButtonItem:close];
1201 [self presentViewController:controller
1202 animated:true
1203 completion:nil];
1204 }
1205
1206 - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
1207 {
1208 [super dismissViewControllerAnimated:flag completion:^() {
1209 if (completion) {
1210 completion();
1211 }
1212 dispatch_async(dispatch_get_main_queue(), ^{
1213 [self start];
1214 });
1215 }];
1216 }
1217
1218 - (void)setNeedsUpdateOfSupportedInterfaceOrientations
1219 {
1220 /* Hack. Some view controllers dismiss without calling the method above. */
1221 dispatch_async(dispatch_get_main_queue(), ^{
1222 [self start];
1223 [super setNeedsUpdateOfSupportedInterfaceOrientations];
1224 });
1225 }
1226
1227 - (void)dismissViewController
1228 {
1229 [self dismissViewControllerAnimated:true completion:nil];
1230 }
1231
1232 - (bool)dismissViewControllerIfSafe
1233 {
1234 if (!self.presentedViewController) return true;
1235
1236 if (![self.presentedViewController isKindOfClass:[UIAlertController class]]) {
1237 [self dismissViewController];
1238 return true;
1239 }
1240
1241 if ([self.presentedViewController isKindOfClass:[GBMenuViewController class]]) {
1242 [self dismissViewController];
1243 return true;
1244 }
1245 return false;
1246 }
1247
1248 - (void)applicationWillResignActive:(UIApplication *)application
1249 {
1250 [self stop];
1251 }
1252
1253 - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration
1254 {
1255 if (_orientation != UIInterfaceOrientationUnknown && !((1 << orientation) & self.supportedInterfaceOrientations)) return;
1256 GBLayout *layout = nil;
1257 _orientation = orientation;
1258 switch (orientation) {
1259 default:
1260 case UIInterfaceOrientationUnknown:
1261 case UIInterfaceOrientationPortrait:
1262 case UIInterfaceOrientationPortraitUpsideDown:
1263 layout = _verticalLayout;
1264 break;
1265 case UIInterfaceOrientationLandscapeRight:
1266 layout = _horizontalLayoutLeft;
1267 break;
1268 case UIInterfaceOrientationLandscapeLeft:
1269 layout = _horizontalLayoutRight;
1270 break;
1271 }
1272
1273 _backgroundView.frame = [layout viewRectForOrientation:orientation];
1274 _backgroundView.layout = layout;
1275 if (!self.presentedViewController) {
1276 _window.backgroundColor = _backgroundView.fullScreenMode? [UIColor blackColor] :
1277 layout.theme.backgroundGradientBottom;
1278 }
1279 }
1280
1281 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
1282 {
1283 UIEdgeInsets insets = self.window.safeAreaInsets;
1284 bool landscape = true;
1285 if (_orientation == UIInterfaceOrientationPortrait || _orientation == UIInterfaceOrientationPortraitUpsideDown) {
1286 landscape = false;
1287 }
1288
1289 _cameraPositionButton.frame = CGRectMake(insets.left + 8,
1290 _backgroundView.bounds.size.height - 8 - insets.bottom - 32,
1291 32,
1292 32);
1293 if (_changeCameraButton) {
1294 _changeCameraButton.frame = CGRectMake(insets.left + 8 + (landscape? (32 + 8) : 0 ),
1295 _backgroundView.bounds.size.height - 8 - insets.bottom - 32 - (landscape? 0 : (32 + 8)),
1296 32,
1297 32);
1298 }
1299 _printerButton.frame = CGRectMake(_backgroundView.bounds.size.width - 8 - insets.right - 32,
1300 _backgroundView.bounds.size.height - 8 - insets.bottom - 32,
1301 32,
1302 32);
1303
1304 _printerSpinner.frame = CGRectMake(_backgroundView.bounds.size.width - 8 - insets.right - 32 - (landscape? (32 + 4) : 0),
1305 _backgroundView.bounds.size.height - 8 - insets.bottom - 32 - (landscape? 0 : (32 + 4)),
1306 32,
1307 32);
1308
1309 [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
1310 }
1311
1312 - (UIInterfaceOrientationMask)supportedInterfaceOrientations
1313 {
1314 if (!self.shouldAutorotate && _orientation != UIInterfaceOrientationUnknown) {
1315 return 1 << _orientation;
1316 }
1317 if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
1318 return UIInterfaceOrientationMaskAll;
1319 }
1320 if (MAX([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width) <= 568) {
1321 return UIInterfaceOrientationMaskLandscape;
1322 }
1323 return UIInterfaceOrientationMaskAllButUpsideDown;
1324 }
1325
1326 - (BOOL)shouldAutorotate
1327 {
1328 if (_running && GB_has_accelerometer(&_gb)) {
1329 return false;
1330 }
1331 return true;
1332 }
1333
1334 - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures
1335 {
1336 return UIRectEdgeTop;
1337 }
1338
1339 - (BOOL)prefersStatusBarHidden
1340 {
1341 switch (_orientation) {
1342 case UIInterfaceOrientationLandscapeRight:
1343 case UIInterfaceOrientationLandscapeLeft:
1344 return true;
1345 default:
1346 return false;
1347 }
1348 }
1349
1350 - (UIStatusBarStyle)preferredStatusBarStyle
1351 {
1352 if (@available(iOS 13.0, *)) {
1353 return (_verticalLayout.theme.isDark || _backgroundView.fullScreenMode)? UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent;
1354 }
1355 return (_verticalLayout.theme.isDark || _backgroundView.fullScreenMode)? UIStatusBarStyleLightContent : UIStatusBarStyleDefault;
1356 }
1357
1358 - (void)preRun
1359 {
1360 dispatch_async(dispatch_get_main_queue(), ^{
1361 UIApplication.sharedApplication.idleTimerDisabled = true;
1362 });
1363 GB_set_pixels_output(&_gb, _gbView.pixels);
1364 GB_set_sample_rate(&_gb, 96000);
1365 if (![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBAudioMode"] isEqual:@"off"]) {
1366 _audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) {
1367 [_audioLock lock];
1368
1369 if (_audioBufferPosition < nFrames) {
1370 _audioBufferNeeded = nFrames;
1371 [_audioLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:(double)(_audioBufferNeeded - _audioBufferPosition) / sampleRate]];
1372 _audioBufferNeeded = 0;
1373 }
1374
1375 if (_stopping) {
1376 memset(buffer, 0, nFrames * sizeof(*buffer));
1377 [_audioLock unlock];
1378 return;
1379 }
1380
1381 if (_audioBufferPosition < nFrames) {
1382 // Not enough audio
1383 memset(buffer, 0, (nFrames - _audioBufferPosition) * sizeof(*buffer));
1384 memcpy(buffer, _audioBuffer, _audioBufferPosition * sizeof(*buffer));
1385 // Do not reset the audio position to avoid more underflows
1386 }
1387 else if (_audioBufferPosition < nFrames + 4800) {
1388 memcpy(buffer, _audioBuffer, nFrames * sizeof(*buffer));
1389 memmove(_audioBuffer, _audioBuffer + nFrames, (_audioBufferPosition - nFrames) * sizeof(*buffer));
1390 _audioBufferPosition = _audioBufferPosition - nFrames;
1391 }
1392 else {
1393 memcpy(buffer, _audioBuffer + (_audioBufferPosition - nFrames), nFrames * sizeof(*buffer));
1394 _audioBufferPosition = 0;
1395 }
1396 [_audioLock unlock];
1397 } andSampleRate:96000];
1398 }
1399
1400 [_audioClient start];
1401 if (GB_has_accelerometer(&_gb)) {
1402 if (@available(iOS 14.0, *)) {
1403 for (GCController *controller in [GCController controllers]) {
1404 if (controller.motion.sensorsRequireManualActivation) {
1405 [controller.motion setSensorsActive:true];
1406 }
1407 }
1408 }
1409 [_motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue]
1410 withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
1411 if (_lastController.motion) return;
1412 CMAcceleration data = accelerometerData.acceleration;
1413 UIInterfaceOrientation orientation = _orientation;
1414 switch (orientation) {
1415 case UIInterfaceOrientationUnknown:
1416 case UIInterfaceOrientationPortrait:
1417 break;
1418 case UIInterfaceOrientationPortraitUpsideDown:
1419 data.x = -data.x;
1420 data.y = -data.y;
1421 break;
1422 case UIInterfaceOrientationLandscapeLeft: {
1423 double tempX = data.x;
1424 data.x = data.y;
1425 data.y = -tempX;
1426 break;
1427 }
1428 case UIInterfaceOrientationLandscapeRight:{
1429 double tempX = data.x;
1430 data.x = -data.y;
1431 data.y = tempX;
1432 break;
1433 }
1434 }
1435 GB_set_accelerometer_values(&_gb, -data.x, data.y);
1436 }];
1437 }
1438
1439 /* Clear pending alarms, don't play alarms while playing */
1440 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) {
1441 UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
1442 [center removeDeliveredNotificationsWithIdentifiers:@[[GBROMManager sharedManager].romFile]];
1443 [center removePendingNotificationRequestsWithIdentifiers:@[[GBROMManager sharedManager].romFile]];
1444 }
1445
1446 if (GB_rom_supports_alarms(&_gb)) {
1447 UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
1448 [center requestAuthorizationWithOptions:UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert completionHandler:nil];
1449 }
1450 }
1451
1452 - (NSString *)formatDate:(NSDate *)date
1453 {
1454
1455 NSCalendar *calendar = [NSCalendar currentCalendar];
1456 NSDate *now = [NSDate date];
1457
1458 NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
1459 formatter.locale = [NSLocale currentLocale];
1460 formatter.timeZone = [NSTimeZone localTimeZone];
1461 formatter.dateStyle = NSDateFormatterNoStyle;
1462 formatter.timeStyle = NSDateFormatterMediumStyle;
1463 NSString *time = [formatter stringFromDate:date];
1464
1465 if ([calendar isDate:date inSameDayAsDate:now]) {
1466 return time;
1467 }
1468
1469 if ([calendar component:NSCalendarUnitYear fromDate:date] == [calendar component:NSCalendarUnitYear fromDate:now]) {
1470 formatter.dateFormat = [NSDateFormatter dateFormatFromTemplate:@"MMM d"
1471 options:0
1472 locale:formatter.locale];
1473 return [[formatter stringFromDate:date] stringByAppendingFormat:@", %@", time];
1474 }
1475
1476 formatter.dateFormat = [NSDateFormatter dateFormatFromTemplate:@"MMM d yyyy"
1477 options:0
1478 locale:formatter.locale];
1479 return [[formatter stringFromDate:date] stringByAppendingFormat:@", %@", time];
1480 }
1481
1482 - (NSData *)batteryHash
1483 {
1484 NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:[GBROMManager sharedManager].batterySaveFile];
1485 if (!file) return nil;
1486
1487 CC_SHA256_CTX ctx;
1488 CC_SHA256_Init(&ctx);
1489
1490 while (true) {
1491 @autoreleasepool {
1492 NSData *chunk = [file readDataOfLength:0x4000];
1493 if (chunk.length == 0) break;
1494 CC_SHA256_Update(&ctx, chunk.bytes, (CC_LONG)chunk.length);
1495 }
1496 }
1497 [file closeFile];
1498
1499 unsigned char digest[CC_SHA256_DIGEST_LENGTH];
1500 CC_SHA256_Final(digest, &ctx);
1501
1502 return [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
1503 }
1504
1505 - (bool)areSavesInSync
1506 {
1507 if (!GBROMManager.sharedManager.currentROM) return true; // No ROM
1508 NSString *saveState = [[GBROMManager sharedManager] autosaveStateFile];
1509 NSString *batterySave = [[GBROMManager sharedManager] batterySaveFile];
1510
1511 // There can't be a mismatch if one of the files is missing
1512 if (access(saveState.UTF8String, F_OK) || access(batterySave.UTF8String, F_OK)) return true;
1513
1514 uint8_t savedHash[CC_SHA256_DIGEST_LENGTH];
1515 if (getxattr(saveState.UTF8String, "battery-hash", &savedHash, sizeof(savedHash), 0, 0) == sizeof(savedHash)) {
1516 NSData *computedHash = [self batteryHash];
1517 if (computedHash && memcmp(savedHash, computedHash.bytes, sizeof(savedHash)) == 0) {
1518 return true; // Battery didn't change
1519 }
1520 }
1521
1522 GB_gameboy_t *gb = NULL;
1523 GB_model_t model;
1524 if (GB_get_state_model(saveState.UTF8String, &model)) {
1525 return true; // Not a valid state, ignore
1526 }
1527 gb = GB_init(GB_alloc(), model);
1528 NSString *romFile = [[GBROMManager sharedManager] romFile];
1529 if ([romFile.pathExtension.lowercaseString isEqualToString:@"isx"]) {
1530 if (GB_load_isx(gb, romFile.fileSystemRepresentation)) {
1531 // Can't load ROM
1532 GB_dealloc(gb);
1533 return true;
1534 }
1535 }
1536 else {
1537 if (GB_load_rom(gb, romFile.fileSystemRepresentation)) {
1538 // Can't load ROM
1539 GB_dealloc(gb);
1540 return true;
1541 }
1542 }
1543 if (GB_load_state(gb, saveState.UTF8String)) {
1544 // Not a valid state again, ignore
1545 GB_dealloc(gb);
1546 return true;
1547 }
1548
1549 size_t ramSize = 0;
1550 const uint8_t *ram = GB_get_direct_access(gb, GB_DIRECT_ACCESS_CART_RAM, &ramSize, NULL);
1551 uint8_t *ramCopy = malloc(ramSize);
1552 memcpy(ramCopy, ram, ramSize);
1553
1554 GB_load_battery(gb, batterySave.UTF8String);
1555 ram = GB_get_direct_access(gb, GB_DIRECT_ACCESS_CART_RAM, &ramSize, NULL);
1556 bool matching = memcmp(ram, ramCopy, ramSize) == 0;
1557 free(ramCopy);
1558 GB_dealloc(gb);
1559 return matching;
1560 }
1561
1562 - (bool)verifySavesInSync
1563 {
1564 if ([self areSavesInSync]) return true; // Saves are in sync
1565 dispatch_semaphore_t sem = dispatch_semaphore_create(0);
1566 __block bool ret = false;
1567 dispatch_async(dispatch_get_main_queue(), ^{
1568 UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Save Conflict Detected"
1569 message:[NSString stringWithFormat: @"“%@” has both an automatic save state and a conflicting battery save file. The battery save may have newer progress that isn’t in the save state. Which one would you like to keep?", [GBROMManager sharedManager].currentROM.lastPathComponent]
1570 preferredStyle:UIAlertControllerStyleAlert];
1571
1572 NSString *saveState = [[GBROMManager sharedManager] autosaveStateFile];
1573 NSString *batterySave = [[GBROMManager sharedManager] batterySaveFile];
1574
1575 NSDate *date;
1576 [[NSURL fileURLWithPath:saveState] getResourceValue:&date
1577 forKey:NSURLContentModificationDateKey
1578 error:nil];
1579
1580 NSString *stateDateString = [self formatDate:date];
1581
1582 UIAlertAction *stateAction = [UIAlertAction actionWithTitle:@"Save State\n"
1583 style:UIAlertActionStyleDefault
1584 handler:^(UIAlertAction *action) {
1585 unlink(batterySave.UTF8String);
1586 ret = true;
1587 dispatch_semaphore_signal(sem);
1588 }];
1589
1590 [[NSURL fileURLWithPath:batterySave] getResourceValue:&date
1591 forKey:NSURLContentModificationDateKey
1592 error:nil];
1593
1594 NSString *batteryDateString = [self formatDate:date];
1595
1596 UIAlertAction *batteryAction = [UIAlertAction actionWithTitle:@"Battery Save\n"
1597 style:UIAlertActionStyleDefault
1598 handler:^(UIAlertAction *action) {
1599 unlink(saveState.UTF8String);
1600 ret = true;
1601 dispatch_semaphore_signal(sem);
1602 }];
1603 [alert addAction:stateAction];
1604 [alert addAction:batteryAction];
1605 [alert addAction:[UIAlertAction actionWithTitle:@"Close ROM"
1606 style:UIAlertActionStyleCancel
1607 handler:^(UIAlertAction *action) {
1608 ret = false;
1609 dispatch_semaphore_signal(sem);
1610 }]];
1611
1612 // hack
1613 _running = false;
1614 [self presentViewController:alert animated:true completion:nil];
1615 dispatch_async(dispatch_get_main_queue(), ^{
1616 NSMutableSet<UIView *> *views = [NSMutableSet setWithObject:alert.view];
1617
1618 UIColor *secondaryLabelColor;
1619 if (@available(iOS 13.0, *)) {
1620 secondaryLabelColor = [UIColor secondaryLabelColor];
1621 }
1622 else {
1623 secondaryLabelColor = [UIColor systemGrayColor];
1624 }
1625
1626 UILabel *view;
1627 while ((view = (UILabel *)views.anyObject)) {
1628 [views removeObject:view];
1629 [views addObjectsFromArray:view.subviews];
1630 if ([view isKindOfClass:[UILabel class]]) {
1631 if ([view.text isEqualToString:stateAction.title]) {
1632 NSMutableAttributedString *text = [view.attributedText mutableCopy];
1633 [text appendAttributedString:[[NSAttributedString alloc] initWithString:stateDateString
1634 attributes:@{
1635 NSFontAttributeName: [UIFont systemFontOfSize:UIFont.smallSystemFontSize],
1636 NSForegroundColorAttributeName: secondaryLabelColor,
1637 }]];
1638 view.attributedText = text;
1639 view.numberOfLines = 2;
1640 view.locksFonts = true;
1641 }
1642 if ([view.text isEqualToString:batteryAction.title]) {
1643 NSMutableAttributedString *text = [view.attributedText mutableCopy];
1644 [text appendAttributedString:[[NSAttributedString alloc] initWithString:batteryDateString
1645 attributes:@{
1646 NSFontAttributeName: [UIFont systemFontOfSize:UIFont.smallSystemFontSize],
1647 NSForegroundColorAttributeName: secondaryLabelColor,
1648 }]];
1649 view.attributedText = text;
1650 view.numberOfLines = 2;
1651 view.locksFonts = true;
1652 }
1653 }
1654 }
1655 });
1656 });
1657 dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
1658 _running = true;
1659 return ret;
1660 }
1661
1662 - (void)run
1663 {
1664 if (![self verifySavesInSync]) {
1665 _romLoaded = false;
1666 _running = false;
1667 _stopping = false;
1668 GBROMManager.sharedManager.currentROM = nil;
1669 return;
1670 }
1671
1672 [self loadROM];
1673 if (!_romLoaded) {
1674 _running = false;
1675 _stopping = false;
1676 return;
1677 }
1678 [self preRun];
1679 const unsigned autosaveFrequency = 60 * 60;
1680 _autosaveCountdown = autosaveFrequency;
1681 while (_running) {
1682 if (_rewind) {
1683 _rewind = false;
1684 GB_rewind_pop(&_gb);
1685 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
1686 if (!GB_rewind_pop(&_gb)) {
1687 dispatch_sync(dispatch_get_main_queue(), ^{
1688 if (_runMode == GBRunModeRewind) {
1689 self.runMode = GBRunModePaused;
1690 }
1691 });
1692 _rewindOver = true;
1693 }
1694 }
1695 else {
1696 for (unsigned i = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindSpeed"]; i--;) {
1697 if (!GB_rewind_pop(&_gb)) {
1698 dispatch_sync(dispatch_get_main_queue(), ^{
1699 if (_runMode == GBRunModeRewind) {
1700 self.runMode = GBRunModePaused;
1701 }
1702 });
1703 _rewindOver = true;
1704 }
1705 }
1706 }
1707 }
1708 if (_runMode != GBRunModePaused) {
1709 GB_run(&_gb);
1710 if (!_autosaveCountdown) {
1711 _autosaveCountdown = autosaveFrequency;
1712 [self performAutosave];
1713 }
1714 }
1715 }
1716 [self postRun];
1717 _stopping = false;
1718 }
1719
1720 - (void)userNotificationCenter:(UNUserNotificationCenter *)center
1721 didReceiveNotificationResponse:(UNNotificationResponse *)response
1722 withCompletionHandler:(void (^)(void))completionHandler
1723 {
1724 if (![response.notification.request.identifier isEqual:[GBROMManager sharedManager].currentROM]) {
1725 [self application:[UIApplication sharedApplication]
1726 openURL:[NSURL fileURLWithPath:response.notification.request.identifier]
1727 options:@{}];
1728 }
1729 completionHandler();
1730 }
1731
1732 - (UIImage *)imageFromData:(NSData *)data width:(unsigned)width height:(unsigned)height
1733 {
1734 /* Convert the screenshot to a CGImageRef */
1735 CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)data);
1736 CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
1737 CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
1738 CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
1739
1740 CGImageRef iref = CGImageCreate(width,
1741 height,
1742 8,
1743 32,
1744 4 * width,
1745 colorSpaceRef,
1746 bitmapInfo,
1747 provider,
1748 NULL,
1749 true,
1750 renderingIntent);
1751
1752 UIImage *ret = [[UIImage alloc] initWithCGImage:iref];
1753 CGColorSpaceRelease(colorSpaceRef);
1754 CGDataProviderRelease(provider);
1755 CGImageRelease(iref);
1756 return ret;
1757 }
1758
1759 - (void)performAutosave
1760 {
1761 GB_save_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation);
1762 [self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile];
1763 // Assoicate the battery save with the save state via a hash xattr
1764 NSData *batteryHash = [self batteryHash];
1765 if (batteryHash) {
1766 setxattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash",
1767 batteryHash.bytes, batteryHash.length, 0, 0);
1768 }
1769 else {
1770 removexattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash", 0);
1771 }
1772 }
1773
1774 - (void)postRun
1775 {
1776 [_audioLock lock];
1777 _audioBufferPosition = _audioBufferNeeded = 0;
1778 [_audioLock signal];
1779 [_audioLock unlock];
1780 [_audioClient stop];
1781 _audioClient = nil;
1782
1783 if (!_swappingROM) {
1784 [self performAutosave];
1785
1786 // Assoicate the battery save with the save state via a hash xattr
1787 NSData *batteryHash = [self batteryHash];
1788 if (batteryHash) {
1789 setxattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash",
1790 batteryHash.bytes, batteryHash.length, 0, 0);
1791 }
1792 else {
1793 removexattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash", 0);
1794 }
1795
1796 NSDate *date;
1797 [[NSURL fileURLWithPath:[GBROMManager sharedManager].autosaveStateFile] getResourceValue:&date
1798 forKey:NSURLContentModificationDateKey
1799 error:nil];
1800 _saveDate = date;
1801 _lastSavedROM = [GBROMManager sharedManager].currentROM;
1802 }
1803 [[GBHapticManager sharedManager] setRumbleStrength:0];
1804 if (@available(iOS 14.0, *)) {
1805 for (GCController *controller in [GCController controllers]) {
1806 if (controller.motion.sensorsRequireManualActivation) {
1807 [controller.motion setSensorsActive:false];
1808 }
1809 }
1810 }
1811 [_motionManager stopAccelerometerUpdates];
1812
1813 unsigned timeToAlarm = GB_time_to_alarm(&_gb);
1814
1815 if (timeToAlarm) {
1816 UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
1817
1818 UNMutableNotificationContent *notificationContent = [[UNMutableNotificationContent alloc] init];
1819 NSString *friendlyName = [[[GBROMManager sharedManager].romFile lastPathComponent] stringByDeletingPathExtension];
1820 NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\([^)]+\\)|\\[[^\\]]+\\]" options:0 error:nil];
1821 friendlyName = [regex stringByReplacingMatchesInString:friendlyName options:0 range:NSMakeRange(0, [friendlyName length]) withTemplate:@""];
1822 friendlyName = [friendlyName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
1823
1824 notificationContent.title = [NSString stringWithFormat:@"%@ Played an Alarm", friendlyName];
1825 notificationContent.body = [NSString stringWithFormat:@"%@ requested your attention by playing a scheduled alarm", friendlyName];
1826 notificationContent.sound = UNNotificationSound.defaultSound;
1827
1828 UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:[GBROMManager sharedManager].romFile
1829 content:notificationContent
1830 trigger:[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timeToAlarm repeats:false]];
1831
1832
1833 [center addNotificationRequest:request withCompletionHandler:nil];
1834 [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBNotificationsUsed"];
1835 }
1836
1837 dispatch_async(dispatch_get_main_queue(), ^{
1838 UIApplication.sharedApplication.idleTimerDisabled = false;
1839 });
1840 }
1841
1842 - (void)start
1843 {
1844 if (_running) return;
1845 if (self.presentedViewController) return;
1846 _running = true;
1847 dispatch_async(_runQueue, ^{
1848 [self run];
1849 });
1850 }
1851
1852 - (void)stop
1853 {
1854 if (!_running) return;
1855 [_audioLock lock];
1856 _stopping = true;
1857 [_audioLock signal];
1858 [_audioLock unlock];
1859 _running = false;
1860 while (_stopping) {
1861 [_audioLock lock];
1862 [_audioLock signal];
1863 [_audioLock unlock];
1864 }
1865 dispatch_sync(_runQueue, ^{});
1866 dispatch_async(dispatch_get_main_queue(), ^{
1867 self.runMode = GBRunModeNormal;
1868 [_backgroundView fadeOverlayOut];
1869 });
1870 }
1871
1872
1873 - (NSString *)bootROMPathForName:(NSString *)name
1874 {
1875 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBCustomBootROMs"]) {
1876 NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0];
1877 path = [path stringByAppendingPathComponent:@"Boot ROMs"];
1878 path = [path stringByAppendingPathComponent:name];
1879 path = [path stringByAppendingPathExtension:@"bin"];
1880 if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
1881 return path;
1882 }
1883 }
1884
1885 return [[NSBundle mainBundle] pathForResource:name ofType:@"bin"];
1886 }
1887
1888 - (void)loadBootROM: (GB_boot_rom_t)type
1889 {
1890 static NSString *const names[] = {
1891 [GB_BOOT_ROM_DMG_0] = @"dmg0_boot",
1892 [GB_BOOT_ROM_DMG] = @"dmg_boot",
1893 [GB_BOOT_ROM_MGB] = @"mgb_boot",
1894 [GB_BOOT_ROM_SGB] = @"sgb_boot",
1895 [GB_BOOT_ROM_SGB2] = @"sgb2_boot",
1896 [GB_BOOT_ROM_CGB_0] = @"cgb0_boot",
1897 [GB_BOOT_ROM_CGB] = @"cgb_boot",
1898 [GB_BOOT_ROM_CGB_E] = @"cgbE_boot",
1899 [GB_BOOT_ROM_AGB_0] = @"agb0_boot",
1900 [GB_BOOT_ROM_AGB] = @"agb_boot",
1901 };
1902 NSString *name = names[type];
1903 NSString *path = [self bootROMPathForName:name];
1904 /* These boot types are not commonly available, and they are indentical
1905 from an emulator perspective, so fall back to the more common variants
1906 if they can't be found. */
1907 if (!path && type == GB_BOOT_ROM_CGB_E) {
1908 [self loadBootROM:GB_BOOT_ROM_CGB];
1909 return;
1910 }
1911 if (!path && type == GB_BOOT_ROM_AGB_0) {
1912 [self loadBootROM:GB_BOOT_ROM_AGB];
1913 return;
1914 }
1915 GB_load_boot_rom(&_gb, [path UTF8String]);
1916 }
1917
1918 - (void)vblankWithType:(GB_vblank_type_t)type
1919 {
1920 if (type != GB_VBLANK_TYPE_REPEAT) {
1921 [_gbView flip];
1922 GB_set_pixels_output(&_gb, _gbView.pixels);
1923 }
1924 if (_rapidA) {
1925 _rapidACount++;
1926 GB_set_key_state(&_gb, GB_KEY_A, !(_rapidACount & 2));
1927 }
1928 if (_rapidB) {
1929 _rapidBCount++;
1930 GB_set_key_state(&_gb, GB_KEY_B, !(_rapidBCount & 2));
1931 }
1932 _autosaveCountdown--;
1933 if (_rapidA || _rapidB) {
1934 if (_runMode == GBRunModeRewind || _runMode == GBRunModePaused) {
1935 self.runMode = GBRunModeNormal;
1936 [_backgroundView fadeOverlayOut];
1937 }
1938 }
1939 _rewind = _runMode == GBRunModeRewind;
1940 }
1941
1942 - (void)gotNewSample:(GB_sample_t *)sample
1943 {
1944 [_audioLock lock];
1945 if (_audioClient.isPlaying) {
1946 if (_audioBufferPosition == _audioBufferSize) {
1947 if (_audioBufferSize >= 0x4000) {
1948 _audioBufferPosition = 0;
1949 [_audioLock unlock];
1950 return;
1951 }
1952
1953 if (_audioBufferSize == 0) {
1954 _audioBufferSize = 512;
1955 }
1956 else {
1957 _audioBufferSize += _audioBufferSize >> 2;
1958 }
1959 _audioBuffer = realloc(_audioBuffer, sizeof(*sample) * _audioBufferSize);
1960 }
1961 _audioBuffer[_audioBufferPosition++] = *sample;
1962 }
1963 if (_audioBufferPosition == _audioBufferNeeded) {
1964 [_audioLock signal];
1965 _audioBufferNeeded = 0;
1966 }
1967 [_audioLock unlock];
1968 }
1969
1970 - (void)rumbleChanged:(double)amp
1971 {
1972 [[GBHapticManager sharedManager] setRumbleStrength:amp];
1973 }
1974
1975 - (void)updatePalette
1976 {
1977 memcpy(&_palette,
1978 [GBPalettePicker paletteForTheme:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"]],
1979 sizeof(_palette));
1980 GB_set_palette(&_gb, &_palette);
1981 }
1982
1983 - (bool)handleOpenURLs:(NSArray <NSURL *> *)urls
1984 openInPlace:(bool)inPlace
1985 {
1986 NSMutableArray<NSURL *> *validURLs = [NSMutableArray array];
1987 NSMutableArray<NSString *> *skippedBasenames = [NSMutableArray array];
1988 NSMutableArray<NSString *> *unusedZips = [NSMutableArray array];
1989 NSString *tempDir = NSTemporaryDirectory();
1990
1991 __block unsigned tempIndex = 0;
1992 for (NSURL *url in urls) {
1993 if ([url.pathExtension.lowercaseString isEqualToString:@"zip"]) {
1994 GBZipReader *reader = [[GBZipReader alloc] initWithPath:url.path];
1995 if (!reader) {
1996 [skippedBasenames addObject:url.lastPathComponent];
1997 continue;
1998 }
1999 __block bool used = false;
2000 [reader iterateFiles:^bool(NSString *filename, size_t uncompressedSize, bool (^getData)(void *), bool (^writeToPath)(NSString *)) {
2001 if ([@[@"gb", @"gbc", @"isx"] containsObject:filename.pathExtension.lowercaseString] && uncompressedSize <= 32 * 1024 * 1024) {
2002 tempIndex++;
2003 NSString *subDir = [tempDir stringByAppendingFormat:@"/%u", tempIndex];
2004 mkdir(subDir.UTF8String, 0755);
2005 NSString *dest = [subDir stringByAppendingPathComponent:filename.lastPathComponent];
2006 if (writeToPath(dest)) {
2007 [validURLs addObject:[NSURL fileURLWithPath:dest]];
2008 used = true;
2009 }
2010 }
2011 return true;
2012 }];
2013 if (!used) {
2014 [unusedZips addObject:url.lastPathComponent];
2015 }
2016 }
2017 else if ([@[@"gb", @"gbc", @"isx"] containsObject:url.pathExtension.lowercaseString]) {
2018 [validURLs addObject:url];
2019 }
2020 else {
2021 [skippedBasenames addObject:url.lastPathComponent];
2022 }
2023 }
2024
2025 if (unusedZips.count) {
2026 UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"No ROMs in archive"
2027 message:[NSString stringWithFormat:@"Could not find any Game Boy ROM files in the following archives:\n%@",
2028 [unusedZips componentsJoinedByString:@"\n"]]
2029 preferredStyle:UIAlertControllerStyleAlert];
2030 [alert addAction:[UIAlertAction actionWithTitle:@"Close"
2031 style:UIAlertActionStyleCancel
2032 handler:nil]];
2033 [self presentViewController:alert animated:true completion:nil];
2034 }
2035
2036 if (skippedBasenames.count) {
2037 UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Unsupported Files"
2038 message:[NSString stringWithFormat:@"Could not import the following files because they're not supported:\n%@",
2039 [skippedBasenames componentsJoinedByString:@"\n"]]
2040 preferredStyle:UIAlertControllerStyleAlert];
2041 [alert addAction:[UIAlertAction actionWithTitle:@"Close"
2042 style:UIAlertActionStyleCancel
2043 handler:^(UIAlertAction *action) {
2044 [[NSUserDefaults standardUserDefaults] setBool:false forKey:@"GBShownUTIWarning"]; // Somebody might need a reminder
2045 }]];
2046 [self presentViewController:alert animated:true completion:nil];
2047 }
2048
2049
2050 if (validURLs.count == 1 && urls.count == 1) {
2051 NSURL *url = validURLs.firstObject;
2052 NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
2053 if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
2054 [GBROMManager sharedManager].currentROM = potentialROM;
2055 }
2056 else {
2057 [url startAccessingSecurityScopedResource];
2058 [GBROMManager sharedManager].currentROM =
2059 [[GBROMManager sharedManager] importROM:url.path
2060 keepOriginal:![url.path hasPrefix:tempDir] && inPlace];
2061 [url stopAccessingSecurityScopedResource];
2062 }
2063 return true;
2064 }
2065 for (NSURL *url in validURLs) {
2066 NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
2067 if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
2068 // That's an already imported ROM
2069 continue;
2070 }
2071 [url startAccessingSecurityScopedResource];
2072 [[GBROMManager sharedManager] importROM:url.path
2073 keepOriginal:![url.path hasPrefix:tempDir] && inPlace];
2074 [url stopAccessingSecurityScopedResource];
2075 }
2076 [self openLibrary];
2077
2078 return validURLs.count;
2079 }
2080
2081 - (void)doImportedPaletteNotification
2082 {
2083 UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleProminent]];
2084 effectView.layer.cornerRadius = 8;
2085 effectView.layer.masksToBounds = true;
2086 [self.view addSubview:effectView];
2087 UILabel *tipLabel = [[UILabel alloc] init];
2088 tipLabel.text = [NSString stringWithFormat:@"Imported palette “%@”", [[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"]];
2089 if (@available(iOS 13.0, *)) {
2090 tipLabel.textColor = [UIColor labelColor];
2091 }
2092 tipLabel.font = [UIFont systemFontOfSize:16];
2093 tipLabel.alpha = 0.8;
2094 [effectView.contentView addSubview:tipLabel];
2095
2096 UIView *view = self.view;
2097 CGSize outerSize = view.frame.size;
2098 CGSize size = [tipLabel textRectForBounds:(CGRect){{0, 0},
2099 {outerSize.width - 32,
2100 outerSize.height - 32}}
2101 limitedToNumberOfLines:1].size;
2102 size.width = ceil(size.width);
2103 tipLabel.frame = (CGRect){{8, 8}, size};
2104 CGRect finalFrame = (CGRect) {
2105 {round((outerSize.width - size.width - 16) / 2), view.window.safeAreaInsets.top + 12},
2106 {size.width + 16, size.height + 16}
2107 };
2108
2109 CGRect initFrame = finalFrame;
2110 initFrame.origin.y = -initFrame.size.height;
2111 effectView.frame = initFrame;
2112
2113 effectView.alpha = 0;
2114 [UIView animateWithDuration:0.5 animations:^{
2115 effectView.alpha = 1.0;
2116 effectView.frame = finalFrame;
2117 }];
2118
2119 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * 1.5)), dispatch_get_main_queue(), ^{
2120 [UIView animateWithDuration:0.5 animations:^{
2121 effectView.alpha = 0.0;
2122 effectView.frame = initFrame;
2123 } completion:^(BOOL finished) {
2124 if (finished) {
2125 [effectView removeFromSuperview];
2126 }
2127 }];
2128 });
2129 }
2130
2131 - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
2132 {
2133 if (self.presentedViewController && ![self.presentedViewController isKindOfClass:[UIAlertController class]]) {
2134 [self dismissViewController];
2135 }
2136 if ([url.pathExtension.lowercaseString isEqual:@"sbp"]) {
2137 [url startAccessingSecurityScopedResource];
2138 bool success = [GBPalettePicker importPalette:url.path];
2139 [url stopAccessingSecurityScopedResource];
2140 if (!success) {
2141 UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Palette Import Failed"
2142 message:@"The imported palette file is invalid."
2143 preferredStyle:UIAlertControllerStyleAlert];
2144 [alertController addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]];
2145 [self presentViewController:alertController animated:true completion:nil];
2146 }
2147 else {
2148 [self doImportedPaletteNotification];
2149 }
2150 return success;
2151 }
2152 NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
2153 if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
2154 [self stop];
2155 [GBROMManager sharedManager].currentROM = potentialROM;
2156 [self start];
2157 return [GBROMManager sharedManager].currentROM != nil;
2158 }
2159 return [self handleOpenURLs:@[url]
2160 openInPlace:[options[UIApplicationOpenURLOptionsOpenInPlaceKey] boolValue]];
2161 }
2162
2163 - (void)setRunMode:(GBRunMode)runMode ignoreDynamicSpeed:(bool)ignoreDynamicSpeed
2164 {
2165 if (runMode == GBRunModeRewind && _rewindOver) {
2166 runMode = GBRunModePaused;
2167 }
2168 if (runMode == _runMode) return;
2169 if (_runMode == GBRunModePaused) {
2170 [_audioClient start];
2171 }
2172 _runMode = runMode;
2173 if (_runMode == GBRunModePaused) {
2174 [_audioClient stop];
2175 }
2176
2177 if (_runMode == GBRunModeNormal || _runMode == GBRunModeTurbo || _runMode == GBRunModeUnderclock) {
2178 _rewindOver = false;
2179 }
2180
2181 if (_runMode == GBRunModeNormal || _runMode == GBRunModeUnderclock || !([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"] && !ignoreDynamicSpeed)) {
2182 if (_runMode == GBRunModeTurbo) {
2183 GB_set_turbo_mode(&_gb, true, false);
2184 }
2185 else if (_runMode == GBRunModeUnderclock) {
2186 GB_set_clock_multiplier(&_gb, 0.5);
2187 }
2188 else {
2189 GB_set_turbo_mode(&_gb, false, false);
2190 GB_set_clock_multiplier(&_gb, 1.0);
2191 }
2192 }
2193 }
2194
2195 - (void)setRunMode:(GBRunMode)runMode
2196 {
2197 [self setRunMode:runMode ignoreDynamicSpeed:false];
2198 }
2199
2200 - (AVCaptureDevice *)captureDevice
2201 {
2202 if (_cameraPosition == AVCaptureDevicePositionFront) {
2203 return _frontCaptureDevice ?: [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
2204 }
2205 return _backCaptureDevice ?: [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
2206 }
2207
2208 - (void)cameraRequestUpdate
2209 {
2210 dispatch_async(dispatch_get_main_queue(), ^{
2211 if (!_cameraSession) {
2212 dispatch_async(_cameraQueue, ^{
2213 @try {
2214 if (!_cameraSession) {
2215 NSError *error;
2216 AVCaptureDevice *device = [self captureDevice];
2217 AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice: device error: &error];
2218
2219 if (!input) {
2220 GB_camera_updated(&_gb);
2221 return;
2222 }
2223
2224 _cameraOutput = [[AVCaptureVideoDataOutput alloc] init];
2225 [_cameraOutput setVideoSettings: @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)}];
2226 [_cameraOutput setSampleBufferDelegate:self
2227 queue:_cameraQueue];
2228
2229
2230
2231 _cameraSession = [AVCaptureSession new];
2232 if ([device supportsAVCaptureSessionPreset:AVCaptureSessionPreset352x288]) {
2233 _cameraSession.sessionPreset = AVCaptureSessionPreset352x288;
2234 }
2235 else if ([device supportsAVCaptureSessionPreset:AVCaptureSessionPresetMedium]) {
2236 _cameraSession.sessionPreset = AVCaptureSessionPresetMedium;
2237 }
2238
2239 [_cameraSession addInput: input];
2240 [_cameraSession addOutput: _cameraOutput];
2241 _cameraConnection = [_cameraOutput connectionWithMediaType: AVMediaTypeVideo];
2242 _cameraConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
2243 [_cameraSession startRunning];
2244 }
2245 }
2246 @catch (NSException *exception) {
2247 /* I have not tested camera support on many devices, so we catch exceptions just in case. */
2248 GB_camera_updated(&_gb);
2249 _cameraSession = nil;
2250 }
2251 });
2252 }
2253
2254 _cameraNeedsUpdate = true;
2255 [_disableCameraTimer invalidate];
2256 if (!_cameraPositionButton.alpha) {
2257 [UIView animateWithDuration:0.25 animations:^{
2258 _cameraPositionButton.alpha = 1;
2259 }];
2260 }
2261 if (_changeCameraButton) {
2262 // The change camera button is only available when we are using a capture device on the back of the device
2263 double changeCameraButtonAlpha = (_cameraPosition == AVCaptureDevicePositionFront) ? 0 : 1;
2264 if (changeCameraButtonAlpha != _changeCameraButton.alpha) {
2265 [UIView animateWithDuration:0.25 animations:^{
2266 _changeCameraButton.alpha = changeCameraButtonAlpha;
2267 }];
2268 }
2269 }
2270
2271 _disableCameraTimer = [NSTimer scheduledTimerWithTimeInterval:1
2272 repeats:false
2273 block:^(NSTimer *timer) {
2274 if (_cameraPositionButton.alpha) {
2275 [UIView animateWithDuration:0.25 animations:^{
2276 _cameraPositionButton.alpha = 0;
2277 }];
2278 }
2279 if (_changeCameraButton.alpha) {
2280 [UIView animateWithDuration:0.25 animations:^{
2281 _changeCameraButton.alpha = 0;
2282 }];
2283 }
2284 dispatch_async(_cameraQueue, ^{
2285 [_cameraSession stopRunning];
2286 _cameraSession = nil;
2287 _cameraConnection = nil;
2288 _cameraOutput = nil;
2289 });
2290 }];
2291 });
2292 }
2293
2294 - (uint8_t)cameraGetPixelAtX:(uint8_t)x andY:(uint8_t)y
2295 {
2296 if (!_cameraImage) {
2297 return 0;
2298 }
2299 if (_cameraNeedsUpdate) {
2300 return 0;
2301 }
2302
2303 y += 8; // Equalize X and Y for rotation as a 128x128 image
2304
2305 if (_cameraPosition == AVCaptureDevicePositionFront) {
2306 x = 127 - x;
2307 }
2308
2309 switch (_orientation) {
2310 case UIInterfaceOrientationUnknown:
2311 case UIInterfaceOrientationPortrait:
2312 break;
2313 case UIInterfaceOrientationPortraitUpsideDown:
2314 x = 127 - x;
2315 y = 127 - y;
2316 break;
2317 case UIInterfaceOrientationLandscapeLeft: {
2318 uint8_t tempX = x;
2319 x = y;
2320 y = 127 - tempX;
2321 break;
2322 }
2323 case UIInterfaceOrientationLandscapeRight:{
2324 uint8_t tempX = x;
2325 x = 127 - y;
2326 y = tempX;
2327 break;
2328 }
2329 }
2330
2331 if (_cameraPosition == AVCaptureDevicePositionFront) {
2332 x = 127 - x;
2333 }
2334
2335 // Center the 128*128 image on the 130*XXX (or XXX*130) captured image
2336 unsigned offsetX = (CVPixelBufferGetWidth(_cameraImage) - 128) / 2;
2337 unsigned offsetY = (CVPixelBufferGetHeight(_cameraImage) - 112) / 2;
2338
2339 uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(_cameraImage);
2340 size_t bytesPerRow = CVPixelBufferGetBytesPerRow(_cameraImage);
2341 uint8_t ret = baseAddress[(x + offsetX) * 4 + (y + offsetY) * bytesPerRow + 2];
2342
2343 return ret;
2344 }
2345
2346 - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
2347 {
2348 if (!_cameraNeedsUpdate) return;
2349 CVImageBufferRef buffer = CMSampleBufferGetImageBuffer(sampleBuffer);
2350 CIImage *image = [CIImage imageWithCVPixelBuffer:buffer
2351 options:[NSDictionary dictionaryWithObjectsAndKeys:[NSNull null], kCIImageColorSpace, nil]];
2352 double scale = MAX(130.0 / CVPixelBufferGetWidth(buffer), 130.0 / CVPixelBufferGetHeight(buffer));
2353 image = [image imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
2354 if (_cameraImage) {
2355 CVPixelBufferUnlockBaseAddress(_cameraImage, 0);
2356 CVBufferRelease(_cameraImage);
2357 _cameraImage = NULL;
2358 }
2359 CGSize size = image.extent.size;
2360 CVPixelBufferCreate(kCFAllocatorDefault,
2361 size.width,
2362 size.height,
2363 kCVPixelFormatType_32BGRA,
2364 NULL,
2365 &_cameraImage);
2366 [[[CIContext alloc] init] render:image toCVPixelBuffer:_cameraImage];
2367 CVPixelBufferLockBaseAddress(_cameraImage, 0);
2368
2369 GB_camera_updated(&_gb);
2370
2371 _cameraNeedsUpdate = false;
2372 }
2373
2374 - (void)rotateCamera
2375 {
2376 dispatch_async(_cameraQueue, ^{
2377 _cameraPosition ^= AVCaptureDevicePositionBack ^ AVCaptureDevicePositionFront;
2378 [_cameraSession stopRunning];
2379 _cameraSession = nil;
2380 _cameraConnection = nil;
2381 _cameraOutput = nil;
2382 if (_cameraNeedsUpdate) {
2383 _cameraNeedsUpdate = false;
2384 GB_camera_updated(&_gb);
2385 }
2386 });
2387 }
2388
2389 - (void)changeCamera
2390 {
2391 dispatch_async(_cameraQueue, ^{
2392 if (![_backCaptureDevice lockForConfiguration:nil]) return;
2393 _currentZoomIndex++;
2394 if (_currentZoomIndex == _zoomLevels.count) {
2395 _currentZoomIndex = 0;
2396 }
2397 [_backCaptureDevice rampToVideoZoomFactor:_zoomLevels[_currentZoomIndex].doubleValue withRate:2];
2398 [_backCaptureDevice unlockForConfiguration];
2399 });
2400 }
2401
2402 - (void)disconnectCable
2403 {
2404 if (!_printerConnected) return;
2405 _printerConnected = false;
2406 _currentPrinterImageData = nil;
2407 [UIView animateWithDuration:0.25 animations:^{
2408 _printerButton.alpha = 0;
2409 }];
2410 [_printerSpinner stopAnimating];
2411 GB_disconnect_serial(&_gb);
2412 }
2413
2414 - (void)connectPrinter
2415 {
2416 if (_printerConnected) return;
2417 _printerConnected = true;
2418 GB_connect_printer(&_gb, printImage, printDone);
2419 }
2420
2421 - (void)openConnectMenu
2422 {
2423 UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad?
2424 UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet;
2425 GBCheckableAlertController *menu = [GBCheckableAlertController alertControllerWithTitle:@"Connect Accessory"
2426 message:@"Choose an accessory to connect."
2427 preferredStyle:style];
2428 [menu addAction:[UIAlertAction actionWithTitle:@"None"
2429 style:UIAlertActionStyleDefault
2430 handler:^(UIAlertAction *action) {
2431 [self disconnectCable];
2432 }]];
2433 [menu addAction:[UIAlertAction actionWithTitle:@"Game Boy Printer"
2434 style:UIAlertActionStyleDefault
2435 handler:^(UIAlertAction *action) {
2436 [self connectPrinter];
2437 }]];
2438 menu.selectedAction = menu.actions[_printerConnected];
2439 [menu addAction:[UIAlertAction actionWithTitle:@"Cancel"
2440 style:UIAlertActionStyleCancel
2441 handler:nil]];
2442 [self presentViewController:menu animated:true completion:nil];
2443 }
2444
2445 - (void)printImage:(uint32_t *)imageBytes height:(unsigned) height
2446 topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin
2447 exposure:(unsigned) exposure
2448 {
2449 uint32_t paddedImage[160 * (topMargin + height + bottomMargin)];
2450 memset(paddedImage, 0xFF, sizeof(paddedImage));
2451 memcpy(paddedImage + (160 * topMargin), imageBytes, 160 * height * sizeof(imageBytes[0]));
2452 if (!_currentPrinterImageData) {
2453 _currentPrinterImageData = [[NSMutableData alloc] init];
2454 }
2455 [_currentPrinterImageData appendBytes:paddedImage length:sizeof(paddedImage)];
2456
2457 dispatch_async(dispatch_get_main_queue(), ^{
2458 [UIView animateWithDuration:0.25 animations:^{
2459 _printerButton.alpha = 1;
2460 }];
2461 [_printerSpinner startAnimating];
2462 });
2463
2464 }
2465
2466 - (void)printDone
2467 {
2468 dispatch_async(dispatch_get_main_queue(), ^{
2469 [_printerSpinner stopAnimating];
2470 });
2471 }
2472
2473 - (void)showPrinterFeed
2474 {
2475 UIImage *image = [self imageFromData:_currentPrinterImageData
2476 width:160
2477 height:_currentPrinterImageData.length / 160 / sizeof(uint32_t)];
2478
2479 _window.backgroundColor = [UIColor blackColor];
2480 [self presentViewController:[[GBPrinterFeedController alloc] initWithImage:image]
2481 animated:true
2482 completion:nil];
2483
2484 }
2485
2486 - (void)emptyPrinterFeed
2487 {
2488 _currentPrinterImageData = nil;
2489 [UIView animateWithDuration:0.25 animations:^{
2490 _printerButton.alpha = 0;
2491 }];
2492 [_printerSpinner stopAnimating];
2493 [self dismissViewController];
2494 }
2495
2496 @end
2497
2498 /* +[UIColor labelColor] is broken in some contexts in iOS 26 and despite being such a critical method
2499 Apple isn't going to fix this in time. */
2500 API_AVAILABLE(ios(19.0))
2501 @implementation UIColor(SolariumBugs)
2502 + (UIColor *)_labelColor
2503 {
2504 return [UIColor colorWithDynamicProvider:^UIColor *(UITraitCollection *traitCollection) {
2505 switch (traitCollection.userInterfaceStyle) {
2506
2507 case UIUserInterfaceStyleUnspecified:
2508 case UIUserInterfaceStyleLight:
2509 return [UIColor blackColor];
2510 case UIUserInterfaceStyleDark:
2511 return [UIColor whiteColor];
2512 }
2513 }];
2514 }
2515
2516 + (void)load
2517 {
2518 if (@available(iOS 19.0, *)) {
2519 method_setImplementation(class_getClassMethod(self, @selector(labelColor)),
2520 [self methodForSelector:@selector(_labelColor)]);
2521 }
2522 }
2523
2524 @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.