diff --git a/AppleCommon/GBAudioClient.m b/AppleCommon/GBAudioClient.m
index 8f57e1f..e650aaf 100644
--- a/AppleCommon/GBAudioClient.m
+++ b/AppleCommon/GBAudioClient.m
@@ -46,11 +46,17 @@ static OSStatus render(
// Get the default playback output unit
AudioComponent defaultOutput = AudioComponentFindNext(NULL, &defaultOutputDescription);
- NSAssert(defaultOutput, @"Can't find default output");
+ if (!defaultOutput) {
+ NSLog(@"Can't find default output");
+ return nil;
+ }
// Create a new unit based on this that we'll use for output
OSErr err = AudioComponentInstanceNew(defaultOutput, &audioUnit);
- NSAssert1(audioUnit, @"Error creating unit: %hd", err);
+ if (!audioUnit) {
+ NSLog(@"Error creating unit: %hd", err);
+ return nil;
+ }
// Set our tone rendering function on the unit
AURenderCallbackStruct input;
@@ -62,7 +68,10 @@ static OSStatus render(
0,
&input,
sizeof(input));
- NSAssert1(err == noErr, @"Error setting callback: %hd", err);
+ if (err) {
+ NSLog(@"Error setting callback: %hd", err);
+ return nil;
+ }
AudioStreamBasicDescription streamFormat;
streamFormat.mSampleRate = rate;
@@ -80,9 +89,16 @@ static OSStatus render(
0,
&streamFormat,
sizeof(AudioStreamBasicDescription));
- NSAssert1(err == noErr, @"Error setting stream format: %hd", err);
+
+ if (err) {
+ NSLog(@"Error setting stream format: %hd", err);
+ return nil;
+ }
err = AudioUnitInitialize(audioUnit);
- NSAssert1(err == noErr, @"Error initializing unit: %hd", err);
+ if (err) {
+ NSLog(@"Error initializing unit: %hd", err);
+ return nil;
+ }
self.renderBlock = block;
_rate = rate;
@@ -93,7 +109,10 @@ static OSStatus render(
-(void) start
{
OSErr err = AudioOutputUnitStart(audioUnit);
- NSAssert1(err == noErr, @"Error starting unit: %hd", err);
+ if (err) {
+ NSLog(@"Error starting unit: %hd", err);
+ return;
+ }
_playing = true;
}
diff --git a/Cocoa/GBApp.m b/Cocoa/GBApp.m
index 01082c0..47b742b 100644
--- a/Cocoa/GBApp.m
+++ b/Cocoa/GBApp.m
@@ -66,7 +66,7 @@ static uint32_t color_to_int(NSColor *color)
@"GBFilter": @"NearestNeighbor",
@"GBColorCorrection": @(GB_COLOR_CORRECTION_MODERN_BALANCED),
@"GBHighpassFilter": @(GB_HIGHPASS_ACCURATE),
- @"GBRewindLength": @(10),
+ @"GBRewindLength": @(120),
@"GBFrameBlendingMode": @([defaults boolForKey:@"DisableFrameBlending"]? GB_FRAME_BLENDING_MODE_DISABLED : GB_FRAME_BLENDING_MODE_ACCURATE),
@"GBDMGModel": @(GB_MODEL_DMG_B),
diff --git a/Cocoa/GBButtons.h b/Cocoa/GBButtons.h
index 3f33cfd..54a2f09 100644
--- a/Cocoa/GBButtons.h
+++ b/Cocoa/GBButtons.h
@@ -1,4 +1,4 @@
-typedef enum : NSUInteger {
+typedef enum {
GBRight,
GBLeft,
GBUp,
diff --git a/Cocoa/Info.plist b/Cocoa/Info.plist
index 8ebf8cd..c437485 100644
--- a/Cocoa/Info.plist
+++ b/Cocoa/Info.plist
@@ -123,7 +123,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- Version @VERSION
+ @VERSION
CFBundleSignature
????
CFBundleSupportedPlatforms
diff --git a/Core/save_state.c b/Core/save_state.c
index 147a861..ed39776 100644
--- a/Core/save_state.c
+++ b/Core/save_state.c
@@ -1347,10 +1347,12 @@ static int load_state_internal(GB_gameboy_t *gb, virtual_file_t *file)
return errno ?: EIO;
}
- size_t orig_ram_size = gb->ram_size;
+ uint32_t ram_size = gb->ram_size;
+ uint32_t mbc_ram_size = gb->mbc_ram_size;
memcpy(gb, &save, sizeof(save));
- gb->ram_size = orig_ram_size;
-
+ gb->ram_size = ram_size;
+ gb->mbc_ram_size = mbc_ram_size;
+
sanitize_state(gb);
GB_rewind_invalidate_for_backstepping(gb);
return 0;
diff --git a/Makefile b/Makefile
index 578fd66..7c31fce 100644
--- a/Makefile
+++ b/Makefile
@@ -143,6 +143,9 @@ override CONF := release
FAT_FLAGS += -arch x86_64 -arch arm64
endif
+IOS_MIN := 11.0
+
+IOS_PNGS := $(shell ls iOS/*.png)
# Support out-of-PATH RGBDS
RGBASM := $(RGBDS)rgbasm
RGBLINK := $(RGBDS)rgblink
@@ -239,13 +242,13 @@ SYSROOT := $(shell xcodebuild -sdk iphoneos -version Path 2> $(NULL))
ifeq ($(SYSROOT),)
$(error Could not find an iOS SDK)
endif
-CFLAGS += -arch arm64 -miphoneos-version-min=11.0 -isysroot $(SYSROOT) -IAppleCommon -DGB_DISABLE_DEBUGGER
+CFLAGS += -arch arm64 -miphoneos-version-min=$(IOS_MIN) -isysroot $(SYSROOT) -IAppleCommon -DGB_DISABLE_DEBUGGER
CORE_FILTER += Core/debugger.c Core/sm83_disassembler.c Core/symbol_hash.c
LDFLAGS += -arch arm64
OCFLAGS += -x objective-c -fobjc-arc -Wno-deprecated-declarations -isysroot $(SYSROOT)
-LDFLAGS += -miphoneos-version-min=11.0 -isysroot $(SYSROOT)
+LDFLAGS += -miphoneos-version-min=$(IOS_MIN) -isysroot $(SYSROOT)
IOS_INSTALLER_LDFLAGS := $(LDFLAGS) -lobjc -framework CoreServices -framework Foundation
-LDFLAGS += -lobjc -framework UIKit -framework Foundation -framework CoreGraphics -framework Metal -framework MetalKit -framework AudioToolbox -framework AVFoundation -framework QuartzCore -framework CoreMotion -framework CoreVideo -framework CoreMedia -framework CoreImage -framework UserNotifications -weak_framework CoreHaptics
+LDFLAGS += -lobjc -framework UIKit -framework Foundation -framework CoreGraphics -framework Metal -framework MetalKit -framework AudioToolbox -framework AVFoundation -framework QuartzCore -framework CoreMotion -framework CoreVideo -framework CoreMedia -framework CoreImage -framework UserNotifications -framework GameController -weak_framework CoreHaptics -framework MobileCoreServices
CODESIGN := codesign -fs -
else
ifeq ($(PLATFORM),Darwin)
@@ -288,6 +291,7 @@ endif
# Don't use function outlining. I breaks Obj-C ARC optimizations and Apple never bothered to fix it. It also hardly has any effect on file size.
ifeq ($(shell $(CC) -x c -c $(NULL) -o $(NULL) -Werror -mno-outline 2> $(NULL); echo $$?),0)
FRONTEND_CFLAGS += -mno-outline
+LDFLAGS += -mno-outline
endif
STRIP := strip
@@ -423,7 +427,7 @@ $(OBJ)/%.m.o: %.m
# iOS Port
$(BIN)/SameBoy-iOS.app: $(BIN)/SameBoy-iOS.app/SameBoy \
- $(shell ls iOS/*.png) \
+ $(IOS_PNGS) \
iOS/License.html \
iOS/Info.plist \
$(BIN)/SameBoy-iOS.app/dmg_boot.bin \
@@ -433,9 +437,10 @@ $(BIN)/SameBoy-iOS.app: $(BIN)/SameBoy-iOS.app/SameBoy \
$(BIN)/SameBoy-iOS.app/agb_boot.bin \
$(BIN)/SameBoy-iOS.app/sgb_boot.bin \
$(BIN)/SameBoy-iOS.app/sgb2_boot.bin \
+ $(BIN)/SameBoy-iOS.app/LaunchScreen.storyboardc \
Shaders
$(MKDIR) -p $(BIN)/SameBoy-iOS.app
- cp iOS/*.png $(BIN)/SameBoy-iOS.app
+ cp $(IOS_PNGS) $(BIN)/SameBoy-iOS.app
sed "s/@VERSION/$(VERSION)/;s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < iOS/Info.plist > $(BIN)/SameBoy-iOS.app/Info.plist
sed "s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < iOS/License.html > $(BIN)/SameBoy-iOS.app/License.html
$(MKDIR) -p $(BIN)/SameBoy-iOS.app/Shaders
@@ -491,6 +496,9 @@ endif
$(BIN)/SameBoy.app/Contents/Resources/%.nib: Cocoa/%.xib
ibtool --target-device mac --minimum-deployment-target 10.9 --compile $@ $^ 2>&1 | cat -
+$(BIN)/SameBoy-iOS.app/%.storyboardc: iOS/%.storyboard
+ ibtool --target-device iphone --target-device ipad --minimum-deployment-target $(IOS_MIN) --compile $@ $^ 2>&1 | cat -
+
# Quick Look generator
$(BIN)/SameBoy.qlgenerator: $(BIN)/SameBoy.qlgenerator/Contents/MacOS/SameBoyQL \
diff --git a/Shaders/MonoLCD.fsh b/Shaders/MonoLCD.fsh
index 61d878d..00b63c2 100644
--- a/Shaders/MonoLCD.fsh
+++ b/Shaders/MonoLCD.fsh
@@ -35,6 +35,7 @@ STATIC vec4 scale(sampler2D image, vec2 position, vec2 input_resolution, vec2 ou
}
vec4 pre_shadow = mix(texture(image, position) * multiplier, mix(r1, r2, s.y), BLOOM);
+ pre_shadow.a = 1.0;
pixel += vec2(-0.6, -0.8);
q11 = texture(image, (floor(pixel) + 0.5) / input_resolution);
diff --git a/iOS/GBAboutController.m b/iOS/GBAboutController.m
index 49ca64b..3941a4e 100644
--- a/iOS/GBAboutController.m
+++ b/iOS/GBAboutController.m
@@ -237,4 +237,9 @@
{
return UIModalPresentationFormSheet;
}
+
+- (void)dismissViewController
+{
+ [self dismissViewControllerAnimated:true completion:nil];
+}
@end
diff --git a/iOS/GBBackgroundView.h b/iOS/GBBackgroundView.h
index 1c34798..c36260f 100644
--- a/iOS/GBBackgroundView.h
+++ b/iOS/GBBackgroundView.h
@@ -3,7 +3,12 @@
#import "GBView.h"
@interface GBBackgroundView : UIImageView
+- (instancetype)initWithLayout:(GBLayout *)layout;
+
@property (readonly) GBView *gbView;
@property (nonatomic) GBLayout *layout;
@property (nonatomic) bool usesSwipePad;
+
+- (void)enterPreviewMode:(bool)showLabel;
+- (void)reloadThemeImages;
@end
diff --git a/iOS/GBBackgroundView.m b/iOS/GBBackgroundView.m
index 05cdc22..ca6e128 100644
--- a/iOS/GBBackgroundView.m
+++ b/iOS/GBBackgroundView.m
@@ -85,9 +85,11 @@ static GB_key_mask_t angleToKeyMask(double angle)
UITouch *_swipePadTouch;
CGPoint _padSwipeOrigin;
UITouch *_screenTouch;
+ UITouch *_logoTouch;
CGPoint _screenSwipeOrigin;
bool _screenSwiped;
bool _inDynamicSpeedMode;
+ bool _previewMode;
UIImageView *_dpadView;
UIImageView *_dpadShadowView;
@@ -104,10 +106,21 @@ static GB_key_mask_t angleToKeyMask(double angle)
GB_key_mask_t _lastMask;
}
-- (instancetype)init
+- (void)reloadThemeImages
+{
+ _aButtonView.image = [_layout.theme imageNamed:@"buttonA"];
+ _bButtonView.image = [_layout.theme imageNamed:@"buttonB"];
+ _startButtonView.image = [_layout.theme imageNamed:@"button2"];
+ _selectButtonView.image = [_layout.theme imageNamed:@"button2"];
+ self.usesSwipePad = self.usesSwipePad;
+}
+
+- (instancetype)initWithLayout:(GBLayout *)layout;
{
self = [super initWithImage:nil];
if (!self) return nil;
+
+ _layout = layout;
_touches = [NSMutableSet set];
_screenLabel = [[UILabel alloc] initWithFrame:CGRectZero];
@@ -119,13 +132,14 @@ static GB_key_mask_t angleToKeyMask(double angle)
_screenLabel.numberOfLines = 0;
[self addSubview:_screenLabel];
- _dpadView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"dpad"]];
+ _dpadView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"dpad"]];
+ _aButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"buttonA"]];
+ _bButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"buttonB"]];
+ _startButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"button2"]];
+ _selectButtonView = [[UIImageView alloc] initWithImage:[_layout.theme imageNamed:@"button2"]];
+
_dpadShadowView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"dpadShadow"]];
_dpadShadowView.hidden = true;
- _aButtonView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"button"]];
- _bButtonView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"button"]];
- _startButtonView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"button2"]];
- _selectButtonView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"button2"]];
_gbView = [[GBViewMetal alloc] initWithFrame:CGRectZero];
[self addSubview:_dpadView];
@@ -168,11 +182,19 @@ static GB_key_mask_t angleToKeyMask(double angle)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
+ if (_previewMode) return;
static const double dpadRadius = 75;
CGPoint dpadLocation = _layout.dpadLocation;
double factor = [UIScreen mainScreen].scale;
dpadLocation.x /= factor;
dpadLocation.y /= factor;
+ CGRect logoRect = _layout.logoRect;
+
+ logoRect.origin.x /= factor;
+ logoRect.origin.y /= factor;
+ logoRect.size.width /= factor;
+ logoRect.size.height /= factor;
+
for (UITouch *touch in touches) {
CGPoint point = [touch locationInView:self];
if (CGRectContainsPoint(self.gbView.frame, point) && !_screenTouch) {
@@ -194,8 +216,10 @@ static GB_key_mask_t angleToKeyMask(double angle)
}
}
}
-
- if (_usesSwipePad && !_swipePadTouch) {
+ else if (CGRectContainsPoint(logoRect, point) && !_logoTouch) {
+ _logoTouch = touch;
+ }
+ else if (_usesSwipePad && !_swipePadTouch) {
if (fabs(point.x - dpadLocation.x) <= dpadRadius &&
fabs(point.y - dpadLocation.y) <= dpadRadius) {
_swipePadTouch = touch;
@@ -209,17 +233,18 @@ static GB_key_mask_t angleToKeyMask(double angle)
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
- if ([touches containsObject:_swipePadTouch]) {
+ if (_swipePadTouch && [touches containsObject:_swipePadTouch]) {
_swipePadTouch = nil;
}
- if ([touches containsObject:_screenTouch]) {
+ if (_screenTouch && [touches containsObject:_screenTouch]) {
_screenTouch = nil;
if (self.viewController.runMode == GBRunModePaused) {
self.viewController.runMode = GBRunModeNormal;
[self fadeOverlayOut];
}
if (!_screenSwiped) {
+ self.window.backgroundColor = nil;
[self.window.rootViewController presentViewController:[GBMenuViewController menu] animated:true completion:nil];
}
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeLock"]) {
@@ -229,6 +254,24 @@ static GB_key_mask_t angleToKeyMask(double angle)
}
}
}
+
+ if (_logoTouch && [touches containsObject:_logoTouch]) {
+
+ double factor = [UIScreen mainScreen].scale;
+ CGRect logoRect = _layout.logoRect;
+
+ logoRect.origin.x /= factor;
+ logoRect.origin.y /= factor;
+ logoRect.size.width /= factor;
+ logoRect.size.height /= factor;
+
+ CGPoint point = [_logoTouch locationInView:self];
+ if (CGRectContainsPoint(logoRect, point)) {
+ self.window.backgroundColor = nil;
+ [self.window.rootViewController presentViewController:[GBMenuViewController menu] animated:true completion:nil];
+ }
+ _logoTouch = nil;
+ }
[_touches minusSet:touches];
[self touchesChanged];
@@ -246,6 +289,7 @@ static GB_key_mask_t angleToKeyMask(double angle)
- (void)touchesChanged
{
+ if (_previewMode) return;
if (!GB_is_inited(_gbView.gb)) return;
GB_key_mask_t mask = 0;
double factor = [UIScreen mainScreen].scale;
@@ -345,10 +389,10 @@ static GB_key_mask_t angleToKeyMask(double angle)
}
}
if (mask != _lastMask) {
- _aButtonView.image = [UIImage imageNamed:(mask & GB_KEY_A_MASK)? @"buttonPressed" : @"button"];
- _bButtonView.image = [UIImage imageNamed:(mask & GB_KEY_B_MASK)? @"buttonPressed" : @"button"];
- _startButtonView.image = [UIImage imageNamed:(mask & GB_KEY_START_MASK) ? @"button2Pressed" : @"button2"];
- _selectButtonView.image = [UIImage imageNamed:(mask & GB_KEY_SELECT_MASK)? @"button2Pressed" : @"button2"];
+ _aButtonView.image = [_layout.theme imageNamed:(mask & GB_KEY_A_MASK)? @"buttonAPressed" : @"buttonA"];
+ _bButtonView.image = [_layout.theme imageNamed:(mask & GB_KEY_B_MASK)? @"buttonBPressed" : @"buttonB"];
+ _startButtonView.image = [_layout.theme imageNamed:(mask & GB_KEY_START_MASK) ? @"button2Pressed" : @"button2"];
+ _selectButtonView.image = [_layout.theme imageNamed:(mask & GB_KEY_SELECT_MASK)? @"button2Pressed" : @"button2"];
bool hidden = false;
bool diagonal = false;
@@ -425,7 +469,8 @@ static GB_key_mask_t angleToKeyMask(double angle)
- (void)setUsesSwipePad:(bool)usesSwipePad
{
_usesSwipePad = usesSwipePad;
- _dpadView.image = [UIImage imageNamed:usesSwipePad? @"swipepad" : @"dpad"];
+ _dpadView.image = nil; // Some bug in UIImage seems to trigger without this?
+ _dpadView.image = [_layout.theme imageNamed:usesSwipePad? @"swipepad" : @"dpad"];
}
- (void)displayOverlayWithImage:(NSString *)imageName orTitle:(NSString *)title
@@ -519,4 +564,16 @@ static GB_key_mask_t angleToKeyMask(double angle)
[self.viewController start];
}
+- (void)enterPreviewMode:(bool)showLabel
+{
+ if (showLabel) {
+ _screenLabel.text = [NSString stringWithFormat:@"Previewing Theme “%@”", _layout.theme.name];
+ }
+ else {
+ [_screenLabel removeFromSuperview];
+ _screenLabel = nil;
+ }
+ _previewMode = true;
+}
+
@end
diff --git a/iOS/GBHapticManager.h b/iOS/GBHapticManager.h
index e2c543c..7f2e2d9 100644
--- a/iOS/GBHapticManager.h
+++ b/iOS/GBHapticManager.h
@@ -1,7 +1,9 @@
#import
+#import
@interface GBHapticManager : NSObject
+ (instancetype)sharedManager;
- (void)doTapHaptic;
- (void)setRumbleStrength:(double)rumble;
+@property (weak) GCController *controller;
@end
diff --git a/iOS/GBHapticManager.m b/iOS/GBHapticManager.m
index 5cc09d3..b82abeb 100644
--- a/iOS/GBHapticManager.m
+++ b/iOS/GBHapticManager.m
@@ -7,8 +7,10 @@
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
CHHapticEngine *_engine;
+ CHHapticEngine *_externalEngine;
id _rumblePlayer;
#pragma clang diagnostic pop
+ __weak GCController *_controller;
double _rumble;
}
@@ -58,18 +60,30 @@
if (_rumble) return;
CHHapticPattern *pattern = [[CHHapticPattern alloc] initWithEvents:@[[self eventWithType:CHHapticEventTypeHapticTransient
- sharpness:0.25
- intensity:0.75
- duration:1.0]]
+ sharpness:0.25
+ intensity:[[NSUserDefaults standardUserDefaults] doubleForKey:@"GBHapticsStrength"]
+ duration:1.0]]
parameters:nil
error:nil];
- id player = [_engine createPlayerWithPattern:pattern error:nil];
-
- [player startAtTime:0 error:nil];
+ @try {
+ id player = [_engine createPlayerWithPattern:pattern error:nil];
+
+ [player startAtTime:0 error:nil];
+ }
+ @catch (NSException *exception) {}
}
- (void)setRumbleStrength:(double)rumble
{
+ if (!_controller) { // Controller disconnected
+ _externalEngine = nil;
+ }
+ if (!_externalEngine && _controller && !_controller.isAttachedToDevice) {
+ /* We have a controller with no rumble support which is not attached to the device,
+ don't rumble since the user is holding neither the device nor a haptic-enabled
+ controller. */
+ rumble = 0;
+ }
if (rumble == 0) {
[_rumblePlayer stopAtTime:0 error:nil];
_rumblePlayer = nil;
@@ -83,11 +97,38 @@
duration:1.0]]
parameters:nil
error:nil];
- id newPlayer = [_engine createPlayerWithPattern:pattern error:nil];
-
- [newPlayer startAtTime:0 error:nil];
- [_rumblePlayer stopAtTime:0 error:nil];
- _rumblePlayer = newPlayer;
+ @try {
+ id newPlayer = [_externalEngine ?: _engine createPlayerWithPattern:pattern error:nil];
+
+ [newPlayer startAtTime:0 error:nil];
+ [_rumblePlayer stopAtTime:0 error:nil];
+ _rumblePlayer = newPlayer;
+ }
+ @catch (NSException *exception) {
+ if (_externalEngine) {
+ // Something might have happened with our controller? Delete and try again
+ _externalEngine = nil;
+ [self setRumbleStrength: rumble];
+ }
+ }
+}
+
+- (void)setController:(GCController *)controller
+{
+ if (_controller != controller) {
+ if (@available(iOS 14.0, *)) {
+ _externalEngine = [controller.haptics createEngineWithLocality:GCHapticsLocalityDefault];
+ _externalEngine.playsHapticsOnly = true;
+ _externalEngine.autoShutdownEnabled = true;
+
+ }
+ _controller = controller;
+ }
+}
+
+- (GCController *)controller
+{
+ return _controller;
}
@end
diff --git a/iOS/GBHorizontalLayout.m b/iOS/GBHorizontalLayout.m
index 94891eb..66f7cb3 100644
--- a/iOS/GBHorizontalLayout.m
+++ b/iOS/GBHorizontalLayout.m
@@ -3,9 +3,9 @@
@implementation GBHorizontalLayout
-- (instancetype)init
+- (instancetype)initWithTheme:(GBTheme *)theme
{
- self = [super init];
+ self = [super initWithTheme:theme];
if (!self) return nil;
CGSize resolution = {self.resolution.height - self.cutout, self.resolution.width};
@@ -89,16 +89,25 @@
(self.aLocation.y + self.bLocation.y) / 2};
- UIGraphicsBeginImageContextWithOptions(resolution, true, 1);
+ if (theme.renderingPreview) {
+ UIGraphicsBeginImageContextWithOptions((CGSize){resolution.width / 8, resolution.height / 8}, true, 1);
+ CGContextScaleCTM(UIGraphicsGetCurrentContext(), 1 / 8.0, 1 / 8.0);
+ }
+ else {
+ UIGraphicsBeginImageContextWithOptions(resolution, true, 1);
+ }
[self drawBackground];
[self drawScreenBezels];
- if (drawSameBoyLogo) {
- double bezelBottom = screenRect.origin.y + screenRect.size.height + screenBorderWidth;
- double freeSpace = resolution.height - bezelBottom;
- [self drawLogoInVerticalRange:(NSRange){bezelBottom + screenBorderWidth * 2, freeSpace - screenBorderWidth * 4}];
- }
- [self drawLabels];
+ [self drawThemedLabelsWithBlock:^{
+ if (drawSameBoyLogo) {
+ double bezelBottom = screenRect.origin.y + screenRect.size.height + screenBorderWidth;
+ double freeSpace = resolution.height - bezelBottom;
+ [self drawLogoInVerticalRange:(NSRange){bezelBottom + screenBorderWidth * 2, freeSpace - screenBorderWidth * 4}];
+ }
+
+ [self drawLabels];
+ }];
self.background = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
@@ -112,4 +121,10 @@
}
return CGRectMake(0, 0, self.background.size.width / self.factor, self.background.size.height / self.factor);
}
+
+- (CGSize)size
+{
+ return (CGSize){self.resolution.height, self.resolution.width};
+}
+
@end
diff --git a/iOS/GBLayout.h b/iOS/GBLayout.h
index acba9b2..cda5741 100644
--- a/iOS/GBLayout.h
+++ b/iOS/GBLayout.h
@@ -1,14 +1,20 @@
#import
+#import "GBTheme.h"
@interface GBLayout : NSObject
+- (instancetype)initWithTheme:(GBTheme *)theme;
+@property (readonly) GBTheme *theme;
+
@property (readonly) UIImage *background;
@property (readonly) CGRect screenRect;
+@property (readonly) CGRect logoRect;
@property (readonly) CGPoint dpadLocation;
@property (readonly) CGPoint aLocation;
@property (readonly) CGPoint bLocation;
@property (readonly) CGPoint abComboLocation;
@property (readonly) CGPoint startLocation;
@property (readonly) CGPoint selectLocation;
+
- (CGRect)viewRectForOrientation:(UIInterfaceOrientation)orientation;
@end
@@ -23,19 +29,19 @@
@property CGPoint abComboLocation;
@property CGPoint startLocation;
@property CGPoint selectLocation;
-@property (readonly) CGSize resolution;
+@property (readonly) CGSize resolution; // Always vertical
+@property (readonly) CGSize size; // Size in pixels, override to make horizontal
@property (readonly) unsigned factor;
@property (readonly) unsigned minY;
@property (readonly) unsigned homeBar;
@property (readonly) unsigned cutout;
@property (readonly) bool hasFractionalPixels;
-@property (readonly) UIColor *brandColor;
-
- (void)drawBackground;
- (void)drawScreenBezels;
- (void)drawLogoInVerticalRange:(NSRange)range;
- (void)drawLabels;
+- (void)drawThemedLabelsWithBlock:(void (^)(void))block;
- (CGSize)buttonDeltaForMaxHorizontalDistance:(double)distance;
@end
diff --git a/iOS/GBLayout.m b/iOS/GBLayout.m
index 6850015..9331c51 100644
--- a/iOS/GBLayout.m
+++ b/iOS/GBLayout.m
@@ -1,15 +1,43 @@
#define GBLayoutInternal
#import "GBLayout.h"
-@interface UIApplication()
-- (double)statusBarHeightForOrientation:(UIInterfaceOrientation)orientation ignoreHidden:(bool)ignoreHidden;
-@end
+static double StatusBarHeight(void)
+{
+ static double ret = 0;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ @autoreleasepool {
+ UIWindow *window = [[UIWindow alloc] init];
+ [window makeKeyAndVisible];
+ UIEdgeInsets insets = window.safeAreaInsets;
+ ret = MAX(MAX(insets.left, insets.right), MAX(insets.top, insets.bottom)) ?: 20;
+ [window setHidden:true];
+ }
+ });
+ return ret;
+}
+
+static bool HasHomeBar(void)
+{
+ static bool ret = false;
+ static dispatch_once_t onceToken;
+ dispatch_once(&onceToken, ^{
+ ret = [UIApplication sharedApplication].windows[0].safeAreaInsets.bottom;
+ });
+ return ret;
+}
@implementation GBLayout
-- (instancetype)init
+{
+ bool _isRenderingMask;
+}
+
+- (instancetype)initWithTheme:(GBTheme *)theme
{
self = [super init];
if (!self) return nil;
+
+ _theme = theme;
_factor = [UIScreen mainScreen].scale;
_resolution = [UIScreen mainScreen].bounds.size;
_resolution.width *= _factor;
@@ -17,11 +45,11 @@
if (_resolution.width > _resolution.height) {
_resolution = (CGSize){_resolution.height, _resolution.width};
}
- _minY = [[UIApplication sharedApplication] statusBarHeightForOrientation:UIInterfaceOrientationPortrait
- ignoreHidden:true] * _factor;
+
+ _minY = StatusBarHeight() * _factor;
_cutout = _minY <= 24 * _factor? 0 : _minY;
- if ([UIApplication sharedApplication].windows[0].safeAreaInsets.bottom) {
+ if (HasHomeBar()) {
_homeBar = 21 * _factor;
}
@@ -36,21 +64,11 @@
return CGRectMake(0, 0, self.background.size.width / self.factor, self.background.size.height / self.factor);
}
-- (UIColor *)brandColor
-{
- static dispatch_once_t onceToken;
- static UIColor *ret = nil;
- dispatch_once(&onceToken, ^{
- ret = [UIColor colorWithRed:0 / 255.0 green:70 / 255.0 blue:141 / 255.0 alpha:1.0];
- });
- return ret;
-}
-
- (void)drawBackground
{
CGContextRef context = UIGraphicsGetCurrentContext();
- CGColorRef top = [UIColor colorWithRed:192 / 255.0 green:195 / 255.0 blue:199 / 255.0 alpha:1.0].CGColor;
- CGColorRef bottom = [UIColor colorWithRed:174 / 255.0 green:176 / 255.0 blue:180 / 255.0 alpha:1.0].CGColor;
+ CGColorRef top = _theme.backgroundGradientTop.CGColor;
+ CGColorRef bottom = _theme.backgroundGradientBottom.CGColor;
CGColorRef colors[] = {top, bottom};
CFArrayRef colorsArray = CFArrayCreate(NULL, (const void **)colors, 2, &kCFTypeArrayCallBacks);
@@ -59,7 +77,7 @@
CGContextDrawLinearGradient(context,
gradient,
(CGPoint){0, 0},
- (CGPoint){0, CGBitmapContextGetHeight(context)},
+ (CGPoint){0, self.size.height},
0);
CFRelease(gradient);
@@ -70,8 +88,8 @@
- (void)drawScreenBezels
{
CGContextRef context = UIGraphicsGetCurrentContext();
- CGColorRef top = [UIColor colorWithWhite:53 / 255.0 alpha:1.0].CGColor;
- CGColorRef bottom = [UIColor colorWithWhite:45 / 255.0 alpha:1.0].CGColor;
+ CGColorRef top = _theme.bezelsGradientTop.CGColor;
+ CGColorRef bottom = _theme.bezelsGradientBottom.CGColor;
CGColorRef colors[] = {top, bottom};
CFArrayRef colorsArray = CFArrayCreate(NULL, (const void **)colors, 2, &kCFTypeArrayCallBacks);
@@ -83,8 +101,8 @@
bezelRect.size.height += borderWidth * 2;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:bezelRect cornerRadius:borderWidth];
CGContextSaveGState(context);
- CGContextSetShadowWithColor(context, (CGSize){0,}, borderWidth / 2, [UIColor colorWithWhite:0 alpha:1.0].CGColor);
- [[UIColor colorWithWhite:0 alpha:0.25] setFill];
+ CGContextSetShadowWithColor(context, (CGSize){0, _factor}, _factor, [UIColor colorWithWhite:1 alpha:0.25].CGColor);
+ [_theme.backgroundGradientBottom setFill];
[path fill];
[path addClip];
@@ -96,10 +114,17 @@
(CGPoint){bezelRect.origin.x, bezelRect.origin.y + bezelRect.size.height},
0);
+ CGContextSetShadowWithColor(context, (CGSize){0, _factor}, _factor, [UIColor colorWithWhite:0 alpha:0.25].CGColor);
+
+ path.usesEvenOddFillRule = true;
+ [path appendPath:[UIBezierPath bezierPathWithRect:(CGRect){{0, 0}, self.size}]];
+ [path fill];
+
+
CGContextRestoreGState(context);
CGContextSaveGState(context);
- CGContextSetShadowWithColor(context, (CGSize){0,}, borderWidth / 2, [UIColor colorWithWhite:0 alpha:0.25].CGColor);
+ CGContextSetShadowWithColor(context, (CGSize){0, 0}, borderWidth / 4, [UIColor colorWithWhite:0 alpha:0.125].CGColor);
[[UIColor blackColor] setFill];
UIRectFill(self.screenRect);
@@ -116,15 +141,26 @@
CGRect rect = CGRectMake(0,
range.location - range.length / 3,
- CGBitmapContextGetWidth(UIGraphicsGetCurrentContext()), range.length * 2);
+ self.size.width, range.length * 2);
NSMutableParagraphStyle *style = [NSParagraphStyle defaultParagraphStyle].mutableCopy;
style.alignment = NSTextAlignmentCenter;
[@"SAMEBOY" drawInRect:rect
withAttributes:@{
NSFontAttributeName: font,
- NSForegroundColorAttributeName:self.brandColor,
+ NSForegroundColorAttributeName:_isRenderingMask? [UIColor whiteColor] : _theme.brandColor,
NSParagraphStyleAttributeName: style,
}];
+
+ _logoRect = (CGRect){
+ {(self.size.width - _screenRect.size.width) / 2, rect.origin.y},
+ {_screenRect.size.width, rect.size.height}
+ };
+}
+
+- (void)drawThemedLabelsWithBlock:(void (^)(void))block
+{
+ // Start with a normal normal pass
+ block();
}
- (void)drawRotatedLabel:(NSString *)label withFont:(UIFont *)font origin:(CGPoint)origin distance:(double)distance
@@ -141,7 +177,7 @@
[label drawInRect:CGRectMake(-256, distance, 512, 256)
withAttributes:@{
NSFontAttributeName: font,
- NSForegroundColorAttributeName:self.brandColor,
+ NSForegroundColorAttributeName:_isRenderingMask? [UIColor whiteColor] : _theme.brandColor,
NSParagraphStyleAttributeName: style,
}];
CGContextRestoreGState(context);
@@ -167,4 +203,9 @@
}
return (CGSize){maxDistance, floor(sqrt(100 * 100 * self.factor * self.factor - maxDistance * maxDistance))};
}
+
+- (CGSize)size
+{
+ return _resolution;
+}
@end
diff --git a/iOS/GBLoadROMTableViewController.m b/iOS/GBLoadROMTableViewController.m
index a27d3d4..828dd74 100644
--- a/iOS/GBLoadROMTableViewController.m
+++ b/iOS/GBLoadROMTableViewController.m
@@ -1,5 +1,11 @@
#import "GBLoadROMTableViewController.h"
#import "GBROMManager.h"
+#import "GBViewController.h"
+#import
+#import
+
+@interface GBLoadROMTableViewController()
+@end
@implementation GBLoadROMTableViewController
{
@@ -15,16 +21,22 @@
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
- return 1;
+ return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
+ if (section == 1) return 1;
return [GBROMManager sharedManager].allROMs.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
+ if (indexPath.section == 1) {
+ UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
+ cell.textLabel.text = @"Import ROM files";
+ return cell;
+ }
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
NSString *rom = [GBROMManager sharedManager].allROMs[[indexPath indexAtPosition:1]];
cell.textLabel.text = rom;
@@ -51,6 +63,8 @@
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
+ if (indexPath.section == 1) return [super tableView:tableView heightForRowAtIndexPath:indexPath];
+
return 60;
}
@@ -61,17 +75,123 @@
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
{
- return @"Import ROMs by opening them in SameBoy using the Files app or a web browser, or by sending them over with AirDrop";
+ if (section == 0) return nil;
+
+ return @"You can also import ROM files by opening them in SameBoy using the Files app or a web browser, or by sending them over with AirDrop.";
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
+ if (indexPath.section == 1) {
+ UIViewController *parent = self.presentingViewController;
+ NSString *gbUTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)@"gb", NULL);
+ NSString *gbcUTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)@"gbc", NULL);
+ NSString *isxUTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)@"isx", NULL);
+
+ NSMutableSet *extensions = [NSMutableSet set];
+ [extensions addObjectsFromArray:(__bridge NSArray *)UTTypeCopyAllTagsWithClass((__bridge CFStringRef)gbUTI, kUTTagClassFilenameExtension)];
+ [extensions addObjectsFromArray:(__bridge NSArray *)UTTypeCopyAllTagsWithClass((__bridge CFStringRef)gbcUTI, kUTTagClassFilenameExtension)];
+ [extensions addObjectsFromArray:(__bridge NSArray *)UTTypeCopyAllTagsWithClass((__bridge CFStringRef)isxUTI, kUTTagClassFilenameExtension)];
+
+ if (extensions.count != 3) {
+ if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBShownUTIWarning"]) {
+ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"File Association Conflict"
+ message:@"Due to a limitation in iOS, the file picker will allow you to select files not supported by SameBoy. SameBoy will only import GB, GBC and ISX files.\n\nIf you have a multi-system emulator installed, updating it could fix this problem."
+ preferredStyle:UIAlertControllerStyleAlert];
+ [alert addAction:[UIAlertAction actionWithTitle:@"Close"
+ style:UIAlertActionStyleCancel
+ handler:^(UIAlertAction *action) {
+ [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBShownUTIWarning"];
+ [self tableView:tableView didSelectRowAtIndexPath:indexPath];
+ }]];
+ [self presentViewController:alert animated:true completion:nil];
+ return;
+ }
+ }
+
+ [self.presentingViewController dismissViewControllerAnimated:true completion:^{
+ UIDocumentPickerViewController *picker = [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[@"com.github.liji32.sameboy.gb",
+ @"com.github.liji32.sameboy.gbc",
+ @"com.github.liji32.sameboy.isx",
+ gbUTI ?: @"",
+ gbcUTI ?: @"",
+ isxUTI ?: @""]
+ inMode:UIDocumentPickerModeImport];
+ picker.allowsMultipleSelection = true;
+ if (@available(iOS 13.0, *)) {
+ picker.shouldShowFileExtensions = true;
+ }
+ picker.delegate = self;
+ objc_setAssociatedObject(picker, @selector(delegate), self, OBJC_ASSOCIATION_RETAIN);
+ [parent presentViewController:picker animated:true completion:nil];
+ }];
+ return;
+ }
[GBROMManager sharedManager].currentROM = [GBROMManager sharedManager].allROMs[[indexPath indexAtPosition:1]];
[self.presentingViewController dismissViewControllerAnimated:true completion:^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"GBROMChanged" object:nil];
}];
}
+- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls
+{
+ NSMutableArray *validURLs = [NSMutableArray array];
+ NSMutableArray *skippedBasenames = [NSMutableArray array];
+
+ for (NSURL *url in urls) {
+ if ([@[@"gb", @"gbc", @"isx"] containsObject:url.pathExtension.lowercaseString]) {
+ [validURLs addObject:url];
+ }
+ else {
+ [skippedBasenames addObject:url.lastPathComponent];
+ }
+ }
+
+ 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
+ }]];
+ [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert animated:true completion:nil];
+ urls = validURLs;
+ }
+
+ if (urls.count == 1) {
+ NSURL *url = urls.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:true];
+ [url stopAccessingSecurityScopedResource];
+ }
+ [[NSNotificationCenter defaultCenter] postNotificationName:@"GBROMChanged" object:nil];
+ }
+ else {
+ for (NSURL *url in urls) {
+ 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:true];
+ [url stopAccessingSecurityScopedResource];
+ }
+ [(GBViewController *)[UIApplication sharedApplication].keyWindow.rootViewController openLibrary];
+ }
+}
+
- (UIModalPresentationStyle)modalPresentationStyle
{
return UIModalPresentationOverFullScreen;
@@ -79,6 +199,8 @@
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
+ if (indexPath.section == 1) return;
+
if (editingStyle != UITableViewCellEditingStyleDelete) return;
NSString *rom = [GBROMManager sharedManager].allROMs[[indexPath indexAtPosition:1]];
UIAlertController *alert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Delete ROM “%@”?", rom]
@@ -102,6 +224,8 @@
- (void)renameRow:(NSIndexPath *)indexPath
{
+ if (indexPath.section == 1) return;
+
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
UITextField *field = [[UITextField alloc] initWithFrame:cell.textLabel.frame];
field.font = cell.textLabel.font;
@@ -140,11 +264,18 @@
_renamingPath = nil;
}
+- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ return indexPath.section == 0;
+}
+
// Leave these ROM management to iOS 13.0 and up for now
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView
contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
point:(CGPoint)point API_AVAILABLE(ios(13.0))
{
+ if (indexPath.section == 1) return nil;
+
return [UIContextMenuConfiguration configurationWithIdentifier:nil
previewProvider:nil
actionProvider:^UIMenu *(NSArray *suggestedActions) {
diff --git a/iOS/GBMenuViewController.m b/iOS/GBMenuViewController.m
index 72a55a3..1a47011 100644
--- a/iOS/GBMenuViewController.m
+++ b/iOS/GBMenuViewController.m
@@ -4,7 +4,31 @@
#import "GBViewController.h"
#import "GBROMManager.h"
+static NSString *const tips[] = {
+ @"Tip: AirDrop ROM files from a Mac or another device to play them.",
+ @"Tip: Swipe right on the Game Boy screen to fast forward emulation.",
+ @"Tip: The D-pad can be replaced with a Swipe-pad in Control Settings. Give it a try!",
+ @"Tip: Swipe left on the Game Boy screen to rewind.",
+ @"Tip: Enable Quick Save and Load in Control Settings to use save state gestures.",
+ @"Tip: The turbo and rewind speeds can be changed in Emulation Settings.",
+ @"Tip: Change turbo and rewind behavior to locking in Controls Settings.",
+ @"Tip: Single Touch A+B combo can be enabled in Controls Settings.",
+ @"Tip: Try different scaling filters in Display Settings.",
+ @"Tip: Dynamically control turbo and rewind speed by enabling Dynamic Control in Control Settings.",
+ @"Tip: Rumble can be enabled even for games without rumble support in Control Settings.",
+ @"Tip: Try different color palettes for monochrome models in Display Settings.",
+ @"Did you know? The Game Boy uses a Sharp SM83 CPU.",
+ @"Did you know? The Game Boy Color has 6 different hardware revisions.",
+ @"Did you know? The Game Boy's frame rate is approximately 59.73 frames per second.",
+ @"Did you know? The original Super Game Boy runs slightly faster than other Game Boys.",
+ @"Did you know? The Game Boy generates audio at a sample rate of over 2MHz!",
+};
+
@implementation GBMenuViewController
+{
+ UILabel *_tipLabel;
+ UIVisualEffectView *_effectView;
+}
+ (instancetype)menu
{
@@ -73,9 +97,53 @@
};
objc_setAssociatedObject(button, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN);
[button addTarget:block action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside];
-
-
}
+
+ _effectView = [[UIVisualEffectView alloc] initWithEffect:[UIBlurEffect effectWithStyle:UIBlurEffectStyleProminent]];
+ _effectView.layer.cornerRadius = 8;
+ _effectView.layer.masksToBounds = true;
+ [self.view.superview addSubview:_effectView];
+ _tipLabel = [[UILabel alloc] init];
+ unsigned tipIndex = [[NSUserDefaults standardUserDefaults] integerForKey:@"GBTipIndex"];
+ _tipLabel.text = tips[tipIndex % (sizeof(tips) / sizeof(tips[0]))];
+ if (@available(iOS 13.0, *)) {
+ _tipLabel.textColor = [UIColor labelColor];
+ }
+ _tipLabel.font = [UIFont systemFontOfSize:14];
+ _tipLabel.alpha = 0.8;
+ [[NSUserDefaults standardUserDefaults] setInteger:tipIndex + 1 forKey:@"GBTipIndex"];
+ _tipLabel.lineBreakMode = NSLineBreakByWordWrapping;
+ _tipLabel.numberOfLines = 3;
+ [_effectView.contentView addSubview:_tipLabel];
+ [self layoutTip];
+ _effectView.alpha = 0;
+ [UIView animateWithDuration:0.25 animations:^{
+ _effectView.alpha = 1.0;
+ }];
+}
+
+- (void)layoutTip
+{
+ UIView *view = self.view.superview;
+ CGSize outerSize = view.frame.size;
+ CGSize size = [_tipLabel textRectForBounds:(CGRect){{0, 0},
+ {outerSize.width - 32,
+ outerSize.height - 32}}
+ limitedToNumberOfLines:3].size;
+ size.width = ceil(size.width);
+ _tipLabel.frame = (CGRect){{8, 8}, size};
+ _effectView.frame = (CGRect) {
+ {round((outerSize.width - size.width - 16) / 2), view.window.safeAreaInsets.top + 12},
+ {size.width + 16, size.height + 16}
+ };
+}
+
+
+- (void)viewWillDisappear:(BOOL)animated
+{
+ [UIView animateWithDuration:0.25 animations:^{
+ _effectView.alpha = 0;
+ }];
}
- (void)viewDidLayoutSubviews
@@ -83,6 +151,7 @@
if (self.view.bounds.size.height < 88 * 2) {
[self.view.heightAnchor constraintEqualToConstant:self.view.bounds.size.height + 88 * 2].active = true;
}
+ [self layoutTip];
[super viewDidLayoutSubviews];
}
diff --git a/iOS/GBOptionViewController.m b/iOS/GBOptionViewController.m
index 36b9f0e..85f3910 100644
--- a/iOS/GBOptionViewController.m
+++ b/iOS/GBOptionViewController.m
@@ -9,7 +9,11 @@
- (instancetype)initWithHeader:(NSString *)header
{
- self = [super initWithStyle:UITableViewStyleGrouped];
+ UITableViewStyle style = UITableViewStyleGrouped;
+ if (@available(iOS 13.0, *)) {
+ style = UITableViewStyleInsetGrouped;
+ }
+ self = [super initWithStyle:style];
self.header = header;
_options = [NSMutableArray array];
_actions = [NSMutableArray array];
diff --git a/iOS/GBSettingsViewController.h b/iOS/GBSettingsViewController.h
index c580dea..5583cd6 100644
--- a/iOS/GBSettingsViewController.h
+++ b/iOS/GBSettingsViewController.h
@@ -1,7 +1,31 @@
#import
#import
+#import "GCExtendedGamepad+AllElements.h"
+#import "GBTheme.h"
+
+typedef enum {
+ GBRight,
+ GBLeft,
+ GBUp,
+ GBDown,
+ GBA,
+ GBB,
+ GBSelect,
+ GBStart,
+ GBTurbo,
+ GBRewind,
+ GBUnderclock,
+ // GBHotkey1, // Todo
+ // GBHotkey2, // Todo
+ GBJoypadButtonCount,
+ GBButtonCount = GBUnderclock + 1,
+ GBGameBoyButtonCount = GBStart + 1,
+ GBUnusedButton = 0xFF,
+} GBButton;
@interface GBSettingsViewController : UITableViewController
+ (UIViewController *)settingsViewControllerWithLeftButton:(UIBarButtonItem *)button;
+ (const GB_palette_t *)paletteForTheme:(NSString *)theme;
++ (GBButton)controller:(GCController *)controller convertUsageToButton:(GBControllerUsage)usage;
++ (GBTheme *)themeNamed:(NSString *)name;
@end
diff --git a/iOS/GBSettingsViewController.m b/iOS/GBSettingsViewController.m
index dd120c1..1581df1 100644
--- a/iOS/GBSettingsViewController.m
+++ b/iOS/GBSettingsViewController.m
@@ -1,10 +1,14 @@
#import "GBSettingsViewController.h"
#import "GBTemperatureSlider.h"
#import "GBViewBase.h"
+#import "GBThemesViewController.h"
+#import "GBHapticManager.h"
+#import "GCExtendedGamepad+AllElements.h"
#import
static NSString const *typeSubmenu = @"submenu";
static NSString const *typeOptionSubmenu = @"optionSubmenu";
+static NSString const *typeBlock = @"block";
static NSString const *typeRadio = @"radio";
static NSString const *typeCheck = @"check";
static NSString const *typeDisabled = @"disabled";
@@ -16,6 +20,7 @@ static NSString const *typeLightTemp = @"typeLightTemp";
{
NSArray *_structure;
UINavigationController *_detailsNavigation;
+ NSArray *> *_themes; // For prewarming
}
+ (const GB_palette_t *)paletteForTheme:(NSString *)theme
@@ -316,10 +321,16 @@ static NSString const *typeLightTemp = @"typeLightTemp";
];
NSArray *controlsMenu = @[
+ @{
+ @"items": @[
+ @{@"type": typeBlock, @"title": @"Configure Game Controllers", @"block": ^bool(GBSettingsViewController *controller){
+ return [controller configureGameControllers];
+ }},
+ ],
+ },
@{
@"header": @"D-pad Style",
@"items": @[
- // TODO: Convert to enum when implemented
@{@"type": typeRadio, @"pref": @"GBSwipeDpad", @"title": @"Standard", @"value": @NO,},
@{@"type": typeRadio, @"pref": @"GBSwipeDpad", @"title": @"Swipe", @"value": @YES,},
],
@@ -379,6 +390,11 @@ static NSString const *typeLightTemp = @"typeLightTemp";
@{
@"items": @[
@{@"type": typeCheck, @"pref": @"GBButtonHaptics", @"title": @"Enable Button Haptics"},
+ @{@"type": typeSlider, @"pref": @"GBHapticsStrength", @"min": @0.25, @"max": @1, @"minImage": @"waveform.weak", @"maxImage": @"waveform",
+ @"previewBlock": ^void(void){
+ [[GBHapticManager sharedManager] doTapHaptic];
+ }
+ }
],
},
];
@@ -410,6 +426,12 @@ static NSString const *typeLightTemp = @"typeLightTemp";
@"submenu": controlsMenu,
@"image": [UIImage imageNamed:@"controlsSettings"],
},
+ @{
+ @"title": @"Themes",
+ @"type": typeSubmenu,
+ @"class": [GBThemesViewController class],
+ @"image": [UIImage imageNamed:@"themeSettings"],
+ },
]
}
];
@@ -418,7 +440,12 @@ static NSString const *typeLightTemp = @"typeLightTemp";
+ (UIViewController *)settingsViewControllerWithLeftButton:(UIBarButtonItem *)button
{
- GBSettingsViewController *root = [[self alloc] initWithStructure:[self rootStructure] title:@"Settings" style:UITableViewStyleGrouped];
+ UITableViewStyle style = UITableViewStyleGrouped;
+ if (@available(iOS 13.0, *)) {
+ style = UITableViewStyleInsetGrouped;
+ }
+ GBSettingsViewController *root = [[self alloc] initWithStructure:[self rootStructure] title:@"Settings" style:style];
+ [root preloadThemePreviews];
UINavigationController *controller = [[UINavigationController alloc] initWithRootViewController:root];
[controller.visibleViewController.navigationItem setLeftBarButtonItem:button];
if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) {
@@ -434,6 +461,220 @@ static NSString const *typeLightTemp = @"typeLightTemp";
return split;
}
+static UIImage *ImageForController(GCController *controller)
+{
+ if (@available(iOS 13.0, *)) {
+
+ NSString *symbolName = @"gamecontroller.fill";
+ UIColor *color = [UIColor grayColor];
+
+ if (@available(iOS 14.5, *)) {
+ if ([controller.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]) {
+ symbolName = @"logo.playstation";
+ color = [UIColor colorWithRed:0 green:0x30 / 255.0 blue:0x87 / 255.0 alpha:1.0];
+ }
+ }
+ if (@available(iOS 14.0, *)) {
+ if ([controller.extendedGamepad isKindOfClass:[GCDualShockGamepad class]]) {
+ symbolName = @"logo.playstation";
+ color = [UIColor colorWithRed:0 green:0x30 / 255.0 blue:0x87 / 255.0 alpha:1.0];
+ }
+ if ([controller.extendedGamepad isKindOfClass:[GCXboxGamepad class]]) {
+ symbolName = @"logo.xbox";
+ color = [UIColor colorWithRed:0xe / 255.0 green:0x7a / 255.0 blue:0xd / 255.0 alpha:1.0];
+ }
+ }
+
+ UIImage *glyph = [[UIImage systemImageNamed:symbolName] imageWithTintColor:[UIColor whiteColor]];
+ if (!glyph) {
+ glyph = [[UIImage systemImageNamed:@"gamecontroller.fill"] imageWithTintColor:[UIColor whiteColor]];
+ }
+
+ UIGraphicsBeginImageContextWithOptions((CGSize){29, 29}, false, [UIScreen mainScreen].scale);
+ [color setFill];
+ [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 29, 29) cornerRadius:7] fill];
+ double height = 25 / glyph.size.width * glyph.size.height;
+ [glyph drawInRect:CGRectMake(2, (29 - height) / 2, 25, height)];
+
+ UIImage *ret = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+ return ret;
+ }
+ return nil;
+
+}
+
++ (GBButton)controller:(GCController *)controller convertUsageToButton:(GBControllerUsage)usage
+{
+ bool isSony = false;
+ if (@available(iOS 14.5, *)) {
+ if ([controller.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]) {
+ isSony = true;
+ }
+ }
+ if (@available(iOS 14.0, *)) {
+ if ([controller.extendedGamepad isKindOfClass:[GCDualShockGamepad class]]) {
+ isSony = true;
+ }
+ }
+
+ NSNumber *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBControllerMappings"][controller.vendorName][[NSString stringWithFormat:@"%u", usage]];
+ if (mapping) {
+ return mapping.intValue;
+ }
+
+ switch (usage) {
+ case GBUsageButtonA: return isSony? GBB : GBA;
+ case GBUsageButtonB: return isSony? GBA : GBB;
+ case GBUsageButtonX: return isSony? GBSelect : GBStart;
+ case GBUsageButtonY: return isSony? GBStart : GBSelect;
+ case GBUsageButtonMenu: return GBStart;
+ case GBUsageButtonOptions: return GBSelect;
+ case GBUsageButtonHome: return GBStart;
+ case GBUsageLeftShoulder: return GBRewind;
+ case GBUsageRightShoulder: return GBTurbo;
+ case GBUsageLeftTrigger: return GBUnderclock;
+ case GBUsageRightTrigger: return GBTurbo;
+ default: return GBUnusedButton;
+ }
+}
+
+static NSString *LocalizedNameForElement(GCControllerElement *element, GBControllerUsage usage)
+{
+ if (@available(iOS 14.0, *)) {
+ return element.localizedName;
+ }
+ switch (usage) {
+ case GBUsageDpad: return @"D-Pad";
+ case GBUsageButtonA: return @"A";
+ case GBUsageButtonB: return @"B";
+ case GBUsageButtonX: return @"X";
+ case GBUsageButtonY: return @"Y";
+ case GBUsageButtonMenu: return @"Menu";
+ case GBUsageButtonOptions: return @"Options";
+ case GBUsageButtonHome: return @"Home";
+ case GBUsageLeftThumbstick: return @"Left Thumbstick";
+ case GBUsageRightThumbstick: return @"Right Thumbstick";
+ case GBUsageLeftShoulder: return @"Left Shoulder";
+ case GBUsageRightShoulder: return @"Right Shoulder";
+ case GBUsageLeftTrigger: return @"Left Trigger";
+ case GBUsageRightTrigger: return @"Right Trigger";
+ case GBUsageLeftThumbstickButton: return @"Left Thumbstick Button";
+ case GBUsageRightThumbstickButton: return @"Right Thumbstick Button";
+ case GBUsageTouchpadButton: return @"Touchpad Button";
+ }
+
+ return @"Button";
+}
+
+- (void)configureGameController:(GCController *)controller
+{
+ NSMutableArray *items = [NSMutableArray array];
+ NSDictionary *elementsDict = controller.extendedGamepad.elementsDictionary;
+ for (NSNumber *usage in [[elementsDict allKeys] sortedArrayUsingSelector:@selector(compare:)]) {
+ GCControllerElement *element = elementsDict[usage];
+ if (![element isKindOfClass:[GCControllerButtonInput class]]) continue;
+
+ id (^getter)(void) = ^id(void) {
+ return @([GBSettingsViewController controller:controller convertUsageToButton:usage.intValue]);
+ };
+
+ void (^setter)(id) = ^void(id value) {
+ NSMutableDictionary *mapping = ([[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBControllerMappings"] ?: @{}).mutableCopy;
+
+ NSMutableDictionary *vendorMapping = ((NSDictionary *)mapping[controller.vendorName] ?: @{}).mutableCopy;
+ vendorMapping[usage.stringValue] = value;
+ mapping[controller.vendorName] = vendorMapping;
+ [[NSUserDefaults standardUserDefaults] setObject:mapping forKey:@"GBControllerMappings"];
+ };
+
+
+ NSDictionary *item = @{
+ @"title": LocalizedNameForElement(element, usage.unsignedIntValue),
+ @"type": typeOptionSubmenu,
+ @"submenu": @[@{@"items": @[
+ @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"None", @"value": @(GBUnusedButton)},
+ @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Right", @"value": @(GBRight)},
+ @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Left", @"value": @(GBLeft)},
+ @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Up", @"value": @(GBUp)},
+ @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Down", @"value": @(GBDown)},
+ @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"A", @"value": @(GBA)},
+ @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"B", @"value": @(GBB)},
+ @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Select", @"value": @(GBSelect)},
+ @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Start", @"value": @(GBStart)},
+ @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Turbo", @"value": @(GBTurbo)},
+ @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Rewind", @"value": @(GBRewind)},
+ @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Slow-motion", @"value": @(GBUnderclock)},
+ ]}],
+ };
+ if (@available(iOS 14.0, *)) {
+ UIImage *image = [[UIImage systemImageNamed:element.sfSymbolsName] imageWithTintColor:UIColor.labelColor renderingMode:UIImageRenderingModeAlwaysOriginal];
+ if (image) {
+ item = [item mutableCopy];
+ ((NSMutableDictionary *)item)[@"image"] = image;
+ }
+ }
+ [items addObject:item];
+ }
+
+ UITableViewStyle style = UITableViewStyleGrouped;
+ if (@available(iOS 13.0, *)) {
+ style = UITableViewStyleInsetGrouped;
+ }
+
+ GBSettingsViewController *submenu = [[GBSettingsViewController alloc] initWithStructure:@[@{@"items": items}]
+ title:controller.vendorName
+ style:style];
+ [self.navigationController pushViewController:submenu animated:true];
+}
+
+- (bool)configureGameControllers
+{
+
+ NSMutableArray *items = [NSMutableArray array];
+ for (GCController *controller in [GCController controllers]) {
+ if (!controller.extendedGamepad) continue;
+ NSDictionary *item = @{
+ @"title": controller.vendorName,
+ @"type": typeBlock,
+ @"block": ^bool(void) {
+ [self configureGameController:controller];
+ return true;
+ }
+ };
+ UIImage *image = ImageForController(controller);
+ if (image) {
+ item = [item mutableCopy];
+ ((NSMutableDictionary *)item)[@"image"] = image;
+ }
+
+ [items addObject:item];
+ }
+ if (items.count) {
+ UITableViewStyle style = UITableViewStyleGrouped;
+ if (@available(iOS 13.0, *)) {
+ style = UITableViewStyleInsetGrouped;
+ }
+
+ GBSettingsViewController *submenu = [[GBSettingsViewController alloc] initWithStructure:@[@{@"items": items}]
+ title:@"Configure Game Controllers"
+ style:style];
+ [self.navigationController pushViewController:submenu animated:true];
+ }
+ else {
+ UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"No Controllers Connected"
+ message:@"There are no connected game controllers to configure"
+ preferredStyle:UIAlertControllerStyleAlert];
+ [alert addAction:[UIAlertAction actionWithTitle:@"Close"
+ style:UIAlertActionStyleCancel
+ handler:nil]];
+ [self presentViewController:alert animated:true completion:nil];
+ return false;
+
+ }
+ return true;
+}
+
- (instancetype)initWithStructure:(NSArray *)structure title:(NSString *)title style:(UITableViewStyle)style
{
self = [super initWithStyle:style];
@@ -490,6 +731,14 @@ static NSString const *typeLightTemp = @"typeLightTemp";
}
}
+static id ValueForItem(NSDictionary *item)
+{
+ if (item[@"getter"]) {
+ return ((id(^)(void))item[@"getter"])();
+ }
+ return [[NSUserDefaults standardUserDefaults] objectForKey:item[@"pref"]] ?: @0;
+}
+
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary *item = [self itemForIndexPath:indexPath];
@@ -497,13 +746,13 @@ static NSString const *typeLightTemp = @"typeLightTemp";
UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:nil];
cell.textLabel.text = item[@"title"];
- if (item[@"type"] == typeSubmenu || item[@"type"] == typeOptionSubmenu) {
+ if (item[@"type"] == typeSubmenu || item[@"type"] == typeOptionSubmenu || item[@"type"] == typeBlock) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.selectionStyle = UITableViewCellSelectionStyleBlue;
if (item[@"type"] == typeOptionSubmenu) {
for (NSDictionary *section in item[@"submenu"]) {
for (NSDictionary *item in section[@"items"]) {
- if (item[@"value"] && [([[NSUserDefaults standardUserDefaults] objectForKey:item[@"pref"]] ?: @0) isEqual:item[@"value"]]) {
+ if (item[@"value"] && [ValueForItem(item) isEqual:item[@"value"]]) {
cell.detailTextLabel.text = item[@"title"];
break;
}
@@ -512,7 +761,7 @@ static NSString const *typeLightTemp = @"typeLightTemp";
}
}
else if (item[@"type"] == typeRadio) {
- if ([([[NSUserDefaults standardUserDefaults] objectForKey:item[@"pref"]] ?: @0) isEqual:item[@"value"]]) {
+ if ([ValueForItem(item) isEqual:item[@"value"]]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
}
@@ -569,21 +818,12 @@ static NSString const *typeLightTemp = @"typeLightTemp";
slider.value = [[NSUserDefaults standardUserDefaults] floatForKey:item[@"pref"]];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
- if (item[@"minImage"] && item[@"maxImage"]) {
- if ([item[@"minImage"] isKindOfClass:[UIImage class]]) {
- slider.minimumValueImage = item[@"minImage"];
+ if (@available(iOS 13.0, *)) {
+ if (item[@"minImage"] && item[@"maxImage"]) {
+ slider.minimumValueImage = [UIImage systemImageNamed:item[@"minImage"]] ?: [[UIImage imageNamed:item[@"minImage"]] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+ slider.maximumValueImage = [UIImage systemImageNamed:item[@"maxImage"]] ?: [[UIImage imageNamed:item[@"maxImage"]] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
+ [GBSettingsViewController fixSliderTint:slider];
}
- else if (@available(iOS 13.0, *)) {
- slider.minimumValueImage = [UIImage systemImageNamed:item[@"minImage"]];
- }
-
- if ([item[@"maxImage"] isKindOfClass:[UIImage class]]) {
- slider.maximumValueImage = item[@"maxImage"];
- }
- else if (@available(iOS 13.0, *)) {
- slider.maximumValueImage = [UIImage systemImageNamed:item[@"maxImage"]];
- }
- [GBSettingsViewController fixSliderTint:slider];
}
id block = ^(){
@@ -592,6 +832,9 @@ static NSString const *typeLightTemp = @"typeLightTemp";
objc_setAssociatedObject(cell, "RetainedBlock", block, OBJC_ASSOCIATION_RETAIN);
[slider addTarget:block action:@selector(invoke) forControlEvents:UIControlEventValueChanged];
+ if (item[@"previewBlock"]) {
+ [slider addTarget:item[@"previewBlock"] action:@selector(invoke) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside | UIControlEventTouchDown];
+ }
}
if ([self followingItemForIndexPath:indexPath][@"type"] == typeSeparator) {
@@ -606,14 +849,20 @@ static NSString const *typeLightTemp = @"typeLightTemp";
NSDictionary *item = [self itemForIndexPath:indexPath];
if (item[@"type"] == typeSubmenu || item[@"type"] == typeOptionSubmenu) {
UITableViewStyle style = UITableViewStyleGrouped;
- if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) {
- if (@available(iOS 13.0, *)) {
- style = UITableViewStyleInsetGrouped;
- }
+ if (@available(iOS 13.0, *)) {
+ style = UITableViewStyleInsetGrouped;
+ }
+ UITableViewController *submenu = nil;
+
+ if (item[@"class"]) {
+ submenu = [(UITableViewController *)[item[@"class"] alloc] initWithStyle:style];
+ submenu.title = item[@"title"];
+ }
+ else {
+ submenu = [[GBSettingsViewController alloc] initWithStructure:item[@"submenu"]
+ title:item[@"title"]
+ style:style];
}
- GBSettingsViewController *submenu = [[GBSettingsViewController alloc] initWithStructure:item[@"submenu"]
- title:item[@"title"]
- style:style];
if (_detailsNavigation) {
[_detailsNavigation setViewControllers:@[submenu] animated:false];
}
@@ -623,9 +872,19 @@ static NSString const *typeLightTemp = @"typeLightTemp";
return indexPath;
}
else if (item[@"type"] == typeRadio) {
- [[NSUserDefaults standardUserDefaults] setObject:item[@"value"] forKey:item[@"pref"]];
+ if (item[@"setter"]) {
+ ((void(^)(id))item[@"setter"])(item[@"value"]);
+ }
+ else {
+ [[NSUserDefaults standardUserDefaults] setObject:item[@"value"] forKey:item[@"pref"]];
+ }
[self.tableView reloadData];
}
+ else if (item[@"type"] == typeBlock) {
+ if (((bool(^)(GBSettingsViewController *))item[@"block"])(self)) {
+ return indexPath;
+ }
+ }
return nil;
}
@@ -649,4 +908,36 @@ static NSString const *typeLightTemp = @"typeLightTemp";
[self.tableView reloadData];
}
+- (void)preloadThemePreviews
+{
+ /* These take some time to render, preload them when loading the root controller */
+ _themes = [GBThemesViewController themes];
+ double time = 0;
+ for (NSArray *section in _themes) {
+ for (GBTheme *theme in section) {
+ /* Sadly they can't be safely rendered outside the main thread, but we can
+ queue each of them individually to not block the main quote for too long. */
+ time += 0.1;
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, time * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
+ [theme verticalPreview];
+ [theme horizontalPreview];
+ });
+ }
+ }
+}
+
++ (GBTheme *)themeNamed:(NSString *)name
+{
+ NSArray *themes = [GBThemesViewController themes];
+ for (NSArray *section in themes) {
+ for (GBTheme *theme in section) {
+ if ([theme.name isEqualToString:name]) {
+ return theme;
+ }
+ }
+ }
+
+ return [themes.firstObject firstObject];
+}
+
@end
diff --git a/iOS/GBTheme.h b/iOS/GBTheme.h
new file mode 100644
index 0000000..e35ad59
--- /dev/null
+++ b/iOS/GBTheme.h
@@ -0,0 +1,26 @@
+#import
+
+@interface GBTheme : NSObject
+@property (readonly, direct) UIColor *brandColor;
+@property (readonly, direct) UIColor *backgroundGradientTop;
+@property (readonly, direct) UIColor *backgroundGradientBottom;
+@property (readonly, direct) UIColor *bezelsGradientTop;
+@property (readonly, direct) UIColor *bezelsGradientBottom;
+
+
+@property (readonly, direct) NSString *name;
+
+@property (readonly, direct) bool renderingPreview; // Kind of a hack
+
+@property (readonly, direct) UIImage *horizontalPreview;
+@property (readonly, direct) UIImage *verticalPreview;
+
+@property (readonly, direct) bool isDark;
+
+- (instancetype)initDefaultTheme __attribute__((objc_direct));
+- (instancetype)initDarkTheme __attribute__((objc_direct));
+
+
+- (UIImage *)imageNamed:(NSString *)name __attribute__((objc_direct));
+
+@end
diff --git a/iOS/GBTheme.m b/iOS/GBTheme.m
new file mode 100644
index 0000000..b015454
--- /dev/null
+++ b/iOS/GBTheme.m
@@ -0,0 +1,226 @@
+#import "GBTheme.h"
+#import "GBVerticalLayout.h"
+#import "GBHorizontalLayout.h"
+#import "GBBackgroundView.h"
+
+@interface GBLazyObject : NSProxy
+@end
+
+@implementation GBLazyObject
+{
+ id _target;
+ id (^_constructor)(void);
+}
+
+
+- (instancetype)initWithConstructor:(id (^)(void))constructor
+{
+ _constructor = constructor;
+ return self;
+}
+
+- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
+{
+ if (GB_likely(!_target)) {
+ _target = _constructor();
+ _constructor = nil;
+ }
+ return [_target methodSignatureForSelector:sel];
+}
+
+- (void)forwardInvocation:(NSInvocation *)invocation
+{
+ if (GB_likely(!_target)) {
+ _target = _constructor();
+ _constructor = nil;
+ }
+ invocation.target = _target;
+ [invocation invoke];
+}
+
+- (instancetype)self
+{
+ if (GB_likely(!_target)) {
+ _target = _constructor();
+ _constructor = nil;
+ }
+ return _target;
+}
+
+@end
+
+#define MakeColor(r, g, b) [UIColor colorWithRed:(r) / 255.0 green:(g) / 255.0 blue:(b) / 255.0 alpha:1.0]
+
+__attribute__((objc_direct_members))
+@implementation GBTheme
+{
+ NSDictionary *_imageOverrides;
+ UIImage *_horizontalPreview;
+ UIImage *_verticalPreview;
+}
+
+
+// Assumes the image has a purple hue
++ (UIImage *)_recolorImage:(UIImage *)image withColor:(UIColor *)color
+{
+ double scale = image.scale;
+
+ image = [UIImage imageWithCGImage:image.CGImage scale:1.0 orientation:UIImageOrientationUp];
+
+ CIImage *ciImage = [CIImage imageWithCGImage:image.CGImage];
+ CIFilter *filter = [CIFilter filterWithName:@"CIColorMatrix"];
+ double r, g, b;
+ [color getRed:&r green:&g blue:&b alpha:NULL];
+
+ [filter setDefaults];
+ [filter setValue:ciImage forKey:@"inputImage"];
+
+ [filter setValue:[CIVector vectorWithX:r * 1.34 Y:1 - r Z:0 W:0] forKey:@"inputRVector"];
+ [filter setValue:[CIVector vectorWithX:g * 1.34 Y:1 - g Z:0 W:0] forKey:@"inputGVector"];
+ [filter setValue:[CIVector vectorWithX:b * 1.34 Y:1 - b Z:0 W:0] forKey:@"inputBVector"];
+
+ [filter setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:1] forKey:@"inputAVector"];
+
+
+ CIContext *context = [CIContext context];
+ CGImageRef cgImage = [context createCGImage:filter.outputImage fromRect:filter.outputImage.extent];
+ image = [UIImage imageWithCGImage:cgImage scale:scale orientation:0];
+ CGImageRelease(cgImage);
+
+ return image;
+}
+
++ (UIImage *)recolorImage:(UIImage *)image withColor:(UIColor *)color
+{
+ return (id)[[GBLazyObject alloc] initWithConstructor:^id{
+ return [self _recolorImage:image withColor:color];
+ }];
+}
+
+- (instancetype)initDefaultTheme
+{
+ self = [super init];
+
+ _brandColor = [UIColor colorWithRed:0 / 255.0 green:70 / 255.0 blue:141 / 255.0 alpha:1.0];
+ _backgroundGradientTop = [UIColor colorWithRed:192 / 255.0 green:195 / 255.0 blue:199 / 255.0 alpha:1.0];
+ _backgroundGradientBottom = [UIColor colorWithRed:174 / 255.0 green:176 / 255.0 blue:180 / 255.0 alpha:1.0];
+
+ _bezelsGradientTop = [UIColor colorWithWhite:53 / 255.0 alpha:1.0];
+ _bezelsGradientBottom = [UIColor colorWithWhite:45 / 255.0 alpha:1.0];
+
+ _name = @"SameBoy";
+
+ return self;
+}
+
+- (void)setupBackgroundWithColor:(uint32_t)color
+{
+ uint8_t r = color >> 16;
+ uint8_t g = color >> 8;
+ uint8_t b = color;
+
+ _backgroundGradientTop = MakeColor(r, g, b);
+ _backgroundGradientBottom = [UIColor colorWithRed:pow(r / 255.0, 1.125) green:pow(g / 255.0, 1.125) blue:pow(b / 255.0, 1.125) alpha:1.0];
+}
+
+- (void)setupButtonsWithColor:(UIColor *)color
+{
+ _imageOverrides = @{
+ @"button": [GBTheme recolorImage:[UIImage imageNamed:@"button"] withColor:color],
+ @"buttonPressed": [GBTheme recolorImage:[UIImage imageNamed:@"buttonPressed"] withColor:color],
+
+ @"dpad": [GBTheme recolorImage:[UIImage imageNamed:@"dpad-tint"] withColor:color],
+ @"swipepad": [GBTheme recolorImage:[UIImage imageNamed:@"swipepad-tint"] withColor:color],
+
+ @"button2": [GBTheme recolorImage:[UIImage imageNamed:@"button2-tint"] withColor:color],
+ @"button2Pressed": [GBTheme recolorImage:[UIImage imageNamed:@"button2Pressed-tint"] withColor:color],
+ };
+}
+
+- (instancetype)initDarkTheme
+{
+ self = [super init];
+
+ [self setupBackgroundWithColor:0x181c23];
+
+ _brandColor = [UIColor colorWithRed:0 / 255.0 green:70 / 255.0 blue:141 / 255.0 alpha:1.0];
+ _bezelsGradientTop = [UIColor colorWithWhite:53 / 255.0 alpha:1.0];
+ _bezelsGradientBottom = [UIColor colorWithWhite:45 / 255.0 alpha:1.0];
+
+ [self setupButtonsWithColor:MakeColor(0x08, 0x0c, 0x12)];
+
+ _name = @"SameBoy Dark";
+
+ return self;
+}
+
+
+- (bool)isDark
+{
+ double r, g, b;
+ [_backgroundGradientTop getRed:&r green:&g blue:&b alpha:NULL];
+ if (r > 0.25) return false;
+ if (g > 0.25) return false;
+ if (b > 0.25) return false;
+ return true;
+}
+
+- (UIImage *)imageNamed:(NSString *)name
+{
+ UIImage *ret = _imageOverrides[name].self ?: [UIImage imageNamed:name];
+ if (!ret) {
+ if ([name isEqual:@"buttonA"] || [name isEqual:@"buttonB"]) {
+ return [self imageNamed:@"button"];
+ }
+ if ([name isEqual:@"buttonAPressed"] || [name isEqual:@"buttonBPressed"]) {
+ return [self imageNamed:@"buttonPressed"];
+ }
+ }
+ return ret;
+}
+
+- (UIImage *)horizontalPreview
+{
+ if (_horizontalPreview) return _horizontalPreview;
+ _renderingPreview = true;
+ GBLayout *layout = [[GBHorizontalLayout alloc] initWithTheme:self];
+ _renderingPreview = false;
+ GBBackgroundView *view = [[GBBackgroundView alloc] initWithLayout:layout];
+ [view enterPreviewMode:false];
+ view.usesSwipePad = [[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipePad"];
+ view.layout = layout;
+ view.bounds = CGRectMake(0, 0,
+ MAX(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height),
+ MIN(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height));
+ UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:(CGSize){view.bounds.size.width / 8,
+ view.bounds.size.height / 8,
+ }];
+ return _horizontalPreview = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) {
+ CGContextScaleCTM(UIGraphicsGetCurrentContext(), 1 / 8.0, 1 / 8.0);
+ [view.layer renderInContext:rendererContext.CGContext];
+ }];
+}
+
+- (UIImage *)verticalPreview
+{
+ if (_verticalPreview) return _verticalPreview;
+ _renderingPreview = true;
+ GBLayout *layout = [[GBVerticalLayout alloc] initWithTheme:self];
+ _renderingPreview = false;
+ GBBackgroundView *view = [[GBBackgroundView alloc] initWithLayout:layout];
+ [view enterPreviewMode:false];
+ view.usesSwipePad = [[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipePad"];
+ view.layout = layout;
+ view.bounds = CGRectMake(0, 0,
+ MIN(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height),
+ MAX(UIScreen.mainScreen.bounds.size.width, UIScreen.mainScreen.bounds.size.height));
+ UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:(CGSize){view.bounds.size.width / 8,
+ view.bounds.size.height / 8,
+ }];
+ return _verticalPreview = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) {
+ CGContextScaleCTM(UIGraphicsGetCurrentContext(), 1 / 8.0, 1 / 8.0);
+ [view.layer renderInContext:rendererContext.CGContext];
+ }];
+}
+
+@end
diff --git a/iOS/GBThemePreviewController.h b/iOS/GBThemePreviewController.h
new file mode 100644
index 0000000..bac864e
--- /dev/null
+++ b/iOS/GBThemePreviewController.h
@@ -0,0 +1,7 @@
+#import
+#import "GBTheme.h"
+
+@interface GBThemePreviewController : UIViewController
+- (instancetype)initWithTheme:(GBTheme *)theme;
+@end
+
diff --git a/iOS/GBThemePreviewController.m b/iOS/GBThemePreviewController.m
new file mode 100644
index 0000000..d1d99f4
--- /dev/null
+++ b/iOS/GBThemePreviewController.m
@@ -0,0 +1,116 @@
+#import "GBThemePreviewController.h"
+#import "GBVerticalLayout.h"
+#import "GBHorizontalLayout.h"
+#import "GBBackgroundView.h"
+
+@implementation GBThemePreviewController
+{
+ GBHorizontalLayout *_horizontalLayout;
+ GBVerticalLayout *_verticalLayout;
+ GBBackgroundView *_backgroundView;
+}
+
+- (instancetype)initWithTheme:(GBTheme *)theme
+{
+ self = [super init];
+ _horizontalLayout = [[GBHorizontalLayout alloc] initWithTheme:theme];
+ _verticalLayout = [[GBVerticalLayout alloc] initWithTheme:theme];
+ return self;
+}
+
+- (void)viewDidLoad
+{
+ [super viewDidLoad];
+ _backgroundView = [[GBBackgroundView alloc] initWithLayout:_verticalLayout];
+ self.view.backgroundColor = _verticalLayout.theme.backgroundGradientBottom;
+ [self.view addSubview:_backgroundView];
+ [_backgroundView enterPreviewMode:true];
+ _backgroundView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin;
+
+ [self willRotateToInterfaceOrientation:self.interfaceOrientation duration:0];
+ [self.view addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
+ action:@selector(showPopup)]];
+}
+
+- (UIModalPresentationStyle)modalPresentationStyle
+{
+ return UIModalPresentationFullScreen;
+}
+
+- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration
+{
+ GBLayout *layout = _horizontalLayout;
+ if (orientation == UIInterfaceOrientationPortrait || orientation == UIInterfaceOrientationPortraitUpsideDown) {
+ layout = _verticalLayout;
+ }
+ _backgroundView.frame = [layout viewRectForOrientation:orientation];
+ _backgroundView.layout = layout;
+}
+
+- (void)showPopup
+{
+ UIAlertController *alert = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"Apply “%@” as the current theme?", _verticalLayout.theme.name]
+ message:nil
+ preferredStyle:[UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad?
+ UIAlertControllerStyleAlert : UIAlertControllerStyleActionSheet];
+ if (false) {
+ // No supporter-only themes outside the App Store release
+ }
+ else {
+ [alert addAction:[UIAlertAction actionWithTitle:@"Apply Theme"
+ style:UIAlertActionStyleDefault
+ handler:^(UIAlertAction *action) {
+ [[NSUserDefaults standardUserDefaults] setObject:_verticalLayout.theme.name forKey:@"GBInterfaceTheme"];
+ [[self presentingViewController] dismissViewControllerAnimated:true completion:nil];
+ }]];
+ }
+ [alert addAction:[UIAlertAction actionWithTitle:@"Exit Preview"
+ style:UIAlertActionStyleDefault
+ handler:^(UIAlertAction *action) {
+ [[self presentingViewController] dismissViewControllerAnimated:true completion:nil];
+ }]];
+ [self presentViewController:alert animated:true completion:^{
+ alert.view.superview.userInteractionEnabled = true;
+ [alert.view.superview addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
+ action:@selector(dismissPopup)]];
+ for (UIView *view in alert.view.superview.subviews) {
+ if (view.backgroundColor) {
+ view.userInteractionEnabled = true;
+ [view addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
+ action:@selector(dismissPopup)]];
+
+ }
+ }
+ }];
+}
+
+- (void)dismissViewController
+{
+ [self dismissViewControllerAnimated:true completion:nil];
+}
+
+- (void)dismissPopup
+{
+ [self dismissViewControllerAnimated:true completion:nil];
+}
+
+- (UIStatusBarStyle)preferredStatusBarStyle
+{
+ if (@available(iOS 13.0, *)) {
+ return _verticalLayout.theme.isDark? UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent;
+ }
+ return _verticalLayout.theme.isDark? UIStatusBarStyleLightContent : UIStatusBarStyleDefault;
+}
+
+- (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;
+}
+
+@end
diff --git a/iOS/GBThemesViewController.h b/iOS/GBThemesViewController.h
new file mode 100644
index 0000000..348597e
--- /dev/null
+++ b/iOS/GBThemesViewController.h
@@ -0,0 +1,7 @@
+#import
+#import "GBTheme.h"
+
+@interface GBThemesViewController : UITableViewController
++ (NSArray *> *)themes;
+@end
+
diff --git a/iOS/GBThemesViewController.m b/iOS/GBThemesViewController.m
new file mode 100644
index 0000000..5d0e223
--- /dev/null
+++ b/iOS/GBThemesViewController.m
@@ -0,0 +1,104 @@
+#import "GBThemesViewController.h"
+#import "GBThemePreviewController.h"
+#import "GBTheme.h"
+
+@interface GBThemesViewController ()
+
+@end
+
+@implementation GBThemesViewController
+{
+ NSArray *> *_themes;
+}
+
++ (NSArray *> *)themes
+{
+ static __weak NSArray *> *cache = nil;
+ if (cache) return cache;
+ id ret = @[
+ @[
+ [[GBTheme alloc] initDefaultTheme],
+ [[GBTheme alloc] initDarkTheme],
+ ],
+ ];
+ cache = ret;
+ return ret;
+}
+
+- (void)viewDidLoad
+{
+ [super viewDidLoad];
+
+ _themes = [[self class] themes];
+}
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+ return _themes.count;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+ return _themes[section].count;
+}
+
+- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ return 60;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+
+ UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
+ GBTheme *theme = _themes[indexPath.section][indexPath.row];
+ cell.textLabel.text = theme.name;
+
+ cell.accessoryType = [[[NSUserDefaults standardUserDefaults] stringForKey:@"GBInterfaceTheme"] isEqual:theme.name]? UITableViewCellAccessoryCheckmark : UITableViewCellAccessoryNone;
+ bool horizontal = self.interfaceOrientation >= UIInterfaceOrientationLandscapeRight;
+ UIImage *preview = horizontal? [theme horizontalPreview] : [theme verticalPreview];
+ UIGraphicsBeginImageContextWithOptions((CGSize){60, 60}, false, self.view.window.screen.scale);
+ unsigned width = 60;
+ unsigned height = 56;
+ if (horizontal) {
+ height = round(preview.size.height / preview.size.width * 60);
+ }
+ else {
+ width = round(preview.size.width / preview.size.height * 56);
+ }
+ UIBezierPath *mask = [UIBezierPath bezierPathWithRoundedRect:CGRectMake((60 - width) / 2, (60 - height) / 2, width, height) cornerRadius:4];
+ [mask addClip];
+ [preview drawInRect:mask.bounds];
+ if (@available(iOS 13.0, *)) {
+ [[UIColor tertiaryLabelColor] set];
+ }
+ else {
+ [[UIColor colorWithWhite:0 alpha:0.5] set];
+ }
+ [mask stroke];
+ cell.imageView.image = UIGraphicsGetImageFromCurrentImageContext();
+ UIGraphicsEndImageContext();
+
+
+ return cell;
+}
+
+- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
+{
+ [self.tableView reloadData];
+ [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
+}
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+ GBTheme *theme = _themes[indexPath.section][indexPath.row];
+ GBThemePreviewController *preview = [[GBThemePreviewController alloc] initWithTheme:theme];
+ [self presentViewController:preview animated:true completion:nil];
+}
+
+- (void)viewWillAppear:(BOOL)animated
+{
+ [self.tableView reloadData];
+}
+
+@end
diff --git a/iOS/GBVerticalLayout.m b/iOS/GBVerticalLayout.m
index 9ee26ae..44797d4 100644
--- a/iOS/GBVerticalLayout.m
+++ b/iOS/GBVerticalLayout.m
@@ -3,9 +3,9 @@
@implementation GBVerticalLayout
-- (instancetype)init
+- (instancetype)initWithTheme:(GBTheme *)theme
{
- self = [super init];
+ self = [super initWithTheme:theme];
if (!self) return nil;
CGSize resolution = self.resolution;
@@ -59,16 +59,24 @@
double controlsTop = self.dpadLocation.y - 80 * self.factor;
double middleSpace = self.bLocation.x - buttonRadius - (self.dpadLocation.x + 80 * self.factor);
- UIGraphicsBeginImageContextWithOptions(resolution, true, 1);
+ if (theme.renderingPreview) {
+ UIGraphicsBeginImageContextWithOptions((CGSize){resolution.width / 8, resolution.height / 8}, true, 1);
+ CGContextScaleCTM(UIGraphicsGetCurrentContext(), 1 / 8.0, 1 / 8.0);
+ }
+ else {
+ UIGraphicsBeginImageContextWithOptions(resolution, true, 1);
+ }
[self drawBackground];
[self drawScreenBezels];
- if (controlsTop - controlAreaStart > 24 * self.factor + screenBorderWidth * 2 ||
- middleSpace > 160 * self.factor) {
- [self drawLogoInVerticalRange:(NSRange){controlAreaStart + screenBorderWidth, 24 * self.factor}];
- }
-
- [self drawLabels];
+ [self drawThemedLabelsWithBlock:^{
+ if (controlsTop - controlAreaStart > 24 * self.factor + screenBorderWidth * 2 ||
+ middleSpace > 160 * self.factor) {
+ [self drawLogoInVerticalRange:(NSRange){controlAreaStart + screenBorderWidth, 24 * self.factor}];
+ }
+
+ [self drawLabels];
+ }];
self.background = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
diff --git a/iOS/GBViewController.h b/iOS/GBViewController.h
index a846a80..096a6b5 100644
--- a/iOS/GBViewController.h
+++ b/iOS/GBViewController.h
@@ -22,6 +22,6 @@ typedef enum {
- (void)openSettings;
- (void)showAbout;
- (void)saveStateToFile:(NSString *)file;
-- (void)loadStateFromFile:(NSString *)file;
+- (bool)loadStateFromFile:(NSString *)file;
@property (nonatomic) GBRunMode runMode;
@end
diff --git a/iOS/GBViewController.m b/iOS/GBViewController.m
index ac01e32..c0c8265 100644
--- a/iOS/GBViewController.m
+++ b/iOS/GBViewController.m
@@ -12,6 +12,7 @@
#import "GBAboutController.h"
#import "GBSettingsViewController.h"
#import "GBStatesViewController.h"
+#import "GCExtendedGamepad+AllElements.h"
#import
#import
@@ -25,6 +26,7 @@
bool _rewind;
bool _rewindOver;
bool _romLoaded;
+ bool _swappingROM;
UIInterfaceOrientation _orientation;
GBHorizontalLayout *_horizontalLayout;
@@ -50,6 +52,8 @@
NSTimer *_disableCameraTimer;
AVCaptureDevicePosition _cameraPosition;
UIButton *_cameraPositionButton;
+
+ __weak GCController *_lastController;
}
static void loadBootROM(GB_gameboy_t *gb, GB_boot_rom_t type)
@@ -177,12 +181,22 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
_window.rootViewController = self;
[_window makeKeyAndVisible];
- _window.backgroundColor = [UIColor colorWithRed:174 / 255.0 green:176 / 255.0 blue:180 / 255.0 alpha:1.0];
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-retain-cycles"
+ [self addDefaultObserver:^(id newValue) {
+ GBTheme *theme = [GBSettingsViewController themeNamed:newValue];
+ _horizontalLayout = [[GBHorizontalLayout alloc] initWithTheme:theme];
+ _verticalLayout = [[GBVerticalLayout alloc] initWithTheme:theme];
+
+ [self willRotateToInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation
+ duration:0];
+ [_backgroundView reloadThemeImages];
+
+ [self setNeedsStatusBarAppearanceUpdate];
+ } forKey:@"GBInterfaceTheme"];
+#pragma clang diagnostic pop
- _horizontalLayout = [[GBHorizontalLayout alloc] init];
- _verticalLayout = [[GBVerticalLayout alloc] init];
-
- _backgroundView = [[GBBackgroundView alloc] init];
+ _backgroundView = [[GBBackgroundView alloc] initWithLayout:_verticalLayout];
[_window addSubview:_backgroundView];
self.view = _backgroundView;
@@ -206,11 +220,11 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[self addDefaultObserver:^(id newValue) {
backgroundView.usesSwipePad = [newValue boolValue];
} forKey:@"GBSwipeDpad"];
-
+
[self willRotateToInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation
duration:0];
-
+
_audioLock = [[NSCondition alloc] init];
@@ -226,10 +240,10 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
_motionManager = [[CMMotionManager alloc] init];
_cameraPosition = AVCaptureDevicePositionBack;
_cameraPositionButton = [[UIButton alloc] initWithFrame:CGRectMake(8,
- _backgroundView.bounds.size.height - 8 - 32,
+ 0,
32,
32)];
- _cameraPositionButton.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin;
+ [self didRotateFromInterfaceOrientation:[UIApplication sharedApplication].statusBarOrientation];
if (@available(iOS 13.0, *)) {
[_cameraPositionButton setImage:[UIImage systemImageNamed:@"camera.rotate"
withConfiguration:[UIImageSymbolConfiguration configurationWithScale:UIImageSymbolScaleLarge]]
@@ -252,9 +266,138 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[UNUserNotificationCenter currentNotificationCenter].delegate = self;
[self verifyEntitlements];
+ [self setControllerHandlers];
+ [[NSNotificationCenter defaultCenter] addObserver:self
+ selector:@selector(setControllerHandlers)
+ name:GCControllerDidConnectNotification
+ object:nil];
+
return true;
}
+
+- (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];
+
+ GBButton gbButton = [GBSettingsViewController controller:controller convertUsageToButton:usage];
+ static const double analogThreshold = 0.0625;
+ switch (gbButton) {
+ case GBRight:
+ case GBLeft:
+ case GBUp:
+ case GBDown:
+ 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);
+ }
+ }
+ else {
+ [self setRunMode:GBRunModeNormal];
+ }
+ 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);
+ }
+ }
+ else {
+ [self setRunMode:GBRunModeNormal];
+ }
+ break;
+ case GBUnderclock:
+ if (button.value > analogThreshold) {
+ if (button.isAnalog && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
+ GB_set_clock_multiplier(&_gb, 1 - ((button.value - analogThreshold) / (1 - analogThreshold) * 0.75));
+ }
+ else {
+ GB_set_clock_multiplier(&_gb, 0.5);
+ }
+ }
+ else {
+ GB_set_clock_multiplier(&_gb, 1.0);
+ }
+ break;
+ default: break;
+ }
+}
+
+- (void)controller:(GCController *)controller axisChanged:(GCControllerDirectionPad *)axis usage:(GBControllerUsage)usage
+{
+ [self updateLastController:controller];
+
+ GB_set_key_state(&_gb, GB_KEY_LEFT, axis.left.value > 0.5);
+ GB_set_key_state(&_gb, GB_KEY_RIGHT, axis.right.value > 0.5);
+ GB_set_key_state(&_gb, GB_KEY_UP, axis.up.value > 0.5);
+ GB_set_key_state(&_gb, GB_KEY_DOWN, axis.down.value > 0.5);
+}
+
+- (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
{
/*
@@ -265,14 +408,12 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
void *libxpc = dlopen("/usr/lib/system/libxpc.dylib", RTLD_NOW);
extern xpc_object_t xpc_copy_entitlements_for_self$(void);
- extern void xpc_release$ (xpc_object_t *object);
- extern const char *xpc_dictionary_get_string$ (xpc_object_t *object, const char *key);
+ 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_release$) *xpc_release = dlsym(libxpc, "xpc_release");
typeof(xpc_dictionary_get_string$) *xpc_dictionary_get_string = dlsym(libxpc, "xpc_dictionary_get_string");
- if (!xpc_copy_entitlements_for_self || !xpc_release || !xpc_dictionary_get_string) return;
+ if (!xpc_copy_entitlements_for_self || !xpc_dictionary_get_string) return;
xpc_object_t entitlements = xpc_copy_entitlements_for_self();
if (!entitlements) return;
@@ -282,7 +423,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
const char *_teamIdentifier = xpc_dictionary_get_string(entitlements, "com.apple.developer.team-identifier");
NSString *teamIdentifier = _teamIdentifier? @(_teamIdentifier) : nil;
- xpc_release(entitlements);
+ CFRelease(entitlements);
if (!entIdentifier) { // No identifier. Installed using a jailbreak, we're fine.
return;
@@ -317,19 +458,22 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[UIImagePNGRepresentation(screenshot) writeToFile:[file stringByAppendingPathExtension:@"png"] atomically:false];
}
-- (void)loadStateFromFile:(NSString *)file
+- (bool)loadStateFromFile:(NSString *)file
{
+ [self stop];
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);
}
- GB_load_state(&_gb, file.fileSystemRepresentation);
+ return GB_load_state(&_gb, file.fileSystemRepresentation) == 0;
}
+ return false;
}
- (void)loadROM
{
+ _swappingROM = true;
[self stop];
GBROMManager *romManager = [GBROMManager sharedManager];
if (romManager.romFile) {
@@ -340,17 +484,28 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
else {
_romLoaded = GB_load_rom(&_gb, romManager.romFile.fileSystemRepresentation) == 0;
}
- GB_rewind_reset(&_gb);
if (_romLoaded) {
GB_reset(&_gb);
GB_load_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation);
- [self loadStateFromFile:[GBROMManager sharedManager].autosaveStateFile];
+ 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) && !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;
}
_gbView.hidden = !_romLoaded;
+ _swappingROM = false;
}
- (void)applicationDidBecomeActive:(UIApplication *)application
@@ -411,8 +566,23 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
model = [[NSUserDefaults standardUserDefaults] integerForKey:items[i].settingKey];
}
[controller addOption:items[i].title withCheckmark:items[i].checked action:^{
+ [self stop];
GB_switch_model_and_reset(&_gb, model);
- [self start];
+ 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";
@@ -473,6 +643,15 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
}];
}
+- (void)setNeedsUpdateOfSupportedInterfaceOrientations
+{
+ /* Hack. Some view controllers dismiss without calling the method above. */
+ [super setNeedsUpdateOfSupportedInterfaceOrientations];
+ if (!self.presentedViewController) {
+ [self start];
+ }
+}
+
- (void)dismissViewController
{
[self dismissViewControllerAnimated:true completion:nil];
@@ -492,6 +671,15 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
}
_backgroundView.frame = [layout viewRectForOrientation:orientation];
_backgroundView.layout = layout;
+ if (!self.presentedViewController) {
+ _window.backgroundColor = layout.theme.backgroundGradientBottom;
+ }
+}
+
+- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
+{
+ UIEdgeInsets insets = self.window.safeAreaInsets;
+ _cameraPositionButton.frame = CGRectMake(insets.left + 8, _backgroundView.bounds.size.height - 8 - insets.bottom - 32, 32, 32);
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
@@ -529,6 +717,15 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
}
}
+- (UIStatusBarStyle)preferredStatusBarStyle
+{
+ if (@available(iOS 13.0, *)) {
+ return _verticalLayout.theme.isDark? UIStatusBarStyleLightContent : UIStatusBarStyleDarkContent;
+ }
+ return _verticalLayout.theme.isDark? UIStatusBarStyleLightContent : UIStatusBarStyleDefault;
+}
+
+
- (void)preRun
{
GB_set_pixels_output(&_gb, _gbView.pixels);
@@ -569,8 +766,16 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp)
[_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) {
@@ -683,16 +888,27 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
- (void)postRun
{
[_audioLock lock];
- memset(_audioBuffer, 0, (_audioBufferSize - _audioBufferPosition) * sizeof(*_audioBuffer));
+ if (_audioBuffer) {
+ memset(_audioBuffer, 0, (_audioBufferSize - _audioBufferPosition) * sizeof(*_audioBuffer));
+ }
_audioBufferPosition = _audioBufferNeeded;
[_audioLock signal];
[_audioLock unlock];
[_audioClient stop];
_audioClient = nil;
- GB_save_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation);
- [self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile];
+ if (!_swappingROM) {
+ GB_save_battery(&_gb, [GBROMManager sharedManager].batterySaveFile.fileSystemRepresentation);
+ [self saveStateToFile:[GBROMManager sharedManager].autosaveStateFile];
+ }
[[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);
@@ -826,7 +1042,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
return [GBROMManager sharedManager].currentROM != nil;
}
-- (void)setRunMode:(GBRunMode)runMode
+- (void)setRunMode:(GBRunMode)runMode ignoreDynamicSpeed:(bool)ignoreDynamicSpeed
{
if (runMode == GBRunModeRewind && _rewindOver) {
runMode = GBRunModePaused;
@@ -844,7 +1060,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
_rewindOver = false;
}
- if (_runMode == GBRunModeNormal || ![[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"]) {
+ if (_runMode == GBRunModeNormal || !([[NSUserDefaults standardUserDefaults] boolForKey:@"GBDynamicSpeed"] && !ignoreDynamicSpeed)) {
if (_runMode == GBRunModeTurbo) {
double multiplier = [[NSUserDefaults standardUserDefaults] doubleForKey:@"GBTurboSpeed"];
GB_set_turbo_mode(&_gb, multiplier == 1, false);
@@ -857,6 +1073,11 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response
}
}
+- (void)setRunMode:(GBRunMode)runMode
+{
+ [self setRunMode:runMode ignoreDynamicSpeed:false];
+}
+
- (AVCaptureDevice *)captureDevice
{
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
diff --git a/iOS/GCExtendedGamepad+AllElements.h b/iOS/GCExtendedGamepad+AllElements.h
new file mode 100644
index 0000000..427d5eb
--- /dev/null
+++ b/iOS/GCExtendedGamepad+AllElements.h
@@ -0,0 +1,25 @@
+#import
+
+typedef enum {
+ GBUsageDpad,
+ GBUsageButtonA,
+ GBUsageButtonB,
+ GBUsageButtonX,
+ GBUsageButtonY,
+ GBUsageButtonMenu,
+ GBUsageButtonOptions,
+ GBUsageButtonHome,
+ GBUsageLeftThumbstick,
+ GBUsageRightThumbstick,
+ GBUsageLeftShoulder,
+ GBUsageRightShoulder,
+ GBUsageLeftTrigger,
+ GBUsageRightTrigger,
+ GBUsageLeftThumbstickButton,
+ GBUsageRightThumbstickButton,
+ GBUsageTouchpadButton,
+} GBControllerUsage;
+
+@interface GCExtendedGamepad (AllElements)
+- (NSDictionary *)elementsDictionary;
+@end
diff --git a/iOS/GCExtendedGamepad+AllElements.m b/iOS/GCExtendedGamepad+AllElements.m
new file mode 100644
index 0000000..006ea3a
--- /dev/null
+++ b/iOS/GCExtendedGamepad+AllElements.m
@@ -0,0 +1,48 @@
+#import "GCExtendedGamepad+AllElements.h"
+#import
+
+@implementation GCExtendedGamepad (AllElements)
+
+- (NSDictionary *)elementsDictionary;
+{
+ NSMutableDictionary *ret = [NSMutableDictionary dictionary];
+ if (self.dpad) ret[@(GBUsageDpad)] = self.dpad;
+ if (self.buttonA) ret[@(GBUsageButtonA)] = self.buttonA;
+ if (self.buttonB) ret[@(GBUsageButtonB)] = self.buttonB;
+ if (self.buttonX) ret[@(GBUsageButtonX)] = self.buttonX;
+ if (self.buttonY) ret[@(GBUsageButtonY)] = self.buttonY;
+ if (@available(iOS 13.0, *)) {
+ if (self.buttonMenu) ret[@(GBUsageButtonMenu)] = self.buttonMenu;
+ if (self.buttonOptions) ret[@(GBUsageButtonOptions)] = self.buttonOptions;
+ }
+ // Can't be used
+ /* if (@available(iOS 14.0, *)) {
+ if (self.buttonHome) ret[@(GBUsageButtonHome)] = self.buttonHome;
+ } */
+ if (self.leftThumbstick) ret[@(GBUsageLeftThumbstick)] = self.leftThumbstick;
+ if (self.rightThumbstick) ret[@(GBUsageRightThumbstick)] = self.rightThumbstick;
+ if (self.leftShoulder) ret[@(GBUsageLeftShoulder)] = self.leftShoulder;
+ if (self.rightShoulder) ret[@(GBUsageRightShoulder)] = self.rightShoulder;
+ if (self.leftTrigger) ret[@(GBUsageLeftTrigger)] = self.leftTrigger;
+ if (self.rightTrigger) ret[@(GBUsageRightTrigger)] = self.rightTrigger;
+ if (@available(iOS 12.1, *)) {
+ if (self.leftThumbstickButton) ret[@(GBUsageLeftThumbstickButton)] = self.leftThumbstickButton;
+ if (self.rightThumbstickButton) ret[@(GBUsageRightThumbstickButton)] = self.rightThumbstickButton;
+ }
+
+ if (@available(iOS 14.0, *)) {
+ if ([self isKindOfClass:[GCDualShockGamepad class]]) {
+ GCDualShockGamepad *dualShock = (GCDualShockGamepad *)self;
+ if (dualShock.touchpadButton) ret[@(GBUsageTouchpadButton)] = dualShock.touchpadButton;
+ }
+ }
+
+ if (@available(iOS 14.5, *)) {
+ if ([self isKindOfClass:[GCDualSenseGamepad class]]) {
+ GCDualSenseGamepad *dualSense = (GCDualSenseGamepad *)self;
+ if (dualSense.touchpadButton) ret[@(GBUsageTouchpadButton)] = dualSense.touchpadButton;
+ }
+ }
+ return ret;
+}
+@end
diff --git a/iOS/Info.plist b/iOS/Info.plist
index 97c1e76..88e3ef4 100644
--- a/iOS/Info.plist
+++ b/iOS/Info.plist
@@ -13,7 +13,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- Version @VERSION
+ @VERSION
CFBundleVersion
1
LSRequiresIPhoneOS
diff --git a/iOS/LaunchScreen.storyboard b/iOS/LaunchScreen.storyboard
new file mode 100644
index 0000000..605cefa
--- /dev/null
+++ b/iOS/LaunchScreen.storyboard
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/iOS/button2-tint@2x.png b/iOS/button2-tint@2x.png
new file mode 100644
index 0000000..88469b2
Binary files /dev/null and b/iOS/button2-tint@2x.png differ
diff --git a/iOS/button2-tint@3x.png b/iOS/button2-tint@3x.png
new file mode 100644
index 0000000..8d8fbfe
Binary files /dev/null and b/iOS/button2-tint@3x.png differ
diff --git a/iOS/button2Pressed-tint@2x.png b/iOS/button2Pressed-tint@2x.png
new file mode 100644
index 0000000..38084ca
Binary files /dev/null and b/iOS/button2Pressed-tint@2x.png differ
diff --git a/iOS/button2Pressed-tint@3x.png b/iOS/button2Pressed-tint@3x.png
new file mode 100644
index 0000000..4bb86a7
Binary files /dev/null and b/iOS/button2Pressed-tint@3x.png differ
diff --git a/iOS/dpad-tint@2x.png b/iOS/dpad-tint@2x.png
new file mode 100644
index 0000000..f4b5fac
Binary files /dev/null and b/iOS/dpad-tint@2x.png differ
diff --git a/iOS/dpad-tint@3x.png b/iOS/dpad-tint@3x.png
new file mode 100644
index 0000000..512a89e
Binary files /dev/null and b/iOS/dpad-tint@3x.png differ
diff --git a/iOS/main.m b/iOS/main.m
index d76bd77..612a8c7 100644
--- a/iOS/main.m
+++ b/iOS/main.m
@@ -11,7 +11,7 @@ int main(int argc, char * argv[])
@"GBColorCorrection": @(GB_COLOR_CORRECTION_MODERN_BALANCED),
@"GBAudioMode": @"switch",
@"GBHighpassFilter": @(GB_HIGHPASS_ACCURATE),
- @"GBRewindLength": @(10),
+ @"GBRewindLength": @(120),
@"GBFrameBlendingMode": @(GB_FRAME_BLENDING_MODE_ACCURATE),
@"GBDMGModel": @(GB_MODEL_DMG_B),
@@ -20,9 +20,12 @@ int main(int argc, char * argv[])
@"GBSGBModel": @(GB_MODEL_SGB2),
@"GBRumbleMode": @(GB_RUMBLE_CARTRIDGE_ONLY),
@"GBButtonHaptics": @YES,
+ @"GBHapticsStrength": @0.75,
@"GBTurboSpeed": @1,
@"GBRewindSpeed": @1,
@"GBDynamicSpeed": @NO,
+
+ @"GBInterfaceTheme": @"SameBoy",
@"GBCurrentTheme": @"Lime (Game Boy)",
// Default themes
diff --git a/iOS/swipepad-tint@2x.png b/iOS/swipepad-tint@2x.png
new file mode 100644
index 0000000..62a9d99
Binary files /dev/null and b/iOS/swipepad-tint@2x.png differ
diff --git a/iOS/swipepad-tint@3x.png b/iOS/swipepad-tint@3x.png
new file mode 100644
index 0000000..2017aca
Binary files /dev/null and b/iOS/swipepad-tint@3x.png differ
diff --git a/iOS/themeSettings@2x.png b/iOS/themeSettings@2x.png
new file mode 100644
index 0000000..668b453
Binary files /dev/null and b/iOS/themeSettings@2x.png differ
diff --git a/iOS/themeSettings@3x.png b/iOS/themeSettings@3x.png
new file mode 100644
index 0000000..3e52ae4
Binary files /dev/null and b/iOS/themeSettings@3x.png differ
diff --git a/iOS/waveform.weak@2x.png b/iOS/waveform.weak@2x.png
new file mode 100644
index 0000000..e206b66
Binary files /dev/null and b/iOS/waveform.weak@2x.png differ
diff --git a/iOS/waveform.weak@3x.png b/iOS/waveform.weak@3x.png
new file mode 100644
index 0000000..be46c96
Binary files /dev/null and b/iOS/waveform.weak@3x.png differ