From 943a16b46378c68b433d335342782408a6931e76 Mon Sep 17 00:00:00 2001 From: Akop Karapetyan Date: Mon, 31 May 2021 02:18:34 -0700 Subject: [PATCH] Initial work on input pref. UI --- .../xcode/Emulator.xcodeproj/project.pbxproj | 34 +++ .../xcode/Emulator/Base.lproj/Preferences.xib | 125 +++++++- .../Controllers/FBPreferencesController.h | 5 +- .../Controllers/FBPreferencesController.mm | 149 ++++++++++ src/dep/macos/hid/AKGamepad.h | 38 +++ src/dep/macos/hid/AKGamepad.m | 237 ++++++++++++++++ src/dep/macos/hid/AKGamepadEventData.h | 21 ++ src/dep/macos/hid/AKGamepadEventData.m | 19 ++ src/dep/macos/hid/AKGamepadManager.h | 56 ++++ src/dep/macos/hid/AKGamepadManager.m | 268 ++++++++++++++++++ 10 files changed, 939 insertions(+), 13 deletions(-) create mode 100644 src/dep/macos/hid/AKGamepad.h create mode 100644 src/dep/macos/hid/AKGamepad.m create mode 100644 src/dep/macos/hid/AKGamepadEventData.h create mode 100644 src/dep/macos/hid/AKGamepadEventData.m create mode 100644 src/dep/macos/hid/AKGamepadManager.h create mode 100644 src/dep/macos/hid/AKGamepadManager.m diff --git a/projectfiles/xcode/Emulator.xcodeproj/project.pbxproj b/projectfiles/xcode/Emulator.xcodeproj/project.pbxproj index ccf4e53b6..f8d35e708 100644 --- a/projectfiles/xcode/Emulator.xcodeproj/project.pbxproj +++ b/projectfiles/xcode/Emulator.xcodeproj/project.pbxproj @@ -1025,6 +1025,9 @@ FEC0A0E424ACDFBF0015AABF /* d_cischeat.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FEC0A0E324ACDFBE0015AABF /* d_cischeat.cpp */; }; FEC28FD1266471FE00EF37C2 /* d_redclash.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FEC28FD0266471FE00EF37C2 /* d_redclash.cpp */; }; FEC28FD22664774B00EF37C2 /* Alloc.c in Sources */ = {isa = PBXBuildFile; fileRef = FE1B1DE323561A650065200C /* Alloc.c */; }; + FEC28FE6266481BD00EF37C2 /* AKGamepad.m in Sources */ = {isa = PBXBuildFile; fileRef = FEC28FE0266481BD00EF37C2 /* AKGamepad.m */; }; + FEC28FE7266481BD00EF37C2 /* AKGamepadManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FEC28FE2266481BD00EF37C2 /* AKGamepadManager.m */; }; + FEC28FE8266481BD00EF37C2 /* AKGamepadEventData.m in Sources */ = {isa = PBXBuildFile; fileRef = FEC28FE3266481BD00EF37C2 /* AKGamepadEventData.m */; }; FEC5D3D6235C136F00ABA9FB /* FBVideo.mm in Sources */ = {isa = PBXBuildFile; fileRef = FEC5D3D5235C136F00ABA9FB /* FBVideo.mm */; }; FEC5D3D9235C160600ABA9FB /* FBScreenView.mm in Sources */ = {isa = PBXBuildFile; fileRef = FEC5D3D8235C160600ABA9FB /* FBScreenView.mm */; }; FED03ADE23FAF8E900E99A10 /* mdeeprom.cpp in Sources */ = {isa = PBXBuildFile; fileRef = FED03ADD23FAF8E900E99A10 /* mdeeprom.cpp */; }; @@ -2622,6 +2625,12 @@ FEC0A0E124A6F99A0015AABF /* d_magmax.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = d_magmax.cpp; sourceTree = ""; }; FEC0A0E324ACDFBE0015AABF /* d_cischeat.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = d_cischeat.cpp; sourceTree = ""; }; FEC28FD0266471FE00EF37C2 /* d_redclash.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = d_redclash.cpp; sourceTree = ""; }; + FEC28FE0266481BD00EF37C2 /* AKGamepad.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AKGamepad.m; sourceTree = ""; }; + FEC28FE1266481BD00EF37C2 /* AKGamepadEventData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AKGamepadEventData.h; sourceTree = ""; }; + FEC28FE2266481BD00EF37C2 /* AKGamepadManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AKGamepadManager.m; sourceTree = ""; }; + FEC28FE3266481BD00EF37C2 /* AKGamepadEventData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AKGamepadEventData.m; sourceTree = ""; }; + FEC28FE4266481BD00EF37C2 /* AKGamepad.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AKGamepad.h; sourceTree = ""; }; + FEC28FE5266481BD00EF37C2 /* AKGamepadManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AKGamepadManager.h; sourceTree = ""; }; FEC5D3D4235C136E00ABA9FB /* FBVideo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBVideo.h; sourceTree = ""; }; FEC5D3D5235C136F00ABA9FB /* FBVideo.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = FBVideo.mm; sourceTree = ""; }; FEC5D3D7235C160600ABA9FB /* FBScreenView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FBScreenView.h; sourceTree = ""; }; @@ -3291,6 +3300,7 @@ children = ( FE811001236B6B29000B5F73 /* generated */, FE1B1DA923561A640065200C /* libs */, + FEC28FDE266481BD00EF37C2 /* macos */, FEA5E79B23564A3200DA2D9D /* scripts */, ); name = dep; @@ -5047,6 +5057,27 @@ path = scripts; sourceTree = ""; }; + FEC28FDE266481BD00EF37C2 /* macos */ = { + isa = PBXGroup; + children = ( + FEC28FDF266481BD00EF37C2 /* hid */, + ); + path = macos; + sourceTree = ""; + }; + FEC28FDF266481BD00EF37C2 /* hid */ = { + isa = PBXGroup; + children = ( + FEC28FE0266481BD00EF37C2 /* AKGamepad.m */, + FEC28FE1266481BD00EF37C2 /* AKGamepadEventData.h */, + FEC28FE2266481BD00EF37C2 /* AKGamepadManager.m */, + FEC28FE3266481BD00EF37C2 /* AKGamepadEventData.m */, + FEC28FE4266481BD00EF37C2 /* AKGamepad.h */, + FEC28FE5266481BD00EF37C2 /* AKGamepadManager.h */, + ); + path = hid; + sourceTree = ""; + }; FEDF04C325EB472400F3EDD9 /* m377 */ = { isa = PBXGroup; children = ( @@ -6144,6 +6175,7 @@ FE1B267723561A770065200C /* d_taitob.cpp in Sources */, FE556116258E95B600A19F2D /* filter_neon_intrinsics.c in Sources */, FE1B24E923561A760065200C /* d_badlands.cpp in Sources */, + FEC28FE6266481BD00EF37C2 /* AKGamepad.m in Sources */, FE1B243123561A750065200C /* Bra.c in Sources */, FE1B250523561A760065200C /* d_shadfrce.cpp in Sources */, FE1B268323561A770065200C /* pgm_crypt.cpp in Sources */, @@ -6238,6 +6270,7 @@ FE1B271023561A780065200C /* d_qbert.cpp in Sources */, FE1B24B223561A750065200C /* d_m58.cpp in Sources */, FE1B270023561A780065200C /* d_holeland.cpp in Sources */, + FEC28FE7266481BD00EF37C2 /* AKGamepadManager.m in Sources */, FE1B261F23561A770065200C /* d_cps3.cpp in Sources */, FE1B259523561A760065200C /* d_deco32.cpp in Sources */, FE1B27D723561A790065200C /* mb87078.cpp in Sources */, @@ -6337,6 +6370,7 @@ FE1B24D623561A750065200C /* d_sailormn.cpp in Sources */, FE1B267023561A770065200C /* d_superchs.cpp in Sources */, FE2BC6B523615C3400B9D150 /* FBLogViewerController.m in Sources */, + FEC28FE8266481BD00EF37C2 /* AKGamepadEventData.m in Sources */, FE1B25C623561A760065200C /* d_ddribble.cpp in Sources */, FE1B254123561A760065200C /* d_blmbycar.cpp in Sources */, FE1B244C23561A750065200C /* 7zCrcOpt.c in Sources */, diff --git a/projectfiles/xcode/Emulator/Base.lproj/Preferences.xib b/projectfiles/xcode/Emulator/Base.lproj/Preferences.xib index 62e82cf0c..0c3096b3f 100644 --- a/projectfiles/xcode/Emulator/Base.lproj/Preferences.xib +++ b/projectfiles/xcode/Emulator/Base.lproj/Preferences.xib @@ -1,8 +1,8 @@ - + - + @@ -10,6 +10,8 @@ + + @@ -31,7 +33,7 @@ - + @@ -119,6 +121,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -126,14 +221,13 @@ - + - - + - + @@ -143,7 +237,6 @@ - @@ -156,7 +249,6 @@ - @@ -193,9 +285,8 @@ - + + + + + + + + + @@ -242,6 +342,7 @@ + diff --git a/projectfiles/xcode/Emulator/Controllers/FBPreferencesController.h b/projectfiles/xcode/Emulator/Controllers/FBPreferencesController.h index a847b472f..67a3d508e 100644 --- a/projectfiles/xcode/Emulator/Controllers/FBPreferencesController.h +++ b/projectfiles/xcode/Emulator/Controllers/FBPreferencesController.h @@ -15,13 +15,16 @@ #import #import "FBMainThread.h" +#import "AKGamepadManager.h" -@interface FBPreferencesController : NSWindowController +@interface FBPreferencesController : NSWindowController { IBOutlet NSToolbar *toolbar; IBOutlet NSTabView *contentTabView; IBOutlet NSTableView *dipswitchTableView; + IBOutlet NSTableView *inputTableView; IBOutlet NSButton *restoreDipButton; + IBOutlet NSPopUpButton *inputDevicesPopUp; } - (IBAction) tabChanged:(id) sender; diff --git a/projectfiles/xcode/Emulator/Controllers/FBPreferencesController.mm b/projectfiles/xcode/Emulator/Controllers/FBPreferencesController.mm index 3ea20970f..c3d2961d2 100644 --- a/projectfiles/xcode/Emulator/Controllers/FBPreferencesController.mm +++ b/projectfiles/xcode/Emulator/Controllers/FBPreferencesController.mm @@ -19,17 +19,24 @@ @interface FBPreferencesController() - (void) resetDipSwitches:(NSArray *) switches; +- (void) resetButtonList; +- (void) resetInputDevices; @end @implementation FBPreferencesController { NSArray *dipSwitches; + NSMutableArray *_inputDeviceList; + NSMutableDictionary *_inputDeviceMap; + NSString *_selectedInputDeviceId; } - (id) init { if (self = [super initWithWindowNibName:@"Preferences"]) { + _inputDeviceList = [NSMutableArray new]; + _inputDeviceMap = [NSMutableDictionary new]; } return self; @@ -38,10 +45,32 @@ - (void) awakeFromNib { [self.runloop addObserver:self]; + _selectedInputDeviceId = @"keyboard"; + NSDictionary *kybd = @{ @"id": @"keyboard", + @"title": NSLocalizedString(@"Keyboard", @"Device") }; + [_inputDeviceList addObject:kybd]; + [_inputDeviceMap setObject:kybd + forKey:@"keyboard"]; + + AKGamepadManager *gm = AKGamepadManager.sharedInstance; + for (int i = 0, n = (int) gm.gamepadCount; i < n; i++) { + AKGamepad *gamepad = [gm gamepadAtIndex:i]; + NSString *key = gamepad.vendorProductString; + NSDictionary *gp = @{ @"id": key, + @"title": gamepad.name }; + + [_inputDeviceList addObject:gp]; + [_inputDeviceMap setObject:gp + forKey:key]; + } + + [self resetInputDevices]; + [gm addObserver:self]; } - (void) dealloc { + [AKGamepadManager.sharedInstance removeObserver:self]; [self.runloop removeObserver:self]; } @@ -53,6 +82,81 @@ [self resetDipSwitches:[self.runloop dipSwitches]]; } +#pragma mark - AKGamepadDelegate + +- (void) gamepadDidConnect:(AKGamepad *) gamepad +{ + NSString *key = gamepad.vendorProductString; + @synchronized (_inputDeviceList) { + if (![_inputDeviceMap objectForKey:key]) { + NSDictionary *gp = @{ @"id": key, + @"title": gamepad.name }; + [_inputDeviceMap setObject:gp + forKey:key]; + [_inputDeviceList addObject:gp]; + } + } + [self resetInputDevices]; +} + +- (void) gamepadDidDisconnect:(AKGamepad *) gamepad +{ + NSString *key = gamepad.vendorProductString; + @synchronized (_inputDeviceList) { + NSDictionary *gp = [_inputDeviceMap objectForKey:key]; + [_inputDeviceMap removeObjectForKey:key]; + [_inputDeviceList removeObject:gp]; + } + [self resetInputDevices]; +} + +- (void) gamepad:(AKGamepad *) gamepad + xChanged:(NSInteger) newValue + center:(NSInteger) center + eventData:(AKGamepadEventData *) eventData +{ + // FIXME!! +// if ([[gamepad vendorProductString] isEqualToString:_selectedInputDeviceId]) { +// if ([[self window] firstResponder] == _joyCaptureView) { +// if (center - newValue > FXDeadzoneSize) { +// [_joyCaptureView captureCode:FXGamepadLeft]; +// } else if (newValue - center > FXDeadzoneSize) { +// [_joyCaptureView captureCode:FXGamepadRight]; +// } +// } +// } +} + +- (void) gamepad:(AKGamepad *) gamepad + yChanged:(NSInteger) newValue + center:(NSInteger) center + eventData:(AKGamepadEventData *) eventData +{ + // FIXME!! +// if ([[gamepad vendorProductString] isEqualToString:_selectedInputDeviceId]) { +// if ([[self window] firstResponder] == _joyCaptureView) { +// if (center - newValue > FXDeadzoneSize) { +// [_joyCaptureView captureCode:FXGamepadUp]; +// } else if (newValue - center > FXDeadzoneSize) { +// [_joyCaptureView captureCode:FXGamepadDown]; +// } +// } +// } +} + +- (void) gamepad:(AKGamepad *) gamepad + button:(NSUInteger) index + isDown:(BOOL) isDown + eventData:(AKGamepadEventData *) eventData +{ + // FIXME!! +// if ([[gamepad vendorProductString] isEqualToString:_selectedInputDeviceId]) { +// if ([[self window] firstResponder] == _joyCaptureView) { +// [_joyCaptureView captureCode:FXMakeButton(index)]; +// } +// } +} + #pragma mark - Actions - (void) tabChanged:(id) sender @@ -201,4 +305,49 @@ objectValueForTableColumn:(NSTableColumn *) tableColumn [dipswitchTableView reloadData]; } +- (void) resetButtonList +{ + [inputTableView abortEditing]; +/* FIXME!! + NSInteger index = inputDevicesPopUp.indexOfSelectedItem; + if (index < _inputDeviceList.count) { + _selectedInputDeviceId = [[_inputDeviceList objectAtIndex:index] objectForKey:@"id"]; + } + + [_inputList removeAllObjects]; + + BOOL isKeyboard = [@"keyboard" isEqualToString:_selectedInputDeviceId]; + FXEmulatorController *emulator = [[FXAppDelegate sharedInstance] emulator]; + + [[[emulator driver] buttons] enumerateObjectsUsingBlock:^(FXButton *b, NSUInteger idx, BOOL *stop) { + if (isKeyboard) { + FXButtonConfig *bc = [FXButtonConfig new]; + [bc setName:[b name]]; + [bc setTitle:[b title]]; + [bc setVirtualCode:[b code]]; + [_inputList addObject:bc]; + } else if ([b playerIndex] == 1) { + FXButtonConfig *bc = [FXButtonConfig new]; + [bc setName:[b name]]; + [bc setTitle:[b neutralTitle]]; + [bc setVirtualCode:[b code]]; + [_inputList addObject:bc]; + } + }]; + [inputTableView setEnabled:[_inputList count] > 0]; + [inputTableView reloadData]; + */ +} + +- (void) resetInputDevices +{ + [inputDevicesPopUp removeAllItems]; + [_inputDeviceList enumerateObjectsUsingBlock:^(NSDictionary *gp, NSUInteger idx, BOOL *stop) { + [inputDevicesPopUp addItemWithTitle:[gp objectForKey:@"title"]]; + }]; + + [inputDevicesPopUp selectItemAtIndex:0]; // select the keyboard + [self resetButtonList]; +} + @end diff --git a/src/dep/macos/hid/AKGamepad.h b/src/dep/macos/hid/AKGamepad.h new file mode 100644 index 000000000..6ed4d0394 --- /dev/null +++ b/src/dep/macos/hid/AKGamepad.h @@ -0,0 +1,38 @@ +// Copyright (c) Akop Karapetyan +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import + +@interface AKGamepad : NSObject + +@property (nonatomic, weak) id delegate; +@property (nonatomic, assign) NSInteger gamepadId; +@property (nonatomic, assign) NSUInteger index; + +@property (nonatomic, readonly) NSInteger locationId; +@property (nonatomic, readonly) NSInteger vendorId; +@property (nonatomic, readonly) NSInteger productId; +@property (nonatomic, readonly) NSString *name; + +- (id) initWithHidDevice:(IOHIDDeviceRef) device; + +- (void) registerForEvents; + +- (NSInteger) vendorProductId; +- (NSString *) vendorProductString; + +- (NSMutableDictionary *) currentAxisValues; + +@end diff --git a/src/dep/macos/hid/AKGamepad.m b/src/dep/macos/hid/AKGamepad.m new file mode 100644 index 000000000..d5aaf8e88 --- /dev/null +++ b/src/dep/macos/hid/AKGamepad.m @@ -0,0 +1,237 @@ +// Copyright (c) Akop Karapetyan +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "AKGamepad.h" + +#import "AKGamepadManager.h" + +#import + +#pragma mark - AKGamepad + +static void gamepadInputValueCallback(void *context, IOReturn result, void *sender, IOHIDValueRef value); + +@interface AKGamepad() + +- (void) unregisterFromEvents; +- (void) didReceiveInputValue:(IOHIDValueRef) valueRef; + +@end + +@implementation AKGamepad +{ + BOOL registeredForEvents; + IOHIDDeviceRef hidDevice; + + NSPoint _axes; +} + +- (id) initWithHidDevice:(IOHIDDeviceRef) device +{ + if ((self = [self init])) { + registeredForEvents = NO; + hidDevice = device; + + IOObjectRetain((io_object_t) hidDevice); + + _vendorId = 0; + _productId = 0; + _gamepadId = (NSInteger) device; + _axes = NSMakePoint(CGFLOAT_MIN, CGFLOAT_MIN); + + CFTypeRef tCFTypeRef; + CFTypeID numericTypeId = CFNumberGetTypeID(); + + tCFTypeRef = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVendorIDKey)); + if (tCFTypeRef && CFGetTypeID(tCFTypeRef) == numericTypeId) { + CFNumberGetValue((CFNumberRef)tCFTypeRef, kCFNumberSInt32Type, &_vendorId); + } + + tCFTypeRef = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductIDKey)); + if (tCFTypeRef && CFGetTypeID(tCFTypeRef) == numericTypeId) { + CFNumberGetValue((CFNumberRef)tCFTypeRef, kCFNumberSInt32Type, &_productId); + } + + tCFTypeRef = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDLocationIDKey)); + if (tCFTypeRef && CFGetTypeID(tCFTypeRef) == numericTypeId) { + CFNumberGetValue((CFNumberRef)tCFTypeRef, kCFNumberSInt32Type, &_locationId); + } + + _name = (__bridge NSString *)IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductKey)); + } + + return self; +} + +- (void) dealloc +{ + [self unregisterFromEvents]; + + IOObjectRelease((io_object_t) hidDevice); +} + +- (void) registerForEvents +{ + if (!registeredForEvents) { + IOHIDDeviceOpen(hidDevice, kIOHIDOptionsTypeNone); + IOHIDDeviceRegisterInputValueCallback(hidDevice, gamepadInputValueCallback, (__bridge void *)(self)); + + registeredForEvents = YES; + + if ([_delegate respondsToSelector:@selector(gamepadDidConnect:)]) { + [_delegate gamepadDidConnect:self]; + } + } +} + +- (void) unregisterFromEvents +{ + if (registeredForEvents) { + // FIXME: why does this crash the emulator? +// IOHIDDeviceClose(hidDevice, kIOHIDOptionsTypeNone); + + registeredForEvents = NO; + + if ([_delegate respondsToSelector:@selector(gamepadDidDisconnect:)]) { + [_delegate gamepadDidDisconnect:self]; + } + } +} + +- (NSString *) vendorProductString +{ + return [NSString stringWithFormat:@"%04lx:%04lx", + (long)[self vendorId], (long)[self productId]]; +} + +- (NSInteger) vendorProductId +{ + return (_vendorId << 16) | _productId; +} + +- (NSString *) description +{ + return [NSString stringWithFormat:@"%@ (0x%04lx:0x%04lx)", + [self name], (long)[self vendorId], (long)[self productId]]; +} + +- (void) didReceiveInputValue:(IOHIDValueRef) valueRef +{ + IOHIDElementRef element = IOHIDValueGetElement(valueRef); + + NSInteger usagePage = IOHIDElementGetUsagePage(element); + NSInteger usage = IOHIDElementGetUsage(element); + NSInteger value = IOHIDValueGetIntegerValue(valueRef); + + if (usagePage == kHIDPage_GenericDesktop) { + if (usage == kHIDUsage_GD_X) { + NSInteger min = IOHIDElementGetLogicalMin(element); + NSInteger max = IOHIDElementGetLogicalMax(element); + NSInteger center = (max + min) / 2; + NSInteger range = max - min; + + if (labs(value) < range * .3) { + value = 0; + } + + if (_axes.x != value) { + _axes.x = value; + if ([_delegate respondsToSelector:@selector(gamepad:xChanged:center:eventData:)]) { + AKGamepadEventData *eventData = [[AKGamepadEventData alloc] init]; + [eventData setSourceId:IOHIDElementGetCookie(element)]; + + [_delegate gamepad:self + xChanged:value + center:center + eventData:eventData]; + } + } + } else if (usage == kHIDUsage_GD_Y) { + NSInteger min = IOHIDElementGetLogicalMin(element); + NSInteger max = IOHIDElementGetLogicalMax(element); + NSInteger center = (max + min) / 2; + NSInteger range = max - min; + + if (labs(value) < range * .3) { + value = 0; + } + + if (_axes.y != value) { + _axes.y = value; + if ([_delegate respondsToSelector:@selector(gamepad:yChanged:center:eventData:)]) { + AKGamepadEventData *eventData = [[AKGamepadEventData alloc] init]; + [eventData setSourceId:IOHIDElementGetCookie(element)]; + + [_delegate gamepad:self + yChanged:value + center:center + eventData:eventData]; + } + } + } + } else if (usagePage == kHIDPage_Button) { + if ([_delegate respondsToSelector:@selector(gamepad:button:isDown:eventData:)]) { + AKGamepadEventData *eventData = [[AKGamepadEventData alloc] init]; + [eventData setSourceId:IOHIDElementGetCookie(element)]; + + [_delegate gamepad:self + button:usage + isDown:(value & 1) + eventData:eventData]; + } + } +} + +- (NSMutableDictionary *) currentAxisValues +{ + CFArrayRef elements = IOHIDDeviceCopyMatchingElements(hidDevice, NULL, kIOHIDOptionsTypeNone); + NSArray *elementArray = (__bridge NSArray *) elements; + + NSMutableDictionary *axesAndValues = [[NSMutableDictionary alloc] init]; + + [elementArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + IOHIDElementRef element = (__bridge IOHIDElementRef) obj; + + NSInteger usagePage = IOHIDElementGetUsagePage(element); + if (usagePage == kHIDPage_GenericDesktop) { + IOHIDElementType type = IOHIDElementGetType(element); + if (type == kIOHIDElementTypeInput_Misc || type == kIOHIDElementTypeInput_Axis) { + NSInteger usage = IOHIDElementGetUsage(element); + if (usage == kHIDUsage_GD_X || usage == kHIDUsage_GD_Y) { + IOHIDValueRef tIOHIDValueRef; + if (IOHIDDeviceGetValue(hidDevice, element, &tIOHIDValueRef) == kIOReturnSuccess) { + IOHIDElementCookie cookie = IOHIDElementGetCookie(element); + NSInteger integerValue = IOHIDValueGetIntegerValue(tIOHIDValueRef); + + [axesAndValues setObject:@(integerValue) + forKey:@((NSInteger)cookie)]; + } + } + } + } + }]; + + return axesAndValues; +} + +@end + +#pragma mark - IOHID C Callbacks + +static void gamepadInputValueCallback(void *context, IOReturn result, void *sender, IOHIDValueRef value) +{ + @autoreleasepool { + [(__bridge AKGamepad *) context didReceiveInputValue:value]; + } +} diff --git a/src/dep/macos/hid/AKGamepadEventData.h b/src/dep/macos/hid/AKGamepadEventData.h new file mode 100644 index 000000000..861cfe2d6 --- /dev/null +++ b/src/dep/macos/hid/AKGamepadEventData.h @@ -0,0 +1,21 @@ +// Copyright (c) Akop Karapetyan +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import + +@interface AKGamepadEventData : NSObject + +@property (nonatomic, assign) NSInteger sourceId; + +@end diff --git a/src/dep/macos/hid/AKGamepadEventData.m b/src/dep/macos/hid/AKGamepadEventData.m new file mode 100644 index 000000000..5d4fd230b --- /dev/null +++ b/src/dep/macos/hid/AKGamepadEventData.m @@ -0,0 +1,19 @@ +// Copyright (c) Akop Karapetyan +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "AKGamepadEventData.h" + +@implementation AKGamepadEventData + +@end diff --git a/src/dep/macos/hid/AKGamepadManager.h b/src/dep/macos/hid/AKGamepadManager.h new file mode 100644 index 000000000..eb06fd6a8 --- /dev/null +++ b/src/dep/macos/hid/AKGamepadManager.h @@ -0,0 +1,56 @@ +// Copyright (c) Akop Karapetyan +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import +#import + +#import "AKGamepad.h" +#import "AKGamepadEventData.h" + +@protocol AKGamepadEventDelegate + +@optional +- (void) gamepadDidConnect:(AKGamepad *) gamepad; +- (void) gamepadDidDisconnect:(AKGamepad *) gamepad; + +- (void) gamepad:(AKGamepad *) gamepad + xChanged:(NSInteger) newValue + center:(NSInteger) center + eventData:(AKGamepadEventData *) eventData; +- (void) gamepad:(AKGamepad *) gamepad + yChanged:(NSInteger) newValue + center:(NSInteger) center + eventData:(AKGamepadEventData *) eventData; + +- (void) gamepad:(AKGamepad *) gamepad + button:(NSUInteger) index + isDown:(BOOL) isDown + eventData:(AKGamepadEventData *) eventData; + +@end + +@interface AKGamepadManager : NSObject + ++ (instancetype) sharedInstance; + +- (AKGamepad *) gamepadWithId:(NSInteger) gamepadId; +- (AKGamepad *) gamepadAtIndex:(NSUInteger) index; +- (NSArray *) allConnected; + +- (NSUInteger) gamepadCount; + +- (void) addObserver:(id) observer; +- (void) removeObserver:(id) observer; + +@end diff --git a/src/dep/macos/hid/AKGamepadManager.m b/src/dep/macos/hid/AKGamepadManager.m new file mode 100644 index 000000000..a8b4bdae2 --- /dev/null +++ b/src/dep/macos/hid/AKGamepadManager.m @@ -0,0 +1,268 @@ +// Copyright (c) Akop Karapetyan +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#import "AKGamepadManager.h" +#import "AKGamepad.h" + +#pragma mark - AKGamepadManager + +void gamepadWasAdded(void *inContext, IOReturn inResult, void *inSender, IOHIDDeviceRef device); +void gamepadWasRemoved(void *inContext, IOReturn inResult, void *inSender, IOHIDDeviceRef device); + +@interface AKGamepadManager () + +- (void) deviceDidConnect:(IOHIDDeviceRef) device; +- (void) deviceDidDisconnect:(IOHIDDeviceRef) device; +- (void) renumber:(BOOL) sort; + +@end + +@implementation AKGamepadManager +{ + IOHIDManagerRef _hidManager; + NSMutableDictionary *_gamepadsByDeviceId; + NSMutableArray *_allGamepads; + NSPointerArray *_observers; +} + ++ (instancetype) sharedInstance +{ + static dispatch_once_t once; + static id sharedInstance; + dispatch_once(&once, ^{ + sharedInstance = [[self alloc] init]; + }); + + return sharedInstance; +} + +- (instancetype) init +{ + if ((self = [super init])) { + _gamepadsByDeviceId = [NSMutableDictionary dictionary]; + _allGamepads = [NSMutableArray array]; + _observers = [NSPointerArray weakObjectsPointerArray]; + _hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + + NSMutableDictionary *gamepadCriterion = [@{ (NSString *) CFSTR(kIOHIDDeviceUsagePageKey): @(kHIDPage_GenericDesktop), + (NSString *) CFSTR(kIOHIDDeviceUsageKey): @(kHIDUsage_GD_GamePad) } mutableCopy]; + NSMutableDictionary *joystickCriterion = [@{ (NSString *) CFSTR(kIOHIDDeviceUsagePageKey): @(kHIDPage_GenericDesktop), + (NSString *) CFSTR(kIOHIDDeviceUsageKey): @(kHIDUsage_GD_Joystick) } mutableCopy]; + + IOHIDManagerSetDeviceMatchingMultiple(_hidManager, (__bridge CFArrayRef) @[ gamepadCriterion, joystickCriterion ]); + IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, gamepadWasAdded, (__bridge void *) self); + IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, gamepadWasRemoved, (__bridge void *) self); + + IOHIDManagerScheduleWithRunLoop(_hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + } + + return self; +} + +- (void) dealloc +{ + IOHIDManagerUnscheduleFromRunLoop(_hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + CFRelease(_hidManager); +} + +- (void) deviceDidConnect:(IOHIDDeviceRef) device +{ + AKGamepad *gamepad; + @synchronized (_gamepadsByDeviceId) { + gamepad = [[AKGamepad alloc] initWithHidDevice:device]; + [_gamepadsByDeviceId setObject:gamepad + forKey:@([gamepad gamepadId])]; + + [_allGamepads addObject:gamepad]; + [self renumber:YES]; + + [gamepad setDelegate:self]; + } + + [gamepad registerForEvents]; +} + +- (void) deviceDidDisconnect:(IOHIDDeviceRef) device +{ + @synchronized (_gamepadsByDeviceId) { + AKGamepad *gamepad = [_gamepadsByDeviceId objectForKey:@((NSInteger) device)]; + if (gamepad) { + [_gamepadsByDeviceId removeObjectForKey:@([gamepad gamepadId])]; + [_allGamepads removeObjectAtIndex:[gamepad index]]; + [self renumber:NO]; + } + } +} + +- (AKGamepad *) gamepadWithId:(NSInteger) gamepadId +{ + return [_gamepadsByDeviceId objectForKey:@(gamepadId)]; +} + +- (AKGamepad *) gamepadAtIndex:(NSUInteger) index +{ + AKGamepad *gp = nil; + if (index < [_allGamepads count]) { + gp = [_allGamepads objectAtIndex:index]; + } + + return gp; +} + +- (NSUInteger) gamepadCount +{ + return [_allGamepads count]; +} + +- (NSArray *) allConnected +{ + return [NSArray arrayWithArray:_allGamepads]; +} + +#pragma mark - AKGamepadDelegate + +- (void) gamepadDidConnect:(AKGamepad *) gamepad +{ + @synchronized (_observers) { + for (id delegate in _observers) { + if ([delegate respondsToSelector:_cmd]) { + [delegate gamepadDidConnect:gamepad]; + } + } + } +} + +- (void) gamepadDidDisconnect:(AKGamepad *) gamepad +{ + @synchronized (_observers) { + for (id delegate in _observers) { + if ([delegate respondsToSelector:_cmd]) { + [delegate gamepadDidDisconnect:gamepad]; + } + } + } +} + +- (void) gamepad:(AKGamepad *) gamepad + xChanged:(NSInteger) newValue + center:(NSInteger) center + eventData:(AKGamepadEventData *) eventData +{ + @synchronized (_observers) { + for (id delegate in _observers) { + if ([delegate respondsToSelector:_cmd]) { + [delegate gamepad:gamepad + xChanged:newValue + center:center + eventData:eventData]; + } + } + } +} + +- (void) gamepad:(AKGamepad *) gamepad + yChanged:(NSInteger) newValue + center:(NSInteger) center + eventData:(AKGamepadEventData *) eventData +{ + @synchronized (_observers) { + for (id delegate in _observers) { + if ([delegate respondsToSelector:_cmd]) { + [delegate gamepad:gamepad + yChanged:newValue + center:center + eventData:eventData]; + } + } + } +} + +- (void) gamepad:(AKGamepad *) gamepad + button:(NSUInteger) index + isDown:(BOOL) isDown + eventData:(AKGamepadEventData *) eventData +{ + @synchronized (_observers) { + for (id delegate in _observers) { + if ([delegate respondsToSelector:_cmd]) { + [delegate gamepad:gamepad + button:index + isDown:isDown + eventData:eventData]; + } + } + } +} + +- (void) addObserver:(id) observer +{ + @synchronized (_observers) { + [_observers addPointer:(__bridge void * _Nullable)(observer)]; + } + +#ifdef DEBUG + NSLog(@"gamepadManager/addObserver"); +#endif +} + +- (void) removeObserver:(id) observer +{ + void *remove = (__bridge void * _Nullable)(observer); + @synchronized (_observers) { + for (int i = (int) [_observers count] - 1; i >= 0; i--) { + void *ptr = [_observers pointerAtIndex:i]; + if (ptr == remove || ptr == NULL) { + [_observers removePointerAtIndex:i]; + } + } + } + +#ifdef DEBUG + NSLog(@"gamepadManager/removeObserver"); +#endif +} + +#pragma mark - Private + +- (void) renumber:(BOOL) sort +{ + @synchronized (_allGamepads) { + if (sort) { + [_allGamepads sortUsingComparator:^NSComparisonResult(AKGamepad *gp1, AKGamepad *gp2) { + return [gp1 locationId] - [gp2 locationId]; + }]; + } + [_allGamepads enumerateObjectsUsingBlock:^(AKGamepad *gp, NSUInteger idx, BOOL * _Nonnull stop) { + [gp setIndex:idx]; + }]; + } +} + +@end + +#pragma mark - IOHID C Callbacks + +void gamepadWasAdded(void *inContext, IOReturn inResult, void *inSender, IOHIDDeviceRef device) +{ + @autoreleasepool { + [((__bridge AKGamepadManager *) inContext) deviceDidConnect:device]; + } +} + +void gamepadWasRemoved(void *inContext, IOReturn inResult, void *inSender, IOHIDDeviceRef device) +{ + @autoreleasepool { + [((__bridge AKGamepadManager *) inContext) deviceDidDisconnect:device]; + } +}