Migrate changes from the App Store version

This commit is contained in:
Lior Halphon 2024-05-25 18:15:14 +03:00
parent 7758713f86
commit 302eaf6eca
46 changed files with 1703 additions and 151 deletions

View File

@ -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;
}

View File

@ -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),

View File

@ -1,4 +1,4 @@
typedef enum : NSUInteger {
typedef enum {
GBRight,
GBLeft,
GBUp,

View File

@ -123,7 +123,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>Version @VERSION</string>
<string>@VERSION</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleSupportedPlatforms</key>

View File

@ -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;

View File

@ -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 \

View File

@ -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);

View File

@ -237,4 +237,9 @@
{
return UIModalPresentationFormSheet;
}
- (void)dismissViewController
{
[self dismissViewControllerAnimated:true completion:nil];
}
@end

View File

@ -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

View File

@ -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<UITouch *> *)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<UITouch *> *)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

View File

@ -1,7 +1,9 @@
#import <Foundation/Foundation.h>
#import <GameController/GameController.h>
@interface GBHapticManager : NSObject
+ (instancetype)sharedManager;
- (void)doTapHaptic;
- (void)setRumbleStrength:(double)rumble;
@property (weak) GCController *controller;
@end

View File

@ -7,8 +7,10 @@
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpartial-availability"
CHHapticEngine *_engine;
CHHapticEngine *_externalEngine;
id<CHHapticPatternPlayer> _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<CHHapticPatternPlayer> player = [_engine createPlayerWithPattern:pattern error:nil];
[player startAtTime:0 error:nil];
@try {
id<CHHapticPatternPlayer> 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<CHHapticPatternPlayer> newPlayer = [_engine createPlayerWithPattern:pattern error:nil];
[newPlayer startAtTime:0 error:nil];
[_rumblePlayer stopAtTime:0 error:nil];
_rumblePlayer = newPlayer;
@try {
id<CHHapticPatternPlayer> 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

View File

@ -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

View File

@ -1,14 +1,20 @@
#import <UIKit/UIKit.h>
#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

View File

@ -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

View File

@ -1,5 +1,11 @@
#import "GBLoadROMTableViewController.h"
#import "GBROMManager.h"
#import "GBViewController.h"
#import <CoreServices/CoreServices.h>
#import <objc/runtime.h>
@interface GBLoadROMTableViewController() <UIDocumentPickerDelegate>
@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 <NSURL *>*)urls
{
NSMutableArray<NSURL *> *validURLs = [NSMutableArray array];
NSMutableArray<NSString *> *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<UIMenuElement *> *suggestedActions) {

View File

@ -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];
}

View File

@ -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];

View File

@ -1,7 +1,31 @@
#import <UIKit/UIKit.h>
#import <Core/gb.h>
#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

View File

@ -1,10 +1,14 @@
#import "GBSettingsViewController.h"
#import "GBTemperatureSlider.h"
#import "GBViewBase.h"
#import "GBThemesViewController.h"
#import "GBHapticManager.h"
#import "GCExtendedGamepad+AllElements.h"
#import <objc/runtime.h>
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<NSDictionary *> *_structure;
UINavigationController *_detailsNavigation;
NSArray<NSArray<GBTheme *> *> *_themes; // For prewarming
}
+ (const GB_palette_t *)paletteForTheme:(NSString *)theme
@ -316,10 +321,16 @@ static NSString const *typeLightTemp = @"typeLightTemp";
];
NSArray<NSDictionary *> *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 <NSNumber *, GCControllerElement *> *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

26
iOS/GBTheme.h Normal file
View File

@ -0,0 +1,26 @@
#import <UIKit/UIKit.h>
@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

226
iOS/GBTheme.m Normal file
View File

@ -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<NSString *, UIImage *> *_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

View File

