Migrate changes from the App Store version
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
typedef enum : NSUInteger {
|
||||
typedef enum {
|
||||
GBRight,
|
||||
GBLeft,
|
||||
GBUp,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
18
Makefile
|
@ -143,6 +143,9 @@ override CONF := release
|
|||
FAT_FLAGS += -arch x86_64 -arch arm64
|
||||
endif
|
||||
|
||||
IOS_MIN := 11.0
|
||||
|
||||
IOS_PNGS := $(shell ls iOS/*.png)
|
||||
# Support out-of-PATH RGBDS
|
||||
RGBASM := $(RGBDS)rgbasm
|
||||
RGBLINK := $(RGBDS)rgblink
|
||||
|
@ -239,13 +242,13 @@ SYSROOT := $(shell xcodebuild -sdk iphoneos -version Path 2> $(NULL))
|
|||
ifeq ($(SYSROOT),)
|
||||
$(error Could not find an iOS SDK)
|
||||
endif
|
||||
CFLAGS += -arch arm64 -miphoneos-version-min=11.0 -isysroot $(SYSROOT) -IAppleCommon -DGB_DISABLE_DEBUGGER
|
||||
CFLAGS += -arch arm64 -miphoneos-version-min=$(IOS_MIN) -isysroot $(SYSROOT) -IAppleCommon -DGB_DISABLE_DEBUGGER
|
||||
CORE_FILTER += Core/debugger.c Core/sm83_disassembler.c Core/symbol_hash.c
|
||||
LDFLAGS += -arch arm64
|
||||
OCFLAGS += -x objective-c -fobjc-arc -Wno-deprecated-declarations -isysroot $(SYSROOT)
|
||||
LDFLAGS += -miphoneos-version-min=11.0 -isysroot $(SYSROOT)
|
||||
LDFLAGS += -miphoneos-version-min=$(IOS_MIN) -isysroot $(SYSROOT)
|
||||
IOS_INSTALLER_LDFLAGS := $(LDFLAGS) -lobjc -framework CoreServices -framework Foundation
|
||||
LDFLAGS += -lobjc -framework UIKit -framework Foundation -framework CoreGraphics -framework Metal -framework MetalKit -framework AudioToolbox -framework AVFoundation -framework QuartzCore -framework CoreMotion -framework CoreVideo -framework CoreMedia -framework CoreImage -framework UserNotifications -weak_framework CoreHaptics
|
||||
LDFLAGS += -lobjc -framework UIKit -framework Foundation -framework CoreGraphics -framework Metal -framework MetalKit -framework AudioToolbox -framework AVFoundation -framework QuartzCore -framework CoreMotion -framework CoreVideo -framework CoreMedia -framework CoreImage -framework UserNotifications -framework GameController -weak_framework CoreHaptics -framework MobileCoreServices
|
||||
CODESIGN := codesign -fs -
|
||||
else
|
||||
ifeq ($(PLATFORM),Darwin)
|
||||
|
@ -288,6 +291,7 @@ endif
|
|||
# Don't use function outlining. I breaks Obj-C ARC optimizations and Apple never bothered to fix it. It also hardly has any effect on file size.
|
||||
ifeq ($(shell $(CC) -x c -c $(NULL) -o $(NULL) -Werror -mno-outline 2> $(NULL); echo $$?),0)
|
||||
FRONTEND_CFLAGS += -mno-outline
|
||||
LDFLAGS += -mno-outline
|
||||
endif
|
||||
|
||||
STRIP := strip
|
||||
|
@ -423,7 +427,7 @@ $(OBJ)/%.m.o: %.m
|
|||
# iOS Port
|
||||
|
||||
$(BIN)/SameBoy-iOS.app: $(BIN)/SameBoy-iOS.app/SameBoy \
|
||||
$(shell ls iOS/*.png) \
|
||||
$(IOS_PNGS) \
|
||||
iOS/License.html \
|
||||
iOS/Info.plist \
|
||||
$(BIN)/SameBoy-iOS.app/dmg_boot.bin \
|
||||
|
@ -433,9 +437,10 @@ $(BIN)/SameBoy-iOS.app: $(BIN)/SameBoy-iOS.app/SameBoy \
|
|||
$(BIN)/SameBoy-iOS.app/agb_boot.bin \
|
||||
$(BIN)/SameBoy-iOS.app/sgb_boot.bin \
|
||||
$(BIN)/SameBoy-iOS.app/sgb2_boot.bin \
|
||||
$(BIN)/SameBoy-iOS.app/LaunchScreen.storyboardc \
|
||||
Shaders
|
||||
$(MKDIR) -p $(BIN)/SameBoy-iOS.app
|
||||
cp iOS/*.png $(BIN)/SameBoy-iOS.app
|
||||
cp $(IOS_PNGS) $(BIN)/SameBoy-iOS.app
|
||||
sed "s/@VERSION/$(VERSION)/;s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < iOS/Info.plist > $(BIN)/SameBoy-iOS.app/Info.plist
|
||||
sed "s/@COPYRIGHT_YEAR/$(COPYRIGHT_YEAR)/" < iOS/License.html > $(BIN)/SameBoy-iOS.app/License.html
|
||||
$(MKDIR) -p $(BIN)/SameBoy-iOS.app/Shaders
|
||||
|
@ -491,6 +496,9 @@ endif
|
|||
$(BIN)/SameBoy.app/Contents/Resources/%.nib: Cocoa/%.xib
|
||||
ibtool --target-device mac --minimum-deployment-target 10.9 --compile $@ $^ 2>&1 | cat -
|
||||
|
||||
$(BIN)/SameBoy-iOS.app/%.storyboardc: iOS/%.storyboard
|
||||
ibtool --target-device iphone --target-device ipad --minimum-deployment-target $(IOS_MIN) --compile $@ $^ 2>&1 | cat -
|
||||
|
||||
# Quick Look generator
|
||||
|
||||
$(BIN)/SameBoy.qlgenerator: $(BIN)/SameBoy.qlgenerator/Contents/MacOS/SameBoyQL \
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -237,4 +237,9 @@
|
|||
{
|
||||
return UIModalPresentationFormSheet;
|
||||
}
|
||||
|
||||
- (void)dismissViewController
|
||||
{
|
||||
[self dismissViewControllerAnimated:true completion:nil];
|
||||
}
|
||||
@end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
#import <UIKit/UIKit.h>
|
||||
#import "GBTheme.h"
|
||||
|
||||
@interface GBThemePreviewController : UIViewController
|
||||
- (instancetype)initWithTheme:(GBTheme *)theme;
|
||||
@end
|
||||
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
#import <UIKit/UIKit.h>
|
||||
#import "GBTheme.h"
|
||||
|
||||
@interface GBThemesViewController : UITableViewController
|
||||
+ (NSArray<NSArray<GBTheme *> *> *)themes;
|
||||
@end
|
||||
|
|
@ -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
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 124 KiB |
|
@ -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
|
||||
|
|
After Width: | Height: | Size: 87 KiB |
After Width: | Height: | Size: 188 KiB |
After Width: | Height: | Size: 584 B |
After Width: | Height: | Size: 895 B |
After Width: | Height: | Size: 326 B |
After Width: | Height: | Size: 505 B |