From 2776c8ad36e06d1f211b5f73438594bcd6e25f9b Mon Sep 17 00:00:00 2001 From: Lior Halphon Date: Sun, 30 Oct 2022 14:42:54 +0200 Subject: [PATCH] Add Joy-Con pairing interface --- Cocoa/GBApp.m | 13 +- Cocoa/GBJoyConManager.h | 13 ++ Cocoa/GBJoyConManager.m | 251 ++++++++++++++++++++++++++++ Cocoa/GBPreferencesWindow.h | 1 + Cocoa/GBPreferencesWindow.m | 13 ++ Cocoa/GBTintedImageCell.h | 5 + Cocoa/GBTintedImageCell.m | 20 +++ Cocoa/JoyConCombinedTemplate.png | Bin 0 -> 315 bytes Cocoa/JoyConCombinedTemplate@2x.png | Bin 0 -> 483 bytes Cocoa/JoyConLeftTemplate.png | Bin 0 -> 266 bytes Cocoa/JoyConLeftTemplate@2x.png | Bin 0 -> 502 bytes Cocoa/JoyConRightTemplate.png | Bin 0 -> 262 bytes Cocoa/JoyConRightTemplate@2x.png | Bin 0 -> 411 bytes Cocoa/Preferences.xib | 138 ++++++++++++++- JoyKit/JOYController.h | 14 +- JoyKit/JOYController.m | 77 +++++---- 16 files changed, 502 insertions(+), 43 deletions(-) create mode 100644 Cocoa/GBJoyConManager.h create mode 100644 Cocoa/GBJoyConManager.m create mode 100644 Cocoa/GBTintedImageCell.h create mode 100644 Cocoa/GBTintedImageCell.m create mode 100644 Cocoa/JoyConCombinedTemplate.png create mode 100644 Cocoa/JoyConCombinedTemplate@2x.png create mode 100644 Cocoa/JoyConLeftTemplate.png create mode 100644 Cocoa/JoyConLeftTemplate@2x.png create mode 100644 Cocoa/JoyConRightTemplate.png create mode 100644 Cocoa/JoyConRightTemplate@2x.png diff --git a/Cocoa/GBApp.m b/Cocoa/GBApp.m index 20efd63..aa54a12 100644 --- a/Cocoa/GBApp.m +++ b/Cocoa/GBApp.m @@ -1,8 +1,9 @@ #import "GBApp.h" -#include "GBButtons.h" -#include "GBView.h" -#include "Document.h" -#include +#import "GBButtons.h" +#import "GBView.h" +#import "Document.h" +#import "GBJoyConManager.h" +#import #import #import #import @@ -79,6 +80,8 @@ static uint32_t color_to_int(NSColor *color) @"GBMBC7JoystickOverride": @NO, @"GBMBC7AllowMouse": @YES, + @"GBJoyConAutoPair": @YES, + // Default themes @"GBThemes": @{ @"Desert": @{ @@ -147,6 +150,8 @@ static uint32_t color_to_int(NSColor *color) JOYHatsEmulateButtonsKey: @YES, }]; + [GBJoyConManager sharedInstance]; // Starts handling Joy-Cons + [JOYController registerListener:self]; if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { diff --git a/Cocoa/GBJoyConManager.h b/Cocoa/GBJoyConManager.h new file mode 100644 index 0000000..0102013 --- /dev/null +++ b/Cocoa/GBJoyConManager.h @@ -0,0 +1,13 @@ +#import +#import +#import + +@interface GBJoyConManager : NSObject ++ (instancetype) sharedInstance; + +@property bool arrangementMode; +@property (weak) IBOutlet NSTableView *tableView; +@property (nonatomic) IBOutlet NSButton *autoPairCheckbox; + +@end + diff --git a/Cocoa/GBJoyConManager.m b/Cocoa/GBJoyConManager.m new file mode 100644 index 0000000..1340094 --- /dev/null +++ b/Cocoa/GBJoyConManager.m @@ -0,0 +1,251 @@ +#import "GBJoyConManager.h" +#import "GBTintedImageCell.h" +#import + +@implementation GBJoyConManager +{ + GBTintedImageCell *_tintedImageCell; + NSImageCell *_imageCell; + NSMutableDictionary *_pairings; + NSButton *_autoPairCheckbox; + bool _unpairing; +} + ++ (instancetype)sharedInstance +{ + static GBJoyConManager *manager = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + manager = [[self alloc] _init]; + }); + return manager; +} + +- (NSArray *)joycons +{ + NSMutableArray *ret = [[JOYController allControllers] mutableCopy]; + for (JOYController *controller in [JOYController allControllers]) { + if (controller.joyconType == JOYJoyConTypeNone) { + [ret removeObject:controller]; + } + } + return ret; +} + +- (instancetype)init +{ + return [self.class sharedInstance]; +} + +- (instancetype) _init +{ + self = [super init]; + _imageCell = [[NSImageCell alloc] init]; + _tintedImageCell = [[GBTintedImageCell alloc] init]; + if (@available(macOS 10.14, *)) { + _tintedImageCell.tint = [NSColor controlAccentColor]; + } + else { + _tintedImageCell.tint = [NSColor selectedMenuItemColor]; + } + _pairings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoyConPairings"] ?: @{} mutableCopy]; + + // Sanity check the pairings + for (NSString *key in _pairings) { + if (![_pairings[_pairings[key]] isEqualToString:key]) { + [_pairings removeAllObjects]; + break; + } + } + + [JOYController registerListener:self]; + for (JOYController *controller in [JOYController allControllers]) { + [self controllerConnected:controller]; + } + + return self; +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView +{ + return self.joycons.count; +} + +- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + if (row >= [self numberOfRowsInTableView:tableView]) return nil; + + unsigned columnIndex = [[tableView tableColumns] indexOfObject:tableColumn]; + switch (columnIndex) { + case 0: { + JOYController *controller = self.joycons[row]; + switch (controller.joyconType) { + case JOYJoyConTypeNone: + return nil; + case JOYJoyConTypeLeft: + return [NSImage imageNamed:@"JoyConLeftTemplate"]; + case JOYJoyConTypeRight: + return [NSImage imageNamed:@"JoyConRightTemplate"]; + case JOYJoyConTypeCombined: + return [NSImage imageNamed:@"JoyConCombinedTemplate"]; + } + } + case 1: { + JOYController *controller = self.joycons[row]; + NSMutableAttributedString *ret = [[NSMutableAttributedString alloc] initWithString:controller.deviceName + attributes:@{NSFontAttributeName: + [NSFont systemFontOfSize:[NSFont systemFontSize]]}]; + + [ret appendAttributedString:[[NSAttributedString alloc] initWithString:[@"\n" stringByAppendingString:controller.uniqueID] + attributes:@{NSFontAttributeName: + [NSFont systemFontOfSize:[NSFont smallSystemFontSize]], + NSForegroundColorAttributeName:[NSColor disabledControlTextColor]}]]; + return ret; + } + case 2: + return @(rand() % 3); + } + return nil; +} + +-(NSCell *)tableView:(NSTableView *)tableView dataCellForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + if (row >= [self numberOfRowsInTableView:tableView]) return [[NSCell alloc] init]; + + unsigned columnIndex = [[tableView tableColumns] indexOfObject:tableColumn]; + if (columnIndex == 2) { + JOYCombinedController *controller = (JOYCombinedController *)self.joycons[row]; + if (controller.joyconType == JOYJoyConTypeCombined) { + NSButtonCell *cell = [[NSButtonCell alloc] initTextCell:@"Separate Joy-Cons"]; + cell.bezelStyle = NSBezelStyleRounded; + cell.action = @selector(invoke); + id block = ^(void) { + for (JOYController *child in controller.children) { + [_pairings removeObjectForKey:child.uniqueID]; + } + [[NSUserDefaults standardUserDefaults] setObject:_pairings forKey:@"GBJoyConPairings"]; + _unpairing = true; + [controller breakApart]; + _unpairing = false; + }; + // To retain the block + objc_setAssociatedObject(cell, @selector(breakApart), block, OBJC_ASSOCIATION_RETAIN); + cell.target = block; + return cell; + } + } + if (columnIndex == 0) { + JOYController *controller = self.joycons[row]; + for (JOYButton *button in controller.buttons) { + if (button.isPressed) { + return _tintedImageCell; + } + } + return _imageCell; + } + return nil; +} + +- (void)controllerConnected:(JOYController *)controller +{ + for (JOYController *partner in [JOYController allControllers]) { + if ([partner.uniqueID isEqualToString:_pairings[controller.uniqueID]]) { + [self pairJoyCon:controller withJoyCon:partner]; + break; + } + } + if (controller.joyconType == JOYJoyConTypeLeft || controller.joyconType == JOYJoyConTypeRight) { + [self autopair]; + } + [self.tableView reloadData]; +} + +- (void)autopair +{ + if (_unpairing) return; + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBJoyConAutoPair"]) return; + NSArray *controllers = [[JOYController allControllers] copy]; + for (JOYController *first in controllers) { + if (_pairings[first.uniqueID]) continue; // Has an established partner + if (first.joyconType != JOYJoyConTypeLeft) continue; + for (JOYController *second in controllers) { + if (_pairings[second.uniqueID]) continue; // Has an established partner + if (second.joyconType != JOYJoyConTypeRight) continue; + [self pairJoyCon:first withJoyCon:second]; + break; + } + } + [self.tableView reloadData]; +} + +- (void)controllerDisconnected:(JOYController *)controller +{ + [self.tableView reloadData]; +} + +- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + return false; +} + +- (JOYCombinedController *)pairJoyCon:(JOYController *)first withJoyCon:(JOYController *)second +{ + if (first.joyconType != JOYJoyConTypeLeft && first.joyconType != JOYJoyConTypeRight) return nil; // Not a Joy-Con + if (second.joyconType != JOYJoyConTypeLeft && second.joyconType != JOYJoyConTypeRight) return nil; // Not a Joy-Con + if (first.joyconType == second.joyconType) return nil; // Not a sensible pair + + _pairings[first.uniqueID] = second.uniqueID; + _pairings[second.uniqueID] = first.uniqueID; + [[NSUserDefaults standardUserDefaults] setObject:_pairings forKey:@"GBJoyConPairings"]; + return [[JOYCombinedController alloc] initWithChildren:@[first, second]]; +} + +- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button +{ + if (!_arrangementMode) return; + if (controller.joyconType == JOYJoyConTypeNone) return; + [self.tableView setNeedsDisplay:true]; + if (controller.joyconType != JOYJoyConTypeLeft && controller.joyconType != JOYJoyConTypeRight) return; + if (button.usage != JOYButtonUsageL1 && button.usage != JOYButtonUsageR1) return; + + + // L or R were pressed on a single Joy-Con, try and pair available Joy-Cons + JOYController *left = nil; + JOYController *right = nil; + for (JOYController *controller in [JOYController allControllers]) { + if (!left && controller.joyconType == JOYJoyConTypeLeft) { + for (JOYButton *button in controller.buttons) { + if (button.usage == JOYButtonUsageL1 && button.isPressed) { + left = controller; + break; + } + } + } + if (!right && controller.joyconType == JOYJoyConTypeRight) { + for (JOYButton *button in controller.buttons) { + if (button.usage == JOYButtonUsageR1 && button.isPressed) { + right = controller; + break; + } + } + } + if (left && right) { + [self pairJoyCon:left withJoyCon:right]; + return; + } + } +} + +- (void)setAutoPairCheckbox:(NSButton *)autoPairCheckbox +{ + _autoPairCheckbox = autoPairCheckbox; + [_autoPairCheckbox setState:[[NSUserDefaults standardUserDefaults] boolForKey:@"GBJoyConAutoPair"]]; +} + +- (IBAction)toggleAutoPair:(NSButton *)sender +{ + [[NSUserDefaults standardUserDefaults] setBool:sender.state forKey:@"GBJoyConAutoPair"]; + [self autopair]; +} + +@end diff --git a/Cocoa/GBPreferencesWindow.h b/Cocoa/GBPreferencesWindow.h index 8e11830..15afceb 100644 --- a/Cocoa/GBPreferencesWindow.h +++ b/Cocoa/GBPreferencesWindow.h @@ -38,4 +38,5 @@ @property IBOutlet NSWindow *paletteEditor; @property IBOutlet NSButton *joystickMBC7Checkbox; @property IBOutlet NSButton *mouseMBC7Checkbox; +@property IBOutlet NSWindow *joyconsSheet; @end diff --git a/Cocoa/GBPreferencesWindow.m b/Cocoa/GBPreferencesWindow.m index a9c2c91..c0efc38 100644 --- a/Cocoa/GBPreferencesWindow.m +++ b/Cocoa/GBPreferencesWindow.m @@ -1,4 +1,5 @@ #import "GBPreferencesWindow.h" +#import "GBJoyConManager.h" #import "NSString+StringForKey.h" #import "GBButtons.h" #import "BigSurToolbar.h" @@ -999,4 +1000,16 @@ static inline NSString *keyEquivalentString(NSMenuItem *item) preferredEdge:NSRectEdgeMaxX]; } +- (IBAction)arrangeJoyCons:(id)sender +{ + [GBJoyConManager sharedInstance].arrangementMode = true; + [self beginSheet:self.joyconsSheet completionHandler:nil]; +} + +- (IBAction)closeJoyConsSheet:(id)sender +{ + [self endSheet:self.joyconsSheet]; + [GBJoyConManager sharedInstance].arrangementMode = false; +} + @end diff --git a/Cocoa/GBTintedImageCell.h b/Cocoa/GBTintedImageCell.h new file mode 100644 index 0000000..eb6c8b3 --- /dev/null +++ b/Cocoa/GBTintedImageCell.h @@ -0,0 +1,5 @@ +#import + +@interface GBTintedImageCell : NSImageCell +@property NSColor *tint; +@end diff --git a/Cocoa/GBTintedImageCell.m b/Cocoa/GBTintedImageCell.m new file mode 100644 index 0000000..af4faa6 --- /dev/null +++ b/Cocoa/GBTintedImageCell.m @@ -0,0 +1,20 @@ +#import "GBTintedImageCell.h" + +@implementation GBTintedImageCell + +- (NSImage *)image +{ + if (!self.tint || !super.image.isTemplate) { + return [super image]; + } + + NSImage *tinted = [super.image copy]; + [tinted lockFocus]; + [self.tint set]; + NSRectFillUsingOperation((NSRect){.size = tinted.size}, NSCompositeSourceIn); + [tinted unlockFocus]; + tinted.template = false; + return tinted; +} + +@end diff --git a/Cocoa/JoyConCombinedTemplate.png b/Cocoa/JoyConCombinedTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..42e7a2711997be841258473f652f5211662e2813 GIT binary patch literal 315 zcmV-B0mS}^P)kdg00033NklA%LC()LxN_p@J4300WpYBWmmwunz=C7@)ymxKeV(U}#-=g(m10*Ohfq#Kom^)BnNjuBKyV}+4 zg8D?H6tr}_7^Y)Doo|ns^ox#`f|Q7j=Rsqv(hgu84UgrQ-ze#KskD{uDY@ydzf&Jj zX)j%KSHHeKtkPDxp8gA^6P32o<>-2S5P<&29iw&67X}z;h#+;U(pEZ!A}BxsZBT#~ zI%VNaMMg|aM#b9^z>}PWgq)`)*`dVzH~Dmzd_*_|kWVz~IYOCMx+W?o0LibuWG1Uj zve|Ct7a*SP#=<5L1sGlLYvI>Z-ULhEJzo=a!X?Yk*R&|$@)+&_a2tqrn80l)-r<^)Zv;BC7TG%iojx((I59s-Zx Z0N;Z<Rs literal 0 HcmV?d00001 diff --git a/Cocoa/JoyConLeftTemplate.png b/Cocoa/JoyConLeftTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..924c427bdfa175382fba48d0eddb481a75159158 GIT binary patch literal 266 zcmV+l0rmcgP)kdg0002dNkl8KwW~miHnSJ{hwG27?#?J#Q^nIR{Mh5nd07f=9TV^F?WVzqC4lt8(7MpC>fYg QM*si-07*qoM6N<$f<1L=T>t<8 literal 0 HcmV?d00001 diff --git a/Cocoa/JoyConLeftTemplate@2x.png b/Cocoa/JoyConLeftTemplate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6b2f9969412096409d6f6595ade5510bdf0063ea GIT binary patch literal 502 zcmVXK3Ih^`+RTDtXbArRATU$I zLbA*rV!yg>T}N{5TUbx^?!8<;?R$N_ysy%D4dt9}vcW2g%rk8_Xr;dS;Dtvjtrfh7 z`XfM!J$45BY1P`S&NWw5%>zU^=7=dbzYYLEfpe;60aiF8G0d+A04~Xx1c-7m{PukS zb#`^d96+8EFFywW@iam<^#h=l8GtO7 zEWiWXvH+hf*mvcgGS9#|arV1)%0GaHt>$ZTI%l(4SZxIV0PHRWedw^>aS$XxQkNXo z1jQf$BKpf=ouf8U0OBDKPeSp}P^1uzaGX|oSxNO&a3tqxytaD28LAY#=;nFRD sMaD1bwmo$huK{LRHY{HIJH!Eg0mI(55Zpw^qW}N^07*qoM6N<$g7wGHX8-^I literal 0 HcmV?d00001 diff --git a/Cocoa/JoyConRightTemplate.png b/Cocoa/JoyConRightTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..1fccf5f67cdcf67e4f5750c04ec51a0ddab654dc GIT binary patch literal 262 zcmV+h0r~!kP)kdg0002ZNkl)dmyModYBqN1y!%9PY&yr*3*&B~MxB?}5dZ)H M07*qoM6N<$f@|(()Bpeg literal 0 HcmV?d00001 diff --git a/Cocoa/JoyConRightTemplate@2x.png b/Cocoa/JoyConRightTemplate@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d9c385caf09b29cc269ff5b517aa668e5a8f1f45 GIT binary patch literal 411 zcmV;M0c8G(P)53r`6GW_I1^||DiKsFJ-7SzhJ<>^tDy9fYjyOOR+3!b(<0g7~ zjy__?Mt-n(*P3gE9Zon~bP_Jekzs=+CV05)fQEs98MgSX0xvopu?_>6!L)&wovzqp z?jNxI8+hC4fQ4Vcs($$c0TzeqE%*ZL9s`3q?R^4HBLG*d{Q=G+02W6~d;#nzfW?vx z*suYa44{C9f`UfT$;@Q{y2;bsJwgVcnmpxZeg)w95CjFZQ}3tet_Tn4``u0~B%p8P zo!Gwy(1(CN3F)6n$0wO>JP{JTZf&?iHN@+mZt^)U!pp*p|C)daV zQYSZ+1td;x@?`+}5YQ(f{WGL5L~$>OfC%_-z=aLS*?^1zdjX+~*I^qSa05JLNPKuFhvNVM002ovPDHLk FV1lA_sXqV! literal 0 HcmV?d00001 diff --git a/Cocoa/Preferences.xib b/Cocoa/Preferences.xib index 7cd82c4..286a6f5 100644 --- a/Cocoa/Preferences.xib +++ b/Cocoa/Preferences.xib @@ -89,6 +89,7 @@ + @@ -940,6 +941,9 @@ + + + @@ -1074,7 +1078,7 @@ - + @@ -1306,6 +1310,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Text Cell +Test + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/JoyKit/JOYController.h b/JoyKit/JOYController.h index 7d110f6..426900b 100644 --- a/JoyKit/JOYController.h +++ b/JoyKit/JOYController.h @@ -29,13 +29,18 @@ typedef enum { JOYControllerCombinedTypeCombined, } JOYControllerCombinedType; +typedef enum { + JOYJoyConTypeNone, + JOYJoyConTypeLeft, + JOYJoyConTypeRight, + JOYJoyConTypeCombined, +} JOYJoyConType; + @interface JOYController : NSObject + (void)startOnRunLoop:(NSRunLoop *)runloop withOptions: (NSDictionary *)options; + (NSArray *) allControllers; + (void) registerListener:(id)listener; + (void) unregisterListener:(id)listener; -- (NSString *)deviceName; -- (NSString *)uniqueID; - (JOYControllerCombinedType)combinedControllerType; - (NSArray *) buttons; - (NSArray *) axes; @@ -47,12 +52,15 @@ typedef enum { - (void)setPlayerLEDs:(uint8_t)mask; - (uint8_t)LEDMaskForPlayer:(unsigned)player; @property (readonly, getter=isConnected) bool connected; +@property (readonly) JOYJoyConType joyconType; +@property (readonly) NSString *deviceName; +@property (readonly) NSString *uniqueID; @end @interface JOYCombinedController : JOYController - (instancetype)initWithChildren:(NSArray *)children; - (void)breakApart; -@property (readonly) NSArray *chidlren; +@property (readonly) NSArray *children; @end diff --git a/JoyKit/JOYController.m b/JoyKit/JOYController.m index 7cba924..68ecadb 100644 --- a/JoyKit/JOYController.m +++ b/JoyKit/JOYController.m @@ -12,13 +12,6 @@ extern NSTextField *globalDebugField; #define PWM_RESOLUTION 16 -typedef enum { - JOYJoyConTypeNone, - JOYJoyConTypeLeft, - JOYJoyConTypeRight, - JOYJoyConTypeCombined, -} JOYJoyConType; - static NSString const *JOYAxisGroups = @"JOYAxisGroups"; static NSString const *JOYReportIDFilters = @"JOYReportIDFilters"; static NSString const *JOYButtonUsageMapping = @"JOYButtonUsageMapping"; @@ -458,6 +451,7 @@ typedef union { _device = (IOHIDDeviceRef)CFRetain(device); _serialSuffix = suffix; _playerLEDs = -1; + [self obtainInfo]; IOHIDDeviceRegisterInputValueCallback(device, HIDInput, (void *)self); IOHIDDeviceScheduleWithRunLoop(device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); @@ -478,6 +472,7 @@ typedef union { _isSwitch = [_hacks[JOYIsSwitch] boolValue]; _isDualShock3 = [_hacks[JOYIsDualShock3] boolValue]; _isSony = [_hacks[JOYIsSony] boolValue]; + _joyconType = [_hacks[JOYJoyCon] unsignedIntValue]; NSDictionary *customReports = hacks[JOYCustomReports]; _lastReport = [NSMutableData dataWithLength:MAX( @@ -657,15 +652,10 @@ typedef union { return self; } -- (NSString *)deviceName -{ - if (!_device) return nil; - return IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDProductKey)); -} -- (NSString *)uniqueID +- (void)obtainInfo { - if (!_device) return nil; + _deviceName = IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDProductKey)); NSString *serial = (__bridge NSString *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDSerialNumberKey)); if (!serial || [(__bridge NSString *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDTransportKey)) isEqualToString:@"USB"]) { serial = [NSString stringWithFormat:@"%04x%04x%08x", @@ -674,9 +664,10 @@ typedef union { [(__bridge NSNumber *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDLocationIDKey)) unsignedIntValue]]; } if (_serialSuffix) { - return [NSString stringWithFormat:@"%@-%@", serial, _serialSuffix]; + _uniqueID = [NSString stringWithFormat:@"%@-%@", serial, _serialSuffix]; + return; } - return serial; + _uniqueID = serial; } - (JOYControllerCombinedType)combinedControllerType @@ -798,7 +789,7 @@ typedef union { [listener controller:_parent ?: self movedAxis:axis]; } } - JOYEmulatedButton *button = _axisEmulatedButtons[@(axis.uniqueID)]; + JOYEmulatedButton *button = _axisEmulatedButtons[@(axis.uniqueID & 0xFFFFFFFF)]; // Mask the combined prefix away if ([button updateStateFromAxis:axis]) { for (id listener in listeners) { if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) { @@ -820,7 +811,7 @@ typedef union { [listener controller:_parent ?: self movedAxes2D:axes]; } } - NSArray *buttons = _axes2DEmulatedButtons[@(axes.uniqueID)]; + NSArray *buttons = _axes2DEmulatedButtons[@(axes.uniqueID & 0xFFFFFFFF)]; // Mask the combined prefix away for (JOYEmulatedButton *button in buttons) { if ([button updateStateFromAxes2D:axes]) { for (id listener in listeners) { @@ -859,7 +850,7 @@ typedef union { } } - NSArray *buttons = _hatEmulatedButtons[@(hat.uniqueID)]; + NSArray *buttons = _hatEmulatedButtons[@(hat.uniqueID & 0xFFFFFFFF)]; // Mask the combined prefix away for (JOYEmulatedButton *button in buttons) { if ([button updateStateFromHat:hat]) { for (id listener in listeners) { @@ -877,6 +868,7 @@ typedef union { - (void)disconnected { + _physicallyConnected = false; [_parent breakApart]; if (_logicallyConnected && [exposedControllers containsObject:self]) { for (id listener in listeners) { @@ -885,7 +877,6 @@ typedef union { } } } - _physicallyConnected = false; [exposedControllers removeObject:self]; [self setRumbleAmplitude:0]; dispatch_sync(_rumbleQueue, ^{ @@ -1211,13 +1202,13 @@ typedef union { { self = [super init]; // Sorting makes the device name and unique id consistent - _chidlren = [children sortedArrayUsingComparator:^NSComparisonResult(JOYController *a, JOYController *b) { + _children = [children sortedArrayUsingComparator:^NSComparisonResult(JOYController *a, JOYController *b) { return [a.uniqueID compare:b.uniqueID]; }]; - if (_chidlren.count == 0) return nil; + if (_children.count == 0) return nil; - for (JOYController *child in _chidlren) { + for (JOYController *child in _children) { if (child.combinedControllerType != JOYControllerCombinedTypeSingle) { NSLog(@"Cannot combine non-single controller %@", child); return nil; @@ -1229,7 +1220,7 @@ typedef union { } unsigned index = 0; - for (JOYController *child in _chidlren) { + for (JOYController *child in _children) { for (id listener in listeners) { if ([listener respondsToSelector:@selector(controllerDisconnected:)]) { [listener controllerDisconnected:child]; @@ -1267,11 +1258,12 @@ typedef union { } } - for (JOYController *child in _chidlren) { + for (JOYController *child in _children) { child->_parent = nil; for (JOYInput *input in child.allInputs) { input.combinedIndex = 0; } + if (!child.connected) break; [exposedControllers addObject:child]; for (id listener in listeners) { if ([listener respondsToSelector:@selector(controllerConnected:)]) { @@ -1284,7 +1276,7 @@ typedef union { - (NSString *)deviceName { NSString *ret = nil; - for (JOYController *child in _chidlren) { + for (JOYController *child in _children) { if (ret) { ret = [ret stringByAppendingFormat:@" + %@", child.deviceName]; } @@ -1298,7 +1290,7 @@ typedef union { - (NSString *)uniqueID { NSString *ret = nil; - for (JOYController *child in _chidlren) { + for (JOYController *child in _children) { if (ret) { ret = [ret stringByAppendingFormat:@"+%@", child.uniqueID]; } @@ -1317,7 +1309,7 @@ typedef union { - (NSArray *)buttons { NSArray *ret = nil; - for (JOYController *child in _chidlren) { + for (JOYController *child in _children) { if (ret) { ret = [ret arrayByAddingObjectsFromArray:child.buttons]; } @@ -1331,7 +1323,7 @@ typedef union { - (NSArray *)axes { NSArray *ret = nil; - for (JOYController *child in _chidlren) { + for (JOYController *child in _children) { if (ret) { ret = [ret arrayByAddingObjectsFromArray:child.axes]; } @@ -1345,7 +1337,7 @@ typedef union { - (NSArray *)axes2D { NSArray *ret = nil; - for (JOYController *child in _chidlren) { + for (JOYController *child in _children) { if (ret) { ret = [ret arrayByAddingObjectsFromArray:child.axes2D]; } @@ -1359,7 +1351,7 @@ typedef union { - (NSArray *)axes3D { NSArray *ret = nil; - for (JOYController *child in _chidlren) { + for (JOYController *child in _children) { if (ret) { ret = [ret arrayByAddingObjectsFromArray:child.axes3D]; } @@ -1373,7 +1365,7 @@ typedef union { - (NSArray *)hats { NSArray *ret = nil; - for (JOYController *child in _chidlren) { + for (JOYController *child in _children) { if (ret) { ret = [ret arrayByAddingObjectsFromArray:child.hats]; } @@ -1386,7 +1378,7 @@ typedef union { - (void)setRumbleAmplitude:(double)amp { - for (JOYController *child in _chidlren) { + for (JOYController *child in _children) { [child setRumbleAmplitude:amp]; } } @@ -1395,7 +1387,7 @@ typedef union { { // Mask is actually just the player ID in a combined controller to // allow combining controllers with different LED layouts - for (JOYController *child in _chidlren) { + for (JOYController *child in _children) { [child setPlayerLEDs:[child LEDMaskForPlayer:mask]]; } } @@ -1412,7 +1404,7 @@ typedef union { return false; } - for (JOYController *child in _chidlren) { + for (JOYController *child in _children) { if (!child.isConnected) { return false; // Should never happen } @@ -1421,4 +1413,19 @@ typedef union { return true; } +- (JOYJoyConType)joyconType +{ + if (_children.count != 2) return JOYJoyConTypeNone; + if (_children[0].joyconType == JOYJoyConTypeLeft && + _children[1].joyconType == JOYJoyConTypeRight) { + return JOYJoyConTypeCombined; + } + + if (_children[1].joyconType == JOYJoyConTypeLeft && + _children[0].joyconType == JOYJoyConTypeRight) { + return JOYJoyConTypeCombined; + } + return JOYJoyConTypeNone; +} + @end