@ -0,0 +1,7 @@
#import <UIKit/UIKit.h>
#import "GBTheme.h"
@interface GBThemePreviewController : UIViewController
- (instancetype)initWithTheme:(GBTheme *)theme;
@end

View File

@ -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

View File

@ -0,0 +1,7 @@
#import <UIKit/UIKit.h>
#import "GBTheme.h"
@interface GBThemesViewController : UITableViewController
+ (NSArray<NSArray<GBTheme *> *> *)themes;
@end

View File

@ -0,0 +1,104 @@
#import "GBThemesViewController.h"
#import "GBThemePreviewController.h"
#import "GBTheme.h"
@interface GBThemesViewController ()
@end
@implementation GBThemesViewController
{
NSArray<NSArray<GBTheme *> *> *_themes;
}
+ (NSArray<NSArray<GBTheme *> *> *)themes
{
static __weak NSArray<NSArray<GBTheme *> *> *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

View File

@ -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();

View File

@ -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

View File

@ -12,6 +12,7 @@
#import "GBAboutController.h"
#import "GBSettingsViewController.h"
#import "GBStatesViewController.h"
#import "GCExtendedGamepad+AllElements.h"
#import <CoreMotion/CoreMotion.h>
#import <dlfcn.h>
@ -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];

View File

@ -0,0 +1,25 @@
#import <GameController/GameController.h>
typedef enum {
GBUsageDpad,
GBUsageButtonA,
GBUsageButtonB,
GBUsageButtonX,
GBUsageButtonY,
GBUsageButtonMenu,
GBUsageButtonOptions,
GBUsageButtonHome,
GBUsageLeftThumbstick,
GBUsageRightThumbstick,
GBUsageLeftShoulder,
GBUsageRightShoulder,
GBUsageLeftTrigger,
GBUsageRightTrigger,
GBUsageLeftThumbstickButton,
GBUsageRightThumbstickButton,
GBUsageTouchpadButton,
} GBControllerUsage;
@interface GCExtendedGamepad (AllElements)
- (NSDictionary <NSNumber *, GCControllerElement *> *)elementsDictionary;
@end

View File

@ -0,0 +1,48 @@
#import "GCExtendedGamepad+AllElements.h"
#import <objc/runtime.h>
@implementation GCExtendedGamepad (AllElements)
- (NSDictionary <NSNumber *, GCControllerElement *> *)elementsDictionary;
{
NSMutableDictionary <NSNumber *, GCControllerElement *> *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

View File

@ -13,7 +13,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>Version @VERSION</string>
<string>@VERSION</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="1Gu-pD-T9U">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="VIU-D8-ZfG">
<rect key="frame" x="86" y="323" width="221" height="206"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES" flexibleMaxY="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="logo.png" translatesAutoresizingMaskIntoConstraints="NO" id="ZD2-H2-y6p">
<rect key="frame" x="44" y="5" width="129" height="129"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</imageView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="SAMEBOY" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" id="Fbh-Ok-vca">
<rect key="frame" x="18" y="142" width="181" height="44"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" name="AvenirNext-BoldItalic" family="Avenir Next" pointSize="32"/>
<color key="textColor" red="0.0" green="0.27450980392156865" blue="0.55294117647058827" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</label>
</subviews>
</view>
</subviews>
<viewLayoutGuide key="safeArea" id="XrF-Y8-hoy"/>
<color key="backgroundColor" red="0.68235294117647061" green="0.69019607843137254" blue="0.70588235294117641" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
<resources>
<image name="logo.png" width="128" height="128"/>
</resources>
</document>

BIN
iOS/button2-tint@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
iOS/button2-tint@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
iOS/dpad-tint@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

BIN
iOS/dpad-tint@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -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

BIN
iOS/swipepad-tint@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
iOS/swipepad-tint@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
iOS/themeSettings@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

BIN
iOS/themeSettings@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 895 B

BIN
iOS/waveform.weak@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

BIN
iOS/waveform.weak@3x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B