mirror of https://github.com/LIJI32/SameBoy.git
1877 lines
76 KiB
Objective-C
1877 lines
76 KiB
Objective-C
#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 "GCExtendedGamepad+AllElements.h"
|
|
#import "GBZipReader.h"
|
|
#import <sys/stat.h>
|
|
#import <CoreMotion/CoreMotion.h>
|
|
#import <dlfcn.h>
|
|
|
|
@implementation GBViewController
|
|
{
|
|
GB_gameboy_t _gb;
|
|
GBView *_gbView;
|
|
|
|
volatile bool _running;
|
|
volatile bool _stopping;
|
|
bool _rewind;
|
|
bool _rewindOver;
|
|
bool _romLoaded;
|
|
bool _swappingROM;
|
|
bool _skipAutoLoad;
|
|
|
|
UIInterfaceOrientation _orientation;
|
|
GBHorizontalLayout *_horizontalLayoutLeft;
|
|
GBHorizontalLayout *_horizontalLayoutRight;
|
|
GBVerticalLayout *_verticalLayout;
|
|
GBBackgroundView *_backgroundView;
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
[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) {
|
|
[[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)];
|
|
}
|
|
|
|
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
|
{
|
|
_window = [[UIWindow alloc] init];
|
|
_window.rootViewController = self;
|
|
[_window makeKeyAndVisible];
|
|
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Warc-retain-cycles"
|
|
[self addDefaultObserver:^(id newValue) {
|
|
GBTheme *theme = [GBSettingsViewController themeNamed:newValue];
|
|
_horizontalLayoutLeft = [[GBHorizontalLayout alloc] initWithTheme:theme cutoutOnRight:false];
|
|
_horizontalLayoutRight = _horizontalLayoutLeft.cutout?
|
|
[[GBHorizontalLayout alloc] initWithTheme:theme cutoutOnRight:true] :
|
|
_horizontalLayoutLeft;
|
|
_verticalLayout = [[GBVerticalLayout alloc] initWithTheme:theme];
|
|
_printerSpinner.color = theme.buttonColor;
|
|
|
|
[self willRotateToInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation
|
|
duration:0];
|
|
[_backgroundView reloadThemeImages];
|
|
|
|
[self setNeedsStatusBarAppearanceUpdate];
|
|
} forKey:@"GBInterfaceTheme"];
|
|
#pragma clang diagnostic pop
|
|
|
|
_backgroundView = [[GBBackgroundView alloc] initWithLayout:_verticalLayout];
|
|
[_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:[UIApplication sharedApplication].statusBarOrientation
|
|
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];
|
|
[self didRotateFromInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation];
|
|
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 = _verticalLayout.theme.buttonColor;
|
|
[self didRotateFromInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation];
|
|
|
|
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];
|
|
|
|
return true;
|
|
}
|
|
|
|
- (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 = false;
|
|
}
|
|
}
|
|
|
|
- (void)setControllerHandlers
|
|
{
|
|
for (GCController *controller in [GCController controllers]) {
|
|
__weak GCController *weakController = controller;
|
|
if (controller.extendedGamepad) {
|
|
[[controller.extendedGamepad elementsDictionary] 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]]) {
|
|
[(GCControllerDirectionPad *)element setValueChangedHandler:^(GCControllerDirectionPad *dpad, float xValue, float yValue) {
|
|
[self controller:weakController axisChanged:dpad usage:usage.unsignedIntValue];
|
|
}];
|
|
}
|
|
}];
|
|
|
|
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 &&
|
|
[[NSUserDefaults standardUserDefaults] boolForKey:@"GBControllersHideInterface"]) {
|
|
_backgroundView.fullScreenMode = true;
|
|
}
|
|
|
|
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);
|
|
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 && _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;
|
|
default: break;
|
|
}
|
|
}
|
|
|
|
- (void)controller:(GCController *)controller axisChanged:(GCControllerDirectionPad *)axis usage:(GBControllerUsage)usage
|
|
{
|
|
[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;
|
|
|
|
if (_running && (left || right || up || down ) &&
|
|
[[NSUserDefaults standardUserDefaults] boolForKey:@"GBControllersHideInterface"]) {
|
|
_backgroundView.fullScreenMode = true;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
- (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"];
|
|
if (!GB_save_state(&_gb, tempPath.UTF8String)) {
|
|
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];
|
|
}
|
|
|
|
- (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
|
|
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;
|
|
}
|
|
if (_romLoaded) {
|
|
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]) {
|
|
// 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"]);
|
|
}
|
|
}
|
|
}
|
|
|
|
NSDate *date = nil;
|
|
@try {
|
|
[[NSURL fileURLWithPath:[GBROMManager sharedManager].autosaveStateFile] getResourceValue:&date
|
|
forKey:NSURLContentModificationDateKey
|
|
error:nil];
|
|
}
|
|
@catch (NSException *exception) {
|
|
/* fileURLWithPath: throws an exception on some crash logs. I don't know why or how to reproduce it,
|
|
but let's at least not crash. */
|
|
GB_rewind_reset(&_gb);
|
|
}
|
|
|
|
// Reset the rewind buffer only if we switched ROMs or had the save state change externally
|
|
if (![_lastSavedROM isEqual:[GBROMManager sharedManager].currentROM] ||
|
|
![_saveDate isEqual:date]) {
|
|
GB_rewind_reset(&_gb);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
_romLoaded = false;
|
|
}
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
_gbView.hidden = !_romLoaded;
|
|
});
|
|
_swappingROM = false;
|
|
_skipAutoLoad = false;
|
|
}
|
|
|
|
- (void)applicationDidBecomeActive:(UIApplication *)application
|
|
{
|
|
[self start];
|
|
}
|
|
|
|
-(void)presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion
|
|
{
|
|
[self stop];
|
|
[super presentViewController:viewControllerToPresent
|
|
animated:flag
|
|
completion:completion];
|
|
}
|
|
|
|
- (void)reset
|
|
{
|
|
[self stop];
|
|
_skipAutoLoad = true;
|
|
GB_reset(&_gb);
|
|
[self start];
|
|
}
|
|
|
|
- (void)openLibrary
|
|
{
|
|
[self presentViewController:[[GBLibraryViewController alloc] init]
|
|
animated:true
|
|
completion:nil];
|
|
}
|
|
|
|
- (void)changeModel
|
|
{
|
|
GBOptionViewController *controller = [[GBOptionViewController alloc] initWithHeader:@"Select a model to emulate"];
|
|
controller.footer = @"Changing the emulated model will reset the emulator";
|
|
|
|
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
|
|
{
|
|
UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBStatesViewController alloc] init]];
|
|
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
|
|
{
|
|
UIBarButtonItem *close = [[UIBarButtonItem alloc] initWithTitle:@"Close"
|
|
style:UIBarButtonItemStylePlain
|
|
target:self
|
|
action:@selector(dismissViewController)];
|
|
[self presentViewController:[GBSettingsViewController settingsViewControllerWithLeftButton:close]
|
|
animated:true
|
|
completion:nil];
|
|
}
|
|
|
|
- (void)showAbout
|
|
{
|
|
[self presentViewController:[[GBAboutController alloc] init] animated:true completion:nil];
|
|
}
|
|
|
|
- (void)openCheats
|
|
{
|
|
UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:[[GBCheatsController alloc] initWithGameBoy:&_gb]];
|
|
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 (completion) {
|
|
completion();
|
|
}
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self start];
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (void)setNeedsUpdateOfSupportedInterfaceOrientations
|
|
{
|
|
/* Hack. Some view controllers dismiss without calling the method above. */
|
|
[super setNeedsUpdateOfSupportedInterfaceOrientations];
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self start];
|
|
});
|
|
}
|
|
|
|
- (void)dismissViewController
|
|
{
|
|
[self dismissViewControllerAnimated:true completion:nil];
|
|
}
|
|
|
|
- (void)applicationWillResignActive:(UIApplication *)application
|
|
{
|
|
[self stop];
|
|
}
|
|
|
|
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration
|
|
{
|
|
GBLayout *layout = nil;
|
|
_orientation = orientation;
|
|
switch (orientation) {
|
|
default:
|
|
case UIInterfaceOrientationUnknown:
|
|
case UIInterfaceOrientationPortrait:
|
|
case UIInterfaceOrientationPortraitUpsideDown:
|
|
layout = _verticalLayout;
|
|
break;
|
|
case UIInterfaceOrientationLandscapeRight:
|
|
layout = _horizontalLayoutLeft;
|
|
break;
|
|
case UIInterfaceOrientationLandscapeLeft:
|
|
layout = _horizontalLayoutRight;
|
|
break;
|
|
}
|
|
|
|
_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;
|
|
}
|
|
|
|
|
|
_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);
|
|
|
|
}
|
|
|
|
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
|
|
{
|
|
if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
|
|
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
|
|
{
|
|
switch (_orientation) {
|
|
case UIInterfaceOrientationLandscapeRight:
|
|
case UIInterfaceOrientationLandscapeLeft:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
- (UIStatusBarStyle)preferredStatusBarStyle
|
|
{
|
|
if (@available(iOS 13.0, *)) {
|
|
return (_verticalLayout.theme.isDark || _backgroundView.fullScreenMode)? UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent;
|
|
}
|
|
return (_verticalLayout.theme.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];
|
|
}
|
|
}
|
|
|
|
- (void)run
|
|
{
|
|
[self loadROM];
|
|
if (!_romLoaded) {
|
|
_running = false;
|
|
_stopping = false;
|
|
return;
|
|
}
|
|
[self preRun];
|
|
while (_running) {
|
|
if (_rewind) {
|
|
_rewind = false;
|
|
GB_rewind_pop(&_gb);
|
|
if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
|
|
if (!GB_rewind_pop(&_gb)) {
|
|
self.runMode = GBRunModePaused;
|
|
_rewindOver = true;
|
|
}
|
|
}
|
|
else {
|
|
for (unsigned i = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindSpeed"]; i--;) {
|
|
if (!GB_rewind_pop(&_gb)) {
|
|
self.runMode = GBRunModePaused;
|
|
_rewindOver = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (_runMode != GBRunModePaused) {
|
|
GB_run(&_gb);
|
|
}
|
|
}
|
|
[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 = CGDataProviderCreateWithData(NULL, data.bytes, data.length, NULL);
|
|
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)postRun
|
|
{
|
|
[_audioLock lock];
|
|
_audioBufferPosition = _audioBufferNeeded = 0;
|
|
[_audioLock signal];
|
|
[_audioLock unlock];
|
|
[_audioClient stop];
|
|
_audioClient = nil;
|
|
|
|
if (!_swappingROM) {
|
|
GB_save_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation);
|
|
[self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile];
|
|
|
|
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;
|
|
[[[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil] start];
|
|
}
|
|
|
|
- (void)stop
|
|
{
|
|
if (!_running) return;
|
|
[_audioLock lock];
|
|
_stopping = true;
|
|
[_audioLock signal];
|
|
[_audioLock unlock];
|
|
_running = false;
|
|
while (_stopping) {
|
|
[_audioLock lock];
|
|
[_audioLock signal];
|
|
[_audioLock unlock];
|
|
}
|
|
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);
|
|
}
|
|
_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) {
|
|
double multiplier = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboSpeed"];
|
|
GB_set_turbo_mode(&_gb, multiplier == 1, false);
|
|
GB_set_clock_multiplier(&_gb, multiplier);
|
|
}
|
|
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];
|
|
_cameraSession.sessionPreset = AVCaptureSessionPreset352x288;
|
|
|
|
[_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);
|
|
}
|
|
});
|
|
}
|
|
|
|
_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)openConnectMenu
|
|
{
|
|
UIAlertControllerStyle style = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad?
|
|
UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet;
|
|
GBCheckableAlertController *menu = [GBCheckableAlertController alertControllerWithTitle:@"Connect which accessory?"
|
|
message:nil
|
|
preferredStyle:style];
|
|
[menu addAction:[UIAlertAction actionWithTitle:@"None"
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction *action) {
|
|
_printerConnected = false;
|
|
_currentPrinterImageData = nil;
|
|
[UIView animateWithDuration:0.25 animations:^{
|
|
_printerButton.alpha = 0;
|
|
}];
|
|
[_printerSpinner stopAnimating];
|
|
GB_disconnect_serial(&_gb);
|
|
}]];
|
|
[menu addAction:[UIAlertAction actionWithTitle:@"Game Boy Printer"
|
|
style:UIAlertActionStyleDefault
|
|
handler:^(UIAlertAction *action) {
|
|
_printerConnected = true;
|
|
GB_connect_printer(&_gb, printImage, printDone);
|
|
}]];
|
|
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];
|
|
}
|
|
|
|
@end
|