diff --git a/Cocoa/Document.m b/Cocoa/Document.m index 431f6fd..ee62991 100644 --- a/Cocoa/Document.m +++ b/Cocoa/Document.m @@ -359,7 +359,7 @@ static void infraredStateChanged(GB_gameboy_t *gb, bool on) } if (self.view.isRewinding) { rewind = true; - [self.osdView displayText:@"Rewinding..."]; + [self.osdView displayText:@"Rewinding…"]; } } diff --git a/Cocoa/GBApp.m b/Cocoa/GBApp.m index 11fba24..bc5a901 100644 --- a/Cocoa/GBApp.m +++ b/Cocoa/GBApp.m @@ -430,13 +430,13 @@ static uint32_t color_to_int(NSColor *color) [self.updateProgressSpinner startAnimation:nil]; self.updateProgressButton.title = @"Cancel"; self.updateProgressButton.enabled = true; - self.updateProgressLabel.stringValue = @"Downloading update..."; + self.updateProgressLabel.stringValue = @"Downloading update…"; _updateState = UPDATE_DOWNLOADING; _updateTask = [[NSURLSession sharedSession] downloadTaskWithURL: [NSURL URLWithString:_updateURL] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { _updateTask = nil; dispatch_sync(dispatch_get_main_queue(), ^{ self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Extracting update..."; + self.updateProgressLabel.stringValue = @"Extracting update…"; _updateState = UPDATE_EXTRACTING; }); @@ -498,7 +498,7 @@ static uint32_t color_to_int(NSColor *color) - (void)performUpgrade { self.updateProgressButton.enabled = false; - self.updateProgressLabel.stringValue = @"Instaling update..."; + self.updateProgressLabel.stringValue = @"Instaling update…"; _updateState = UPDATE_INSTALLING; self.updateProgressButton.enabled = false; [self.updateProgressSpinner startAnimation:nil]; diff --git a/Cocoa/GBCheatWindowController.m b/Cocoa/GBCheatWindowController.m index ca46332..1f69b6f 100644 --- a/Cocoa/GBCheatWindowController.m +++ b/Cocoa/GBCheatWindowController.m @@ -58,7 +58,7 @@ return @NO; case 2: - return @"Add Cheat..."; + return @"Add Cheat…"; case 3: return @""; diff --git a/Cocoa/GBPreferencesWindow.m b/Cocoa/GBPreferencesWindow.m index c0efc38..52a6d4f 100644 --- a/Cocoa/GBPreferencesWindow.m +++ b/Cocoa/GBPreferencesWindow.m @@ -297,7 +297,7 @@ static inline NSString *keyEquivalentString(NSMenuItem *item) } if (is_button_being_modified && button_being_modified == row) { - return @"Select a new key..."; + return @"Select a new key…"; } NSNumber *key = [[NSUserDefaults standardUserDefaults] valueForKey:button_to_preference_name(row, self.playerListButton.selectedTag)]; diff --git a/Cocoa/GBView.m b/Cocoa/GBView.m index 5ed5e10..ae15813 100644 --- a/Cocoa/GBView.m +++ b/Cocoa/GBView.m @@ -255,11 +255,11 @@ static const uint8_t workboy_vk_to_key[] = { } if ((!analogClockMultiplierValid && clockMultiplier > 1) || _turbo || (analogClockMultiplierValid && analogClockMultiplier > 1)) { - [self.osdView displayText:@"Fast forwarding..."]; + [self.osdView displayText:@"Fast forwarding…"]; } else if ((!analogClockMultiplierValid && clockMultiplier < 1) || (analogClockMultiplierValid && analogClockMultiplier < 1)) { - [self.osdView displayText:@"Slow motion..."]; + [self.osdView displayText:@"Slow motion…"]; } [super flip]; } diff --git a/iOS/GBBackgroundView.m b/iOS/GBBackgroundView.m index 8d78414..ded6f45 100644 --- a/iOS/GBBackgroundView.m +++ b/iOS/GBBackgroundView.m @@ -2,6 +2,8 @@ #import "GBViewMetal.h" #import "GBHapticManager.h" #import "GBMenuViewController.h" +#import "GBViewController.h" +#import "GBROMManager.h" double CGPointSquaredDistance(CGPoint a, CGPoint b) { @@ -81,7 +83,11 @@ static GB_key_mask_t angleToKeyMask(double angle) { NSMutableSet *_touches; UITouch *_swipePadTouch; - CGPoint _swipeOrigin; + CGPoint _padSwipeOrigin; + UITouch *_screenTouch; + CGPoint _screenSwipeOrigin; + bool _screenSwiped; + UIImageView *_dpadView; UIImageView *_dpadShadowView; UIImageView *_aButtonView; @@ -89,6 +95,11 @@ static GB_key_mask_t angleToKeyMask(double angle) UIImageView *_startButtonView; UIImageView *_selectButtonView; UILabel *_screenLabel; + + UIVisualEffectView *_overlayView; + UIView *_overlayViewContents; + NSTimer *_fadeTimer; + GB_key_mask_t _lastMask; } @@ -124,9 +135,35 @@ static GB_key_mask_t angleToKeyMask(double angle) [self addSubview:_gbView]; [_dpadView addSubview:_dpadShadowView]; + + UIVisualEffect *effect = [UIBlurEffect effectWithStyle:(UIBlurEffectStyle)UIBlurEffectStyleDark]; + _overlayView = [[UIVisualEffectView alloc] initWithEffect:effect]; + _overlayView.frame = CGRectMake(8, 8, 32, 32); + _overlayView.layer.cornerRadius = 12; + _overlayView.layer.masksToBounds = true; + _overlayView.alpha = 0; + + if (@available(iOS 13.0, *)) { + _overlayViewContents = [[UIImageView alloc] init]; + _overlayViewContents.tintColor = [UIColor whiteColor]; + } + else { + _overlayViewContents = [[UILabel alloc] init]; + ((UILabel *)_overlayViewContents).font = [UIFont systemFontOfSize:UIFont.systemFontSize weight:UIFontWeightMedium]; + ((UILabel *)_overlayViewContents).textColor = [UIColor whiteColor]; + } + _overlayViewContents.frame = CGRectMake(8, 8, 160, 20.5); + [_overlayView.contentView addSubview:_overlayViewContents]; + [_gbView addSubview:_overlayView]; + return self; } +- (GBViewController *)viewController +{ + return (GBViewController *)[UIApplication sharedApplication].delegate; +} + - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { static const double dpadRadius = 75; @@ -136,15 +173,26 @@ static GB_key_mask_t angleToKeyMask(double angle) dpadLocation.y /= factor; for (UITouch *touch in touches) { CGPoint point = [touch locationInView:self]; - if (CGRectContainsPoint(self.gbView.frame, point)) { - [self.window.rootViewController presentViewController:[GBMenuViewController menu] animated:true completion:nil]; + if (CGRectContainsPoint(self.gbView.frame, point) && !_screenTouch) { + if (self.viewController.runMode != GBRunModeNormal) { + self.viewController.runMode = GBRunModeNormal; + [self fadeOverlayOut]; + } + else { + _screenTouch = touch; + _screenSwipeOrigin = point; + _screenSwiped = false; + _overlayView.alpha = 0; + [_fadeTimer invalidate]; + _fadeTimer = nil; + } } if (_usesSwipePad && !_swipePadTouch) { if (fabs(point.x - dpadLocation.x) <= dpadRadius && fabs(point.y - dpadLocation.y) <= dpadRadius) { _swipePadTouch = touch; - _swipeOrigin = point; + _padSwipeOrigin = point; } } } @@ -157,14 +205,27 @@ static GB_key_mask_t angleToKeyMask(double angle) if ([touches containsObject:_swipePadTouch]) { _swipePadTouch = nil; } + + if ([touches containsObject:_screenTouch]) { + _screenTouch = nil; + if (!_screenSwiped) { + [self.window.rootViewController presentViewController:[GBMenuViewController menu] animated:true completion:nil]; + } + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeLock"]) { + if (self.viewController.runMode != GBRunModeNormal) { + self.viewController.runMode = GBRunModeNormal; + [self fadeOverlayOut]; + } + } + } + [_touches minusSet:touches]; [self touchesChanged]; } - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event { - [_touches minusSet:touches]; - [self touchesChanged]; + [self touchesEnded:touches withEvent:event]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event @@ -184,16 +245,16 @@ static GB_key_mask_t angleToKeyMask(double angle) dpadHandled = true; if (_swipePadTouch) { CGPoint point = [_swipePadTouch locationInView:self]; - double squaredDistance = CGPointSquaredDistance(point, _swipeOrigin); + double squaredDistance = CGPointSquaredDistance(point, _padSwipeOrigin); if (squaredDistance > 16 * 16) { - double angle = CGPointAngle(point, _swipeOrigin); + double angle = CGPointAngle(point, _padSwipeOrigin); mask |= angleToKeyMask(angle); if (squaredDistance > 24 * 24) { - double deltaX = point.x - _swipeOrigin.x; - double deltaY = point.y - _swipeOrigin.y; + double deltaX = point.x - _padSwipeOrigin.x; + double deltaY = point.y - _padSwipeOrigin.y; double distance = sqrt(squaredDistance); - _swipeOrigin.x = point.x - deltaX / distance * 24; - _swipeOrigin.y = point.y - deltaY / distance * 24; + _padSwipeOrigin.x = point.x - deltaX / distance * 24; + _padSwipeOrigin.y = point.y - deltaY / distance * 24; } } } @@ -201,6 +262,24 @@ static GB_key_mask_t angleToKeyMask(double angle) for (UITouch *touch in _touches) { if (touch == _swipePadTouch) continue; CGPoint point = [touch locationInView:self]; + + if (touch == _screenTouch) { + if (_screenSwiped) continue; + if (point.x - _screenSwipeOrigin.x > 32) { + [self turboSwipe]; + } + else if (point.x - _screenSwipeOrigin.x < -32) { + [self rewindSwipe]; + } + else if (point.y - _screenSwipeOrigin.y > 32) { + [self saveSwipe]; + } + else if (point.y - _screenSwipeOrigin.y < -32) { + [self loadSwipe]; + } + continue; + } + point.x *= factor; point.y *= factor; if (CGPointSquaredDistance(point, _layout.aLocation) <= buttonRadiusSquared) { @@ -307,4 +386,82 @@ static GB_key_mask_t angleToKeyMask(double angle) _dpadView.image = [UIImage imageNamed:usesSwipePad? @"swipepad" : @"dpad"]; } +- (void)displayOverlayWithImage:(NSString *)imageName orTitle:(NSString *)title +{ + if (@available(iOS 13.0, *)) { + ((UIImageView *)_overlayViewContents).image = [UIImage systemImageNamed:imageName + withConfiguration:[UIImageSymbolConfiguration configurationWithWeight:UIImageSymbolWeightMedium]]; + } + else { + ((UILabel *)_overlayViewContents).text = title; + } + [_overlayViewContents sizeToFit]; + + CGRect bounds = _overlayViewContents.bounds; + bounds.origin = (CGPoint){8, 8}; + bounds.size.width += 16; + bounds.size.height += 16; + _overlayView.frame = bounds; + + _overlayView.alpha = 1.0; +} + +- (void)fadeOverlayOut +{ + [UIView animateWithDuration:1 animations:^{ + _overlayView.alpha = 0; + }]; + [_fadeTimer invalidate]; + _fadeTimer = nil; +} + +- (void)turboSwipe +{ + _screenSwiped = true; + [self displayOverlayWithImage:@"forward" orTitle:@"Fast-forwarding…"]; + self.viewController.runMode = GBRunModeTurbo; +} + +- (void)rewindSwipe +{ + _screenSwiped = true; + [self displayOverlayWithImage:@"backward" orTitle:@"Rewinding…"]; + self.viewController.runMode = GBRunModeRewind; +} + +- (NSString *)swipeStateFile +{ + return [[GBROMManager sharedManager] stateFile:1]; +} + +- (void)saveSwipe +{ + _screenSwiped = true; + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeState"]) { + return; + } + [self displayOverlayWithImage:@"square.and.arrow.down" orTitle:@"Saved state to Slot 1"]; + _fadeTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:false block:^(NSTimer *timer) { + [self fadeOverlayOut]; + }]; + [self.viewController stop]; + [self.viewController saveStateToFile:self.swipeStateFile]; + [self.viewController start]; +} + +- (void)loadSwipe +{ + _screenSwiped = true; + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBSwipeState"]) { + return; + } + [self displayOverlayWithImage:@"square.and.arrow.up" orTitle:@"Loaded state from Slot 1"]; + _fadeTimer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:false block:^(NSTimer *timer) { + [self fadeOverlayOut]; + }]; + [self.viewController stop]; + [self.viewController loadStateFromFile:self.swipeStateFile]; + [self.viewController start]; +} + @end diff --git a/iOS/GBViewController.h b/iOS/GBViewController.h index fc08162..86e2a01 100644 --- a/iOS/GBViewController.h +++ b/iOS/GBViewController.h @@ -1,5 +1,12 @@ #import +typedef enum { + GBRunModeNormal, + GBRunModeTurbo, + GBRunModeRewind, + GBRunModeRewindPaused, +} GBRunMode; + @interface GBViewController : UIViewController @property (nonatomic, strong) UIWindow *window; - (void)reset; @@ -12,4 +19,5 @@ - (void)showAbout; - (void)saveStateToFile:(NSString *)file; - (void)loadStateFromFile:(NSString *)file; +@property (nonatomic) GBRunMode runMode; @end diff --git a/iOS/GBViewController.m b/iOS/GBViewController.m index ffc2a89..07daab1 100644 --- a/iOS/GBViewController.m +++ b/iOS/GBViewController.m @@ -32,6 +32,7 @@ GBAudioClient *_audioClient; NSMutableSet *_defaultsObservers; GB_palette_t _palette; + bool _rewind; } static void loadBootROM(GB_gameboy_t *gb, GB_boot_rom_t type) @@ -110,6 +111,9 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) [self addDefaultObserver:^(id newValue) { GB_set_interference_volume(gb, [newValue doubleValue]); } forKey:@"GBInterferenceVolume"]; + [self addDefaultObserver:^(id newValue) { + GB_set_rewind_length(gb, [newValue unsignedIntValue]); + } forKey:@"GBRewindLength"]; } - (void)addDefaultObserver:(void(^)(id newValue))block forKey:(NSString *)key @@ -185,6 +189,17 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) return true; } +- (void)saveStateToFile:(NSString *)file +{ + GB_save_state(&_gb, file.fileSystemRepresentation); + NSData *data = [NSData dataWithBytes:_gbView.previousBuffer + length:GB_get_screen_width(&_gb) * + GB_get_screen_height(&_gb) * + sizeof(*_gbView.previousBuffer)]; + UIImage *screenshot = [self imageFromData:data width:GB_get_screen_width(&_gb) height:GB_get_screen_height(&_gb)]; + [UIImagePNGRepresentation(screenshot) writeToFile:[file stringByAppendingPathExtension:@"png"] atomically:false]; +} + - (void)loadStateFromFile:(NSString *)file { GB_model_t model; @@ -417,7 +432,16 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) { [self preRun]; while (_running) { - GB_run(&_gb); + if (_rewind) { + _rewind = false; + GB_rewind_pop(&_gb); + if (!GB_rewind_pop(&_gb)) { + self.runMode = GBRunModeRewindPaused; + } + } + if (_runMode != GBRunModeRewindPaused) { + GB_run(&_gb); + } } [self postRun]; _stopping = false; @@ -450,17 +474,6 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) return ret; } -- (void)saveStateToFile:(NSString *)file -{ - GB_save_state(&_gb, file.fileSystemRepresentation); - NSData *data = [NSData dataWithBytes:_gbView.previousBuffer - length:GB_get_screen_width(&_gb) * - GB_get_screen_height(&_gb) * - sizeof(*_gbView.previousBuffer)]; - UIImage *screenshot = [self imageFromData:data width:GB_get_screen_width(&_gb) height:GB_get_screen_height(&_gb)]; - [UIImagePNGRepresentation(screenshot) writeToFile:[file stringByAppendingPathExtension:@"png"] atomically:false]; -} - - (void)postRun { [_audioLock lock]; @@ -519,6 +532,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) [_gbView flip]; GB_set_pixels_output(&_gb, _gbView.pixels); } + _rewind = _runMode == GBRunModeRewind; } - (void)gotNewSample:(GB_sample_t *)sample @@ -580,4 +594,17 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) [self start]; return [GBROMManager sharedManager].currentROM != nil; } + +- (void)setRunMode:(GBRunMode)runMode +{ + if (runMode == _runMode) return; + if (_runMode == GBRunModeRewindPaused) { + [_audioClient start]; + } + _runMode = runMode; + if (_runMode == GBRunModeRewindPaused) { + [_audioClient stop]; + } + GB_set_turbo_mode(&_gb, runMode == GBRunModeTurbo, false); +} @end