Files
SameBoy/iOS/GBViewController.m

2664 lines
109 KiB
Objective-C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#import "GBViewController.h"
#import "GBHorizontalLayout.h"
#import "GBVerticalLayout.h"
#import "GBViewMetal.h"
#import "GBAudioClient.h"
#import "GBROMManager.h"
#import "GBLibraryViewController.h"
#import "GBBackgroundView.h"
#import "GBHapticManager.h"
#import "GBMenuViewController.h"
#import "GBOptionViewController.h"
#import "GBAboutController.h"
#import "GBSettingsViewController.h"
#import "GBPalettePicker.h"
#import "GBStatesViewController.h"
#import "GBCheckableAlertController.h"
#import "GBPrinterFeedController.h"
#import "GBCheatsController.h"
#import "GBLazyObject.h"
#import "UILabel+LockFonts.h"
#import <CommonCrypto/CommonCrypto.h>
#import <sys/xattr.h>
#import "GCControllerGetElements.h"
#import "GBZipReader.h"
#import <sys/stat.h>
#import <CoreMotion/CoreMotion.h>
#import <dlfcn.h>
#import <objc/runtime.h>
@interface UIWindow()
- (bool)_windowOwnsInterfaceOrientation;
@end
static UIInterfaceOrientation CurrentOrientation(void)
{
if (@available(iOS 16.0, *)) {
return [(UIWindowScene *)[UIApplication sharedApplication].connectedScenes.anyObject effectiveGeometry].interfaceOrientation;
}
if (@available(iOS 13.0, *)) {
return [(UIWindowScene *)[UIApplication sharedApplication].connectedScenes.anyObject interfaceOrientation];
}
return [UIApplication sharedApplication].statusBarOrientation;
}
static UIImage *CreateMenuImage(NSString *name)
{
static const unsigned size = 20;
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(size, size)];
CGRect destRect = {0,};
UIImage *source = [UIImage imageNamed:name];
CGSize sourceSize = source.size;
if (sourceSize.width > sourceSize.height) {
destRect.size.width = size;
destRect.size.height = sourceSize.height * size / sourceSize.width;
destRect.origin.y = (size - destRect.size.height) / 2;
}
else {
destRect.size.height = size;
destRect.size.width = sourceSize.width * size / sourceSize.height;
destRect.origin.x = (size - destRect.size.width) / 2;
}
UIImage *image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *myContext) {
[source drawInRect:destRect];
}];
return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
API_AVAILABLE(ios(13.0))
@interface GBSceneDelegate : UIResponder <UIWindowSceneDelegate>
@end
API_AVAILABLE(ios(13.0))
@implementation UIKeyCommand (KeyCommandWithImage)
+ (instancetype)keyCommandWithInput:(NSString *)input modifierFlags:(UIKeyModifierFlags)modifierFlags action:(SEL)action title:(NSString *)title image:(UIImage *)image
{
UIKeyCommand *ret = [self keyCommandWithInput:input modifierFlags:modifierFlags action:action];
ret.title = title;
ret.image = image;
return ret;
}
@end
@implementation GBViewController
{
GB_gameboy_t _gb;
GBView *_gbView;
dispatch_queue_t _runQueue;
volatile bool _running;
volatile bool _stopping;
bool _rewind;
bool _rewindOver;
bool _romLoaded;
bool _swappingROM;
bool _skipAutoLoad;
bool _rapidA, _rapidB;
uint8_t _rapidACount, _rapidBCount;
UIInterfaceOrientation _orientation;
GBHorizontalLayout *_horizontalLayoutLeft;
GBHorizontalLayout *_horizontalLayoutRight;
GBVerticalLayout *_verticalLayout;
NSCondition *_audioLock;
GB_sample_t *_audioBuffer;
size_t _audioBufferSize;
size_t _audioBufferPosition;
size_t _audioBufferNeeded;
GBAudioClient *_audioClient;
NSMutableSet *_defaultsObservers;
GB_palette_t _palette;
CMMotionManager *_motionManager;
CVImageBufferRef _cameraImage;
AVCaptureSession *_cameraSession;
AVCaptureConnection *_cameraConnection;
AVCaptureVideoDataOutput *_cameraOutput;
bool _cameraNeedsUpdate;
NSTimer *_disableCameraTimer;
AVCaptureDevicePosition _cameraPosition;
UIButton *_cameraPositionButton;
UIButton *_changeCameraButton;
AVCaptureDevice *_frontCaptureDevice;
AVCaptureDevice *_backCaptureDevice;
NSMutableArray<NSNumber *> *_zoomLevels;
unsigned _currentZoomIndex;
__weak GCController *_lastController;
dispatch_queue_t _cameraQueue;
bool _runModeFromController;
UIWindow *_mirrorWindow;
GBView *_mirrorView;
bool _printerConnected;
UIButton *_printerButton;
UIActivityIndicatorView *_printerSpinner;
NSMutableData *_currentPrinterImageData;
NSString *_lastSavedROM;
NSDate *_saveDate;
unsigned _autosaveCountdown;
API_AVAILABLE(ios(27.1)) UIHingeStatus _lastHingeStatus;
@public
API_AVAILABLE(ios(27.1)) UIHinge *_hinge;
}
static void loadBootROM(GB_gameboy_t *gb, GB_boot_rom_t type)
{
GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
[self loadBootROM:type];
}
static void vblank(GB_gameboy_t *gb, GB_vblank_type_t type)
{
GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
[self vblankWithType:type];
}
static void printImage(GB_gameboy_t *gb, uint32_t *image, uint8_t height,
uint8_t top_margin, uint8_t bottom_margin, uint8_t exposure)
{
GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
[self printImage:image height:height topMargin:top_margin bottomMargin:bottom_margin exposure:exposure];
}
static void printDone(GB_gameboy_t *gb)
{
GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
[self printDone];
}
static void consoleLog(GB_gameboy_t *gb, const char *string, GB_log_attributes_t attributes)
{
static NSString *buffer = @"";
buffer = [buffer stringByAppendingString:@(string)];
if ([buffer containsString:@"\n"]) {
NSLog(@"%@", buffer);
buffer = @"";
}
}
static uint32_t rgbEncode(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b)
{
return (r << 0) | (g << 8) | (b << 16) | 0xFF000000;
}
static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample)
{
GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
[self gotNewSample:sample];
}
static void cameraRequestUpdate(GB_gameboy_t *gb)
{
GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
[self cameraRequestUpdate];
}
static uint8_t cameraGetPixel(GB_gameboy_t *gb, uint8_t x, uint8_t y)
{
GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
return [self cameraGetPixelAtX:x andY:y];
}
static void rumbleCallback(GB_gameboy_t *gb, double amp)
{
GBViewController *self = (__bridge GBViewController *)GB_get_user_data(gb);
double strength = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBRumbleStrength"];
if (strength != 1) {
amp = pow(amp, strength) * strength;
}
[self rumbleChanged:amp];
}
- (void)initGameBoy
{
GB_gameboy_t *gb = &_gb;
GB_init(gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]);
GB_set_user_data(gb, (__bridge void *)(self));
GB_set_boot_rom_load_callback(gb, (GB_boot_rom_load_callback_t)loadBootROM);
GB_set_vblank_callback(gb, (GB_vblank_callback_t) vblank);
GB_set_log_callback(gb, (GB_log_callback_t) consoleLog);
GB_set_camera_get_pixel_callback(gb, cameraGetPixel);
GB_set_camera_update_request_callback(gb, cameraRequestUpdate);
[self addDefaultObserver:^(id newValue) {
GB_set_color_correction_mode(gb, (GB_color_correction_mode_t)[newValue integerValue]);
} forKey:@"GBColorCorrection"];
[self addDefaultObserver:^(id newValue) {
GB_set_light_temperature(gb, [newValue doubleValue]);
} forKey:@"GBLightTemperature"];
GB_set_border_mode(gb, GB_BORDER_NEVER);
__weak typeof(self) weakSelf = self;
[self addDefaultObserver:^(id newValue) {
[weakSelf updatePalette];
} forKey:@"GBCurrentTheme"];
GB_set_rgb_encode_callback(gb, rgbEncode);
[self addDefaultObserver:^(id newValue) {
GB_set_highpass_filter_mode(gb, (GB_highpass_mode_t)[newValue integerValue]);
} forKey:@"GBHighpassFilter"];
[self addDefaultObserver:^(id newValue) {
GB_set_rtc_mode(gb, [newValue integerValue]);
} forKey:@"GBRTCMode"];
GB_apu_set_sample_callback(gb, audioCallback);
GB_set_rumble_callback(gb, rumbleCallback);
[self addDefaultObserver:^(id newValue) {
GB_set_rumble_mode(gb, [newValue integerValue]);
} forKey:@"GBRumbleMode"];
[self addDefaultObserver:^(id newValue) {
GB_set_interference_volume(gb, [newValue doubleValue]);
} forKey:@"GBInterferenceVolume"];
[self addDefaultObserver:^(id newValue) {
GB_set_rewind_length(gb, [newValue unsignedIntValue]);
} forKey:@"GBRewindLength"];
[self addDefaultObserver:^(id newValue) {
GB_set_turbo_cap(gb, [newValue doubleValue]);
} forKey:@"GBTurboCap"];
[self addDefaultObserver:^(id newValue) {
[[AVAudioSession sharedInstance] setCategory:[newValue isEqual:@"on"]? AVAudioSessionCategoryPlayback : AVAudioSessionCategorySoloAmbient
mode:AVAudioSessionModeDefault
routeSharingPolicy:AVAudioSessionRouteSharingPolicyDefault
options:0
error:nil];
} forKey:@"GBAudioMode"];
}
- (void)addDefaultObserver:(void(^)(id newValue))block forKey:(NSString *)key
{
if (!_defaultsObservers) {
_defaultsObservers = [NSMutableSet set];
}
block = [block copy];
[_defaultsObservers addObject:block];
[[NSUserDefaults standardUserDefaults] addObserver:self
forKeyPath:key
options:NSKeyValueObservingOptionNew
context:(void *)block];
block([[NSUserDefaults standardUserDefaults] objectForKey:key]);
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
((__bridge void(^)(id))context)(change[NSKeyValueChangeNewKey]);
}
- (NSArray<NSNumber *> *)zoomFactorsForDevice:(AVCaptureDevice *)device
{
if (@available(iOS 13.0, *)) {
return device.virtualDeviceSwitchOverVideoZoomFactors;
}
double factor = device.dualCameraSwitchOverVideoZoomFactor;
if (factor == 1.0) {
return @[];
}
return @[@(factor)];
}
- (void)recreateLayoutsWithThemeNamed:(NSString *)name;
{
if (@available(iOS 16.0, *)) {
if (_hinge) {
_lastHingeStatus = _hinge.status;
[super setNeedsUpdateOfSupportedInterfaceOrientations];
}
}
GBTheme *theme = [GBSettingsViewController themeNamed:name];
_horizontalLayoutLeft = [[GBHorizontalLayout alloc] initWithTheme:theme cutoutOnRight:false];
_horizontalLayoutRight = _horizontalLayoutLeft.asymmetric?
[[GBHorizontalLayout alloc] initWithTheme:theme cutoutOnRight:true] :
_horizontalLayoutLeft;
_verticalLayout = [[GBVerticalLayout alloc] initWithTheme:theme];
_printerSpinner.color = theme.buttonColor;
[self willRotateToInterfaceOrientation:CurrentOrientation()
duration:0];
[_backgroundView reloadThemeImages];
[self setNeedsStatusBarAppearanceUpdate];
}
- (bool)setupWindow
{
_runQueue = dispatch_queue_create("SameBoy Emulation Queue", NULL);
if (@available(iOS 27.1, *)) {
// Hack to prevent start-up flashing on iPhone Duo
if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone) {
if ((_window.bounds.size.width == 951 && _window.bounds.size.height == 669) ||
(_window.bounds.size.width == 669 && _window.bounds.size.height == 951)) {
_lastHingeStatus = UIHingeStatusFullyOpen;
}
else if ((_window.bounds.size.width == 678 && _window.bounds.size.height == 466) ||
(_window.bounds.size.width == 466 && _window.bounds.size.height == 678)) {
_lastHingeStatus = UIHingeStatusClosed;
}
}
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
[self addDefaultObserver:^(id newValue) {
[self recreateLayoutsWithThemeNamed:newValue];
} forKey:@"GBInterfaceTheme"];
#pragma clang diagnostic pop
_backgroundView = [[GBBackgroundView alloc] initWithLayout:[self layoutForOrientation:CurrentOrientation()]];
[_window addSubview:_backgroundView];
self.view = _backgroundView;
[self initGameBoy];
_gbView = _backgroundView.gbView;
_gbView.hidden = true;
_gbView.gb = &_gb;
[_gbView screenSizeChanged];
[self addDefaultObserver:^(id newValue) {
[[NSNotificationCenter defaultCenter] postNotificationName:@"GBFilterChanged" object:nil];
} forKey:@"GBFilter"];
__weak GBView *gbview = _gbView;
[self addDefaultObserver:^(id newValue) {
gbview.frameBlendingMode = [newValue integerValue];
} forKey:@"GBFrameBlendingMode"];
__weak GBBackgroundView *backgroundView = _backgroundView;
[self addDefaultObserver:^(id newValue) {
backgroundView.usesSwipePad = [newValue boolValue];
} forKey:@"GBSwipeDpad"];
[self willRotateToInterfaceOrientation:CurrentOrientation()
duration:0];
_audioLock = [[NSCondition alloc] init];
[[NSNotificationCenter defaultCenter] addObserverForName:@"GBROMChanged"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
_swappingROM = true;
[self stop];
[self start];
}];
_motionManager = [[CMMotionManager alloc] init];
_cameraPosition = AVCaptureDevicePositionBack;
// Back camera setup
NSArray *deviceTypes = @[AVCaptureDeviceTypeBuiltInWideAngleCamera,
AVCaptureDeviceTypeBuiltInTelephotoCamera,
AVCaptureDeviceTypeBuiltInDualCamera];
if (@available(iOS 13.0, *)) {
// AVCaptureDeviceTypeBuiltInUltraWideCamera is only available in iOS 13+
deviceTypes = @[AVCaptureDeviceTypeBuiltInWideAngleCamera,
AVCaptureDeviceTypeBuiltInUltraWideCamera,
AVCaptureDeviceTypeBuiltInTelephotoCamera,
AVCaptureDeviceTypeBuiltInTripleCamera,
AVCaptureDeviceTypeBuiltInDualWideCamera,
AVCaptureDeviceTypeBuiltInDualCamera];
}
// Use a discovery session to gather the capture devices (all back cameras as well as the front camera)
AVCaptureDeviceDiscoverySession *cameraDiscoverySession = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:deviceTypes
mediaType:AVMediaTypeVideo
position:AVCaptureDevicePositionUnspecified];
for (AVCaptureDevice *device in cameraDiscoverySession.devices) {
if ([device position] == AVCaptureDevicePositionBack) {
if (!_backCaptureDevice ||
[self zoomFactorsForDevice:_backCaptureDevice].count < [self zoomFactorsForDevice:device].count) {
_backCaptureDevice = device;
}
}
else if ([device position] == AVCaptureDevicePositionFront) {
_frontCaptureDevice = device;
}
}
_zoomLevels = [self zoomFactorsForDevice:_backCaptureDevice].mutableCopy;
[_zoomLevels insertObject:@1 atIndex:0];
if (_zoomLevels.count == 3 && _zoomLevels[2].doubleValue > 5.5 && _zoomLevels[1].doubleValue < 3.5) {
[_zoomLevels insertObject:@4 atIndex:2];
}
_cameraPositionButton = [[UIButton alloc] init];
if (@available(iOS 13.0, *)) {
[_cameraPositionButton setImage:[UIImage systemImageNamed:@"camera.rotate"
withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]]
forState:UIControlStateNormal];
_cameraPositionButton.backgroundColor = [UIColor systemBackgroundColor];
// Configure the change camera button stacked on top of the camera position button
_changeCameraButton = [[UIButton alloc] init];
[_changeCameraButton setImage:[UIImage systemImageNamed:@"camera.aperture"
withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]]
forState:UIControlStateNormal];
_changeCameraButton.backgroundColor = [UIColor systemBackgroundColor];
_changeCameraButton.layer.cornerRadius = 6;
_changeCameraButton.alpha = 0;
[_changeCameraButton addTarget:self
action:@selector(changeCamera)
forControlEvents:UIControlEventTouchUpInside];
// Only show the change camera button if we have more than one back camera to swap between.
if (_zoomLevels.count > 1) {
[_backgroundView addSubview:_changeCameraButton];
}
}
else {
UIImage *rotateImage = [[UIImage imageNamed:@"CameraRotateTemplate"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[_cameraPositionButton setImage:rotateImage
forState:UIControlStateNormal];
_cameraPositionButton.backgroundColor = [UIColor whiteColor];
}
_cameraPositionButton.layer.cornerRadius = 6;
_cameraPositionButton.alpha = 0;
[_cameraPositionButton addTarget:self
action:@selector(rotateCamera)
forControlEvents:UIControlEventTouchUpInside];
[_backgroundView addSubview:_cameraPositionButton];
_cameraQueue = dispatch_queue_create("SameBoy Camera Queue", NULL);
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
[self verifyEntitlements];
[self setControllerHandlers];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(setControllerHandlers)
name:GCControllerDidConnectNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(controllerDisconnected:)
name:GCControllerDidDisconnectNotification
object:nil];
for (NSString *name in @[UIScreenDidConnectNotification,
UIScreenDidDisconnectNotification,
UIScreenModeDidChangeNotification]) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateMirrorWindow)
name:name
object:nil];
}
_printerButton = [[UIButton alloc] init];
_printerSpinner = [[UIActivityIndicatorView alloc] init];
_printerSpinner.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite;
_printerSpinner.color = _backgroundView.layout.theme.buttonColor;
[self didRotateFromInterfaceOrientation:CurrentOrientation()];
if (@available(iOS 13.0, *)) {
[_printerButton setImage:[UIImage systemImageNamed:@"printer"
withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]]
forState:UIControlStateNormal];
_printerButton.backgroundColor = [UIColor systemBackgroundColor];
}
else {
UIImage *rotateImage = [[UIImage imageNamed:@"PrinterTemplate"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[_printerButton setImage:rotateImage
forState:UIControlStateNormal];
_printerButton.backgroundColor = [UIColor whiteColor];
}
_printerButton.layer.cornerRadius = 6;
_printerButton.alpha = 0;
[_printerButton addTarget:self
action:@selector(showPrinterFeed)
forControlEvents:UIControlEventTouchUpInside];
[_backgroundView addSubview:_printerButton];
[_backgroundView addSubview:_printerSpinner];
[self updateMirrorWindow];
if (@available(iOS 26.0, *)) {
UIMainMenuSystemConfiguration *conf = [[objc_getClass("UIMainMenuSystemConfiguration") alloc] init];
conf.newScenePreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.documentPreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.printingPreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.findingPreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.toolbarPreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.sidebarPreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.inspectorPreference = UIMenuSystemElementGroupPreferenceRemoved;
conf.textFormattingPreference = UIMenuSystemElementGroupPreferenceRemoved;
UIMainMenuSystem *system = (id)[objc_getClass("UIMainMenuSystem") sharedSystem];
[system setBuildConfiguration:conf
buildHandler:^(id<UIMenuBuilder> builder) {
[builder removeMenuForIdentifier:UIMenuView]; // This menu's always empty
[builder removeMenuForIdentifier:UIMenuOpenRecent]; // This will list files to re-import, bad
[(id)builder insertElements:@[[UICommand commandWithTitle:@"About SameBoy"
image:nil
action:@selector(showAbout)
propertyList:nil]]
atStartOfMenuForIdentifier:UIMenuApplication];
UIMenu *emulationMenu = [UIMenu menuWithTitle:@"Emulation" children:@[
[UIKeyCommand keyCommandWithInput:@"r" modifierFlags:UIKeyModifierCommand action:@selector(reset) title:@"Reset" image:[UIImage systemImageNamed:@"arrow.2.circlepath"]],
[UICommand commandWithTitle:@"Change Model…" image:CreateMenuImage(@"ModelTemplate") action:@selector(changeModel) propertyList:nil],
[UICommand commandWithTitle:@"Save States…" image:[UIImage systemImageNamed:@"square.stack"] action:@selector(openStates) propertyList:nil],
[UIMenu menuWithTitle:@"Cheats" image:CreateMenuImage(@"CheatsTemplate") identifier:nil options:0 children:@[
[UIKeyCommand keyCommandWithInput:@"c" modifierFlags:UIKeyModifierCommand | UIKeyModifierShift action:@selector(toggleCheats) title:@"Enable Cheats" image:nil],
[UICommand commandWithTitle:@"Show Cheats…" image:nil action:@selector(openCheats) propertyList:nil],
]],
[UIMenu menuWithTitle:@"Connect" image:CreateMenuImage(@"LinkCableTemplate") identifier:nil options:0 children:@[
[UICommand commandWithTitle:@"None" image:nil action:@selector(disconnectCable) propertyList:nil],
[UICommand commandWithTitle:@"Printer" image:[UIImage systemImageNamed:@"printer"] action:@selector(connectPrinter) propertyList:nil],
]],
]];
[builder insertSiblingMenu:emulationMenu beforeMenuForIdentifier:UIMenuWindow];
}];
}
// Started in windowed/split mode?
[self viewWillTransitionToSize:self.window.bounds.size withTransitionCoordinator:nil];
return true;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
if (@available(iOS 13.0, *)) {
return true;
}
if (!self.window) {
self.window = [[UIWindow alloc] init];
}
self.window.rootViewController = self;
[self.window makeKeyAndVisible];
return true;
}
- (UISceneConfiguration *)application:(UIApplication *)application
configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession
options:(UISceneConnectionOptions *)options API_AVAILABLE(ios(13.0))
{
if (@available(iOS 13.0, *)) {
UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:@"Default Configuration"
sessionRole:connectingSceneSession.role];
configuration.delegateClass = [GBSceneDelegate class];
return configuration;
}
return nil;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
if (action == @selector(reset)) {
if (self.presentedViewController || ![GBROMManager sharedManager].currentROM) return false;
}
if (action == @selector(openStates) || action == @selector(changeModel) || action == @selector(openCheats) ||
action == @selector(toggleCheats) || action == @selector(disconnectCable) || action == @selector(connectPrinter)) {
if (![GBROMManager sharedManager].currentROM) return false;
}
return [super canPerformAction:action withSender:sender];
}
- (void)validateCommand:(UICommand *)command
{
if (command.action == @selector(toggleCheats)) {
command.state = GB_is_inited(&_gb) && GB_cheats_enabled(&_gb);
}
if (command.action == @selector(connectPrinter)) {
command.state = _printerConnected;
}
if (command.action == @selector(disconnectCable)) {
command.state = !_printerConnected;
}
[super validateCommand:command];
}
- (void)toggleCheats
{
GB_set_cheats_enabled(&_gb, !GB_cheats_enabled(&_gb));
}
- (void)orderFrontPreferencesPanel:(id)sender
{
[self openSettings];
}
- (void)open:(id)sender
{
[self openLibrary];
}
- (void)updateMirrorWindow
{
if ([UIScreen screens].count == 1) {
_mirrorWindow = nil;
_mirrorView = nil;
return;
}
if (_mirrorWindow && ![[UIScreen screens] containsObject:_mirrorWindow.screen]) {
_mirrorWindow = nil;
_mirrorView = nil;
}
for (UIScreen *screen in [UIScreen screens]) {
if (screen == UIScreen.mainScreen) continue;
CGRect rect = screen.bounds;
rect.size.height = floor(rect.size.height / 144) * 144;
rect.size.width = rect.size.height / 144 * 160;
rect.origin.x = (screen.bounds.size.width - rect.size.width) / 2;
rect.origin.y = (screen.bounds.size.height - rect.size.height) / 2;
_mirrorWindow = [[UIWindow alloc] initWithFrame:screen.bounds];
_mirrorWindow.screen = screen;
_mirrorView = [_gbView mirroredView];
_mirrorView.frame = rect;
_mirrorWindow.backgroundColor = [UIColor blackColor];
[_mirrorWindow addSubview:_mirrorView];
[_mirrorWindow setHidden:false];
break;
}
}
- (void)controllerDisconnected:(NSNotification *)notification
{
if (notification.object == _lastController && _backgroundView.fullScreenMode != GBControllerFocusForcedViaSplitView) {
_backgroundView.fullScreenMode = GBControllerFocusOff;
}
}
- (void)setControllerHandlers
{
for (GCController *controller in [GCController controllers]) {
__weak GCController *weakController = controller;
NSDictionary <NSNumber *, GCControllerElement *> *elements = GCControllerGetElements(controller);
if (!elements) continue; // Controller not supported
[elements enumerateKeysAndObjectsUsingBlock:^(NSNumber *usage, GCControllerElement *element, BOOL *stop) {
if ([element isKindOfClass:[GCControllerButtonInput class]]) {
[(GCControllerButtonInput *)element setValueChangedHandler:^(GCControllerButtonInput *button, float value, BOOL pressed) {
[self controller:weakController buttonChanged:button usage:usage.unsignedIntValue];
}];
}
else if ([element isKindOfClass:[GCControllerDirectionPad class]]) {
NSMutableSet *childrenUsages = [NSMutableSet set];
[elements enumerateKeysAndObjectsUsingBlock:^(NSNumber *childUsage, GCControllerElement *child, BOOL *stop) {
if (child.collection == element) {
[childrenUsages addObject:childUsage];
}
}];
[(GCControllerDirectionPad *)element setValueChangedHandler:^(GCControllerDirectionPad *dpad, float xValue, float yValue) {
[self controller:weakController axisChanged:dpad usage:usage.unsignedIntValue childrenUsages:childrenUsages];
}];
}
}];
if (controller.motion) {
[controller.motion setValueChangedHandler:^(GCMotion *motion) {
[self controller:weakController motionChanged:motion];
}];
}
}
}
- (void)updateLastController:(GCController *)controller
{
if (_lastController == controller) return;
_lastController = controller;
[GBHapticManager sharedManager].controller = controller;
}
- (void)controller:(GCController *)controller buttonChanged:(GCControllerButtonInput *)button usage:(GBControllerUsage)usage
{
[self updateLastController:controller];
if (_running && button.value > 0.25) {
if (_backgroundView.fullScreenMode != GBControllerFocusForcedViaSplitView) {
_backgroundView.fullScreenMode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBControllersHideInterface"];
}
}
GBButton gbButton = [GBSettingsViewController controller:controller convertUsageToButton:usage];
static const double analogThreshold = 0.0625;
switch (gbButton) {
case GBRight:
case GBLeft:
case GBUp:
case GBDown:
GB_set_use_faux_analog_inputs(&_gb, 0, false);
case GBA:
case GBB:
case GBSelect:
case GBStart:
GB_set_key_state(&_gb, (GB_key_t)gbButton, button.value > 0.25);
if (_runMode == GBRunModeRewind || _runMode == GBRunModePaused) {
self.runMode = GBRunModeNormal;
[_backgroundView fadeOverlayOut];
}
break;
case GBRapidA:
_rapidA = button.value > 0.25;
_rapidACount = 0;
break;
case GBRapidB:
_rapidB = button.value > 0.25;
_rapidBCount = 0;
break;
case GBTurbo:
if (button.value > analogThreshold) {
[self setRunMode:GBRunModeTurbo ignoreDynamicSpeed:!button.isAnalog];
if (button.isAnalog && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
GB_set_clock_multiplier(&_gb, (button.value - analogThreshold) / (1 - analogThreshold) * 3 + 1);
}
_runModeFromController = true;
[_backgroundView fadeOverlayOut];
}
else {
if (self.runMode == GBRunModeTurbo && _runModeFromController) {
[self setRunMode:GBRunModeNormal];
_runModeFromController = false;
}
}
break;
case GBRewind:
if (button.value > analogThreshold) {
[self setRunMode:GBRunModeRewind ignoreDynamicSpeed:!button.isAnalog];
if (button.isAnalog && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
GB_set_clock_multiplier(&_gb, (button.value - analogThreshold) / (1 - analogThreshold) * 4);
}
_runModeFromController = true;
[_backgroundView fadeOverlayOut];
}
else {
if ((self.runMode == GBRunModeRewind || self.runMode == GBRunModePaused) && _runModeFromController) {
[self setRunMode:GBRunModeNormal];
_runModeFromController = false;
}
}
break;
case GBUnderclock:
if (button.value > analogThreshold) {
[self setRunMode:GBRunModeUnderclock ignoreDynamicSpeed:!button.isAnalog];
if (button.isAnalog && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
GB_set_clock_multiplier(&_gb, 1 - ((button.value - analogThreshold) / (1 - analogThreshold) * 0.75));
}
_runModeFromController = true;
[_backgroundView fadeOverlayOut];
}
else {
if (self.runMode == GBRunModeUnderclock && _runModeFromController) {
[self setRunMode:GBRunModeNormal];
_runModeFromController = false;
}
}
break;
case GBSaveState1:
if (_romLoaded) {
[_backgroundView saveSwipeFromController:true];
}
break;
case GBLoadState1:
if (_romLoaded) {
[_backgroundView loadSwipeFromController:true];
}
break;
case GBReset:
if (_romLoaded) {
[self stop];
_skipAutoLoad = true;
GB_reset(&_gb);
[self start];
}
break;
case GBOpenMenu:
self.window.backgroundColor = nil;
[self presentViewController:[GBMenuViewController menu] animated:true completion:nil];
break;
default: break;
}
}
- (void)controller:(GCController *)controller axisChanged:(GCControllerDirectionPad *)axis usage:(GBControllerUsage)usage childrenUsages:(NSSet *)childrenUsages
{
[self updateLastController:controller];
bool left = axis.left.value > 0.5;
bool right = axis.right.value > 0.5;
bool up = axis.up.value > 0.5;
bool down = axis.down.value > 0.5;
bool hasUnmappedChild = false;
for (NSNumber *childUsage in childrenUsages) {
if ([GBSettingsViewController controller:controller convertUsageToButton:childUsage.unsignedIntValue] != GBUnusedButton) {
GCControllerButtonInput *child = (id)(GCControllerGetElements(controller)[childUsage]);
if ([child isKindOfClass:[GCControllerButtonInput class]] && child.pressed) {
left = right = up = down = false;
}
}
else {
hasUnmappedChild = true;
}
}
if (!hasUnmappedChild) return;
if (_running && (left || right || up || down) && _backgroundView.fullScreenMode != GBControllerFocusForcedViaSplitView) {
_backgroundView.fullScreenMode = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBControllersHideInterface"];
}
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBFauxAnalogInputs"]) {
GB_set_use_faux_analog_inputs(&_gb, 0, true);
GB_set_faux_analog_inputs(&_gb, 0, axis.right.value - axis.left.value, axis.down.value - axis.up.value);
}
GB_set_key_state(&_gb, GB_KEY_LEFT, left);
GB_set_key_state(&_gb, GB_KEY_RIGHT, right);
GB_set_key_state(&_gb, GB_KEY_UP, up);
GB_set_key_state(&_gb, GB_KEY_DOWN, down);
if (_runMode == GBRunModeRewind || _runMode == GBRunModePaused) {
self.runMode = GBRunModeNormal;
[_backgroundView fadeOverlayOut];
}
}
- (void)controller:(GCController *)controller motionChanged:(GCMotion *)motion
{
if (controller != _lastController) return;
GCAcceleration gravity = {0,};
GCAcceleration userAccel = {0,};
if (@available(iOS 14.0, *)) {
if (motion.hasGravityAndUserAcceleration) {
gravity = motion.gravity;
userAccel = motion.userAcceleration;
}
else {
gravity = motion.acceleration;
}
}
else {
gravity = motion.gravity;
userAccel = motion.userAcceleration;
}
GB_set_accelerometer_values(&_gb, -(gravity.x + userAccel.x), gravity.y + userAccel.y);
}
- (void)verifyEntitlements
{
/*
Make sure SameBoy is properly signed. If the bundle identifier the Info.plist file does not match the bundle
identifier in the application-identifier entitlement, iOS will not allow SameBoy to open files.
*/
typedef void *xpc_object_t;
void *libxpc = dlopen("/usr/lib/system/libxpc.dylib", RTLD_NOW);
extern xpc_object_t xpc_copy_entitlements_for_self$(void);
extern const char *xpc_dictionary_get_string$(xpc_object_t *object, const char *key);
typeof(xpc_copy_entitlements_for_self$) *xpc_copy_entitlements_for_self = dlsym(libxpc, "xpc_copy_entitlements_for_self");
typeof(xpc_dictionary_get_string$) *xpc_dictionary_get_string = dlsym(libxpc, "xpc_dictionary_get_string");
if (!xpc_copy_entitlements_for_self || !xpc_dictionary_get_string) return;
xpc_object_t entitlements = xpc_copy_entitlements_for_self();
if (!entitlements) return;
const char *_entIdentifier = xpc_dictionary_get_string(entitlements, "application-identifier");
NSString *entIdentifier = _entIdentifier? @(_entIdentifier) : nil;
const char *_teamIdentifier = xpc_dictionary_get_string(entitlements, "com.apple.developer.team-identifier");
NSString *teamIdentifier = _teamIdentifier? @(_teamIdentifier) : nil;
if (!entIdentifier) { // No identifier. Installed using a jailbreak, we're fine.
return;
}
if (teamIdentifier && [entIdentifier hasPrefix:[teamIdentifier stringByAppendingString:@"."]]) {
entIdentifier = [entIdentifier substringFromIndex:teamIdentifier.length + 1];
}
NSString *plistIdentifier = [NSBundle mainBundle].bundleIdentifier;
if (![entIdentifier isEqual:plistIdentifier]) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"SameBoy is not properly signed and might not be able to open ROMs"
message:[NSString stringWithFormat:@"The bundle identifier in the Info.plist file (“%@”) does not match the one in the entitlements (“%@”)", plistIdentifier, entIdentifier]
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"Close"
style:UIAlertActionStyleCancel
handler:nil]];
[self presentViewController:alert animated:true completion:nil];
}
}
- (void)saveStateToFile:(NSString *)file
{
NSString *tempPath = [file stringByAppendingPathExtension:@"tmp"];
int error = GB_save_state(&_gb, tempPath.UTF8String);
if (!error) {
rename(tempPath.UTF8String, file.UTF8String);
NSData *data = [NSData dataWithBytes:_gbView.previousBuffer
length:GB_get_screen_width(&_gb) *
GB_get_screen_height(&_gb) *
sizeof(*_gbView.previousBuffer)];
UIImage *screenshot = [self imageFromData:data width:GB_get_screen_width(&_gb) height:GB_get_screen_height(&_gb)];
[UIImagePNGRepresentation(screenshot) writeToFile:[file stringByAppendingPathExtension:@"png"] atomically:false];
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Could not Save State"
message:[NSString stringWithFormat:@"An error occured while attempting to save: %s", strerror(error)]
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"Close"
style:UIAlertActionStyleCancel
handler:nil]];
UIViewController *top = self;
while (top.presentedViewController) {
top = top.presentedViewController;
}
[top presentViewController:alert animated:true completion:nil];
});
}
}
- (bool)loadStateFromFile:(NSString *)file
{
_skipAutoLoad = true;
GB_model_t model;
if (!GB_get_state_model(file.fileSystemRepresentation, &model)) {
if (GB_get_model(&_gb) != model) {
GB_switch_model_and_reset(&_gb, model);
}
return GB_load_state(&_gb, file.fileSystemRepresentation) == 0;
}
return false;
}
- (void)loadROM
{
GBROMManager *romManager = [GBROMManager sharedManager];
if (romManager.romFile) {
if (!_skipAutoLoad) {
// Todo: display errors and warnings
bool needsStateLoad = false;
if (![_lastSavedROM isEqual:[GBROMManager sharedManager].currentROM]) {
if ([romManager.romFile.pathExtension.lowercaseString isEqualToString:@"isx"]) {
_romLoaded = GB_load_isx(&_gb, romManager.romFile.fileSystemRepresentation) == 0;
}
else {
_romLoaded = GB_load_rom(&_gb, romManager.romFile.fileSystemRepresentation) == 0;
}
needsStateLoad = true;
if (@available(iOS 16.0, *)) {
dispatch_async(dispatch_get_main_queue(), ^{
[super setNeedsUpdateOfSupportedInterfaceOrientations];
});
}
}
else if (access(romManager.romFile.fileSystemRepresentation, R_OK)) {
_romLoaded = false;
}
if (!_romLoaded) {
dispatch_async(dispatch_get_main_queue(), ^{
romManager.currentROM = nil;
});
}
if (!needsStateLoad) {
NSDate *date = nil;
[[NSURL fileURLWithPath:[GBROMManager sharedManager].autosaveStateFile] getResourceValue:&date
forKey:NSURLContentModificationDateKey
error:nil];
if (![_saveDate isEqual:date]) {
needsStateLoad = true;
}
}
if (_romLoaded && needsStateLoad) {
GB_reset(&_gb);
GB_load_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation);
GB_remove_all_cheats(&_gb);
GB_load_cheats(&_gb, [GBROMManager sharedManager].cheatsFile.UTF8String, false);
if (![self loadStateFromFile:[GBROMManager sharedManager].autosaveStateFile]) {
if ([_lastSavedROM isEqual:[GBROMManager sharedManager].currentROM]) {
/* Something weird just happened: we didn't change a ROM, but we failed to load the
latest save state. Save over the existing file, it's probably corrupt in some
way. */
[self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile];
}
else {
// Newly played ROM, pick the best model
uint8_t *rom = GB_get_direct_access(&_gb, GB_DIRECT_ACCESS_ROM, NULL, NULL);
if ((rom[0x143] & 0x80)) {
if (!GB_is_cgb(&_gb)) {
GB_switch_model_and_reset(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBCGBModel"]);
}
}
else if ((rom[0x146] == 3) && !GB_is_sgb(&_gb)) {
GB_switch_model_and_reset(&_gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBSGBModel"]);
}
}
}
GB_rewind_reset(&_gb);
}
}
}
else {
_romLoaded = false;
}
dispatch_async(dispatch_get_main_queue(), ^{
_gbView.hidden = !_romLoaded;
});
_swappingROM = false;
_skipAutoLoad = false;
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self setupWindow];
});
if (_lastHingeStatus != _hinge.status) {
[self recreateLayoutsWithThemeNamed:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBInterfaceTheme"]];
[[NSNotificationCenter defaultCenter] postNotificationName:@"HingeStatusChanged" object:nil];
[self didRotateFromInterfaceOrientation:UIInterfaceOrientationUnknown];
}
[self start];
}
-(void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion
{
[self stop];
[super presentViewController:viewControllerToPresent
animated:flag
completion:completion];
}
- (void)reset
{
UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad?
UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet;
UIAlertController *menu = [UIAlertController alertControllerWithTitle:@"Reset Emulation?"
message:@"Unsaved progress will be lost."
preferredStyle:style];
[menu addAction:[UIAlertAction actionWithTitle:@"Reset"
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *action) {
[self stop];
_skipAutoLoad = true;
GB_reset(&_gb);
[self start];
}]];
[menu addAction:[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:nil]];
[self presentViewController:menu animated:true completion:nil];
}
- (void)openLibrary
{
static __weak UIViewController *presentedController;
if (presentedController && self.presentedViewController == presentedController) return;
if (![self dismissViewControllerIfSafe]) return;
UIViewController *controller = [[GBLibraryViewController alloc] init];
presentedController = controller;
[self presentViewController:controller
animated:true
completion:nil];
}
- (void)changeModel
{
static __weak UIViewController *presentedController;
if (presentedController && self.presentedViewController == presentedController) return;
if (![self dismissViewControllerIfSafe]) return;
GBOptionViewController *controller = [[GBOptionViewController alloc] initWithHeader:@"Select a model to emulate"];
controller.footer = @"Changing the emulated model will reset the emulator";
presentedController = controller;
GB_model_t currentModel = GB_get_model(&_gb);
struct {
NSString *title;
NSString *settingKey;
bool checked;
} items[] = {
{@"Game Boy", @"GBDMGModel", currentModel < GB_MODEL_SGB},
{@"Game Boy Pocket/Light", nil, currentModel == GB_MODEL_MGB},
{@"Super Game Boy", @"GBSGBModel", GB_is_sgb(&_gb)},
{@"Game Boy Color", @"GBCGBModel", GB_is_cgb(&_gb) && currentModel <= GB_MODEL_CGB_E},
{@"Game Boy Advance", @"GBAGBModel", currentModel > GB_MODEL_CGB_E},
};
for (unsigned i = 0; i < sizeof(items) / sizeof(items[0]); i++) {
GB_model_t model = GB_MODEL_MGB;
if (items[i].settingKey) {
model = [[NSUserDefaults standardUserDefaults] integerForKey:items[i].settingKey];
}
[controller addOption:items[i].title withCheckmark:items[i].checked action:^{
[self stop];
_skipAutoLoad = true;
GB_switch_model_and_reset(&_gb, model);
if (model > GB_MODEL_CGB_E && ![[NSUserDefaults standardUserDefaults] boolForKey:@"GBShownGBAWarning"]) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"SameBoy is not a Game Boy Advance Emulator"
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."
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"Close"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action) {
[self start];
[[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBShownGBAWarning"];
}]];
[self presentViewController:alert animated:true completion:nil];
}
else {
[self start];
}
}];
}
controller.title = @"Change Model";
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:controller];
UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
style:UIBarButtonItemStylePlain
target:self
action:@selector(dismissViewController)];
[navController.visibleViewController.navigationItem setLeftBarButtonItem:close];
[self presentViewController:navController animated:true completion:nil];
}
- (void)openStates
{
static __weak UIViewController *presentedController;
if (presentedController && self.presentedViewController == presentedController) return;
if (![self dismissViewControllerIfSafe]) return;
UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBStatesViewController alloc] init]];
presentedController = controller;
UIVisualEffect *effect = [UIBlurEffect effectWithStyle:(UIBlurEffectStyle)UIBlurEffectStyleProminent];
UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
effectView.frame = controller.view.bounds;
effectView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
[controller.view insertSubview:effectView atIndex:0];
UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
style:UIBarButtonItemStylePlain
target:self
action:@selector(dismissViewController)];
[controller.visibleViewController.navigationItem setLeftBarButtonItem:close];
[self presentViewController:controller
animated:true
completion:nil];
}
- (void)openSettings
{
static __weak UIViewController *presentedController;
if (presentedController && self.presentedViewController == presentedController) return;
if (![self dismissViewControllerIfSafe]) return;
UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
style:UIBarButtonItemStylePlain
target:self
action:@selector(dismissViewController)];
UIViewController *controller = [GBSettingsViewController settingsViewControllerWithLeftButton:close];
presentedController = controller;
[self presentViewController:controller
animated:true
completion:nil];
}
- (void)showAbout
{
static __weak UIViewController *presentedController;
if (presentedController && self.presentedViewController == presentedController) return;
if (![self dismissViewControllerIfSafe]) return;
UIViewController *controller = [[GBAboutController alloc] init];
presentedController = controller;
[self presentViewController:controller animated:true completion:nil];
}
- (void)openCheats
{
static __weak UIViewController *presentedController;
if (presentedController && self.presentedViewController == presentedController) return;
if (![self dismissViewControllerIfSafe]) return;
UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBCheatsController alloc] initWithGameBoy:&_gb]];
presentedController = controller;
UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
style:UIBarButtonItemStylePlain
target:self
action:@selector(dismissViewController)];
[controller.visibleViewController.navigationItem setLeftBarButtonItem:close];
[self presentViewController:controller
animated:true
completion:nil];
}
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
{
[super dismissViewControllerAnimated:flag completion:^() {
if (@available(iOS 27.1, *)) {
if (_lastHingeStatus == UIHingeStatusClosed) {
[self setNeedsStatusBarAppearanceUpdate];
}
}
if (completion) {
completion();
}
dispatch_async(dispatch_get_main_queue(), ^{
[self start];
});
}];
}
- (void)setNeedsUpdateOfSupportedInterfaceOrientations
{
/* Hack. Some view controllers dismiss without calling the method above. */
dispatch_async(dispatch_get_main_queue(), ^{
[self start];
[super setNeedsUpdateOfSupportedInterfaceOrientations];
});
}
- (void)dismissViewController
{
[self dismissViewControllerAnimated:true completion:nil];
}
- (bool)dismissViewControllerIfSafe
{
if (!self.presentedViewController) return true;
if (![self.presentedViewController isKindOfClass:[UIAlertController class]]) {
[self dismissViewController];
return true;
}
if ([self.presentedViewController isKindOfClass:[GBMenuViewController class]]) {
[self dismissViewController];
return true;
}
return false;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
[self stop];
}
- (GBLayout *)layoutForOrientation:(UIInterfaceOrientation)orientation
{
switch (orientation) {
default:
case UIInterfaceOrientationUnknown:
case UIInterfaceOrientationPortrait:
case UIInterfaceOrientationPortraitUpsideDown:
return _verticalLayout;
case UIInterfaceOrientationLandscapeRight:
return _horizontalLayoutLeft;
break;
case UIInterfaceOrientationLandscapeLeft:
return _horizontalLayoutRight;
}
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration
{
if (_orientation != UIInterfaceOrientationUnknown && !((1 << orientation) & self.supportedInterfaceOrientations)) return;
GBLayout *layout = nil;
_orientation = orientation;
layout = [self layoutForOrientation:orientation];
_backgroundView.frame = [layout viewRectForOrientation:orientation];
_backgroundView.layout = layout;
if (!self.presentedViewController) {
_window.backgroundColor = _backgroundView.fullScreenMode? [UIColor blackColor] :
layout.theme.backgroundGradientBottom;
}
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
UIEdgeInsets insets = self.window.safeAreaInsets;
bool landscape = true;
if (_orientation == UIInterfaceOrientationPortrait || _orientation == UIInterfaceOrientationPortraitUpsideDown) {
landscape = false;
}
if (@available(iOS 27.1, *)) {
if (_lastHingeStatus == UIHingeStatusClosed) {
insets.left = insets.right = MIN(insets.left, insets.right);
landscape = false;
if (_orientation == UIInterfaceOrientationLandscapeLeft) {
insets.bottom += 24;
}
}
}
_cameraPositionButton.frame = CGRectMake(insets.left + 8,
_backgroundView.bounds.size.height - 8 - insets.bottom - 32,
32,
32);
if (_changeCameraButton) {
_changeCameraButton.frame = CGRectMake(insets.left + 8 + (landscape? (32 + 8) : 0 ),
_backgroundView.bounds.size.height - 8 - insets.bottom - 32 - (landscape? 0 : (32 + 8)),
32,
32);
}
_printerButton.frame = CGRectMake(_backgroundView.bounds.size.width - 8 - insets.right - 32,
_backgroundView.bounds.size.height - 8 - insets.bottom - 32,
32,
32);
_printerSpinner.frame = CGRectMake(_backgroundView.bounds.size.width - 8 - insets.right - 32 - (landscape? (32 + 4) : 0),
_backgroundView.bounds.size.height - 8 - insets.bottom - 32 - (landscape? 0 : (32 + 4)),
32,
32);
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
if (!self.shouldAutorotate && _orientation != UIInterfaceOrientationUnknown) {
return 1 << _orientation;
}
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
return UIInterfaceOrientationMaskAll;
}
if (@available(iOS 27.1, *)) {
if (_lastHingeStatus >= UIHingeStatusPartiallyOpen) {
return UIInterfaceOrientationMaskAll;
}
}
if (MAX([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width) <= 568) {
return UIInterfaceOrientationMaskLandscape;
}
return UIInterfaceOrientationMaskAllButUpsideDown;
}
- (BOOL)shouldAutorotate
{
if (_running && GB_has_accelerometer(&_gb)) {
return false;
}
return true;
}
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures
{
return UIRectEdgeTop;
}
- (BOOL)prefersStatusBarHidden
{
if (@available(iOS 27.1, *)) {
switch (_orientation) {
case UIInterfaceOrientationLandscapeRight:
case UIInterfaceOrientationLandscapeLeft:
return _lastHingeStatus == UIHingeStatusPartiallyOpen;
default:
return _lastHingeStatus >= UIHingeStatusPartiallyOpen;
}
}
switch (_orientation) {
case UIInterfaceOrientationLandscapeRight:
case UIInterfaceOrientationLandscapeLeft:
return true;
default:
return false;
}
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
/* Working around a UIKit bug */
if (@available(iOS 27.1, *)) {
if (_lastHingeStatus == UIHingeStatusClosed && self.presentedViewController &&
![self.presentedViewController isKindOfClass:[UIAlertController class]]) {
return self.presentedViewController.traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark? UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent;
}
}
if (@available(iOS 13.0, *)) {
return (_backgroundView.layout.isDark || _backgroundView.fullScreenMode)? UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent;
}
return (_backgroundView.layout.isDark || _backgroundView.fullScreenMode)? UIStatusBarStyleLightContent : UIStatusBarStyleDefault;
}
- (void)preRun
{
dispatch_async(dispatch_get_main_queue(), ^{
UIApplication.sharedApplication.idleTimerDisabled = true;
});
GB_set_pixels_output(&_gb, _gbView.pixels);
GB_set_sample_rate(&_gb, 96000);
if (![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBAudioMode"] isEqual:@"off"]) {
_audioClient = [[GBAudioClient alloc] initWithRendererBlock:^(UInt32 sampleRate, UInt32 nFrames, GB_sample_t *buffer) {
[_audioLock lock];
if (_audioBufferPosition < nFrames) {
_audioBufferNeeded = nFrames;
[_audioLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:(double)(_audioBufferNeeded - _audioBufferPosition) / sampleRate]];
_audioBufferNeeded = 0;
}
if (_stopping) {
memset(buffer, 0, nFrames * sizeof(*buffer));
[_audioLock unlock];
return;
}
if (_audioBufferPosition < nFrames) {
// Not enough audio
memset(buffer, 0, (nFrames - _audioBufferPosition) * sizeof(*buffer));
memcpy(buffer, _audioBuffer, _audioBufferPosition * sizeof(*buffer));
// Do not reset the audio position to avoid more underflows
}
else if (_audioBufferPosition < nFrames + 4800) {
memcpy(buffer, _audioBuffer, nFrames * sizeof(*buffer));
memmove(_audioBuffer, _audioBuffer + nFrames, (_audioBufferPosition - nFrames) * sizeof(*buffer));
_audioBufferPosition = _audioBufferPosition - nFrames;
}
else {
memcpy(buffer, _audioBuffer + (_audioBufferPosition - nFrames), nFrames * sizeof(*buffer));
_audioBufferPosition = 0;
}
[_audioLock unlock];
} andSampleRate:96000];
}
[_audioClient start];
if (GB_has_accelerometer(&_gb)) {
if (@available(iOS 14.0, *)) {
for (GCController *controller in [GCController controllers]) {
if (controller.motion.sensorsRequireManualActivation) {
[controller.motion setSensorsActive:true];
}
}
}
[_motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue]
withHandler:^(CMAccelerometerData *accelerometerData, NSError *error) {
if (_lastController.motion) return;
CMAcceleration data = accelerometerData.acceleration;
UIInterfaceOrientation orientation = _orientation;
switch (orientation) {
case UIInterfaceOrientationUnknown:
case UIInterfaceOrientationPortrait:
break;
case UIInterfaceOrientationPortraitUpsideDown:
data.x = -data.x;
data.y = -data.y;
break;
case UIInterfaceOrientationLandscapeLeft: {
double tempX = data.x;
data.x = data.y;
data.y = -tempX;
break;
}
case UIInterfaceOrientationLandscapeRight:{
double tempX = data.x;
data.x = -data.y;
data.y = tempX;
break;
}
}
GB_set_accelerometer_values(&_gb, -data.x, data.y);
}];
}
/* Clear pending alarms, don't play alarms while playing */
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center removeDeliveredNotificationsWithIdentifiers:@[[GBROMManager sharedManager].romFile]];
[center removePendingNotificationRequestsWithIdentifiers:@[[GBROMManager sharedManager].romFile]];
}
if (GB_rom_supports_alarms(&_gb)) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert completionHandler:nil];
}
}
- (NSString *)formatDate:(NSDate *)date
{
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDate *now = [NSDate date];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.locale = [NSLocale currentLocale];
formatter.timeZone = [NSTimeZone localTimeZone];
formatter.dateStyle = NSDateFormatterNoStyle;
formatter.timeStyle = NSDateFormatterMediumStyle;
NSString *time = [formatter stringFromDate:date];
if ([calendar isDate:date inSameDayAsDate:now]) {
return time;
}
if ([calendar component:NSCalendarUnitYear fromDate:date] == [calendar component:NSCalendarUnitYear fromDate:now]) {
formatter.dateFormat = [NSDateFormatter dateFormatFromTemplate:@"MMM d"
options:0
locale:formatter.locale];
return [[formatter stringFromDate:date] stringByAppendingFormat:@", %@", time];
}
formatter.dateFormat = [NSDateFormatter dateFormatFromTemplate:@"MMM d yyyy"
options:0
locale:formatter.locale];
return [[formatter stringFromDate:date] stringByAppendingFormat:@", %@", time];
}
- (NSData *)batteryHash
{
NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:[GBROMManager sharedManager].batterySaveFile];
if (!file) return nil;
CC_SHA256_CTX ctx;
CC_SHA256_Init(&ctx);
while (true) {
@autoreleasepool {
NSData *chunk = [file readDataOfLength:0x4000];
if (chunk.length == 0) break;
CC_SHA256_Update(&ctx, chunk.bytes, (CC_LONG)chunk.length);
}
}
[file closeFile];
unsigned char digest[CC_SHA256_DIGEST_LENGTH];
CC_SHA256_Final(digest, &ctx);
return [NSData dataWithBytes:digest length:CC_SHA256_DIGEST_LENGTH];
}
- (bool)areSavesInSync
{
if (!GBROMManager.sharedManager.currentROM) return true; // No ROM
NSString *saveState = [[GBROMManager sharedManager] autosaveStateFile];
NSString *batterySave = [[GBROMManager sharedManager] batterySaveFile];
// There can't be a mismatch if one of the files is missing
if (access(saveState.UTF8String, F_OK) || access(batterySave.UTF8String, F_OK)) return true;
uint8_t savedHash[CC_SHA256_DIGEST_LENGTH];
if (getxattr(saveState.UTF8String, "battery-hash", &savedHash, sizeof(savedHash), 0, 0) == sizeof(savedHash)) {
NSData *computedHash = [self batteryHash];
if (computedHash && memcmp(savedHash, computedHash.bytes, sizeof(savedHash)) == 0) {
return true; // Battery didn't change
}
}
GB_gameboy_t *gb = NULL;
GB_model_t model;
if (GB_get_state_model(saveState.UTF8String, &model)) {
return true; // Not a valid state, ignore
}
gb = GB_init(GB_alloc(), model);
NSString *romFile = [[GBROMManager sharedManager] romFile];
if ([romFile.pathExtension.lowercaseString isEqualToString:@"isx"]) {
if (GB_load_isx(gb, romFile.fileSystemRepresentation)) {
// Can't load ROM
GB_dealloc(gb);
return true;
}
}
else {
if (GB_load_rom(gb, romFile.fileSystemRepresentation)) {
// Can't load ROM
GB_dealloc(gb);
return true;
}
}
if (GB_load_state(gb, saveState.UTF8String)) {
// Not a valid state again, ignore
GB_dealloc(gb);
return true;
}
size_t ramSize = 0;
const uint8_t *ram = GB_get_direct_access(gb, GB_DIRECT_ACCESS_CART_RAM, &ramSize, NULL);
uint8_t *ramCopy = malloc(ramSize);
memcpy(ramCopy, ram, ramSize);
GB_load_battery(gb, batterySave.UTF8String);
ram = GB_get_direct_access(gb, GB_DIRECT_ACCESS_CART_RAM, &ramSize, NULL);
bool matching = memcmp(ram, ramCopy, ramSize) == 0;
free(ramCopy);
GB_dealloc(gb);
return matching;
}
- (bool)verifySavesInSync
{
if ([self areSavesInSync]) return true; // Saves are in sync
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
__block bool ret = false;
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Save Conflict Detected"
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]
preferredStyle:UIAlertControllerStyleAlert];
NSString *saveState = [[GBROMManager sharedManager] autosaveStateFile];
NSString *batterySave = [[GBROMManager sharedManager] batterySaveFile];
NSDate *date;
[[NSURL fileURLWithPath:saveState] getResourceValue:&date
forKey:NSURLContentModificationDateKey
error:nil];
NSString *stateDateString = [self formatDate:date];
UIAlertAction *stateAction = [UIAlertAction actionWithTitle:@"Save State\n"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
unlink(batterySave.UTF8String);
ret = true;
dispatch_semaphore_signal(sem);
}];
[[NSURL fileURLWithPath:batterySave] getResourceValue:&date
forKey:NSURLContentModificationDateKey
error:nil];
NSString *batteryDateString = [self formatDate:date];
UIAlertAction *batteryAction = [UIAlertAction actionWithTitle:@"Battery Save\n"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
unlink(saveState.UTF8String);
ret = true;
dispatch_semaphore_signal(sem);
}];
[alert addAction:stateAction];
[alert addAction:batteryAction];
[alert addAction:[UIAlertAction actionWithTitle:@"Close ROM"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action) {
ret = false;
dispatch_semaphore_signal(sem);
}]];
// hack
_running = false;
[self presentViewController:alert animated:true completion:nil];
dispatch_async(dispatch_get_main_queue(), ^{
NSMutableSet<UIView *> *views = [NSMutableSet setWithObject:alert.view];
UIColor *secondaryLabelColor;
if (@available(iOS 13.0, *)) {
secondaryLabelColor = [UIColor secondaryLabelColor];
}
else {
secondaryLabelColor = [UIColor systemGrayColor];
}
UILabel *view;
while ((view = (UILabel *)views.anyObject)) {
[views removeObject:view];
[views addObjectsFromArray:view.subviews];
if ([view isKindOfClass:[UILabel class]]) {
if ([view.text isEqualToString:stateAction.title]) {
NSMutableAttributedString *text = [view.attributedText mutableCopy];
[text appendAttributedString:[[NSAttributedString alloc] initWithString:stateDateString
attributes:@{
NSFontAttributeName: [UIFont systemFontOfSize:UIFont.smallSystemFontSize],
NSForegroundColorAttributeName: secondaryLabelColor,
}]];
view.attributedText = text;
view.numberOfLines = 2;
view.locksFonts = true;
}
if ([view.text isEqualToString:batteryAction.title]) {
NSMutableAttributedString *text = [view.attributedText mutableCopy];
[text appendAttributedString:[[NSAttributedString alloc] initWithString:batteryDateString
attributes:@{
NSFontAttributeName: [UIFont systemFontOfSize:UIFont.smallSystemFontSize],
NSForegroundColorAttributeName: secondaryLabelColor,
}]];
view.attributedText = text;
view.numberOfLines = 2;
view.locksFonts = true;
}
}
}
});
});
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
_running = true;
return ret;
}
- (void)run
{
if (![self verifySavesInSync]) {
_romLoaded = false;
_running = false;
_stopping = false;
GBROMManager.sharedManager.currentROM = nil;
return;
}
[self loadROM];
if (!_romLoaded) {
_running = false;
_stopping = false;
return;
}
[self preRun];
const unsigned autosaveFrequency = 60 * 60;
_autosaveCountdown = autosaveFrequency;
while (_running) {
if (_rewind) {
_rewind = false;
GB_rewind_pop(&_gb);
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
if (!GB_rewind_pop(&_gb)) {
dispatch_sync(dispatch_get_main_queue(), ^{
if (_runMode == GBRunModeRewind) {
self.runMode = GBRunModePaused;
}
});
_rewindOver = true;
}
}
else {
for (unsigned i = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindSpeed"]; i--;) {
if (!GB_rewind_pop(&_gb)) {
dispatch_sync(dispatch_get_main_queue(), ^{
if (_runMode == GBRunModeRewind) {
self.runMode = GBRunModePaused;
}
});
_rewindOver = true;
}
}
}
}
if (_runMode != GBRunModePaused) {
GB_run(&_gb);
if (!_autosaveCountdown) {
_autosaveCountdown = autosaveFrequency;
[self performAutosave];
}
}
}
[self postRun];
_stopping = false;
}
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
didReceiveNotificationResponse:(UNNotificationResponse *)response
withCompletionHandler:(void (^)(void))completionHandler
{
if (![response.notification.request.identifier isEqual:[GBROMManager sharedManager].currentROM]) {
[self application:[UIApplication sharedApplication]
openURL:[NSURL fileURLWithPath:response.notification.request.identifier]
options:@{}];
}
completionHandler();
}
- (UIImage *)imageFromData:(NSData *)data width:(unsigned)width height:(unsigned)height
{
/* Convert the screenshot to a CGImageRef */
CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)data);
CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
CGImageRef iref = CGImageCreate(width,
height,
8,
32,
4 * width,
colorSpaceRef,
bitmapInfo,
provider,
NULL,
true,
renderingIntent);
UIImage *ret = [[UIImage alloc] initWithCGImage:iref];
CGColorSpaceRelease(colorSpaceRef);
CGDataProviderRelease(provider);
CGImageRelease(iref);
return ret;
}
- (void)performAutosave
{
GB_save_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation);
[self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile];
// Assoicate the battery save with the save state via a hash xattr
NSData *batteryHash = [self batteryHash];
if (batteryHash) {
setxattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash",
batteryHash.bytes, batteryHash.length, 0, 0);
}
else {
removexattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash", 0);
}
}
- (void)postRun
{
[_audioLock lock];
_audioBufferPosition = _audioBufferNeeded = 0;
[_audioLock signal];
[_audioLock unlock];
[_audioClient stop];
_audioClient = nil;
if (!_swappingROM) {
[self performAutosave];
// Assoicate the battery save with the save state via a hash xattr
NSData *batteryHash = [self batteryHash];
if (batteryHash) {
setxattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash",
batteryHash.bytes, batteryHash.length, 0, 0);
}
else {
removexattr([GBROMManager sharedManager].autosaveStateFile.UTF8String, "battery-hash", 0);
}
NSDate *date;
[[NSURL fileURLWithPath:[GBROMManager sharedManager].autosaveStateFile] getResourceValue:&date
forKey:NSURLContentModificationDateKey
error:nil];
_saveDate = date;
_lastSavedROM = [GBROMManager sharedManager].currentROM;
}
[[GBHapticManager sharedManager] setRumbleStrength:0];
if (@available(iOS 14.0, *)) {
for (GCController *controller in [GCController controllers]) {
if (controller.motion.sensorsRequireManualActivation) {
[controller.motion setSensorsActive:false];
}
}
}
[_motionManager stopAccelerometerUpdates];
unsigned timeToAlarm = GB_time_to_alarm(&_gb);
if (timeToAlarm) {
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
UNMutableNotificationContent *notificationContent = [[UNMutableNotificationContent alloc] init];
NSString *friendlyName = [[[GBROMManager sharedManager].romFile lastPathComponent] stringByDeletingPathExtension];
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"\\([^)]+\\)|\\[[^\\]]+\\]" options:0 error:nil];
friendlyName = [regex stringByReplacingMatchesInString:friendlyName options:0 range:NSMakeRange(0, [friendlyName length]) withTemplate:@""];
friendlyName = [friendlyName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
notificationContent.title = [NSString stringWithFormat:@"%@ Played an Alarm", friendlyName];
notificationContent.body = [NSString stringWithFormat:@"%@ requested your attention by playing a scheduled alarm", friendlyName];
notificationContent.sound = UNNotificationSound.defaultSound;
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:[GBROMManager sharedManager].romFile
content:notificationContent
trigger:[UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timeToAlarm repeats:false]];
[center addNotificationRequest:request withCompletionHandler:nil];
[[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBNotificationsUsed"];
}
dispatch_async(dispatch_get_main_queue(), ^{
UIApplication.sharedApplication.idleTimerDisabled = false;
});
}
- (void)start
{
if (_running) return;
if (self.presentedViewController) return;
_running = true;
dispatch_async(_runQueue, ^{
[self run];
});
}
- (void)stop
{
if (!_running) return;
[_audioLock lock];
_stopping = true;
[_audioLock signal];
[_audioLock unlock];
_running = false;
while (_stopping) {
[_audioLock lock];
[_audioLock signal];
[_audioLock unlock];
}
dispatch_sync(_runQueue, ^{});
dispatch_async(dispatch_get_main_queue(), ^{
self.runMode = GBRunModeNormal;
[_backgroundView fadeOverlayOut];
});
}
- (NSString *)bootROMPathForName:(NSString *)name
{
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBCustomBootROMs"]) {
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0];
path = [path stringByAppendingPathComponent:@"Boot ROMs"];
path = [path stringByAppendingPathComponent:name];
path = [path stringByAppendingPathExtension:@"bin"];
if ([[NSFileManager defaultManager] fileExistsAtPath:path]) {
return path;
}
}
return [[NSBundle mainBundle] pathForResource:name ofType:@"bin"];
}
- (void)loadBootROM: (GB_boot_rom_t)type
{
static NSString *const names[] = {
[GB_BOOT_ROM_DMG_0] = @"dmg0_boot",
[GB_BOOT_ROM_DMG] = @"dmg_boot",
[GB_BOOT_ROM_MGB] = @"mgb_boot",
[GB_BOOT_ROM_SGB] = @"sgb_boot",
[GB_BOOT_ROM_SGB2] = @"sgb2_boot",
[GB_BOOT_ROM_CGB_0] = @"cgb0_boot",
[GB_BOOT_ROM_CGB] = @"cgb_boot",
[GB_BOOT_ROM_CGB_E] = @"cgbE_boot",
[GB_BOOT_ROM_AGB_0] = @"agb0_boot",
[GB_BOOT_ROM_AGB] = @"agb_boot",
};
NSString *name = names[type];
NSString *path = [self bootROMPathForName:name];
/* These boot types are not commonly available, and they are indentical
from an emulator perspective, so fall back to the more common variants
if they can't be found. */
if (!path && type == GB_BOOT_ROM_CGB_E) {
[self loadBootROM:GB_BOOT_ROM_CGB];
return;
}
if (!path && type == GB_BOOT_ROM_AGB_0) {
[self loadBootROM:GB_BOOT_ROM_AGB];
return;
}
GB_load_boot_rom(&_gb, [path UTF8String]);
}
- (void)vblankWithType:(GB_vblank_type_t)type
{
if (type != GB_VBLANK_TYPE_REPEAT) {
[_gbView flip];
GB_set_pixels_output(&_gb, _gbView.pixels);
}
if (_rapidA) {
_rapidACount++;
GB_set_key_state(&_gb, GB_KEY_A, !(_rapidACount & 2));
}
if (_rapidB) {
_rapidBCount++;
GB_set_key_state(&_gb, GB_KEY_B, !(_rapidBCount & 2));
}
_autosaveCountdown--;
if (_rapidA || _rapidB) {
if (_runMode == GBRunModeRewind || _runMode == GBRunModePaused) {
self.runMode = GBRunModeNormal;
[_backgroundView fadeOverlayOut];
}
}
_rewind = _runMode == GBRunModeRewind;
}
- (void)gotNewSample:(GB_sample_t *)sample
{
[_audioLock lock];
if (_audioClient.isPlaying) {
if (_audioBufferPosition == _audioBufferSize) {
if (_audioBufferSize >= 0x4000) {
_audioBufferPosition = 0;
[_audioLock unlock];
return;
}
if (_audioBufferSize == 0) {
_audioBufferSize = 512;
}
else {
_audioBufferSize += _audioBufferSize >> 2;
}
_audioBuffer = realloc(_audioBuffer, sizeof(*sample) * _audioBufferSize);
}
_audioBuffer[_audioBufferPosition++] = *sample;
}
if (_audioBufferPosition == _audioBufferNeeded) {
[_audioLock signal];
_audioBufferNeeded = 0;
}
[_audioLock unlock];
}
- (void)rumbleChanged:(double)amp
{
[[GBHapticManager sharedManager] setRumbleStrength:amp];
}
- (void)updatePalette
{
memcpy(&_palette,
[GBPalettePicker paletteForTheme:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"]],
sizeof(_palette));
GB_set_palette(&_gb, &_palette);
}
- (bool)handleOpenURLs:(NSArray <NSURL *> *)urls
openInPlace:(bool)inPlace
{
NSMutableArray<NSURL *> *validURLs = [NSMutableArray array];
NSMutableArray<NSString *> *skippedBasenames = [NSMutableArray array];
NSMutableArray<NSString *> *unusedZips = [NSMutableArray array];
NSString *tempDir = NSTemporaryDirectory();
__block unsigned tempIndex = 0;
for (NSURL *url in urls) {
if ([url.pathExtension.lowercaseString isEqualToString:@"zip"]) {
GBZipReader *reader = [[GBZipReader alloc] initWithPath:url.path];
if (!reader) {
[skippedBasenames addObject:url.lastPathComponent];
continue;
}
__block bool used = false;
[reader iterateFiles:^bool(NSString *filename, size_t uncompressedSize, bool (^getData)(void *), bool (^writeToPath)(NSString *)) {
if ([@[@"gb", @"gbc", @"isx"] containsObject:filename.pathExtension.lowercaseString] && uncompressedSize <= 32 * 1024 * 1024) {
tempIndex++;
NSString *subDir = [tempDir stringByAppendingFormat:@"/%u", tempIndex];
mkdir(subDir.UTF8String, 0755);
NSString *dest = [subDir stringByAppendingPathComponent:filename.lastPathComponent];
if (writeToPath(dest)) {
[validURLs addObject:[NSURL fileURLWithPath:dest]];
used = true;
}
}
return true;
}];
if (!used) {
[unusedZips addObject:url.lastPathComponent];
}
}
else if ([@[@"gb", @"gbc", @"isx"] containsObject:url.pathExtension.lowercaseString]) {
[validURLs addObject:url];
}
else {
[skippedBasenames addObject:url.lastPathComponent];
}
}
if (unusedZips.count) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"No ROMs in archive"
message:[NSString stringWithFormat:@"Could not find any Game Boy ROM files in the following archives:\n%@",
[unusedZips componentsJoinedByString:@"\n"]]
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"Close"
style:UIAlertActionStyleCancel
handler:nil]];
[self presentViewController:alert animated:true completion:nil];
}
if (skippedBasenames.count) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Unsupported Files"
message:[NSString stringWithFormat:@"Could not import the following files because they're not supported:\n%@",
[skippedBasenames componentsJoinedByString:@"\n"]]
preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"Close"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action) {
[[NSUserDefaults standardUserDefaults] setBool:false forKey:@"GBShownUTIWarning"]; // Somebody might need a reminder
}]];
[self presentViewController:alert animated:true completion:nil];
}
if (validURLs.count == 1 && urls.count == 1) {
NSURL *url = validURLs.firstObject;
NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
[GBROMManager sharedManager].currentROM = potentialROM;
}
else {
[url startAccessingSecurityScopedResource];
[GBROMManager sharedManager].currentROM =
[[GBROMManager sharedManager] importROM:url.path
keepOriginal:![url.path hasPrefix:tempDir] && inPlace];
[url stopAccessingSecurityScopedResource];
}
return true;
}
for (NSURL *url in validURLs) {
NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
// That's an already imported ROM
continue;
}
[url startAccessingSecurityScopedResource];
[[GBROMManager sharedManager] importROM:url.path
keepOriginal:![url.path hasPrefix:tempDir] && inPlace];
[url stopAccessingSecurityScopedResource];
}
[self openLibrary];
return validURLs.count;
}
- (void)doImportedPaletteNotification
{
UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleProminent]];
effectView.layer.cornerRadius = 8;
effectView.layer.masksToBounds = true;
[self.view addSubview:effectView];
UILabel *tipLabel = [[UILabel alloc] init];
tipLabel.text = [NSString stringWithFormat:@"Imported palette “%@”", [[NSUserDefaults standardUserDefaults] stringForKey:@"GBCurrentTheme"]];
if (@available(iOS 13.0, *)) {
tipLabel.textColor = [UIColor labelColor];
}
tipLabel.font = [UIFont systemFontOfSize:16];
tipLabel.alpha = 0.8;
[effectView.contentView addSubview:tipLabel];
UIView *view = self.view;
CGSize outerSize = view.frame.size;
CGSize size = [tipLabel textRectForBounds:(CGRect){{0, 0},
{outerSize.width - 32,
outerSize.height - 32}}
limitedToNumberOfLines:1].size;
size.width = ceil(size.width);
tipLabel.frame = (CGRect){{8, 8}, size};
CGRect finalFrame = (CGRect) {
{round((outerSize.width - size.width - 16) / 2), view.window.safeAreaInsets.top + 12},
{size.width + 16, size.height + 16}
};
CGRect initFrame = finalFrame;
initFrame.origin.y = -initFrame.size.height;
effectView.frame = initFrame;
effectView.alpha = 0;
[UIView animateWithDuration:0.5 animations:^{
effectView.alpha = 1.0;
effectView.frame = finalFrame;
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * 1.5)), dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.5 animations:^{
effectView.alpha = 0.0;
effectView.frame = initFrame;
} completion:^(BOOL finished) {
if (finished) {
[effectView removeFromSuperview];
}
}];
});
}
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
if (self.presentedViewController && ![self.presentedViewController isKindOfClass:[UIAlertController class]]) {
[self dismissViewController];
}
if ([url.pathExtension.lowercaseString isEqual:@"sbp"]) {
[url startAccessingSecurityScopedResource];
bool success = [GBPalettePicker importPalette:url.path];
[url stopAccessingSecurityScopedResource];
if (!success) {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Palette Import Failed"
message:@"The imported palette file is invalid."
preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"Close" style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alertController animated:true completion:nil];
}
else {
[self doImportedPaletteNotification];
}
return success;
}
NSString *potentialROM = [[url.path stringByDeletingLastPathComponent] lastPathComponent];
if ([[[GBROMManager sharedManager] romFileForROM:potentialROM].stringByStandardizingPath isEqualToString:url.path.stringByStandardizingPath]) {
[self stop];
[GBROMManager sharedManager].currentROM = potentialROM;
[self start];
return [GBROMManager sharedManager].currentROM != nil;
}
return [self handleOpenURLs:@[url]
openInPlace:[options[UIApplicationOpenURLOptionsOpenInPlaceKey] boolValue]];
}
- (void)setRunMode:(GBRunMode)runMode ignoreDynamicSpeed:(bool)ignoreDynamicSpeed
{
if (runMode == GBRunModeRewind && _rewindOver) {
runMode = GBRunModePaused;
}
if (runMode == _runMode) return;
if (_runMode == GBRunModePaused) {
[_audioClient start];
}
_runMode = runMode;
if (_runMode == GBRunModePaused) {
[_audioClient stop];
}
if (_runMode == GBRunModeNormal || _runMode == GBRunModeTurbo || _runMode == GBRunModeUnderclock) {
_rewindOver = false;
}
if (_runMode == GBRunModeNormal || _runMode == GBRunModeUnderclock || !([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"] && !ignoreDynamicSpeed)) {
if (_runMode == GBRunModeTurbo) {
GB_set_turbo_mode(&_gb, true, false);
}
else if (_runMode == GBRunModeUnderclock) {
GB_set_clock_multiplier(&_gb, 0.5);
}
else {
GB_set_turbo_mode(&_gb, false, false);
GB_set_clock_multiplier(&_gb, 1.0);
}
}
}
- (void)setRunMode:(GBRunMode)runMode
{
[self setRunMode:runMode ignoreDynamicSpeed:false];
}
- (AVCaptureDevice *)captureDevice
{
if (_cameraPosition == AVCaptureDevicePositionFront) {
return _frontCaptureDevice ?: [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
}
return _backCaptureDevice ?: [AVCaptureDevice defaultDeviceWithMediaType: AVMediaTypeVideo];
}
- (void)cameraRequestUpdate
{
dispatch_async(dispatch_get_main_queue(), ^{
if (!_cameraSession) {
dispatch_async(_cameraQueue, ^{
@try {
if (!_cameraSession) {
NSError *error;
AVCaptureDevice *device = [self captureDevice];
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice: device error: &error];
if (!input) {
GB_camera_updated(&_gb);
return;
}
_cameraOutput = [[AVCaptureVideoDataOutput alloc] init];
[_cameraOutput setVideoSettings: @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_32BGRA)}];
[_cameraOutput setSampleBufferDelegate:self
queue:_cameraQueue];
_cameraSession = [AVCaptureSession new];
if ([device supportsAVCaptureSessionPreset:AVCaptureSessionPreset352x288]) {
_cameraSession.sessionPreset = AVCaptureSessionPreset352x288;
}
else if ([device supportsAVCaptureSessionPreset:AVCaptureSessionPresetMedium]) {
_cameraSession.sessionPreset = AVCaptureSessionPresetMedium;
}
[_cameraSession addInput: input];
[_cameraSession addOutput: _cameraOutput];
_cameraConnection = [_cameraOutput connectionWithMediaType: AVMediaTypeVideo];
_cameraConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
[_cameraSession startRunning];
}
}
@catch (NSException *exception) {
/* I have not tested camera support on many devices, so we catch exceptions just in case. */
GB_camera_updated(&_gb);
_cameraSession = nil;
}
});
}
_cameraNeedsUpdate = true;
[_disableCameraTimer invalidate];
if (!_cameraPositionButton.alpha) {
[UIView animateWithDuration:0.25 animations:^{
_cameraPositionButton.alpha = 1;
}];
}
if (_changeCameraButton) {
// The change camera button is only available when we are using a capture device on the back of the device
double changeCameraButtonAlpha = (_cameraPosition == AVCaptureDevicePositionFront) ? 0 : 1;
if (changeCameraButtonAlpha != _changeCameraButton.alpha) {
[UIView animateWithDuration:0.25 animations:^{
_changeCameraButton.alpha = changeCameraButtonAlpha;
}];
}
}
_disableCameraTimer = [NSTimer scheduledTimerWithTimeInterval:1
repeats:false
block:^(NSTimer *timer) {
if (_cameraPositionButton.alpha) {
[UIView animateWithDuration:0.25 animations:^{
_cameraPositionButton.alpha = 0;
}];
}
if (_changeCameraButton.alpha) {
[UIView animateWithDuration:0.25 animations:^{
_changeCameraButton.alpha = 0;
}];
}
dispatch_async(_cameraQueue, ^{
[_cameraSession stopRunning];
_cameraSession = nil;
_cameraConnection = nil;
_cameraOutput = nil;
});
}];
});
}
- (uint8_t)cameraGetPixelAtX:(uint8_t)x andY:(uint8_t)y
{
if (!_cameraImage) {
return 0;
}
if (_cameraNeedsUpdate) {
return 0;
}
y += 8; // Equalize X and Y for rotation as a 128x128 image
if (_cameraPosition == AVCaptureDevicePositionFront) {
x = 127 - x;
}
switch (_orientation) {
case UIInterfaceOrientationUnknown:
case UIInterfaceOrientationPortrait:
break;
case UIInterfaceOrientationPortraitUpsideDown:
x = 127 - x;
y = 127 - y;
break;
case UIInterfaceOrientationLandscapeLeft: {
uint8_t tempX = x;
x = y;
y = 127 - tempX;
break;
}
case UIInterfaceOrientationLandscapeRight:{
uint8_t tempX = x;
x = 127 - y;
y = tempX;
break;
}
}
if (_cameraPosition == AVCaptureDevicePositionFront) {
x = 127 - x;
}
// Center the 128*128 image on the 130*XXX (or XXX*130) captured image
unsigned offsetX = (CVPixelBufferGetWidth(_cameraImage) - 128) / 2;
unsigned offsetY = (CVPixelBufferGetHeight(_cameraImage) - 112) / 2;
uint8_t *baseAddress = (uint8_t *)CVPixelBufferGetBaseAddress(_cameraImage);
size_t bytesPerRow = CVPixelBufferGetBytesPerRow(_cameraImage);
uint8_t ret = baseAddress[(x + offsetX) * 4 + (y + offsetY) * bytesPerRow + 2];
return ret;
}
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
if (!_cameraNeedsUpdate) return;
CVImageBufferRef buffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CIImage *image = [CIImage imageWithCVPixelBuffer:buffer
options:[NSDictionary dictionaryWithObjectsAndKeys:[NSNull null], kCIImageColorSpace, nil]];
double scale = MAX(130.0 / CVPixelBufferGetWidth(buffer), 130.0 / CVPixelBufferGetHeight(buffer));
image = [image imageByApplyingTransform:CGAffineTransformMakeScale(scale, scale)];
if (_cameraImage) {
CVPixelBufferUnlockBaseAddress(_cameraImage, 0);
CVBufferRelease(_cameraImage);
_cameraImage = NULL;
}
CGSize size = image.extent.size;
CVPixelBufferCreate(kCFAllocatorDefault,
size.width,
size.height,
kCVPixelFormatType_32BGRA,
NULL,
&_cameraImage);
[[[CIContext alloc] init] render:image toCVPixelBuffer:_cameraImage];
CVPixelBufferLockBaseAddress(_cameraImage, 0);
GB_camera_updated(&_gb);
_cameraNeedsUpdate = false;
}
- (void)rotateCamera
{
dispatch_async(_cameraQueue, ^{
_cameraPosition ^= AVCaptureDevicePositionBack ^ AVCaptureDevicePositionFront;
[_cameraSession stopRunning];
_cameraSession = nil;
_cameraConnection = nil;
_cameraOutput = nil;
if (_cameraNeedsUpdate) {
_cameraNeedsUpdate = false;
GB_camera_updated(&_gb);
}
});
}
- (void)changeCamera
{
dispatch_async(_cameraQueue, ^{
if (![_backCaptureDevice lockForConfiguration:nil]) return;
_currentZoomIndex++;
if (_currentZoomIndex == _zoomLevels.count) {
_currentZoomIndex = 0;
}
[_backCaptureDevice rampToVideoZoomFactor:_zoomLevels[_currentZoomIndex].doubleValue withRate:2];
[_backCaptureDevice unlockForConfiguration];
});
}
- (void)disconnectCable
{
if (!_printerConnected) return;
_printerConnected = false;
_currentPrinterImageData = nil;
[UIView animateWithDuration:0.25 animations:^{
_printerButton.alpha = 0;
}];
[_printerSpinner stopAnimating];
GB_disconnect_serial(&_gb);
}
- (void)connectPrinter
{
if (_printerConnected) return;
_printerConnected = true;
GB_connect_printer(&_gb, printImage, printDone);
}
- (void)openConnectMenu
{
UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad?
UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet;
GBCheckableAlertController *menu = [GBCheckableAlertController alertControllerWithTitle:@"Connect Accessory"
message:@"Choose an accessory to connect."
preferredStyle:style];
[menu addAction:[UIAlertAction actionWithTitle:@"None"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[self disconnectCable];
}]];
[menu addAction:[UIAlertAction actionWithTitle:@"Game Boy Printer"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[self connectPrinter];
}]];
menu.selectedAction = menu.actions[_printerConnected];
[menu addAction:[UIAlertAction actionWithTitle:@"Cancel"
style:UIAlertActionStyleCancel
handler:nil]];
[self presentViewController:menu animated:true completion:nil];
}
- (void)printImage:(uint32_t *)imageBytes height:(unsigned) height
topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin
exposure:(unsigned) exposure
{
uint32_t paddedImage[160 * (topMargin + height + bottomMargin)];
memset(paddedImage, 0xFF, sizeof(paddedImage));
memcpy(paddedImage + (160 * topMargin), imageBytes, 160 * height * sizeof(imageBytes[0]));
if (!_currentPrinterImageData) {
_currentPrinterImageData = [[NSMutableData alloc] init];
}
[_currentPrinterImageData appendBytes:paddedImage length:sizeof(paddedImage)];
dispatch_async(dispatch_get_main_queue(), ^{
[UIView animateWithDuration:0.25 animations:^{
_printerButton.alpha = 1;
}];
[_printerSpinner startAnimating];
});
}
- (void)printDone
{
dispatch_async(dispatch_get_main_queue(), ^{
[_printerSpinner stopAnimating];
});
}
- (void)showPrinterFeed
{
UIImage *image = [self imageFromData:_currentPrinterImageData
width:160
height:_currentPrinterImageData.length / 160 / sizeof(uint32_t)];
_window.backgroundColor = [UIColor blackColor];
[self presentViewController:[[GBPrinterFeedController alloc] initWithImage:image]
animated:true
completion:nil];
}
- (void)emptyPrinterFeed
{
_currentPrinterImageData = nil;
[UIView animateWithDuration:0.25 animations:^{
_printerButton.alpha = 0;
}];
[_printerSpinner stopAnimating];
[self dismissViewController];
}
- (UIHingeStatus)hingeStatus
{
return _lastHingeStatus;
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
bool isSplitView = !CGSizeEqualToSize(size, self.window.screen.bounds.size);
if (isSplitView) {
self.window.bounds = (CGRect){{0, 0}, size};
_backgroundView.fullScreenMode = GBControllerFocusForcedViaSplitView;
[_backgroundView repositionForcedFullscreen];
}
else {
_backgroundView.fullScreenMode = GBControllerFocusOff;
dispatch_async(dispatch_get_main_queue(), ^{
[self recreateLayoutsWithThemeNamed:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBInterfaceTheme"]];
});
}
}
@end
API_AVAILABLE(ios(13.0))
@implementation GBSceneDelegate
- (void)scene:(UIScene *)scene
willConnectToSession:(UISceneSession *)session
options:(UISceneConnectionOptions *)connectionOptions
{
GBViewController *controller = (id)[UIApplication sharedApplication].delegate;
controller.window = [[UIWindow alloc] initWithWindowScene:(id)scene];
if (!controller.window) {
controller.window = [[UIWindow alloc] init];
}
controller.window.rootViewController = controller;
[controller.window makeKeyAndVisible];
if (@available(iOS 27.1, *)) {
[controller.window addInteraction:[[objc_getClass("UIHingeInteraction") alloc] initWithUpdateHandler:^(UIHingeInteraction *interaction, UIHingeInteractionUpdate *update) {
controller->_hinge = update.hinge;
[controller recreateLayoutsWithThemeNamed:[[NSUserDefaults standardUserDefaults] stringForKey:@"GBInterfaceTheme"]];
[[NSNotificationCenter defaultCenter] postNotificationName:@"HingeStatusChanged" object:nil];
}]];
}
for (UIOpenURLContext *context in connectionOptions.URLContexts) {
[controller application:[UIApplication sharedApplication]
openURL:context.URL
options:@{UIApplicationOpenURLOptionsOpenInPlaceKey: @(context.options.openInPlace)}];
}
}
- (void)sceneDidBecomeActive:(UIScene *)scene
{
[[UIApplication sharedApplication].delegate applicationDidBecomeActive:[UIApplication sharedApplication]];
}
- (void)sceneWillResignActive:(UIScene *)scene
{
[[UIApplication sharedApplication].delegate applicationWillResignActive:[UIApplication sharedApplication]];
}
- (void)scene:(UIScene *)scene openURLContexts:(nonnull NSSet<UIOpenURLContext *> *)URLContexts
{
GBViewController *controller = (id)[UIApplication sharedApplication].delegate;
for (UIOpenURLContext *context in URLContexts) {
[controller application:[UIApplication sharedApplication]
openURL:context.URL
options:@{UIApplicationOpenURLOptionsOpenInPlaceKey: @(context.options.openInPlace)}];
}
}
@end