SameBoy | Accurate GB/GBC emulator |
| download: https://git.y1.nz/archives/sameboy.tar.gz | |
| README | Files | Log | Refs | LICENSE |
commit dadbcf1ea318d36395987545c4b7dc9009f2c1ff parent 86bf58ae27c4654dd96db34a83f139b7dc75bb15 Author: Lior Halphon <LIJI32@gmail.com> Date: Wed, 24 Sep 2025 14:02:29 +0300 Improvements to iOS controller support, adds support to more controllers including singular Joy-Cons. Closes #719 Diffstat:
| M | iOS/GBSettingsViewController.h | 2 | +- |
| M | iOS/GBSettingsViewController.m | 43 | ++++++++++++++++++++++++++----------------- |
| M | iOS/GBViewController.m | 59 | ++++++++++++++++++++++++++++++++++++++++------------------- |
| A | iOS/GCControllerGetElements.h | 25 | +++++++++++++++++++++++++ |
| A | iOS/GCControllerGetElements.m | 118 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| D | iOS/GCExtendedGamepad+AllElements.h | 25 | ------------------------- |
| D | iOS/GCExtendedGamepad+AllElements.m | 48 | ------------------------------------------------ |
7 files changed, 210 insertions(+), 110 deletions(-)
diff --git a/iOS/GBSettingsViewController.h b/iOS/GBSettingsViewController.h @@ -1,6 +1,6 @@ #import <UIKit/UIKit.h> #import <Core/gb.h> -#import "GCExtendedGamepad+AllElements.h" +#import "GCControllerGetElements.h" #import "GBTheme.h" typedef enum { diff --git a/iOS/GBSettingsViewController.m b/iOS/GBSettingsViewController.m @@ -4,7 +4,7 @@ #import "GBThemesViewController.h" #import "GBPalettePicker.h" #import "GBHapticManager.h" -#import "GCExtendedGamepad+AllElements.h" +#import "GCControllerGetElements.h" #import <objc/runtime.h> static NSString const *typeSubmenu = @"submenu"; @@ -573,6 +573,14 @@ static NSString *LocalizedNameForElement(GCControllerElement *element, GBControl case GBUsageLeftThumbstickButton: return @"Left Thumbstick Button"; case GBUsageRightThumbstickButton: return @"Right Thumbstick Button"; case GBUsageTouchpadButton: return @"Touchpad Button"; + default: { + if (@available(iOS 14.0, *)) { + NSArray *aliases = [element.aliases.allObjects sortedArrayUsingSelector:@selector(compare:)]; + if (aliases.count) { + return aliases[0]; + } + } + } } return @"Button"; @@ -581,7 +589,7 @@ static NSString *LocalizedNameForElement(GCControllerElement *element, GBControl - (void)configureGameController:(GCController *)controller { NSMutableArray *items = [NSMutableArray array]; - NSDictionary <NSNumber *, GCControllerElement *> *elementsDict = controller.extendedGamepad.elementsDictionary; + NSDictionary <NSNumber *, GCControllerElement *> *elementsDict = GCControllerGetElements(controller); for (NSNumber *usage in [[elementsDict allKeys] sortedArrayUsingSelector:@selector(compare:)]) { GCControllerElement *element = elementsDict[usage]; if (![element isKindOfClass:[GCControllerButtonInput class]]) continue; @@ -599,25 +607,26 @@ static NSString *LocalizedNameForElement(GCControllerElement *element, GBControl [[NSUserDefaults standardUserDefaults] setObject:mapping forKey:@"GBControllerMappings"]; }; + bool isDpadElement = [element.collection isKindOfClass:[GCControllerDirectionPad class]]; 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": @"Rapid A", @"value": @(GBRapidA)}, - @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Rapid B", @"value": @(GBRapidB)}, - @{@"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)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": isDpadElement? @"D-pad" : @"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": @"Rapid A", @"value": @(GBRapidA)}, + @{@"type": typeRadio, @"getter": getter, @"setter": setter, @"title": @"Rapid B", @"value": @(GBRapidB)}, + @{@"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, *)) { @@ -646,7 +655,7 @@ static NSString *LocalizedNameForElement(GCControllerElement *element, GBControl NSMutableArray *items = [NSMutableArray array]; for (GCController *controller in [GCController controllers]) { - if (!controller.extendedGamepad) continue; + if (!GCControllerGetElements(controller)) continue; NSDictionary *item = @{ @"title": controller.vendorName, @"type": typeBlock, diff --git a/iOS/GBViewController.m b/iOS/GBViewController.m @@ -16,7 +16,7 @@ #import "GBCheckableAlertController.h" #import "GBPrinterFeedController.h" #import "GBCheatsController.h" -#import "GCExtendedGamepad+AllElements.h" +#import "GCControllerGetElements.h" #import "GBZipReader.h" #import <sys/stat.h> #import <CoreMotion/CoreMotion.h> @@ -640,25 +640,31 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) { 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]; + NSDictionary <NSNumber *, GCControllerElement *> *elements = GCControllerGetElements(controller); + if (!elements) continue; // Controller not supported + [elements 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]]) { + NSMutableSet *childrenUsages = [NSMutableSet set]; + [elements enumerateKeysAndObjectsUsingBlock:^(NSNumber *childUsage, GCControllerElement *child, BOOL *stop) { + if (child.collection == element) { + [childrenUsages addObject:childUsage]; + } + }]; + [(GCControllerDirectionPad *)element setValueChangedHandler:^(GCControllerDirectionPad *dpad, float xValue, float yValue) { + [self controller:weakController axisChanged:dpad usage:usage.unsignedIntValue childrenUsages:childrenUsages]; + }]; + } + }]; + + if (controller.motion) { + [controller.motion setValueChangedHandler:^(GCMotion *motion) { + [self controller:weakController motionChanged:motion]; + }]; } } } @@ -752,7 +758,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) } } -- (void)controller:(GCController *)controller axisChanged:(GCControllerDirectionPad *)axis usage:(GBControllerUsage)usage +- (void)controller:(GCController *)controller axisChanged:(GCControllerDirectionPad *)axis usage:(GBControllerUsage)usage childrenUsages:(NSSet *)childrenUsages { [self updateLastController:controller]; bool left = axis.left.value > 0.5; @@ -760,6 +766,21 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) bool up = axis.up.value > 0.5; bool down = axis.down.value > 0.5; + bool hasUnmappedChild = false; + for (NSNumber *childUsage in childrenUsages) { + if ([GBSettingsViewController controller:controller convertUsageToButton:childUsage.unsignedIntValue] != GBUnusedButton) { + GCControllerButtonInput *child = (id)(GCControllerGetElements(controller)[childUsage]); + if ([child isKindOfClass:[GCControllerButtonInput class]] && child.pressed) { + left = right = up = down = false; + } + } + else { + hasUnmappedChild = true; + } + } + + if (!hasUnmappedChild) return; + if (_running && (left || right || up || down ) && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBControllersHideInterface"]) { _backgroundView.fullScreenMode = true; diff --git a/iOS/GCControllerGetElements.h b/iOS/GCControllerGetElements.h @@ -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, + GBUsageMiscStartIndex, // Add to this +} GBControllerUsage; + + +NSDictionary <NSNumber *, GCControllerElement *> *GCControllerGetElements(GCController *self); diff --git a/iOS/GCControllerGetElements.m b/iOS/GCControllerGetElements.m @@ -0,0 +1,118 @@ +#import "GCControllerGetElements.h" +#import <objc/runtime.h> + +static NSDictionary <NSNumber *, GCControllerElement *> *GCControllerGetElementsLegacy(GCExtendedGamepad *self) +{ + if (!self) return nil; + 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; + } + return ret; +} + +API_AVAILABLE(ios(14.0)) +static NSDictionary <NSNumber *, GCControllerElement *> *GCControllerGetElementsModern(GCPhysicalInputProfile *self) +{ + static NSDictionary * nameToUsage = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + nameToUsage = @{ + GCInputButtonA: @(GBUsageButtonA), + GCInputButtonB: @(GBUsageButtonB), + GCInputButtonX: @(GBUsageButtonX), + GCInputButtonY: @(GBUsageButtonY), + + GCInputLeftThumbstick: @(GBUsageLeftThumbstick), + GCInputRightThumbstick: @(GBUsageRightThumbstick), + GCInputLeftThumbstickButton: @(GBUsageLeftThumbstickButton), + GCInputRightThumbstickButton: @(GBUsageRightThumbstickButton), + GCInputLeftShoulder: @(GBUsageLeftShoulder), + GCInputRightShoulder: @(GBUsageRightShoulder), + GCInputLeftTrigger: @(GBUsageLeftTrigger), + GCInputRightTrigger: @(GBUsageRightTrigger), + + GCInputButtonMenu: @(GBUsageButtonMenu), + GCInputButtonOptions: @(GBUsageButtonOptions), + GCInputDualShockTouchpadButton: @(GBUsageTouchpadButton), + }; + }); + + NSArray<NSString *> *elementNames = [self.elements.allKeys sortedArrayUsingSelector:@selector(compare:)]; + + NSMutableDictionary <NSNumber *, GCControllerElement *> *ret = [NSMutableDictionary dictionary]; + GBControllerUsage miscUsage = GBUsageMiscStartIndex; + for (NSString *name in elementNames) { + if ([name isEqualToString:GCInputButtonHome]) continue; + GCControllerElement *input = self.elements[name]; + NSNumber *_usage = nameToUsage[name]; + GBControllerUsage usage; + if (_usage) { + usage = _usage.unsignedIntValue; + // These get weird mappings by default + if ([self.device.vendorName isEqualToString:@"Joy-Con (L)"] || + [self.device.vendorName isEqualToString:@"Joy-Con (R)"]) { + switch (usage) { + case GBUsageButtonA: usage = GBUsageButtonB; break; + case GBUsageButtonB: usage = GBUsageButtonY; break; + case GBUsageButtonX: usage = GBUsageButtonA; break; + case GBUsageButtonY: usage = GBUsageButtonX; break; + default: break; + } + } + } + else if ([input isKindOfClass:[GCControllerDirectionPad class]] && !ret[@(GBUsageDpad)]) { + usage = GBUsageDpad; + } + else { + usage = miscUsage++; + } + if ([ret.allValues containsObject:input]) { + NSNumber *alias = [ret allKeysForObject:input][0]; + if (alias.unsignedIntValue >= GBUsageMiscStartIndex) { + [ret removeObjectForKey:alias]; + } + else { + continue; + } + } + ret[@(usage)] = input; + } + + return ret; +} + +NSDictionary <NSNumber *, GCControllerElement *> *GCControllerGetElements(GCController *self) +{ + NSDictionary <NSNumber *, GCControllerElement *> *ret = nil; + ret = objc_getAssociatedObject(self, GCControllerGetElements); + if (ret) return ret; + if (@available(iOS 14.0, *)) { + ret = GCControllerGetElementsModern(self.physicalInputProfile); + } + else { + ret = GCControllerGetElementsLegacy(self.extendedGamepad); + } + objc_setAssociatedObject(self, GCControllerGetElements, ret, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + return ret; +} diff --git a/iOS/GCExtendedGamepad+AllElements.h b/iOS/GCExtendedGamepad+AllElements.h @@ -1,25 +0,0 @@ -#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 diff --git a/iOS/GCExtendedGamepad+AllElements.m b/iOS/GCExtendedGamepad+AllElements.m @@ -1,48 +0,0 @@ -#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
This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.