#import "GBBackgroundView.h" #import "GBViewMetal.h" #import "GBHapticManager.h" #import "GBMenuViewController.h" #import "GBViewController.h" #import "GBROMManager.h" double CGPointSquaredDistance(CGPoint a, CGPoint b) { double deltaX = a.x - b.x; double deltaY = a.y - b.y; return deltaX * deltaX + deltaY * deltaY; } double CGPointAngle(CGPoint a, CGPoint b) { double deltaX = a.x - b.x; double deltaY = a.y - b.y; return atan2(deltaY, deltaX); } static void positionView(UIImageView *view, CGPoint position) { double center = view.image.size.width / 2 * [UIScreen mainScreen].scale; view.frame = (CGRect){ { round(position.x - center) / [UIScreen mainScreen].scale, round(position.y - center) / [UIScreen mainScreen].scale }, view.image.size }; } static GB_key_mask_t angleToKeyMask(double angle) { signed quantizedAngle = round(angle / M_PI * 16); if (quantizedAngle < 0) { quantizedAngle += 32; } switch (quantizedAngle) { case 32: case 0: return GB_KEY_RIGHT_MASK; case 1: return GB_KEY_RIGHT_MASK; case 2: return GB_KEY_RIGHT_MASK; case 3: return GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK; case 4: return GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK; case 5: return GB_KEY_DOWN_MASK; case 6: return GB_KEY_DOWN_MASK; case 7: return GB_KEY_DOWN_MASK; case 8: return GB_KEY_DOWN_MASK; case 9: return GB_KEY_DOWN_MASK; case 10: return GB_KEY_DOWN_MASK; case 11: return GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK; case 12: return GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK; case 13: return GB_KEY_LEFT_MASK; case 14: return GB_KEY_LEFT_MASK; case 15: return GB_KEY_LEFT_MASK; case 16: return GB_KEY_LEFT_MASK; case 17: return GB_KEY_LEFT_MASK; case 18: return GB_KEY_LEFT_MASK; case 19: return GB_KEY_LEFT_MASK | GB_KEY_UP_MASK; case 20: return GB_KEY_LEFT_MASK | GB_KEY_UP_MASK; case 21: return GB_KEY_UP_MASK; case 22: return GB_KEY_UP_MASK; case 23: return GB_KEY_UP_MASK; case 24: return GB_KEY_UP_MASK; case 25: return GB_KEY_UP_MASK; case 26: return GB_KEY_UP_MASK; case 27: return GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK; case 28: return GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK; case 29: return GB_KEY_RIGHT_MASK; case 30: return GB_KEY_RIGHT_MASK; case 31: return GB_KEY_RIGHT_MASK; } return 0; } @implementation GBBackgroundView { NSMutableSet *_touches; UITouch *_swipePadTouch; CGPoint _padSwipeOrigin; UITouch *_screenTouch; CGPoint _screenSwipeOrigin; bool _screenSwiped; UIImageView *_dpadView; UIImageView *_dpadShadowView; UIImageView *_aButtonView; UIImageView *_bButtonView; UIImageView *_startButtonView; UIImageView *_selectButtonView; UILabel *_screenLabel; UIVisualEffectView *_overlayView; UIView *_overlayViewContents; NSTimer *_fadeTimer; GB_key_mask_t _lastMask; } - (instancetype)init { self = [super initWithImage:nil]; if (!self) return nil; _touches = [NSMutableSet set]; _screenLabel = [[UILabel alloc] initWithFrame:CGRectZero]; _screenLabel.text = @"Tap the Game Boy screen to open the menu and load a ROM from the library."; _screenLabel.font = [UIFont systemFontOfSize:20 weight:UIFontWeightMedium]; _screenLabel.textAlignment = NSTextAlignmentCenter; _screenLabel.textColor = [UIColor whiteColor]; _screenLabel.lineBreakMode = NSLineBreakByWordWrapping; _screenLabel.numberOfLines = 0; [self addSubview:_screenLabel]; _dpadView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"dpad"]]; _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]; [self addSubview:_aButtonView]; [self addSubview:_bButtonView]; [self addSubview:_startButtonView]; [self addSubview:_selectButtonView]; [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; CGPoint dpadLocation = _layout.dpadLocation; double factor = [UIScreen mainScreen].scale; dpadLocation.x /= factor; dpadLocation.y /= factor; for (UITouch *touch in touches) { CGPoint point = [touch locationInView:self]; 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; _padSwipeOrigin = point; } } } [_touches unionSet:touches]; [self touchesChanged]; } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { 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 { [self touchesEnded:touches withEvent:event]; } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [self touchesChanged]; } - (void)touchesChanged { if (!GB_is_inited(_gbView.gb)) return; GB_key_mask_t mask = 0; double factor = [UIScreen mainScreen].scale; double buttonRadiusSquared = 36 * 36 * factor * factor; double dpadRadius = 75 * factor; bool dpadHandled = false; if (_usesSwipePad) { dpadHandled = true; if (_swipePadTouch) { CGPoint point = [_swipePadTouch locationInView:self]; double squaredDistance = CGPointSquaredDistance(point, _padSwipeOrigin); if (squaredDistance > 16 * 16) { double angle = CGPointAngle(point, _padSwipeOrigin); mask |= angleToKeyMask(angle); if (squaredDistance > 24 * 24) { double deltaX = point.x - _padSwipeOrigin.x; double deltaY = point.y - _padSwipeOrigin.y; double distance = sqrt(squaredDistance); _padSwipeOrigin.x = point.x - deltaX / distance * 24; _padSwipeOrigin.y = point.y - deltaY / distance * 24; } } } } 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) { mask |= GB_KEY_A_MASK; } else if (CGPointSquaredDistance(point, _layout.bLocation) <= buttonRadiusSquared) { mask |= GB_KEY_B_MASK; } else if (CGPointSquaredDistance(point, _layout.startLocation) <= buttonRadiusSquared) { mask |= GB_KEY_START_MASK; } else if (CGPointSquaredDistance(point, _layout.selectLocation) <= buttonRadiusSquared) { mask |= GB_KEY_SELECT_MASK; } else if (!dpadHandled && fabs(point.x - _layout.dpadLocation.x) <= dpadRadius && fabs(point.y - _layout.dpadLocation.y) <= dpadRadius) { dpadHandled = true; // Don't handle the dpad twice double angle = CGPointAngle(point, _layout.dpadLocation); mask |= angleToKeyMask(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"]; bool hidden = false; bool diagonal = false; double rotation = 0; switch (mask & (GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK | GB_KEY_LEFT_MASK | GB_KEY_UP_MASK)) { case GB_KEY_RIGHT_MASK: break; case GB_KEY_RIGHT_MASK | GB_KEY_DOWN_MASK: diagonal = true; break; case GB_KEY_DOWN_MASK: rotation = M_PI_2; break; case GB_KEY_LEFT_MASK | GB_KEY_DOWN_MASK: diagonal = true; rotation = M_PI_2; break; case GB_KEY_LEFT_MASK: rotation = M_PI; break; case GB_KEY_LEFT_MASK | GB_KEY_UP_MASK: diagonal = true; rotation = M_PI; break; case GB_KEY_UP_MASK: rotation = -M_PI_2; break; case GB_KEY_RIGHT_MASK | GB_KEY_UP_MASK: diagonal = true; rotation = -M_PI_2; break; default: hidden = true; } _dpadShadowView.hidden = hidden; if (!hidden) { if (_usesSwipePad) { _dpadShadowView.image = [UIImage imageNamed:diagonal? @"swipepadShadowDiagonal" : @"swipepadShadow"]; } else { _dpadShadowView.image = [UIImage imageNamed:diagonal? @"dpadShadowDiagonal" : @"dpadShadow"]; } _dpadShadowView.transform = CGAffineTransformMakeRotation(rotation); } GB_set_key_mask(_gbView.gb, mask); if ((mask & ~_lastMask) && ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBButtonHaptics"])) { [[GBHapticManager sharedManager] doTapHaptic]; } _lastMask = mask; } } - (BOOL)isMultipleTouchEnabled { return true; } - (BOOL)isUserInteractionEnabled { return true; } - (void)setLayout:(GBLayout *)layout { _layout = layout; self.image = layout.background; positionView(_dpadView, layout.dpadLocation); positionView(_aButtonView, layout.aLocation); positionView(_bButtonView, layout.bLocation); positionView(_startButtonView, layout.startLocation); positionView(_selectButtonView, layout.selectLocation); CGRect screenFrame = layout.screenRect; screenFrame.origin.x /= [UIScreen mainScreen].scale; screenFrame.origin.y /= [UIScreen mainScreen].scale; screenFrame.size.width /= [UIScreen mainScreen].scale; screenFrame.size.height /= [UIScreen mainScreen].scale; _gbView.frame = screenFrame; screenFrame.origin.x += 8; screenFrame.origin.y += 8; screenFrame.size.width -= 16; screenFrame.size.height -= 16; _screenLabel.frame = screenFrame; } - (void)setUsesSwipePad:(bool)usesSwipePad { _usesSwipePad = usesSwipePad; _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