Replace the SDL-derived controller support with my own JoyKit framework. Adds rumble support, LED support, better manual and automatic configurations, analog speed controls.
This commit is contained in:
parent
7d6cdf3819
commit
0ece21bca7
@ -2,6 +2,7 @@
|
|||||||
#include "GBButtons.h"
|
#include "GBButtons.h"
|
||||||
#include <Core/gb.h>
|
#include <Core/gb.h>
|
||||||
#import <Carbon/Carbon.h>
|
#import <Carbon/Carbon.h>
|
||||||
|
#import <JoyKit/JoyKit.h>
|
||||||
|
|
||||||
@implementation AppDelegate
|
@implementation AppDelegate
|
||||||
{
|
{
|
||||||
@ -41,6 +42,11 @@
|
|||||||
@"GBCGBModel": @(GB_MODEL_CGB_E),
|
@"GBCGBModel": @(GB_MODEL_CGB_E),
|
||||||
@"GBSGBModel": @(GB_MODEL_SGB2),
|
@"GBSGBModel": @(GB_MODEL_SGB2),
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
[JOYController startOnRunLoop:[NSRunLoop currentRunLoop] withOptions:@{
|
||||||
|
JOYAxes2DEmulateButtonsKey: @YES,
|
||||||
|
JOYHatsEmulateButtonsKey: @YES,
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (IBAction)toggleDeveloperMode:(id)sender
|
- (IBAction)toggleDeveloperMode:(id)sender
|
||||||
|
@ -74,6 +74,7 @@ enum model {
|
|||||||
topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin
|
topMargin:(unsigned) topMargin bottomMargin: (unsigned) bottomMargin
|
||||||
exposure:(unsigned) exposure;
|
exposure:(unsigned) exposure;
|
||||||
- (void) gotNewSample:(GB_sample_t *)sample;
|
- (void) gotNewSample:(GB_sample_t *)sample;
|
||||||
|
- (void) rumbleChanged:(double)amp;
|
||||||
@end
|
@end
|
||||||
|
|
||||||
static void vblank(GB_gameboy_t *gb)
|
static void vblank(GB_gameboy_t *gb)
|
||||||
@ -131,6 +132,12 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample)
|
|||||||
[self gotNewSample:sample];
|
[self gotNewSample:sample];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void rumbleCallback(GB_gameboy_t *gb, double amp)
|
||||||
|
{
|
||||||
|
Document *self = (__bridge Document *)GB_get_user_data(gb);
|
||||||
|
[self rumbleChanged:amp];
|
||||||
|
}
|
||||||
|
|
||||||
@implementation Document
|
@implementation Document
|
||||||
{
|
{
|
||||||
GB_gameboy_t gb;
|
GB_gameboy_t gb;
|
||||||
@ -199,6 +206,7 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample)
|
|||||||
GB_set_highpass_filter_mode(&gb, (GB_highpass_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"]);
|
GB_set_highpass_filter_mode(&gb, (GB_highpass_mode_t) [[NSUserDefaults standardUserDefaults] integerForKey:@"GBHighpassFilter"]);
|
||||||
GB_set_rewind_length(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]);
|
GB_set_rewind_length(&gb, [[NSUserDefaults standardUserDefaults] integerForKey:@"GBRewindLength"]);
|
||||||
GB_apu_set_sample_callback(&gb, audioCallback);
|
GB_apu_set_sample_callback(&gb, audioCallback);
|
||||||
|
GB_set_rumble_callback(&gb, rumbleCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) vblank
|
- (void) vblank
|
||||||
@ -244,6 +252,11 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample)
|
|||||||
[audioLock unlock];
|
[audioLock unlock];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)rumbleChanged:(double)amp
|
||||||
|
{
|
||||||
|
[_view setRumble:amp];
|
||||||
|
}
|
||||||
|
|
||||||
- (void) run
|
- (void) run
|
||||||
{
|
{
|
||||||
running = true;
|
running = true;
|
||||||
@ -295,6 +308,7 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample)
|
|||||||
self.audioClient = nil;
|
self.audioClient = nil;
|
||||||
self.view.mouseHidingEnabled = NO;
|
self.view.mouseHidingEnabled = NO;
|
||||||
GB_save_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]);
|
GB_save_battery(&gb, [[[self.fileName stringByDeletingPathExtension] stringByAppendingPathExtension:@"sav"] UTF8String]);
|
||||||
|
[_view setRumble:false];
|
||||||
stopping = false;
|
stopping = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1563,5 +1577,4 @@ static void audioCallback(GB_gameboy_t *gb, GB_sample_t *sample)
|
|||||||
self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [[fileURL path] lastPathComponent]];
|
self.consoleWindow.title = [NSString stringWithFormat:@"Debug Console – %@", [[fileURL path] lastPathComponent]];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -19,6 +19,11 @@ typedef enum : NSUInteger {
|
|||||||
|
|
||||||
extern NSString const *GBButtonNames[GBButtonCount];
|
extern NSString const *GBButtonNames[GBButtonCount];
|
||||||
|
|
||||||
|
static inline NSString *n2s(uint64_t number)
|
||||||
|
{
|
||||||
|
return [NSString stringWithFormat:@"%llx", number];
|
||||||
|
}
|
||||||
|
|
||||||
static inline NSString *button_to_preference_name(GBButton button, unsigned player)
|
static inline NSString *button_to_preference_name(GBButton button, unsigned player)
|
||||||
{
|
{
|
||||||
if (player) {
|
if (player) {
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
#import <AppKit/AppKit.h>
|
|
||||||
|
|
||||||
@protocol GBJoystickListener <NSObject>
|
|
||||||
|
|
||||||
- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state;
|
|
||||||
- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value;
|
|
||||||
- (void) joystick:(NSString *)joystick_name hat: (unsigned)hat changedState: (int8_t) value;
|
|
||||||
|
|
||||||
@end
|
|
@ -1,9 +1,10 @@
|
|||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#import "GBJoystickListener.h"
|
#import <JoyKit/JoyKit.h>
|
||||||
|
|
||||||
@interface GBPreferencesWindow : NSWindow <NSTableViewDelegate, NSTableViewDataSource, GBJoystickListener>
|
@interface GBPreferencesWindow : NSWindow <NSTableViewDelegate, NSTableViewDataSource, JOYListener>
|
||||||
@property IBOutlet NSTableView *controlsTableView;
|
@property IBOutlet NSTableView *controlsTableView;
|
||||||
@property IBOutlet NSPopUpButton *graphicsFilterPopupButton;
|
@property IBOutlet NSPopUpButton *graphicsFilterPopupButton;
|
||||||
|
@property (strong) IBOutlet NSButton *analogControlsCheckbox;
|
||||||
@property (strong) IBOutlet NSButton *aspectRatioCheckbox;
|
@property (strong) IBOutlet NSButton *aspectRatioCheckbox;
|
||||||
@property (strong) IBOutlet NSPopUpButton *highpassFilterPopupButton;
|
@property (strong) IBOutlet NSPopUpButton *highpassFilterPopupButton;
|
||||||
@property (strong) IBOutlet NSPopUpButton *colorCorrectionPopupButton;
|
@property (strong) IBOutlet NSPopUpButton *colorCorrectionPopupButton;
|
||||||
|
@ -9,13 +9,14 @@
|
|||||||
NSInteger button_being_modified;
|
NSInteger button_being_modified;
|
||||||
signed joystick_configuration_state;
|
signed joystick_configuration_state;
|
||||||
NSString *joystick_being_configured;
|
NSString *joystick_being_configured;
|
||||||
signed last_axis;
|
bool joypad_wait;
|
||||||
|
|
||||||
NSPopUpButton *_graphicsFilterPopupButton;
|
NSPopUpButton *_graphicsFilterPopupButton;
|
||||||
NSPopUpButton *_highpassFilterPopupButton;
|
NSPopUpButton *_highpassFilterPopupButton;
|
||||||
NSPopUpButton *_colorCorrectionPopupButton;
|
NSPopUpButton *_colorCorrectionPopupButton;
|
||||||
NSPopUpButton *_rewindPopupButton;
|
NSPopUpButton *_rewindPopupButton;
|
||||||
NSButton *_aspectRatioCheckbox;
|
NSButton *_aspectRatioCheckbox;
|
||||||
|
NSButton *_analogControlsCheckbox;
|
||||||
NSEventModifierFlags previousModifiers;
|
NSEventModifierFlags previousModifiers;
|
||||||
|
|
||||||
NSPopUpButton *_dmgPopupButton, *_sgbPopupButton, *_cgbPopupButton;
|
NSPopUpButton *_dmgPopupButton, *_sgbPopupButton, *_cgbPopupButton;
|
||||||
@ -51,7 +52,7 @@
|
|||||||
joystick_configuration_state = -1;
|
joystick_configuration_state = -1;
|
||||||
[self.configureJoypadButton setEnabled:YES];
|
[self.configureJoypadButton setEnabled:YES];
|
||||||
[self.skipButton setEnabled:NO];
|
[self.skipButton setEnabled:NO];
|
||||||
[self.configureJoypadButton setTitle:@"Configure Joypad"];
|
[self.configureJoypadButton setTitle:@"Configure Controller"];
|
||||||
[super close];
|
[super close];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,6 +185,12 @@
|
|||||||
[[NSNotificationCenter defaultCenter] postNotificationName:@"GBHighpassFilterChanged" object:nil];
|
[[NSNotificationCenter defaultCenter] postNotificationName:@"GBHighpassFilterChanged" object:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (IBAction)changeAnalogControls:(id)sender
|
||||||
|
{
|
||||||
|
[[NSUserDefaults standardUserDefaults] setBool: [(NSButton *)sender state] == NSOnState
|
||||||
|
forKey:@"GBAnalogControls"];
|
||||||
|
}
|
||||||
|
|
||||||
- (IBAction)changeAspectRatio:(id)sender
|
- (IBAction)changeAspectRatio:(id)sender
|
||||||
{
|
{
|
||||||
[[NSUserDefaults standardUserDefaults] setBool: [(NSButton *)sender state] != NSOnState
|
[[NSUserDefaults standardUserDefaults] setBool: [(NSButton *)sender state] != NSOnState
|
||||||
@ -212,7 +219,6 @@
|
|||||||
[self.skipButton setEnabled:YES];
|
[self.skipButton setEnabled:YES];
|
||||||
joystick_being_configured = nil;
|
joystick_being_configured = nil;
|
||||||
[self advanceConfigurationStateMachine];
|
[self advanceConfigurationStateMachine];
|
||||||
last_axis = -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (IBAction) skipButton:(id)sender
|
- (IBAction) skipButton:(id)sender
|
||||||
@ -223,11 +229,11 @@
|
|||||||
- (void) advanceConfigurationStateMachine
|
- (void) advanceConfigurationStateMachine
|
||||||
{
|
{
|
||||||
joystick_configuration_state++;
|
joystick_configuration_state++;
|
||||||
if (joystick_configuration_state < GBButtonCount) {
|
if (joystick_configuration_state == GBUnderclock) {
|
||||||
[self.configureJoypadButton setTitle:[NSString stringWithFormat:@"Press Button for %@", GBButtonNames[joystick_configuration_state]]];
|
[self.configureJoypadButton setTitle:@"Press Button for Slo-Mo"]; // Full name is too long :<
|
||||||
}
|
}
|
||||||
else if (joystick_configuration_state == GBButtonCount) {
|
else if (joystick_configuration_state < GBButtonCount) {
|
||||||
[self.configureJoypadButton setTitle:@"Move the Analog Stick"];
|
[self.configureJoypadButton setTitle:[NSString stringWithFormat:@"Press Button for %@", GBButtonNames[joystick_configuration_state]]];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
joystick_configuration_state = -1;
|
joystick_configuration_state = -1;
|
||||||
@ -237,112 +243,97 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state
|
- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button
|
||||||
{
|
{
|
||||||
if (!state) return;
|
/* Debounce */
|
||||||
|
if (joypad_wait) return;
|
||||||
|
joypad_wait = true;
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||||
|
joypad_wait = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
NSLog(@"%@", button);
|
||||||
|
|
||||||
|
if (!button.isPressed) return;
|
||||||
if (joystick_configuration_state == -1) return;
|
if (joystick_configuration_state == -1) return;
|
||||||
if (joystick_configuration_state == GBButtonCount) return;
|
if (joystick_configuration_state == GBButtonCount) return;
|
||||||
if (!joystick_being_configured) {
|
if (!joystick_being_configured) {
|
||||||
joystick_being_configured = joystick_name;
|
joystick_being_configured = controller.uniqueID;
|
||||||
}
|
}
|
||||||
else if (![joystick_being_configured isEqualToString:joystick_name]) {
|
else if (![joystick_being_configured isEqualToString:controller.uniqueID]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSMutableDictionary *all_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"] mutableCopy];
|
NSMutableDictionary *instance_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"] mutableCopy];
|
||||||
|
|
||||||
if (!all_mappings) {
|
NSMutableDictionary *name_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"] mutableCopy];
|
||||||
all_mappings = [[NSMutableDictionary alloc] init];
|
|
||||||
|
|
||||||
|
if (!instance_mappings) {
|
||||||
|
instance_mappings = [[NSMutableDictionary alloc] init];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSMutableDictionary *mapping = [[all_mappings objectForKey:joystick_name] mutableCopy];
|
if (!name_mappings) {
|
||||||
|
name_mappings = [[NSMutableDictionary alloc] init];
|
||||||
|
}
|
||||||
|
|
||||||
if (!mapping) {
|
NSMutableDictionary *mapping = nil;
|
||||||
|
if (joystick_configuration_state != 0) {
|
||||||
|
mapping = [instance_mappings[controller.uniqueID] mutableCopy];
|
||||||
|
}
|
||||||
|
else {
|
||||||
mapping = [[NSMutableDictionary alloc] init];
|
mapping = [[NSMutableDictionary alloc] init];
|
||||||
}
|
}
|
||||||
|
|
||||||
mapping[GBButtonNames[joystick_configuration_state]] = @(button);
|
|
||||||
|
|
||||||
all_mappings[joystick_name] = mapping;
|
static const unsigned gb_to_joykit[] = {
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:all_mappings forKey:@"GBJoypadMappings"];
|
[GBRight]=JOYButtonUsageDPadRight,
|
||||||
[self refreshJoypadMenu:nil];
|
[GBLeft]=JOYButtonUsageDPadLeft,
|
||||||
|
[GBUp]=JOYButtonUsageDPadUp,
|
||||||
|
[GBDown]=JOYButtonUsageDPadDown,
|
||||||
|
[GBA]=JOYButtonUsageA,
|
||||||
|
[GBB]=JOYButtonUsageB,
|
||||||
|
[GBSelect]=JOYButtonUsageSelect,
|
||||||
|
[GBStart]=JOYButtonUsageStart,
|
||||||
|
[GBTurbo]=JOYButtonUsageL1,
|
||||||
|
[GBRewind]=JOYButtonUsageL2,
|
||||||
|
[GBUnderclock]=JOYButtonUsageR1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (joystick_configuration_state == GBUnderclock) {
|
||||||
|
for (JOYAxis *axis in controller.axes) {
|
||||||
|
if (axis.value > 0.5) {
|
||||||
|
mapping[@"AnalogUnderclock"] = @(axis.uniqueID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (joystick_configuration_state == GBTurbo) {
|
||||||
|
for (JOYAxis *axis in controller.axes) {
|
||||||
|
if (axis.value > 0.5) {
|
||||||
|
mapping[@"AnalogTurbo"] = @(axis.uniqueID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping[n2s(button.uniqueID)] = @(gb_to_joykit[joystick_configuration_state]);
|
||||||
|
|
||||||
|
instance_mappings[controller.uniqueID] = mapping;
|
||||||
|
name_mappings[controller.deviceName] = mapping;
|
||||||
|
[[NSUserDefaults standardUserDefaults] setObject:instance_mappings forKey:@"JoyKitInstanceMapping"];
|
||||||
|
[[NSUserDefaults standardUserDefaults] setObject:name_mappings forKey:@"JoyKitNameMapping"];
|
||||||
[self advanceConfigurationStateMachine];
|
[self advanceConfigurationStateMachine];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value
|
- (NSButton *)analogControlsCheckbox
|
||||||
{
|
{
|
||||||
if (abs(value) < 0x4000) return;
|
return _analogControlsCheckbox;
|
||||||
if (joystick_configuration_state != GBButtonCount) return;
|
|
||||||
if (!joystick_being_configured) {
|
|
||||||
joystick_being_configured = joystick_name;
|
|
||||||
}
|
|
||||||
else if (![joystick_being_configured isEqualToString:joystick_name]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (last_axis == -1) {
|
|
||||||
last_axis = axis;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (axis == last_axis) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSMutableDictionary *all_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"] mutableCopy];
|
|
||||||
|
|
||||||
if (!all_mappings) {
|
|
||||||
all_mappings = [[NSMutableDictionary alloc] init];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSMutableDictionary *mapping = [[all_mappings objectForKey:joystick_name] mutableCopy];
|
|
||||||
|
|
||||||
if (!mapping) {
|
|
||||||
mapping = [[NSMutableDictionary alloc] init];
|
|
||||||
}
|
|
||||||
|
|
||||||
mapping[@"XAxis"] = @(MIN(axis, last_axis));
|
|
||||||
mapping[@"YAxis"] = @(MAX(axis, last_axis));
|
|
||||||
|
|
||||||
all_mappings[joystick_name] = mapping;
|
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:all_mappings forKey:@"GBJoypadMappings"];
|
|
||||||
[self advanceConfigurationStateMachine];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) joystick:(NSString *)joystick_name hat: (unsigned)hat changedState: (int8_t) state
|
- (void)setAnalogControlsCheckbox:(NSButton *)analogControlsCheckbox
|
||||||
{
|
{
|
||||||
/* Hats are always mapped to the D-pad, ignore them on non-Dpad keys and skip the D-pad configuration if used*/
|
_analogControlsCheckbox = analogControlsCheckbox;
|
||||||
if (!state) return;
|
[_analogControlsCheckbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAnalogControls"]];
|
||||||
if (joystick_configuration_state == -1) return;
|
|
||||||
if (joystick_configuration_state > GBDown) return;
|
|
||||||
if (!joystick_being_configured) {
|
|
||||||
joystick_being_configured = joystick_name;
|
|
||||||
}
|
|
||||||
else if (![joystick_being_configured isEqualToString:joystick_name]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSMutableDictionary *all_mappings = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"] mutableCopy];
|
|
||||||
|
|
||||||
if (!all_mappings) {
|
|
||||||
all_mappings = [[NSMutableDictionary alloc] init];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSMutableDictionary *mapping = [[all_mappings objectForKey:joystick_name] mutableCopy];
|
|
||||||
|
|
||||||
if (!mapping) {
|
|
||||||
mapping = [[NSMutableDictionary alloc] init];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (joystick_configuration_state = 0;; joystick_configuration_state++) {
|
|
||||||
[mapping removeObjectForKey:GBButtonNames[joystick_configuration_state]];
|
|
||||||
if (joystick_configuration_state == GBDown) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
all_mappings[joystick_name] = mapping;
|
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:all_mappings forKey:@"GBJoypadMappings"];
|
|
||||||
[self refreshJoypadMenu:nil];
|
|
||||||
[self advanceConfigurationStateMachine];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSButton *)aspectRatioCheckbox
|
- (NSButton *)aspectRatioCheckbox
|
||||||
@ -361,10 +352,13 @@
|
|||||||
[super awakeFromNib];
|
[super awakeFromNib];
|
||||||
[self updateBootROMFolderButton];
|
[self updateBootROMFolderButton];
|
||||||
[[NSDistributedNotificationCenter defaultCenter] addObserver:self.controlsTableView selector:@selector(reloadData) name:(NSString*)kTISNotifySelectedKeyboardInputSourceChanged object:nil];
|
[[NSDistributedNotificationCenter defaultCenter] addObserver:self.controlsTableView selector:@selector(reloadData) name:(NSString*)kTISNotifySelectedKeyboardInputSourceChanged object:nil];
|
||||||
|
[JOYController registerListener:self];
|
||||||
|
joystick_configuration_state = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc
|
- (void)dealloc
|
||||||
{
|
{
|
||||||
|
[JOYController unregisterListener:self];
|
||||||
[[NSDistributedNotificationCenter defaultCenter] removeObserver:self.controlsTableView];
|
[[NSDistributedNotificationCenter defaultCenter] removeObserver:self.controlsTableView];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,21 +477,47 @@
|
|||||||
return _preferredJoypadButton;
|
return _preferredJoypadButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)controllerConnected:(JOYController *)controller
|
||||||
|
{
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||||
|
[self refreshJoypadMenu:nil];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)controllerDisconnected:(JOYController *)controller
|
||||||
|
{
|
||||||
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.25 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||||
|
[self refreshJoypadMenu:nil];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
- (IBAction)refreshJoypadMenu:(id)sender
|
- (IBAction)refreshJoypadMenu:(id)sender
|
||||||
{
|
{
|
||||||
NSArray *joypads = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"] allKeys];
|
bool preferred_is_connected = false;
|
||||||
for (NSString *joypad in joypads) {
|
NSString *player_string = n2s(self.playerListButton.selectedTag);
|
||||||
if ([self.preferredJoypadButton indexOfItemWithTitle:joypad] == -1) {
|
NSString *selected_controller = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitDefaultControllers"][player_string];
|
||||||
[self.preferredJoypadButton addItemWithTitle:joypad];
|
|
||||||
|
[self.preferredJoypadButton removeAllItems];
|
||||||
|
[self.preferredJoypadButton addItemWithTitle:@"None"];
|
||||||
|
for (JOYController *controller in [JOYController allControllers]) {
|
||||||
|
[self.preferredJoypadButton addItemWithTitle:[NSString stringWithFormat:@"%@ (%@)", controller.deviceName, controller.uniqueID]];
|
||||||
|
|
||||||
|
self.preferredJoypadButton.lastItem.identifier = controller.uniqueID;
|
||||||
|
|
||||||
|
if ([controller.uniqueID isEqualToString:selected_controller]) {
|
||||||
|
preferred_is_connected = true;
|
||||||
|
[self.preferredJoypadButton selectItem:self.preferredJoypadButton.lastItem];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString *player_string = [NSString stringWithFormat: @"%ld", (long)self.playerListButton.selectedTag];
|
if (!preferred_is_connected && selected_controller) {
|
||||||
NSString *selected_joypad = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBDefaultJoypads"][player_string];
|
[self.preferredJoypadButton addItemWithTitle:[NSString stringWithFormat:@"Unavailable Controller (%@)", selected_controller]];
|
||||||
if (selected_joypad && [self.preferredJoypadButton indexOfItemWithTitle:selected_joypad] != -1) {
|
self.preferredJoypadButton.lastItem.identifier = selected_controller;
|
||||||
[self.preferredJoypadButton selectItemWithTitle:selected_joypad];
|
[self.preferredJoypadButton selectItem:self.preferredJoypadButton.lastItem];
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
|
|
||||||
|
if (!selected_controller) {
|
||||||
[self.preferredJoypadButton selectItemWithTitle:@"None"];
|
[self.preferredJoypadButton selectItemWithTitle:@"None"];
|
||||||
}
|
}
|
||||||
[self.controlsTableView reloadData];
|
[self.controlsTableView reloadData];
|
||||||
@ -505,18 +525,18 @@
|
|||||||
|
|
||||||
- (IBAction)changeDefaultJoypad:(id)sender
|
- (IBAction)changeDefaultJoypad:(id)sender
|
||||||
{
|
{
|
||||||
NSMutableDictionary *default_joypads = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBDefaultJoypads"] mutableCopy];
|
NSMutableDictionary *default_joypads = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitDefaultControllers"] mutableCopy];
|
||||||
if (!default_joypads) {
|
if (!default_joypads) {
|
||||||
default_joypads = [[NSMutableDictionary alloc] init];
|
default_joypads = [[NSMutableDictionary alloc] init];
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString *player_string = [NSString stringWithFormat: @"%ld", self.playerListButton.selectedTag];
|
NSString *player_string = n2s(self.playerListButton.selectedTag);
|
||||||
if ([[sender titleOfSelectedItem] isEqualToString:@"None"]) {
|
if ([[sender titleOfSelectedItem] isEqualToString:@"None"]) {
|
||||||
[default_joypads removeObjectForKey:player_string];
|
[default_joypads removeObjectForKey:player_string];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
default_joypads[player_string] = [sender titleOfSelectedItem];
|
default_joypads[player_string] = [[sender selectedItem] identifier];
|
||||||
}
|
}
|
||||||
[[NSUserDefaults standardUserDefaults] setObject:default_joypads forKey:@"GBDefaultJoypads"];
|
[[NSUserDefaults standardUserDefaults] setObject:default_joypads forKey:@"JoyKitDefaultControllers"];
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
#include <Core/gb.h>
|
#include <Core/gb.h>
|
||||||
#import "GBJoystickListener.h"
|
#import <JoyKit/JoyKit.h>
|
||||||
|
|
||||||
@interface GBView<GBJoystickListener> : NSView
|
@interface GBView : NSView<JOYListener>
|
||||||
- (void) flip;
|
- (void) flip;
|
||||||
- (uint32_t *) pixels;
|
- (uint32_t *) pixels;
|
||||||
@property GB_gameboy_t *gb;
|
@property GB_gameboy_t *gb;
|
||||||
@ -14,4 +14,5 @@
|
|||||||
- (uint32_t *)currentBuffer;
|
- (uint32_t *)currentBuffer;
|
||||||
- (uint32_t *)previousBuffer;
|
- (uint32_t *)previousBuffer;
|
||||||
- (void)screenSizeChanged;
|
- (void)screenSizeChanged;
|
||||||
|
- (void)setRumble: (bool)on;
|
||||||
@end
|
@end
|
||||||
|
212
Cocoa/GBView.m
212
Cocoa/GBView.m
@ -1,4 +1,4 @@
|
|||||||
#import <Carbon/Carbon.h>
|
#import <IOKit/pwr_mgt/IOPMLib.h>
|
||||||
#import "GBView.h"
|
#import "GBView.h"
|
||||||
#import "GBViewGL.h"
|
#import "GBViewGL.h"
|
||||||
#import "GBViewMetal.h"
|
#import "GBViewMetal.h"
|
||||||
@ -18,7 +18,10 @@
|
|||||||
bool axisActive[2];
|
bool axisActive[2];
|
||||||
bool underclockKeyDown;
|
bool underclockKeyDown;
|
||||||
double clockMultiplier;
|
double clockMultiplier;
|
||||||
|
double analogClockMultiplier;
|
||||||
|
bool analogClockMultiplierValid;
|
||||||
NSEventModifierFlags previousModifiers;
|
NSEventModifierFlags previousModifiers;
|
||||||
|
JOYController *lastController;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (instancetype)alloc
|
+ (instancetype)alloc
|
||||||
@ -55,6 +58,7 @@
|
|||||||
[self createInternalView];
|
[self createInternalView];
|
||||||
[self addSubview:self.internalView];
|
[self addSubview:self.internalView];
|
||||||
self.internalView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
self.internalView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||||
|
[JOYController registerListener:self];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)screenSizeChanged
|
- (void)screenSizeChanged
|
||||||
@ -100,6 +104,8 @@
|
|||||||
[NSCursor unhide];
|
[NSCursor unhide];
|
||||||
}
|
}
|
||||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||||
|
[lastController setRumbleAmplitude:0];
|
||||||
|
[JOYController unregisterListener:self];
|
||||||
}
|
}
|
||||||
- (instancetype)initWithCoder:(NSCoder *)coder
|
- (instancetype)initWithCoder:(NSCoder *)coder
|
||||||
{
|
{
|
||||||
@ -147,13 +153,21 @@
|
|||||||
|
|
||||||
- (void) flip
|
- (void) flip
|
||||||
{
|
{
|
||||||
if (underclockKeyDown && clockMultiplier > 0.5) {
|
if (analogClockMultiplierValid && [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAnalogControls"]) {
|
||||||
clockMultiplier -= 1.0/16;
|
GB_set_clock_multiplier(_gb, analogClockMultiplier);
|
||||||
GB_set_clock_multiplier(_gb, clockMultiplier);
|
if (analogClockMultiplier == 1.0) {
|
||||||
|
analogClockMultiplierValid = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!underclockKeyDown && clockMultiplier < 1.0) {
|
else {
|
||||||
clockMultiplier += 1.0/16;
|
if (underclockKeyDown && clockMultiplier > 0.5) {
|
||||||
GB_set_clock_multiplier(_gb, clockMultiplier);
|
clockMultiplier -= 1.0/16;
|
||||||
|
GB_set_clock_multiplier(_gb, clockMultiplier);
|
||||||
|
}
|
||||||
|
if (!underclockKeyDown && clockMultiplier < 1.0) {
|
||||||
|
clockMultiplier += 1.0/16;
|
||||||
|
GB_set_clock_multiplier(_gb, clockMultiplier);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
current_buffer = (current_buffer + 1) % self.numberOfBuffers;
|
current_buffer = (current_buffer + 1) % self.numberOfBuffers;
|
||||||
}
|
}
|
||||||
@ -180,6 +194,7 @@
|
|||||||
switch (button) {
|
switch (button) {
|
||||||
case GBTurbo:
|
case GBTurbo:
|
||||||
GB_set_turbo_mode(_gb, true, self.isRewinding);
|
GB_set_turbo_mode(_gb, true, self.isRewinding);
|
||||||
|
analogClockMultiplierValid = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GBRewind:
|
case GBRewind:
|
||||||
@ -189,6 +204,7 @@
|
|||||||
|
|
||||||
case GBUnderclock:
|
case GBUnderclock:
|
||||||
underclockKeyDown = true;
|
underclockKeyDown = true;
|
||||||
|
analogClockMultiplierValid = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -221,6 +237,7 @@
|
|||||||
switch (button) {
|
switch (button) {
|
||||||
case GBTurbo:
|
case GBTurbo:
|
||||||
GB_set_turbo_mode(_gb, false, false);
|
GB_set_turbo_mode(_gb, false, false);
|
||||||
|
analogClockMultiplierValid = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case GBRewind:
|
case GBRewind:
|
||||||
@ -229,6 +246,7 @@
|
|||||||
|
|
||||||
case GBUnderclock:
|
case GBUnderclock:
|
||||||
underclockKeyDown = false;
|
underclockKeyDown = false;
|
||||||
|
analogClockMultiplierValid = false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -243,126 +261,100 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state
|
- (void)setRumble:(bool)on
|
||||||
{
|
{
|
||||||
|
[lastController setRumbleAmplitude:(double)on];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)controller:(JOYController *)controller movedAxis:(JOYAxis *)axis
|
||||||
|
{
|
||||||
|
if (![self.window isMainWindow]) return;
|
||||||
|
|
||||||
|
NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID];
|
||||||
|
if (!mapping) {
|
||||||
|
mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((axis.usage == JOYAxisUsageR1 && !mapping) ||
|
||||||
|
axis.uniqueID == [mapping[@"AnalogUnderclock"] unsignedLongValue]){
|
||||||
|
analogClockMultiplier = MIN(MAX(1 - axis.value + 0.2, 1.0 / 3), 1.0);
|
||||||
|
analogClockMultiplierValid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if ((axis.usage == JOYAxisUsageL1 && !mapping) ||
|
||||||
|
axis.uniqueID == [mapping[@"AnalogTurbo"] unsignedLongValue]){
|
||||||
|
analogClockMultiplier = MIN(MAX(axis.value * 3 + 0.8, 1.0), 3.0);
|
||||||
|
analogClockMultiplierValid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)controller:(JOYController *)controller buttonChangedState:(JOYButton *)button
|
||||||
|
{
|
||||||
|
if (![self.window isMainWindow]) return;
|
||||||
|
if (controller != lastController) {
|
||||||
|
[lastController setRumbleAmplitude:0];
|
||||||
|
lastController = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
unsigned player_count = GB_get_player_count(_gb);
|
unsigned player_count = GB_get_player_count(_gb);
|
||||||
|
|
||||||
UpdateSystemActivity(UsrActivity);
|
IOPMAssertionID assertionID;
|
||||||
|
IOPMAssertionDeclareUserActivity(CFSTR(""), kIOPMUserActiveLocal, &assertionID);
|
||||||
|
|
||||||
for (unsigned player = 0; player < player_count; player++) {
|
for (unsigned player = 0; player < player_count; player++) {
|
||||||
NSString *preferred_joypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBDefaultJoypads"]
|
NSString *preferred_joypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitDefaultControllers"]
|
||||||
objectForKey:[NSString stringWithFormat:@"%u", player]];
|
objectForKey:n2s(player)];
|
||||||
if (player_count != 1 && // Single player, accpet inputs from all joypads
|
if (player_count != 1 && // Single player, accpet inputs from all joypads
|
||||||
!(player == 0 && !preferred_joypad) && // Multiplayer, but player 1 has no joypad configured, so it takes inputs from all joypads
|
!(player == 0 && !preferred_joypad) && // Multiplayer, but player 1 has no joypad configured, so it takes inputs from all joypads
|
||||||
![preferred_joypad isEqualToString:joystick_name]) {
|
![preferred_joypad isEqualToString:controller.uniqueID]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"][joystick_name];
|
[controller setPlayerLEDs:1 << player];
|
||||||
|
NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID];
|
||||||
|
if (!mapping) {
|
||||||
|
mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName];
|
||||||
|
}
|
||||||
|
|
||||||
for (GBButton i = 0; i < GBButtonCount; i++) {
|
JOYButtonUsage usage = ((JOYButtonUsage)[mapping[n2s(button.uniqueID)] unsignedIntValue]) ?: button.usage;
|
||||||
NSNumber *mapped_button = [mapping objectForKey:GBButtonNames[i]];
|
if (!mapping && usage >= JOYButtonUsageGeneric0) {
|
||||||
if (mapped_button && [mapped_button integerValue] == button) {
|
usage = (const JOYButtonUsage[]){JOYButtonUsageY, JOYButtonUsageA, JOYButtonUsageB, JOYButtonUsageX}[(usage - JOYButtonUsageGeneric0) & 3];
|
||||||
switch (i) {
|
}
|
||||||
case GBTurbo:
|
|
||||||
GB_set_turbo_mode(_gb, state, state && self.isRewinding);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case GBRewind:
|
switch (usage) {
|
||||||
self.isRewinding = state;
|
|
||||||
if (state) {
|
|
||||||
GB_set_turbo_mode(_gb, false, false);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case GBUnderclock:
|
case JOYButtonUsageNone: break;
|
||||||
underclockKeyDown = state;
|
case JOYButtonUsageA: GB_set_key_state_for_player(_gb, GB_KEY_A, player, button.isPressed); break;
|
||||||
break;
|
case JOYButtonUsageB: GB_set_key_state_for_player(_gb, GB_KEY_B, player, button.isPressed); break;
|
||||||
|
case JOYButtonUsageC: break;
|
||||||
default:
|
case JOYButtonUsageStart:
|
||||||
GB_set_key_state_for_player(_gb, (GB_key_t)i, player, state);
|
case JOYButtonUsageX: GB_set_key_state_for_player(_gb, GB_KEY_START, player, button.isPressed); break;
|
||||||
break;
|
case JOYButtonUsageSelect:
|
||||||
|
case JOYButtonUsageY: GB_set_key_state_for_player(_gb, GB_KEY_SELECT, player, button.isPressed); break;
|
||||||
|
case JOYButtonUsageR2:
|
||||||
|
case JOYButtonUsageL2:
|
||||||
|
case JOYButtonUsageZ: {
|
||||||
|
self.isRewinding = button.isPressed;
|
||||||
|
if (button.isPressed) {
|
||||||
|
GB_set_turbo_mode(_gb, false, false);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case JOYButtonUsageL1: GB_set_turbo_mode(_gb, button.isPressed, button.isPressed && self.isRewinding); break;
|
||||||
|
|
||||||
|
case JOYButtonUsageR1: underclockKeyDown = button.isPressed; break;
|
||||||
|
case JOYButtonUsageDPadLeft: GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, button.isPressed); break;
|
||||||
|
case JOYButtonUsageDPadRight: GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, button.isPressed); break;
|
||||||
|
case JOYButtonUsageDPadUp: GB_set_key_state_for_player(_gb, GB_KEY_UP, player, button.isPressed); break;
|
||||||
|
case JOYButtonUsageDPadDown: GB_set_key_state_for_player(_gb, GB_KEY_DOWN, player, button.isPressed); break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value
|
|
||||||
{
|
|
||||||
unsigned player_count = GB_get_player_count(_gb);
|
|
||||||
|
|
||||||
UpdateSystemActivity(UsrActivity);
|
|
||||||
for (unsigned player = 0; player < player_count; player++) {
|
|
||||||
NSString *preferred_joypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBDefaultJoypads"]
|
|
||||||
objectForKey:[NSString stringWithFormat:@"%u", player]];
|
|
||||||
if (player_count != 1 && // Single player, accpet inputs from all joypads
|
|
||||||
!(player == 0 && !preferred_joypad) && // Multiplayer, but player 1 has no joypad configured, so it takes inputs from all joypads
|
|
||||||
![preferred_joypad isEqualToString:joystick_name]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBJoypadMappings"][joystick_name];
|
|
||||||
NSNumber *x_axis = [mapping objectForKey:@"XAxis"];
|
|
||||||
NSNumber *y_axis = [mapping objectForKey:@"YAxis"];
|
|
||||||
|
|
||||||
if (axis == [x_axis integerValue]) {
|
|
||||||
if (value > JOYSTICK_HIGH) {
|
|
||||||
axisActive[0] = true;
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, true);
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, false);
|
|
||||||
}
|
|
||||||
else if (value < -JOYSTICK_HIGH) {
|
|
||||||
axisActive[0] = true;
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, false);
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, true);
|
|
||||||
}
|
|
||||||
else if (axisActive[0] && value < JOYSTICK_LOW && value > -JOYSTICK_LOW) {
|
|
||||||
axisActive[0] = false;
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, false);
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (axis == [y_axis integerValue]) {
|
|
||||||
if (value > JOYSTICK_HIGH) {
|
|
||||||
axisActive[1] = true;
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_DOWN, player, true);
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_UP, player, false);
|
|
||||||
}
|
|
||||||
else if (value < -JOYSTICK_HIGH) {
|
|
||||||
axisActive[1] = true;
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_DOWN, player, false);
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_UP, player, true);
|
|
||||||
}
|
|
||||||
else if (axisActive[1] && value < JOYSTICK_LOW && value > -JOYSTICK_LOW) {
|
|
||||||
axisActive[1] = false;
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_DOWN, player, false);
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_UP, player, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void) joystick:(NSString *)joystick_name hat: (unsigned)hat changedState: (int8_t) state
|
|
||||||
{
|
|
||||||
unsigned player_count = GB_get_player_count(_gb);
|
|
||||||
|
|
||||||
UpdateSystemActivity(UsrActivity);
|
|
||||||
for (unsigned player = 0; player < player_count; player++) {
|
|
||||||
NSString *preferred_joypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBDefaultJoypads"]
|
|
||||||
objectForKey:[NSString stringWithFormat:@"%u", player]];
|
|
||||||
if (player_count != 1 && // Single player, accpet inputs from all joypads
|
|
||||||
!(player == 0 && !preferred_joypad) && // Multiplayer, but player 1 has no joypad configured, so it takes inputs from all joypads
|
|
||||||
![preferred_joypad isEqualToString:joystick_name]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
assert(state + 1 < 9);
|
|
||||||
/* - N NE E SE S SW W NW */
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_UP, player, (bool []){0, 1, 1, 0, 0, 0, 0, 0, 1}[state + 1]);
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, (bool []){0, 0, 1, 1, 1, 0, 0, 0, 0}[state + 1]);
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_DOWN, player, (bool []){0, 0, 0, 0, 1, 1, 1, 0, 0}[state + 1]);
|
|
||||||
GB_set_key_state_for_player(_gb, GB_KEY_LEFT, player, (bool []){0, 0, 0, 0, 0, 0, 1, 1, 1}[state + 1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)acceptsFirstResponder
|
- (BOOL)acceptsFirstResponder
|
||||||
{
|
{
|
||||||
return YES;
|
return YES;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="13771" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14868" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13771"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14868"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<objects>
|
<objects>
|
||||||
@ -17,7 +17,7 @@
|
|||||||
</customObject>
|
</customObject>
|
||||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||||
<window title="Preferences" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="GBPreferencesWindow">
|
<window title="Preferences" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="GBPreferencesWindow">
|
||||||
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
<windowStyleMask key="styleMask" titled="YES" closable="YES"/>
|
||||||
<windowCollectionBehavior key="collectionBehavior" fullScreenAuxiliary="YES"/>
|
<windowCollectionBehavior key="collectionBehavior" fullScreenAuxiliary="YES"/>
|
||||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||||
@ -58,6 +58,7 @@
|
|||||||
</defaultToolbarItems>
|
</defaultToolbarItems>
|
||||||
</toolbar>
|
</toolbar>
|
||||||
<connections>
|
<connections>
|
||||||
|
<outlet property="analogControlsCheckbox" destination="RuW-Db-dzW" id="FRE-hI-mnU"/>
|
||||||
<outlet property="aspectRatioCheckbox" destination="Vfj-tg-7OP" id="Yw0-xS-DBr"/>
|
<outlet property="aspectRatioCheckbox" destination="Vfj-tg-7OP" id="Yw0-xS-DBr"/>
|
||||||
<outlet property="bootROMsButton" destination="T3Y-Ln-Onl" id="tdL-Yv-E2K"/>
|
<outlet property="bootROMsButton" destination="T3Y-Ln-Onl" id="tdL-Yv-E2K"/>
|
||||||
<outlet property="bootROMsFolderItem" destination="Dzv-Gc-zoL" id="yhV-ZI-avD"/>
|
<outlet property="bootROMsFolderItem" destination="Dzv-Gc-zoL" id="yhV-ZI-avD"/>
|
||||||
@ -369,22 +370,11 @@
|
|||||||
<point key="canvasLocation" x="-176" y="890"/>
|
<point key="canvasLocation" x="-176" y="890"/>
|
||||||
</customView>
|
</customView>
|
||||||
<customView id="8TU-6J-NCg">
|
<customView id="8TU-6J-NCg">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="292" height="376"/>
|
<rect key="frame" x="0.0" y="0.0" width="292" height="401"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Qa7-Z7-yfO">
|
|
||||||
<rect key="frame" x="20" y="9" width="188" height="32"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
|
||||||
<buttonCell key="cell" type="push" title="Configure a Joypad" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="GdK-tQ-Wim">
|
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
|
||||||
<font key="font" metaFont="system"/>
|
|
||||||
</buttonCell>
|
|
||||||
<connections>
|
|
||||||
<action selector="configureJoypad:" target="QvC-M9-y7g" id="IfY-Kc-PKU"/>
|
|
||||||
</connections>
|
|
||||||
</button>
|
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Utu-t4-cLx">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Utu-t4-cLx">
|
||||||
<rect key="frame" x="10" y="339" width="122" height="17"/>
|
<rect key="frame" x="10" y="364" width="122" height="17"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Control settings for" id="YqW-Ds-VIC">
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Control settings for" id="YqW-Ds-VIC">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@ -393,7 +383,7 @@
|
|||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<scrollView focusRingType="none" fixedFrame="YES" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" hasHorizontalScroller="NO" hasVerticalScroller="NO" usesPredominantAxisScrolling="NO" horizontalScrollElasticity="none" verticalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="PBp-dj-EIa">
|
<scrollView focusRingType="none" fixedFrame="YES" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" hasHorizontalScroller="NO" hasVerticalScroller="NO" usesPredominantAxisScrolling="NO" horizontalScrollElasticity="none" verticalScrollElasticity="none" translatesAutoresizingMaskIntoConstraints="NO" id="PBp-dj-EIa">
|
||||||
<rect key="frame" x="32" y="117" width="240" height="211"/>
|
<rect key="frame" x="32" y="142" width="240" height="211"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<clipView key="contentView" focusRingType="none" ambiguous="YES" drawsBackground="NO" id="AMs-PO-nid">
|
<clipView key="contentView" focusRingType="none" ambiguous="YES" drawsBackground="NO" id="AMs-PO-nid">
|
||||||
<rect key="frame" x="1" y="1" width="238" height="209"/>
|
<rect key="frame" x="1" y="1" width="238" height="209"/>
|
||||||
@ -441,28 +431,28 @@
|
|||||||
</subviews>
|
</subviews>
|
||||||
<nil key="backgroundColor"/>
|
<nil key="backgroundColor"/>
|
||||||
</clipView>
|
</clipView>
|
||||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="31h-at-Znm">
|
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="31h-at-Znm">
|
||||||
<rect key="frame" x="-100" y="-100" width="210" height="16"/>
|
<rect key="frame" x="-100" y="-100" width="210" height="16"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</scroller>
|
</scroller>
|
||||||
<scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="JkP-U1-jdy">
|
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="JkP-U1-jdy">
|
||||||
<rect key="frame" x="-100" y="-100" width="15" height="102"/>
|
<rect key="frame" x="-100" y="-100" width="15" height="102"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
</scroller>
|
</scroller>
|
||||||
</scrollView>
|
</scrollView>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fcF-wc-KwM">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fcF-wc-KwM">
|
||||||
<rect key="frame" x="30" y="92" width="187" height="17"/>
|
<rect key="frame" x="30" y="117" width="203" height="17"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Joypad for multiplayer games:" id="AJA-9b-VKI">
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Controller for multiplayer games:" id="AJA-9b-VKI">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0Az-0R-oNw">
|
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0Az-0R-oNw">
|
||||||
<rect key="frame" x="42" y="61" width="208" height="26"/>
|
<rect key="frame" x="42" y="86" width="208" height="26"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<popUpButtonCell key="cell" type="push" title="None" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="hy8-cr-RrE" id="uEC-vN-8Jq">
|
<popUpButtonCell key="cell" type="push" title="None" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingMiddle" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="hy8-cr-RrE" id="uEC-vN-8Jq">
|
||||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="menu"/>
|
<font key="font" metaFont="menu"/>
|
||||||
<menu key="menu" id="vzY-GQ-t9J">
|
<menu key="menu" id="vzY-GQ-t9J">
|
||||||
@ -476,11 +466,11 @@
|
|||||||
</connections>
|
</connections>
|
||||||
</popUpButton>
|
</popUpButton>
|
||||||
<box verticalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="VEc-Ed-Z6f">
|
<box verticalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="VEc-Ed-Z6f">
|
||||||
<rect key="frame" x="12" y="48" width="268" height="5"/>
|
<rect key="frame" x="12" y="73" width="268" height="5"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
</box>
|
</box>
|
||||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ReM-uo-H0r">
|
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ReM-uo-H0r">
|
||||||
<rect key="frame" x="215" y="339" width="8" height="17"/>
|
<rect key="frame" x="215" y="364" width="8" height="17"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title=":" id="VhO-3T-glt">
|
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title=":" id="VhO-3T-glt">
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
@ -489,7 +479,7 @@
|
|||||||
</textFieldCell>
|
</textFieldCell>
|
||||||
</textField>
|
</textField>
|
||||||
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gWx-7h-0xq">
|
<popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gWx-7h-0xq">
|
||||||
<rect key="frame" x="131" y="332" width="87" height="26"/>
|
<rect key="frame" x="131" y="357" width="87" height="26"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<popUpButtonCell key="cell" type="push" title="Player 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="TO3-R7-9HN" id="pbt-Lr-bU1">
|
<popUpButtonCell key="cell" type="push" title="Player 1" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="TO3-R7-9HN" id="pbt-Lr-bU1">
|
||||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||||
@ -507,10 +497,21 @@
|
|||||||
<action selector="refreshJoypadMenu:" target="QvC-M9-y7g" id="5hY-tg-9VE"/>
|
<action selector="refreshJoypadMenu:" target="QvC-M9-y7g" id="5hY-tg-9VE"/>
|
||||||
</connections>
|
</connections>
|
||||||
</popUpButton>
|
</popUpButton>
|
||||||
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="d2I-jU-sLb">
|
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="RuW-Db-dzW">
|
||||||
<rect key="frame" x="212" y="9" width="60" height="32"/>
|
<rect key="frame" x="18" y="44" width="264" height="25"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
<buttonCell key="cell" type="push" title="Skip" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="sug-xy-tbw">
|
<buttonCell key="cell" type="check" title="Analog turbo and slow-motion controls" bezelStyle="regularSquare" imagePosition="left" lineBreakMode="charWrapping" inset="2" id="Mvp-oc-N3t">
|
||||||
|
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="changeAnalogControls:" target="QvC-M9-y7g" id="1xR-gY-WKo"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="d2I-jU-sLb">
|
||||||
|
<rect key="frame" x="206" y="13" width="72" height="32"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<buttonCell key="cell" type="push" title="Clear" bezelStyle="rounded" alignment="center" enabled="NO" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="sug-xy-tbw">
|
||||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
<font key="font" metaFont="system"/>
|
<font key="font" metaFont="system"/>
|
||||||
</buttonCell>
|
</buttonCell>
|
||||||
@ -518,8 +519,19 @@
|
|||||||
<action selector="skipButton:" target="QvC-M9-y7g" id="aw8-sw-yJw"/>
|
<action selector="skipButton:" target="QvC-M9-y7g" id="aw8-sw-yJw"/>
|
||||||
</connections>
|
</connections>
|
||||||
</button>
|
</button>
|
||||||
|
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Qa7-Z7-yfO">
|
||||||
|
<rect key="frame" x="18" y="13" width="188" height="32"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||||
|
<buttonCell key="cell" type="push" title="Configure a Controller" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="GdK-tQ-Wim">
|
||||||
|
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||||
|
<font key="font" metaFont="system"/>
|
||||||
|
</buttonCell>
|
||||||
|
<connections>
|
||||||
|
<action selector="configureJoypad:" target="QvC-M9-y7g" id="IfY-Kc-PKU"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
</subviews>
|
</subviews>
|
||||||
<point key="canvasLocation" x="-159" y="1116"/>
|
<point key="canvasLocation" x="-159" y="1128.5"/>
|
||||||
</customView>
|
</customView>
|
||||||
</objects>
|
</objects>
|
||||||
<resources>
|
<resources>
|
||||||
|
748
Cocoa/joypad.m
748
Cocoa/joypad.m
@ -1,748 +0,0 @@
|
|||||||
/*
|
|
||||||
Joypad support is based on a stripped-down version of SDL's Darwin implementation
|
|
||||||
of the Joystick API, under the following license:
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
Simple DirectMedia Layer
|
|
||||||
Copyright (C) 1997-2017 Sam Lantinga <slouken@libsdl.org>
|
|
||||||
|
|
||||||
This software is provided 'as-is', without any express or implied
|
|
||||||
warranty. In no event will the authors be held liable for any damages
|
|
||||||
arising from the use of this software.
|
|
||||||
|
|
||||||
Permission is granted to anyone to use this software for any purpose,
|
|
||||||
including commercial applications, and to alter it and redistribute it
|
|
||||||
freely, subject to the following restrictions:
|
|
||||||
|
|
||||||
1. The origin of this software must not be misrepresented; you must not
|
|
||||||
claim that you wrote the original software. If you use this software
|
|
||||||
in a product, an acknowledgment in the product documentation would be
|
|
||||||
appreciated but is not required.
|
|
||||||
2. Altered source versions must be plainly marked as such, and must not be
|
|
||||||
misrepresented as being the original software.
|
|
||||||
3. This notice may not be removed or altered from any source distribution.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include <AppKit/AppKit.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <IOKit/hid/IOHIDLib.h>
|
|
||||||
#include "GBJoystickListener.h"
|
|
||||||
|
|
||||||
typedef signed SDL_JoystickID;
|
|
||||||
typedef struct _SDL_Joystick SDL_Joystick;
|
|
||||||
|
|
||||||
typedef struct _SDL_JoystickAxisInfo
|
|
||||||
{
|
|
||||||
int16_t initial_value; /* Initial axis state */
|
|
||||||
int16_t value; /* Current axis state */
|
|
||||||
int16_t zero; /* Zero point on the axis (-32768 for triggers) */
|
|
||||||
bool has_initial_value; /* Whether we've seen a value on the axis yet */
|
|
||||||
bool sent_initial_value; /* Whether we've sent the initial axis value */
|
|
||||||
} SDL_JoystickAxisInfo;
|
|
||||||
|
|
||||||
struct _SDL_Joystick
|
|
||||||
{
|
|
||||||
SDL_JoystickID instance_id; /* Device instance, monotonically increasing from 0 */
|
|
||||||
char *name; /* Joystick name - system dependent */
|
|
||||||
|
|
||||||
int naxes; /* Number of axis controls on the joystick */
|
|
||||||
SDL_JoystickAxisInfo *axes;
|
|
||||||
|
|
||||||
int nbuttons; /* Number of buttons on the joystick */
|
|
||||||
uint8_t *buttons; /* Current button states */
|
|
||||||
|
|
||||||
int nhats;
|
|
||||||
uint8_t *hats;
|
|
||||||
|
|
||||||
struct joystick_hwdata *hwdata; /* Driver dependent information */
|
|
||||||
|
|
||||||
int ref_count; /* Reference count for multiple opens */
|
|
||||||
|
|
||||||
bool is_game_controller;
|
|
||||||
bool force_recentering; /* SDL_TRUE if this device needs to have its state reset to 0 */
|
|
||||||
struct _SDL_Joystick *next; /* pointer to next joystick we have allocated */
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t data[16];
|
|
||||||
} SDL_JoystickGUID;
|
|
||||||
|
|
||||||
struct recElement
|
|
||||||
{
|
|
||||||
IOHIDElementRef elementRef;
|
|
||||||
IOHIDElementCookie cookie;
|
|
||||||
uint32_t usagePage, usage; /* HID usage */
|
|
||||||
SInt32 min; /* reported min value possible */
|
|
||||||
SInt32 max; /* reported max value possible */
|
|
||||||
|
|
||||||
/* runtime variables used for auto-calibration */
|
|
||||||
SInt32 minReport; /* min returned value */
|
|
||||||
SInt32 maxReport; /* max returned value */
|
|
||||||
|
|
||||||
struct recElement *pNext; /* next element in list */
|
|
||||||
};
|
|
||||||
typedef struct recElement recElement;
|
|
||||||
|
|
||||||
struct joystick_hwdata
|
|
||||||
{
|
|
||||||
IOHIDDeviceRef deviceRef; /* HIDManager device handle */
|
|
||||||
io_service_t ffservice; /* Interface for force feedback, 0 = no ff */
|
|
||||||
|
|
||||||
char product[256]; /* name of product */
|
|
||||||
uint32_t usage; /* usage page from IOUSBHID Parser.h which defines general usage */
|
|
||||||
uint32_t usagePage; /* usage within above page from IOUSBHID Parser.h which defines specific usage */
|
|
||||||
|
|
||||||
int axes; /* number of axis (calculated, not reported by device) */
|
|
||||||
int buttons; /* number of buttons (calculated, not reported by device) */
|
|
||||||
int hats;
|
|
||||||
int elements; /* number of total elements (should be total of above) (calculated, not reported by device) */
|
|
||||||
|
|
||||||
recElement *firstAxis;
|
|
||||||
recElement *firstButton;
|
|
||||||
recElement *firstHat;
|
|
||||||
|
|
||||||
bool removed;
|
|
||||||
|
|
||||||
int instance_id;
|
|
||||||
SDL_JoystickGUID guid;
|
|
||||||
|
|
||||||
SDL_Joystick joystick;
|
|
||||||
};
|
|
||||||
typedef struct joystick_hwdata recDevice;
|
|
||||||
|
|
||||||
/* The base object of the HID Manager API */
|
|
||||||
static IOHIDManagerRef hidman = NULL;
|
|
||||||
|
|
||||||
/* static incrementing counter for new joystick devices seen on the system. Devices should start with index 0 */
|
|
||||||
static int s_joystick_instance_id = -1;
|
|
||||||
|
|
||||||
#define SDL_JOYSTICK_AXIS_MAX 32767
|
|
||||||
|
|
||||||
void SDL_PrivateJoystickAxis(SDL_Joystick * joystick, uint8_t axis, int16_t value)
|
|
||||||
{
|
|
||||||
/* Make sure we're not getting garbage or duplicate events */
|
|
||||||
if (axis >= joystick->naxes) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!joystick->axes[axis].has_initial_value) {
|
|
||||||
joystick->axes[axis].initial_value = value;
|
|
||||||
joystick->axes[axis].value = value;
|
|
||||||
joystick->axes[axis].zero = value;
|
|
||||||
joystick->axes[axis].has_initial_value = true;
|
|
||||||
}
|
|
||||||
if (value == joystick->axes[axis].value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!joystick->axes[axis].sent_initial_value) {
|
|
||||||
/* Make sure we don't send motion until there's real activity on this axis */
|
|
||||||
const int MAX_ALLOWED_JITTER = SDL_JOYSTICK_AXIS_MAX / 80; /* ShanWan PS3 controller needed 96 */
|
|
||||||
if (abs(value - joystick->axes[axis].value) <= MAX_ALLOWED_JITTER) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
joystick->axes[axis].sent_initial_value = true;
|
|
||||||
joystick->axes[axis].value = value; /* Just so we pass the check above */
|
|
||||||
SDL_PrivateJoystickAxis(joystick, axis, joystick->axes[axis].initial_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Update internal joystick state */
|
|
||||||
joystick->axes[axis].value = value;
|
|
||||||
|
|
||||||
NSResponder<GBJoystickListener> *responder = (typeof(responder)) [[NSApp keyWindow] firstResponder];
|
|
||||||
while (responder) {
|
|
||||||
if ([responder respondsToSelector:@selector(joystick:axis:movedTo:)]) {
|
|
||||||
[responder joystick:@(joystick->name) axis:axis movedTo:value];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
responder = (typeof(responder)) [responder nextResponder];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SDL_PrivateJoystickButton(SDL_Joystick *joystick, uint8_t button, uint8_t state)
|
|
||||||
{
|
|
||||||
|
|
||||||
/* Make sure we're not getting garbage or duplicate events */
|
|
||||||
if (button >= joystick->nbuttons) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state == joystick->buttons[button]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Update internal joystick state */
|
|
||||||
joystick->buttons[button] = state;
|
|
||||||
|
|
||||||
NSResponder<GBJoystickListener> *responder = (typeof(responder)) [[NSApp keyWindow] firstResponder];
|
|
||||||
while (responder) {
|
|
||||||
if ([responder respondsToSelector:@selector(joystick:button:changedState:)]) {
|
|
||||||
[responder joystick:@(joystick->name) button:button changedState:state];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
responder = (typeof(responder)) [responder nextResponder];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SDL_PrivateJoystickHat(SDL_Joystick *joystick, uint8_t hat, uint8_t state)
|
|
||||||
{
|
|
||||||
|
|
||||||
/* Make sure we're not getting garbage or duplicate events */
|
|
||||||
if (hat >= joystick->nhats) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (state == joystick->hats[hat]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Update internal joystick state */
|
|
||||||
joystick->hats[hat] = state;
|
|
||||||
|
|
||||||
NSResponder<GBJoystickListener> *responder = (typeof(responder)) [[NSApp keyWindow] firstResponder];
|
|
||||||
while (responder) {
|
|
||||||
if ([responder respondsToSelector:@selector(joystick:button:changedState:)]) {
|
|
||||||
[responder joystick:@(joystick->name) hat:hat changedState:state];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
responder = (typeof(responder)) [responder nextResponder];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
FreeElementList(recElement *pElement)
|
|
||||||
{
|
|
||||||
while (pElement) {
|
|
||||||
recElement *pElementNext = pElement->pNext;
|
|
||||||
free(pElement);
|
|
||||||
pElement = pElementNext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static recDevice *
|
|
||||||
FreeDevice(recDevice *removeDevice)
|
|
||||||
{
|
|
||||||
recDevice *pDeviceNext = NULL;
|
|
||||||
if (removeDevice) {
|
|
||||||
if (removeDevice->deviceRef) {
|
|
||||||
IOHIDDeviceUnscheduleFromRunLoop(removeDevice->deviceRef, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
|
|
||||||
removeDevice->deviceRef = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* free element lists */
|
|
||||||
FreeElementList(removeDevice->firstAxis);
|
|
||||||
FreeElementList(removeDevice->firstButton);
|
|
||||||
FreeElementList(removeDevice->firstHat);
|
|
||||||
|
|
||||||
free(removeDevice);
|
|
||||||
}
|
|
||||||
return pDeviceNext;
|
|
||||||
}
|
|
||||||
|
|
||||||
static SInt32
|
|
||||||
GetHIDElementState(recDevice *pDevice, recElement *pElement)
|
|
||||||
{
|
|
||||||
SInt32 value = 0;
|
|
||||||
|
|
||||||
if (pDevice && pElement) {
|
|
||||||
IOHIDValueRef valueRef;
|
|
||||||
if (IOHIDDeviceGetValue(pDevice->deviceRef, pElement->elementRef, &valueRef) == kIOReturnSuccess) {
|
|
||||||
value = (SInt32) IOHIDValueGetIntegerValue(valueRef);
|
|
||||||
|
|
||||||
/* record min and max for auto calibration */
|
|
||||||
if (value < pElement->minReport) {
|
|
||||||
pElement->minReport = value;
|
|
||||||
}
|
|
||||||
if (value > pElement->maxReport) {
|
|
||||||
pElement->maxReport = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
static SInt32
|
|
||||||
GetHIDScaledCalibratedState(recDevice * pDevice, recElement * pElement, SInt32 min, SInt32 max)
|
|
||||||
{
|
|
||||||
const float deviceScale = max - min;
|
|
||||||
const float readScale = pElement->maxReport - pElement->minReport;
|
|
||||||
const SInt32 value = GetHIDElementState(pDevice, pElement);
|
|
||||||
if (readScale == 0) {
|
|
||||||
return value; /* no scaling at all */
|
|
||||||
}
|
|
||||||
return ((value - pElement->minReport) * deviceScale / readScale) + min;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void
|
|
||||||
JoystickDeviceWasRemovedCallback(void *ctx, IOReturn result, void *sender)
|
|
||||||
{
|
|
||||||
recDevice *device = (recDevice *) ctx;
|
|
||||||
device->removed = true;
|
|
||||||
device->deviceRef = NULL; // deviceRef was invalidated due to the remove
|
|
||||||
FreeDevice(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void AddHIDElement(const void *value, void *parameter);
|
|
||||||
|
|
||||||
/* Call AddHIDElement() on all elements in an array of IOHIDElementRefs */
|
|
||||||
static void
|
|
||||||
AddHIDElements(CFArrayRef array, recDevice *pDevice)
|
|
||||||
{
|
|
||||||
const CFRange range = { 0, CFArrayGetCount(array) };
|
|
||||||
CFArrayApplyFunction(array, range, AddHIDElement, pDevice);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
ElementAlreadyAdded(const IOHIDElementCookie cookie, const recElement *listitem) {
|
|
||||||
while (listitem) {
|
|
||||||
if (listitem->cookie == cookie) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
listitem = listitem->pNext;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* See if we care about this HID element, and if so, note it in our recDevice. */
|
|
||||||
static void
|
|
||||||
AddHIDElement(const void *value, void *parameter)
|
|
||||||
{
|
|
||||||
recDevice *pDevice = (recDevice *) parameter;
|
|
||||||
IOHIDElementRef refElement = (IOHIDElementRef) value;
|
|
||||||
const CFTypeID elementTypeID = refElement ? CFGetTypeID(refElement) : 0;
|
|
||||||
|
|
||||||
if (refElement && (elementTypeID == IOHIDElementGetTypeID())) {
|
|
||||||
const IOHIDElementCookie cookie = IOHIDElementGetCookie(refElement);
|
|
||||||
const uint32_t usagePage = IOHIDElementGetUsagePage(refElement);
|
|
||||||
const uint32_t usage = IOHIDElementGetUsage(refElement);
|
|
||||||
recElement *element = NULL;
|
|
||||||
recElement **headElement = NULL;
|
|
||||||
|
|
||||||
/* look at types of interest */
|
|
||||||
switch (IOHIDElementGetType(refElement)) {
|
|
||||||
case kIOHIDElementTypeInput_Misc:
|
|
||||||
case kIOHIDElementTypeInput_Button:
|
|
||||||
case kIOHIDElementTypeInput_Axis: {
|
|
||||||
switch (usagePage) { /* only interested in kHIDPage_GenericDesktop and kHIDPage_Button */
|
|
||||||
case kHIDPage_GenericDesktop:
|
|
||||||
switch (usage) {
|
|
||||||
case kHIDUsage_GD_X:
|
|
||||||
case kHIDUsage_GD_Y:
|
|
||||||
case kHIDUsage_GD_Z:
|
|
||||||
case kHIDUsage_GD_Rx:
|
|
||||||
case kHIDUsage_GD_Ry:
|
|
||||||
case kHIDUsage_GD_Rz:
|
|
||||||
case kHIDUsage_GD_Slider:
|
|
||||||
case kHIDUsage_GD_Dial:
|
|
||||||
case kHIDUsage_GD_Wheel:
|
|
||||||
if (!ElementAlreadyAdded(cookie, pDevice->firstAxis)) {
|
|
||||||
element = (recElement *) calloc(1, sizeof (recElement));
|
|
||||||
if (element) {
|
|
||||||
pDevice->axes++;
|
|
||||||
headElement = &(pDevice->firstAxis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case kHIDUsage_GD_Hatswitch:
|
|
||||||
if (!ElementAlreadyAdded(cookie, pDevice->firstHat)) {
|
|
||||||
element = (recElement *) calloc(1, sizeof (recElement));
|
|
||||||
if (element) {
|
|
||||||
pDevice->hats++;
|
|
||||||
headElement = &(pDevice->firstHat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case kHIDUsage_GD_DPadUp:
|
|
||||||
case kHIDUsage_GD_DPadDown:
|
|
||||||
case kHIDUsage_GD_DPadRight:
|
|
||||||
case kHIDUsage_GD_DPadLeft:
|
|
||||||
case kHIDUsage_GD_Start:
|
|
||||||
case kHIDUsage_GD_Select:
|
|
||||||
case kHIDUsage_GD_SystemMainMenu:
|
|
||||||
if (!ElementAlreadyAdded(cookie, pDevice->firstButton)) {
|
|
||||||
element = (recElement *) calloc(1, sizeof (recElement));
|
|
||||||
if (element) {
|
|
||||||
pDevice->buttons++;
|
|
||||||
headElement = &(pDevice->firstButton);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case kHIDPage_Simulation:
|
|
||||||
switch (usage) {
|
|
||||||
case kHIDUsage_Sim_Rudder:
|
|
||||||
case kHIDUsage_Sim_Throttle:
|
|
||||||
case kHIDUsage_Sim_Accelerator:
|
|
||||||
case kHIDUsage_Sim_Brake:
|
|
||||||
if (!ElementAlreadyAdded(cookie, pDevice->firstAxis)) {
|
|
||||||
element = (recElement *) calloc(1, sizeof (recElement));
|
|
||||||
if (element) {
|
|
||||||
pDevice->axes++;
|
|
||||||
headElement = &(pDevice->firstAxis);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case kHIDPage_Button:
|
|
||||||
case kHIDPage_Consumer: /* e.g. 'pause' button on Steelseries MFi gamepads. */
|
|
||||||
if (!ElementAlreadyAdded(cookie, pDevice->firstButton)) {
|
|
||||||
element = (recElement *) calloc(1, sizeof (recElement));
|
|
||||||
if (element) {
|
|
||||||
pDevice->buttons++;
|
|
||||||
headElement = &(pDevice->firstButton);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case kIOHIDElementTypeCollection: {
|
|
||||||
CFArrayRef array = IOHIDElementGetChildren(refElement);
|
|
||||||
if (array) {
|
|
||||||
AddHIDElements(array, pDevice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (element && headElement) { /* add to list */
|
|
||||||
recElement *elementPrevious = NULL;
|
|
||||||
recElement *elementCurrent = *headElement;
|
|
||||||
while (elementCurrent && usage >= elementCurrent->usage) {
|
|
||||||
elementPrevious = elementCurrent;
|
|
||||||
elementCurrent = elementCurrent->pNext;
|
|
||||||
}
|
|
||||||
if (elementPrevious) {
|
|
||||||
elementPrevious->pNext = element;
|
|
||||||
} else {
|
|
||||||
*headElement = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
element->elementRef = refElement;
|
|
||||||
element->usagePage = usagePage;
|
|
||||||
element->usage = usage;
|
|
||||||
element->pNext = elementCurrent;
|
|
||||||
|
|
||||||
element->minReport = element->min = (SInt32) IOHIDElementGetLogicalMin(refElement);
|
|
||||||
element->maxReport = element->max = (SInt32) IOHIDElementGetLogicalMax(refElement);
|
|
||||||
element->cookie = IOHIDElementGetCookie(refElement);
|
|
||||||
|
|
||||||
pDevice->elements++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
GetDeviceInfo(IOHIDDeviceRef hidDevice, recDevice *pDevice)
|
|
||||||
{
|
|
||||||
const uint16_t BUS_USB = 0x03;
|
|
||||||
const uint16_t BUS_BLUETOOTH = 0x05;
|
|
||||||
int32_t vendor = 0;
|
|
||||||
int32_t product = 0;
|
|
||||||
int32_t version = 0;
|
|
||||||
CFTypeRef refCF = NULL;
|
|
||||||
CFArrayRef array = NULL;
|
|
||||||
uint16_t *guid16 = (uint16_t *)pDevice->guid.data;
|
|
||||||
|
|
||||||
/* get usage page and usage */
|
|
||||||
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDPrimaryUsagePageKey));
|
|
||||||
if (refCF) {
|
|
||||||
CFNumberGetValue(refCF, kCFNumberSInt32Type, &pDevice->usagePage);
|
|
||||||
}
|
|
||||||
if (pDevice->usagePage != kHIDPage_GenericDesktop) {
|
|
||||||
return false; /* Filter device list to non-keyboard/mouse stuff */
|
|
||||||
}
|
|
||||||
|
|
||||||
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDPrimaryUsageKey));
|
|
||||||
if (refCF) {
|
|
||||||
CFNumberGetValue(refCF, kCFNumberSInt32Type, &pDevice->usage);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((pDevice->usage != kHIDUsage_GD_Joystick &&
|
|
||||||
pDevice->usage != kHIDUsage_GD_GamePad &&
|
|
||||||
pDevice->usage != kHIDUsage_GD_MultiAxisController)) {
|
|
||||||
return false; /* Filter device list to non-keyboard/mouse stuff */
|
|
||||||
}
|
|
||||||
|
|
||||||
pDevice->deviceRef = hidDevice;
|
|
||||||
|
|
||||||
/* get device name */
|
|
||||||
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductKey));
|
|
||||||
if (!refCF) {
|
|
||||||
/* Maybe we can't get "AwesomeJoystick2000", but we can get "Logitech"? */
|
|
||||||
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDManufacturerKey));
|
|
||||||
}
|
|
||||||
if ((!refCF) || (!CFStringGetCString(refCF, pDevice->product, sizeof (pDevice->product), kCFStringEncodingUTF8))) {
|
|
||||||
strlcpy(pDevice->product, "Unidentified joystick", sizeof (pDevice->product));
|
|
||||||
}
|
|
||||||
|
|
||||||
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVendorIDKey));
|
|
||||||
if (refCF) {
|
|
||||||
CFNumberGetValue(refCF, kCFNumberSInt32Type, &vendor);
|
|
||||||
}
|
|
||||||
|
|
||||||
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDProductIDKey));
|
|
||||||
if (refCF) {
|
|
||||||
CFNumberGetValue(refCF, kCFNumberSInt32Type, &product);
|
|
||||||
}
|
|
||||||
|
|
||||||
refCF = IOHIDDeviceGetProperty(hidDevice, CFSTR(kIOHIDVersionNumberKey));
|
|
||||||
if (refCF) {
|
|
||||||
CFNumberGetValue(refCF, kCFNumberSInt32Type, &version);
|
|
||||||
}
|
|
||||||
|
|
||||||
memset(pDevice->guid.data, 0, sizeof(pDevice->guid.data));
|
|
||||||
|
|
||||||
if (vendor && product) {
|
|
||||||
*guid16++ = BUS_USB;
|
|
||||||
*guid16++ = 0;
|
|
||||||
*guid16++ = vendor;
|
|
||||||
*guid16++ = 0;
|
|
||||||
*guid16++ = product;
|
|
||||||
*guid16++ = 0;
|
|
||||||
*guid16++ = version;
|
|
||||||
*guid16++ = 0;
|
|
||||||
} else {
|
|
||||||
*guid16++ = BUS_BLUETOOTH;
|
|
||||||
*guid16++ = 0;
|
|
||||||
strlcpy((char*)guid16, pDevice->product, sizeof(pDevice->guid.data) - 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
array = IOHIDDeviceCopyMatchingElements(hidDevice, NULL, kIOHIDOptionsTypeNone);
|
|
||||||
if (array) {
|
|
||||||
AddHIDElements(array, pDevice);
|
|
||||||
CFRelease(array);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
SDL_SYS_JoystickUpdate(SDL_Joystick * joystick)
|
|
||||||
{
|
|
||||||
recDevice *device = joystick->hwdata;
|
|
||||||
recElement *element;
|
|
||||||
SInt32 value;
|
|
||||||
int i;
|
|
||||||
|
|
||||||
if (!device) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (device->removed) { /* device was unplugged; ignore it. */
|
|
||||||
if (joystick->hwdata) {
|
|
||||||
joystick->force_recentering = true;
|
|
||||||
joystick->hwdata = NULL;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
element = device->firstAxis;
|
|
||||||
i = 0;
|
|
||||||
while (element) {
|
|
||||||
value = GetHIDScaledCalibratedState(device, element, -32768, 32767);
|
|
||||||
SDL_PrivateJoystickAxis(joystick, i, value);
|
|
||||||
element = element->pNext;
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
|
|
||||||
element = device->firstButton;
|
|
||||||
i = 0;
|
|
||||||
while (element) {
|
|
||||||
value = GetHIDElementState(device, element);
|
|
||||||
if (value > 1) { /* handle pressure-sensitive buttons */
|
|
||||||
value = 1;
|
|
||||||
}
|
|
||||||
SDL_PrivateJoystickButton(joystick, i, value);
|
|
||||||
element = element->pNext;
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
|
|
||||||
element = device->firstHat;
|
|
||||||
i = 0;
|
|
||||||
while (element) {
|
|
||||||
signed range = (element->max - element->min + 1);
|
|
||||||
value = GetHIDElementState(device, element) - element->min;
|
|
||||||
if (range == 4) { /* 4 position hatswitch - scale up value */
|
|
||||||
value *= 2;
|
|
||||||
} else if (range != 8) { /* Neither a 4 nor 8 positions - fall back to default position (centered) */
|
|
||||||
value = -1;
|
|
||||||
}
|
|
||||||
if ((unsigned)value >= 8) {
|
|
||||||
value = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_PrivateJoystickHat(joystick, i, value);
|
|
||||||
|
|
||||||
element = element->pNext;
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static void JoystickInputCallback(
|
|
||||||
SDL_Joystick * joystick,
|
|
||||||
IOReturn result,
|
|
||||||
void * _Nullable sender,
|
|
||||||
IOHIDReportType type,
|
|
||||||
uint32_t reportID,
|
|
||||||
uint8_t * report,
|
|
||||||
CFIndex reportLength)
|
|
||||||
{
|
|
||||||
SDL_SYS_JoystickUpdate(joystick);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
JoystickDeviceWasAddedCallback(void *ctx, IOReturn res, void *sender, IOHIDDeviceRef ioHIDDeviceObject)
|
|
||||||
{
|
|
||||||
recDevice *device;
|
|
||||||
io_service_t ioservice;
|
|
||||||
|
|
||||||
if (res != kIOReturnSuccess) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
device = (recDevice *) calloc(1, sizeof(recDevice));
|
|
||||||
|
|
||||||
if (!device) {
|
|
||||||
abort();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!GetDeviceInfo(ioHIDDeviceObject, device)) {
|
|
||||||
free(device);
|
|
||||||
return; /* not a device we care about, probably. */
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_Joystick *joystick = &device->joystick;
|
|
||||||
|
|
||||||
joystick->instance_id = device->instance_id;
|
|
||||||
joystick->hwdata = device;
|
|
||||||
joystick->name = device->product;
|
|
||||||
|
|
||||||
joystick->naxes = device->axes;
|
|
||||||
joystick->nbuttons = device->buttons;
|
|
||||||
joystick->nhats = device->hats;
|
|
||||||
|
|
||||||
if (joystick->naxes > 0) {
|
|
||||||
joystick->axes = (SDL_JoystickAxisInfo *) calloc(joystick->naxes, sizeof(SDL_JoystickAxisInfo));
|
|
||||||
}
|
|
||||||
if (joystick->nbuttons > 0) {
|
|
||||||
joystick->buttons = (uint8_t *) calloc(joystick->nbuttons, 1);
|
|
||||||
}
|
|
||||||
if (joystick->nhats > 0) {
|
|
||||||
joystick->hats = (uint8_t *) calloc(joystick->nhats, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Get notified when this device is disconnected. */
|
|
||||||
IOHIDDeviceRegisterRemovalCallback(ioHIDDeviceObject, JoystickDeviceWasRemovedCallback, device);
|
|
||||||
static uint8_t junk[80];
|
|
||||||
IOHIDDeviceRegisterInputReportCallback(ioHIDDeviceObject, junk, sizeof(junk), (IOHIDReportCallback) JoystickInputCallback, joystick);
|
|
||||||
IOHIDDeviceScheduleWithRunLoop(ioHIDDeviceObject, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
|
|
||||||
|
|
||||||
/* Allocate an instance ID for this device */
|
|
||||||
device->instance_id = ++s_joystick_instance_id;
|
|
||||||
|
|
||||||
/* We have to do some storage of the io_service_t for SDL_HapticOpenFromJoystick */
|
|
||||||
ioservice = IOHIDDeviceGetService(ioHIDDeviceObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
ConfigHIDManager(CFArrayRef matchingArray)
|
|
||||||
{
|
|
||||||
CFRunLoopRef runloop = CFRunLoopGetCurrent();
|
|
||||||
|
|
||||||
if (IOHIDManagerOpen(hidman, kIOHIDOptionsTypeNone) != kIOReturnSuccess) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
IOHIDManagerSetDeviceMatchingMultiple(hidman, matchingArray);
|
|
||||||
IOHIDManagerRegisterDeviceMatchingCallback(hidman, JoystickDeviceWasAddedCallback, NULL);
|
|
||||||
IOHIDManagerScheduleWithRunLoop(hidman, runloop, kCFRunLoopDefaultMode);
|
|
||||||
|
|
||||||
return true; /* good to go. */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static CFDictionaryRef
|
|
||||||
CreateHIDDeviceMatchDictionary(const UInt32 page, const UInt32 usage, int *okay)
|
|
||||||
{
|
|
||||||
CFDictionaryRef retval = NULL;
|
|
||||||
CFNumberRef pageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page);
|
|
||||||
CFNumberRef usageNumRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage);
|
|
||||||
const void *keys[2] = { (void *) CFSTR(kIOHIDDeviceUsagePageKey), (void *) CFSTR(kIOHIDDeviceUsageKey) };
|
|
||||||
const void *vals[2] = { (void *) pageNumRef, (void *) usageNumRef };
|
|
||||||
|
|
||||||
if (pageNumRef && usageNumRef) {
|
|
||||||
retval = CFDictionaryCreate(kCFAllocatorDefault, keys, vals, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pageNumRef) {
|
|
||||||
CFRelease(pageNumRef);
|
|
||||||
}
|
|
||||||
if (usageNumRef) {
|
|
||||||
CFRelease(usageNumRef);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!retval) {
|
|
||||||
*okay = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return retval;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
CreateHIDManager(void)
|
|
||||||
{
|
|
||||||
bool retval = false;
|
|
||||||
int okay = 1;
|
|
||||||
const void *vals[] = {
|
|
||||||
(void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick, &okay),
|
|
||||||
(void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad, &okay),
|
|
||||||
(void *) CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController, &okay),
|
|
||||||
};
|
|
||||||
const size_t numElements = sizeof(vals) / sizeof(vals[0]);
|
|
||||||
CFArrayRef array = okay ? CFArrayCreate(kCFAllocatorDefault, vals, numElements, &kCFTypeArrayCallBacks) : NULL;
|
|
||||||
size_t i;
|
|
||||||
|
|
||||||
for (i = 0; i < numElements; i++) {
|
|
||||||
if (vals[i]) {
|
|
||||||
CFRelease((CFTypeRef) vals[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (array) {
|
|
||||||
hidman = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
|
|
||||||
if (hidman != NULL) {
|
|
||||||
retval = ConfigHIDManager(array);
|
|
||||||
}
|
|
||||||
CFRelease(array);
|
|
||||||
}
|
|
||||||
|
|
||||||
return retval;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void __attribute__((constructor)) SDL_SYS_JoystickInit(void)
|
|
||||||
{
|
|
||||||
if (!CreateHIDManager()) {
|
|
||||||
fprintf(stderr, "Joystick: Couldn't initialize HID Manager");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
int main(int argc, const char * argv[]) {
|
int main(int argc, const char * argv[])
|
||||||
|
{
|
||||||
return NSApplicationMain(argc, argv);
|
return NSApplicationMain(argc, argv);
|
||||||
}
|
}
|
||||||
|
@ -1448,7 +1448,7 @@ static bool mbc(GB_gameboy_t *gb, char *arguments, char *modifiers, const debugg
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cartridge->has_rumble) {
|
if (cartridge->has_rumble) {
|
||||||
GB_log(gb, "Cart contains a rumble pak\n");
|
GB_log(gb, "Cart contains a Rumble Pak\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cartridge->has_rtc) {
|
if (cartridge->has_rtc) {
|
||||||
|
@ -151,6 +151,13 @@ static void display_vblank(GB_gameboy_t *gb)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gb->rumble_callback) {
|
||||||
|
if (gb->rumble_on_cycles + gb->rumble_off_cycles) {
|
||||||
|
gb->rumble_callback(gb, gb->rumble_on_cycles / (double)(gb->rumble_on_cycles + gb->rumble_off_cycles));
|
||||||
|
gb->rumble_on_cycles = gb->rumble_off_cycles = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
gb->vblank_callback(gb);
|
gb->vblank_callback(gb);
|
||||||
GB_timing_sync(gb);
|
GB_timing_sync(gb);
|
||||||
}
|
}
|
||||||
|
@ -241,7 +241,7 @@ typedef void (*GB_log_callback_t)(GB_gameboy_t *gb, const char *string, GB_log_a
|
|||||||
typedef char *(*GB_input_callback_t)(GB_gameboy_t *gb);
|
typedef char *(*GB_input_callback_t)(GB_gameboy_t *gb);
|
||||||
typedef uint32_t (*GB_rgb_encode_callback_t)(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b);
|
typedef uint32_t (*GB_rgb_encode_callback_t)(GB_gameboy_t *gb, uint8_t r, uint8_t g, uint8_t b);
|
||||||
typedef void (*GB_infrared_callback_t)(GB_gameboy_t *gb, bool on, long cycles_since_last_update);
|
typedef void (*GB_infrared_callback_t)(GB_gameboy_t *gb, bool on, long cycles_since_last_update);
|
||||||
typedef void (*GB_rumble_callback_t)(GB_gameboy_t *gb, bool rumble_on);
|
typedef void (*GB_rumble_callback_t)(GB_gameboy_t *gb, double rumble_amplitude);
|
||||||
typedef void (*GB_serial_transfer_bit_start_callback_t)(GB_gameboy_t *gb, bool bit_to_send);
|
typedef void (*GB_serial_transfer_bit_start_callback_t)(GB_gameboy_t *gb, bool bit_to_send);
|
||||||
typedef bool (*GB_serial_transfer_bit_end_callback_t)(GB_gameboy_t *gb);
|
typedef bool (*GB_serial_transfer_bit_end_callback_t)(GB_gameboy_t *gb);
|
||||||
typedef void (*GB_update_input_hint_callback_t)(GB_gameboy_t *gb);
|
typedef void (*GB_update_input_hint_callback_t)(GB_gameboy_t *gb);
|
||||||
@ -614,6 +614,8 @@ struct GB_gameboy_internal_s {
|
|||||||
bool vblank_just_occured; // For slow operations involving syscalls; these should only run once per vblank
|
bool vblank_just_occured; // For slow operations involving syscalls; these should only run once per vblank
|
||||||
uint8_t cycles_since_run; // How many cycles have passed since the last call to GB_run(), in 8MHz units
|
uint8_t cycles_since_run; // How many cycles have passed since the last call to GB_run(), in 8MHz units
|
||||||
double clock_multiplier;
|
double clock_multiplier;
|
||||||
|
uint32_t rumble_on_cycles;
|
||||||
|
uint32_t rumble_off_cycles;
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -478,9 +478,6 @@ static void write_mbc(GB_gameboy_t *gb, uint16_t addr, uint8_t value)
|
|||||||
if (gb->cartridge_type->has_rumble) {
|
if (gb->cartridge_type->has_rumble) {
|
||||||
if (!!(value & 8) != gb->rumble_state) {
|
if (!!(value & 8) != gb->rumble_state) {
|
||||||
gb->rumble_state = !gb->rumble_state;
|
gb->rumble_state = !gb->rumble_state;
|
||||||
if (gb->rumble_callback) {
|
|
||||||
gb->rumble_callback(gb, gb->rumble_state);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
value &= 7;
|
value &= 7;
|
||||||
}
|
}
|
||||||
|
@ -252,10 +252,6 @@ int GB_load_state(GB_gameboy_t *gb, const char *path)
|
|||||||
|
|
||||||
errno = 0;
|
errno = 0;
|
||||||
|
|
||||||
if (gb->cartridge_type->has_rumble && gb->rumble_callback) {
|
|
||||||
gb->rumble_callback(gb, gb->rumble_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (unsigned i = 0; i < 32; i++) {
|
for (unsigned i = 0; i < 32; i++) {
|
||||||
GB_palette_changed(gb, false, i * 2);
|
GB_palette_changed(gb, false, i * 2);
|
||||||
GB_palette_changed(gb, true, i * 2);
|
GB_palette_changed(gb, true, i * 2);
|
||||||
@ -357,10 +353,6 @@ int GB_load_state_from_buffer(GB_gameboy_t *gb, const uint8_t *buffer, size_t le
|
|||||||
|
|
||||||
memcpy(gb, &save, sizeof(save));
|
memcpy(gb, &save, sizeof(save));
|
||||||
|
|
||||||
if (gb->cartridge_type->has_rumble && gb->rumble_callback) {
|
|
||||||
gb->rumble_callback(gb, gb->rumble_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (unsigned i = 0; i < 32; i++) {
|
for (unsigned i = 0; i < 32; i++) {
|
||||||
GB_palette_changed(gb, false, i * 2);
|
GB_palette_changed(gb, false, i * 2);
|
||||||
GB_palette_changed(gb, true, i * 2);
|
GB_palette_changed(gb, true, i * 2);
|
||||||
|
@ -232,6 +232,14 @@ void GB_advance_cycles(GB_gameboy_t *gb, uint8_t cycles)
|
|||||||
gb->cycles_since_input_ir_change += cycles;
|
gb->cycles_since_input_ir_change += cycles;
|
||||||
gb->cycles_since_last_sync += cycles;
|
gb->cycles_since_last_sync += cycles;
|
||||||
gb->cycles_since_run += cycles;
|
gb->cycles_since_run += cycles;
|
||||||
|
|
||||||
|
if (gb->rumble_state) {
|
||||||
|
gb->rumble_on_cycles++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
gb->rumble_off_cycles++;
|
||||||
|
}
|
||||||
|
|
||||||
if (!gb->stopped) { // TODO: Verify what happens in STOP mode
|
if (!gb->stopped) { // TODO: Verify what happens in STOP mode
|
||||||
GB_dma_run(gb);
|
GB_dma_run(gb);
|
||||||
GB_hdma_run(gb);
|
GB_hdma_run(gb);
|
||||||
|
369
JoyKit/ControllerConfiguration.inc
Normal file
369
JoyKit/ControllerConfiguration.inc
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
#define BUTTON(x) @(JOYButtonUsageGeneric0 + (x))
|
||||||
|
#define AXIS(x) @(JOYAxisUsageGeneric0 + (x))
|
||||||
|
#define AXES2D(x) @(JOYAxes2DUsageGeneric0 + (x))
|
||||||
|
|
||||||
|
hacksByManufacturer = @{
|
||||||
|
@(0x045E): @{ // Microsoft
|
||||||
|
/* Generally untested, but Microsoft goes by the book when it comes to HID report descriptors, so
|
||||||
|
it should work out of the box. The hack is only here for automatic mapping */
|
||||||
|
|
||||||
|
JOYAxisGroups: @{
|
||||||
|
@(kHIDUsage_GD_X): @(0),
|
||||||
|
@(kHIDUsage_GD_Y): @(0),
|
||||||
|
@(kHIDUsage_GD_Z): @(2),
|
||||||
|
@(kHIDUsage_GD_Rx): @(1),
|
||||||
|
@(kHIDUsage_GD_Ry): @(1),
|
||||||
|
@(kHIDUsage_GD_Rz): @(3),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYButtonUsageMapping: @{
|
||||||
|
BUTTON(1): @(JOYButtonUsageA),
|
||||||
|
BUTTON(2): @(JOYButtonUsageB),
|
||||||
|
BUTTON(3): @(JOYButtonUsageX),
|
||||||
|
BUTTON(4): @(JOYButtonUsageY),
|
||||||
|
BUTTON(5): @(JOYButtonUsageL1),
|
||||||
|
BUTTON(6): @(JOYButtonUsageR1),
|
||||||
|
BUTTON(7): @(JOYButtonUsageLStick),
|
||||||
|
BUTTON(8): @(JOYButtonUsageRStick),
|
||||||
|
BUTTON(9): @(JOYButtonUsageStart),
|
||||||
|
BUTTON(10): @(JOYButtonUsageSelect),
|
||||||
|
BUTTON(11): @(JOYButtonUsageHome),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYAxisUsageMapping: @{
|
||||||
|
AXIS(3): @(JOYAxisUsageL1),
|
||||||
|
AXIS(6): @(JOYAxisUsageR1),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYAxes2DUsageMapping: @{
|
||||||
|
AXES2D(1): @(JOYAxes2DUsageLeftStick),
|
||||||
|
AXES2D(4): @(JOYAxes2DUsageRightStick),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
@(0x054C): @{ // Sony
|
||||||
|
/* Generally untested, but should work */
|
||||||
|
|
||||||
|
JOYAxisGroups: @{
|
||||||
|
@(kHIDUsage_GD_X): @(0),
|
||||||
|
@(kHIDUsage_GD_Y): @(0),
|
||||||
|
@(kHIDUsage_GD_Z): @(1),
|
||||||
|
@(kHIDUsage_GD_Rx): @(2),
|
||||||
|
@(kHIDUsage_GD_Ry): @(3),
|
||||||
|
@(kHIDUsage_GD_Rz): @(1),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYButtonUsageMapping: @{
|
||||||
|
BUTTON(1): @(JOYButtonUsageY),
|
||||||
|
BUTTON(2): @(JOYButtonUsageB),
|
||||||
|
BUTTON(3): @(JOYButtonUsageA),
|
||||||
|
BUTTON(4): @(JOYButtonUsageX),
|
||||||
|
BUTTON(5): @(JOYButtonUsageL1),
|
||||||
|
BUTTON(6): @(JOYButtonUsageR1),
|
||||||
|
BUTTON(7): @(JOYButtonUsageL2),
|
||||||
|
BUTTON(8): @(JOYButtonUsageR2),
|
||||||
|
BUTTON(9): @(JOYButtonUsageSelect),
|
||||||
|
BUTTON(10): @(JOYButtonUsageStart),
|
||||||
|
BUTTON(11): @(JOYButtonUsageLStick),
|
||||||
|
BUTTON(12): @(JOYButtonUsageRStick),
|
||||||
|
BUTTON(13): @(JOYButtonUsageHome),
|
||||||
|
BUTTON(14): @(JOYButtonUsageMisc),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYAxisUsageMapping: @{
|
||||||
|
AXIS(4): @(JOYAxisUsageL1),
|
||||||
|
AXIS(5): @(JOYAxisUsageR1),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYAxes2DUsageMapping: @{
|
||||||
|
AXES2D(1): @(JOYAxes2DUsageLeftStick),
|
||||||
|
AXES2D(4): @(JOYAxes2DUsageRightStick),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
hacksByName = @{
|
||||||
|
@"WUP-028": @{ // Nintendo GameCube Controller Adapter
|
||||||
|
JOYReportIDFilters: @[@[@1], @[@2], @[@3], @[@4]],
|
||||||
|
JOYButtonUsageMapping: @{
|
||||||
|
BUTTON(1): @(JOYButtonUsageA),
|
||||||
|
BUTTON(2): @(JOYButtonUsageB),
|
||||||
|
BUTTON(3): @(JOYButtonUsageX),
|
||||||
|
BUTTON(4): @(JOYButtonUsageY),
|
||||||
|
BUTTON(5): @(JOYButtonUsageStart),
|
||||||
|
BUTTON(6): @(JOYButtonUsageZ),
|
||||||
|
BUTTON(7): @(JOYButtonUsageR1),
|
||||||
|
BUTTON(8): @(JOYButtonUsageL1),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYAxisUsageMapping: @{
|
||||||
|
AXIS(3): @(JOYAxisUsageL1),
|
||||||
|
AXIS(6): @(JOYAxisUsageR1),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYAxes2DUsageMapping: @{
|
||||||
|
AXES2D(1): @(JOYAxes2DUsageLeftStick),
|
||||||
|
AXES2D(4): @(JOYAxes2DUsageRightStick),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYAxisGroups: @{
|
||||||
|
@(kHIDUsage_GD_X): @(0),
|
||||||
|
@(kHIDUsage_GD_Y): @(0),
|
||||||
|
@(kHIDUsage_GD_Z): @(2),
|
||||||
|
@(kHIDUsage_GD_Rx): @(1),
|
||||||
|
@(kHIDUsage_GD_Ry): @(1),
|
||||||
|
@(kHIDUsage_GD_Rz): @(3),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYRumbleUsage: @1,
|
||||||
|
JOYRumbleUsagePage: @0xFF00,
|
||||||
|
|
||||||
|
JOYConnectedUsage: @2,
|
||||||
|
JOYConnectedUsagePage: @0xFF00,
|
||||||
|
|
||||||
|
JOYSubElementStructs: @{
|
||||||
|
|
||||||
|
// Rumble
|
||||||
|
@(1364): @[
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@0, @"usagePage":@(0xFF00), @"usage":@1, @"min": @0, @"max": @1},
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@1, @"usagePage":@(0xFF00), @"usage":@1, @"min": @0, @"max": @1},
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@2, @"usagePage":@(0xFF00), @"usage":@1, @"min": @0, @"max": @1},
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@3, @"usagePage":@(0xFF00), @"usage":@1, @"min": @0, @"max": @1},
|
||||||
|
],
|
||||||
|
|
||||||
|
@(11): @[
|
||||||
|
|
||||||
|
// Player 1
|
||||||
|
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@4, @"usagePage":@(0xFF00), @"usage":@2, @"min": @0, @"max": @1},
|
||||||
|
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@8, @"usagePage":@(kHIDPage_Button), @"usage":@1},
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@9, @"usagePage":@(kHIDPage_Button), @"usage":@2},
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@10, @"usagePage":@(kHIDPage_Button), @"usage":@3},
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@11, @"usagePage":@(kHIDPage_Button), @"usage":@4},
|
||||||
|
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@12, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)},
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@13, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)},
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@14, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)},
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@15, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)},
|
||||||
|
|
||||||
|
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@16, @"usagePage":@(kHIDPage_Button), @"usage":@5},
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@17, @"usagePage":@(kHIDPage_Button), @"usage":@6},
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@18, @"usagePage":@(kHIDPage_Button), @"usage":@7},
|
||||||
|
@{@"reportID": @(1), @"size":@1, @"offset":@19, @"usagePage":@(kHIDPage_Button), @"usage":@8},
|
||||||
|
|
||||||
|
@{@"reportID": @(1), @"size":@8, @"offset":@24, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @255},
|
||||||
|
@{@"reportID": @(1), @"size":@8, @"offset":@32, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @255, @"max": @0},
|
||||||
|
|
||||||
|
@{@"reportID": @(1), @"size":@8, @"offset":@40, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255},
|
||||||
|
@{@"reportID": @(1), @"size":@8, @"offset":@48, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @255, @"max": @0},
|
||||||
|
|
||||||
|
@{@"reportID": @(1), @"size":@8, @"offset":@56, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Z), @"min": @0, @"max": @255},
|
||||||
|
@{@"reportID": @(1), @"size":@8, @"offset":@64, @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rz), @"min": @0, @"max": @255},
|
||||||
|
|
||||||
|
// Player 2
|
||||||
|
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@(4 + 72), @"usagePage":@(0xFF00), @"usage":@2, @"min": @0, @"max": @1},
|
||||||
|
|
||||||
|
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@(8 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@1},
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@(9 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@2},
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@(10 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@3},
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@(11 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@4},
|
||||||
|
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@(12 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)},
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@(13 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)},
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@(14 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)},
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@(15 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)},
|
||||||
|
|
||||||
|
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@(16 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@5},
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@(17 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@6},
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@(18 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@7},
|
||||||
|
@{@"reportID": @(2), @"size":@1, @"offset":@(19 + 72), @"usagePage":@(kHIDPage_Button), @"usage":@8},
|
||||||
|
|
||||||
|
@{@"reportID": @(2), @"size":@8, @"offset":@(24 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @255},
|
||||||
|
@{@"reportID": @(2), @"size":@8, @"offset":@(32 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @255, @"max": @0},
|
||||||
|
|
||||||
|
@{@"reportID": @(2), @"size":@8, @"offset":@(40 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255},
|
||||||
|
@{@"reportID": @(2), @"size":@8, @"offset":@(48 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @255, @"max": @0},
|
||||||
|
|
||||||
|
@{@"reportID": @(2), @"size":@8, @"offset":@(56 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Z), @"min": @0, @"max": @255},
|
||||||
|
@{@"reportID": @(2), @"size":@8, @"offset":@(64 + 72), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rz), @"min": @0, @"max": @255},
|
||||||
|
|
||||||
|
// Player 3
|
||||||
|
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@(4 + 144), @"usagePage":@(0xFF00), @"usage":@2, @"min": @0, @"max": @1},
|
||||||
|
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@(8 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@1},
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@(9 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@2},
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@(10 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@3},
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@(11 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@4},
|
||||||
|
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@(12 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)},
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@(13 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)},
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@(14 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)},
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@(15 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)},
|
||||||
|
|
||||||
|
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@(16 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@5},
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@(17 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@6},
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@(18 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@7},
|
||||||
|
@{@"reportID": @(3), @"size":@1, @"offset":@(19 + 144), @"usagePage":@(kHIDPage_Button), @"usage":@8},
|
||||||
|
|
||||||
|
@{@"reportID": @(3), @"size":@8, @"offset":@(24 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @255},
|
||||||
|
@{@"reportID": @(3), @"size":@8, @"offset":@(32 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @255, @"max": @0},
|
||||||
|
|
||||||
|
@{@"reportID": @(3), @"size":@8, @"offset":@(40 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255},
|
||||||
|
@{@"reportID": @(3), @"size":@8, @"offset":@(48 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @255, @"max": @0},
|
||||||
|
|
||||||
|
@{@"reportID": @(3), @"size":@8, @"offset":@(56 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Z), @"min": @0, @"max": @255},
|
||||||
|
@{@"reportID": @(3), @"size":@8, @"offset":@(64 + 144), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rz), @"min": @0, @"max": @255},
|
||||||
|
|
||||||
|
// Player 4
|
||||||
|
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@(4 + 216), @"usagePage":@(0xFF00), @"usage":@2, @"min": @0, @"max": @1},
|
||||||
|
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@(8 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@1},
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@(9 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@2},
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@(10 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@3},
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@(11 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@4},
|
||||||
|
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@(12 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadLeft)},
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@(13 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadRight)},
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@(14 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadDown)},
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@(15 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_DPadUp)},
|
||||||
|
|
||||||
|
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@(16 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@5},
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@(17 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@6},
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@(18 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@7},
|
||||||
|
@{@"reportID": @(4), @"size":@1, @"offset":@(19 + 216), @"usagePage":@(kHIDPage_Button), @"usage":@8},
|
||||||
|
|
||||||
|
@{@"reportID": @(4), @"size":@8, @"offset":@(24 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_X), @"min": @0, @"max": @255},
|
||||||
|
@{@"reportID": @(4), @"size":@8, @"offset":@(32 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Y), @"min": @255, @"max": @0},
|
||||||
|
|
||||||
|
@{@"reportID": @(4), @"size":@8, @"offset":@(40 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rx), @"min": @0, @"max": @255},
|
||||||
|
@{@"reportID": @(4), @"size":@8, @"offset":@(48 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Ry), @"min": @255, @"max": @0},
|
||||||
|
|
||||||
|
@{@"reportID": @(4), @"size":@8, @"offset":@(56 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Z), @"min": @0, @"max": @255},
|
||||||
|
@{@"reportID": @(4), @"size":@8, @"offset":@(64 + 216), @"usagePage":@(kHIDPage_GenericDesktop), @"usage":@(kHIDUsage_GD_Rz), @"min": @0, @"max": @255},
|
||||||
|
|
||||||
|
|
||||||
|
]},
|
||||||
|
},
|
||||||
|
|
||||||
|
@"GameCube Controller Adapter": @{ // GameCube Controller PC Adapter
|
||||||
|
JOYAxisGroups: @{
|
||||||
|
@(kHIDUsage_GD_X): @(0),
|
||||||
|
@(kHIDUsage_GD_Y): @(0),
|
||||||
|
@(kHIDUsage_GD_Z): @(1),
|
||||||
|
@(kHIDUsage_GD_Rx): @(2),
|
||||||
|
@(kHIDUsage_GD_Ry): @(3),
|
||||||
|
@(kHIDUsage_GD_Rz): @(1),
|
||||||
|
},
|
||||||
|
JOYReportIDFilters: @[@[@1], @[@2], @[@3], @[@4]],
|
||||||
|
JOYButtonUsageMapping: @{
|
||||||
|
BUTTON(1): @(JOYButtonUsageX),
|
||||||
|
BUTTON(2): @(JOYButtonUsageA),
|
||||||
|
BUTTON(3): @(JOYButtonUsageB),
|
||||||
|
BUTTON(4): @(JOYButtonUsageY),
|
||||||
|
BUTTON(5): @(JOYButtonUsageL1),
|
||||||
|
BUTTON(6): @(JOYButtonUsageR1),
|
||||||
|
BUTTON(8): @(JOYButtonUsageZ),
|
||||||
|
BUTTON(10): @(JOYButtonUsageStart),
|
||||||
|
BUTTON(13): @(JOYButtonUsageDPadUp),
|
||||||
|
BUTTON(14): @(JOYButtonUsageDPadRight),
|
||||||
|
BUTTON(15): @(JOYButtonUsageDPadDown),
|
||||||
|
BUTTON(16): @(JOYButtonUsageDPadLeft),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYAxisUsageMapping: @{
|
||||||
|
AXIS(4): @(JOYAxisUsageL1),
|
||||||
|
AXIS(5): @(JOYAxisUsageR1),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYAxes2DUsageMapping: @{
|
||||||
|
AXES2D(1): @(JOYAxes2DUsageLeftStick),
|
||||||
|
AXES2D(3): @(JOYAxes2DUsageRightStick),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYRumbleUsage: @1,
|
||||||
|
JOYRumbleUsagePage: @0xFF00,
|
||||||
|
JOYRumbleMin: @0,
|
||||||
|
JOYRumbleMax: @255,
|
||||||
|
JOYSwapZRz: @YES,
|
||||||
|
},
|
||||||
|
|
||||||
|
@"Twin USB Joystick": @{ // DualShock PC Adapter
|
||||||
|
JOYAxisGroups: @{
|
||||||
|
@(kHIDUsage_GD_X): @(0),
|
||||||
|
@(kHIDUsage_GD_Y): @(0),
|
||||||
|
@(kHIDUsage_GD_Z): @(1),
|
||||||
|
@(kHIDUsage_GD_Rx): @(2),
|
||||||
|
@(kHIDUsage_GD_Ry): @(2),
|
||||||
|
@(kHIDUsage_GD_Rz): @(1),
|
||||||
|
},
|
||||||
|
JOYReportIDFilters: @[@[@1], @[@2]],
|
||||||
|
JOYButtonUsageMapping: @{
|
||||||
|
BUTTON(1): @(JOYButtonUsageX),
|
||||||
|
BUTTON(2): @(JOYButtonUsageA),
|
||||||
|
BUTTON(3): @(JOYButtonUsageB),
|
||||||
|
BUTTON(4): @(JOYButtonUsageY),
|
||||||
|
BUTTON(5): @(JOYButtonUsageL2),
|
||||||
|
BUTTON(6): @(JOYButtonUsageR2),
|
||||||
|
BUTTON(7): @(JOYButtonUsageL1),
|
||||||
|
BUTTON(8): @(JOYButtonUsageR1),
|
||||||
|
BUTTON(9): @(JOYButtonUsageSelect),
|
||||||
|
BUTTON(10): @(JOYButtonUsageStart),
|
||||||
|
BUTTON(11): @(JOYButtonUsageLStick),
|
||||||
|
BUTTON(12): @(JOYButtonUsageRStick),
|
||||||
|
BUTTON(13): @(JOYButtonUsageDPadUp),
|
||||||
|
BUTTON(14): @(JOYButtonUsageDPadRight),
|
||||||
|
BUTTON(15): @(JOYButtonUsageDPadDown),
|
||||||
|
BUTTON(16): @(JOYButtonUsageDPadLeft),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYAxes2DUsageMapping: @{
|
||||||
|
AXES2D(1): @(JOYAxes2DUsageLeftStick),
|
||||||
|
AXES2D(6): @(JOYAxes2DUsageRightStick),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYSwapZRz: @YES,
|
||||||
|
},
|
||||||
|
|
||||||
|
@"Pro Controller": @{ // Switch Pro Controller
|
||||||
|
JOYIsSwitch: @YES,
|
||||||
|
JOYAxisGroups: @{
|
||||||
|
@(kHIDUsage_GD_X): @(0),
|
||||||
|
@(kHIDUsage_GD_Y): @(0),
|
||||||
|
@(kHIDUsage_GD_Z): @(0),
|
||||||
|
@(kHIDUsage_GD_Rx): @(1),
|
||||||
|
@(kHIDUsage_GD_Ry): @(1),
|
||||||
|
@(kHIDUsage_GD_Rz): @(1),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYButtonUsageMapping: @{
|
||||||
|
BUTTON(1): @(JOYButtonUsageB),
|
||||||
|
BUTTON(2): @(JOYButtonUsageA),
|
||||||
|
BUTTON(3): @(JOYButtonUsageY),
|
||||||
|
BUTTON(4): @(JOYButtonUsageX),
|
||||||
|
BUTTON(5): @(JOYButtonUsageL1),
|
||||||
|
BUTTON(6): @(JOYButtonUsageR1),
|
||||||
|
BUTTON(7): @(JOYButtonUsageL2),
|
||||||
|
BUTTON(8): @(JOYButtonUsageR2),
|
||||||
|
BUTTON(9): @(JOYButtonUsageSelect),
|
||||||
|
BUTTON(10): @(JOYButtonUsageStart),
|
||||||
|
BUTTON(11): @(JOYButtonUsageLStick),
|
||||||
|
BUTTON(12): @(JOYButtonUsageRStick),
|
||||||
|
BUTTON(13): @(JOYButtonUsageHome),
|
||||||
|
BUTTON(14): @(JOYButtonUsageMisc),
|
||||||
|
},
|
||||||
|
|
||||||
|
JOYAxes2DUsageMapping: @{
|
||||||
|
AXES2D(1): @(JOYAxes2DUsageLeftStick),
|
||||||
|
AXES2D(4): @(JOYAxes2DUsageRightStick),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
24
JoyKit/JOYAxes2D.h
Normal file
24
JoyKit/JOYAxes2D.h
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
JOYAxes2DUsageNone,
|
||||||
|
JOYAxes2DUsageLeftStick,
|
||||||
|
JOYAxes2DUsageRightStick,
|
||||||
|
JOYAxes2DUsageMiddleStick,
|
||||||
|
JOYAxes2DUsagePointer,
|
||||||
|
JOYAxes2DUsageNonGenericMax,
|
||||||
|
|
||||||
|
JOYAxes2DUsageGeneric0 = 0x10000,
|
||||||
|
} JOYAxes2DUsage;
|
||||||
|
|
||||||
|
@interface JOYAxes2D : NSObject
|
||||||
|
- (NSString *)usageString;
|
||||||
|
+ (NSString *)usageToString: (JOYAxes2DUsage) usage;
|
||||||
|
- (uint64_t)uniqueID;
|
||||||
|
- (double)distance;
|
||||||
|
- (double)angle;
|
||||||
|
- (NSPoint)value;
|
||||||
|
@property JOYAxes2DUsage usage;
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
168
JoyKit/JOYAxes2D.m
Normal file
168
JoyKit/JOYAxes2D.m
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
#import "JOYAxes2D.h"
|
||||||
|
#import "JOYElement.h"
|
||||||
|
|
||||||
|
@implementation JOYAxes2D
|
||||||
|
{
|
||||||
|
JOYElement *_element1, *_element2;
|
||||||
|
double _state1, _state2;
|
||||||
|
int32_t initialX, initialY;
|
||||||
|
int32_t minX, minY;
|
||||||
|
int32_t maxX, maxY;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (NSString *)usageToString: (JOYAxes2DUsage) usage
|
||||||
|
{
|
||||||
|
if (usage < JOYAxes2DUsageNonGenericMax) {
|
||||||
|
return (NSString *[]) {
|
||||||
|
@"None",
|
||||||
|
@"Left Stick",
|
||||||
|
@"Right Stick",
|
||||||
|
@"Middle Stick",
|
||||||
|
@"Pointer",
|
||||||
|
}[usage];
|
||||||
|
}
|
||||||
|
if (usage >= JOYAxes2DUsageGeneric0) {
|
||||||
|
return [NSString stringWithFormat:@"Generic 2D Analog Control %d", usage - JOYAxes2DUsageGeneric0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [NSString stringWithFormat:@"Unknown Usage 2D Axes %d", usage];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)usageString
|
||||||
|
{
|
||||||
|
return [self.class usageToString:_usage];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (uint64_t)uniqueID
|
||||||
|
{
|
||||||
|
return _element1.uniqueID;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)description
|
||||||
|
{
|
||||||
|
return [NSString stringWithFormat:@"<%@: %p, %@ (%llu); State: %.2f%%, %.2f degrees>", self.className, self, self.usageString, self.uniqueID, self.distance * 100, self.angle];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithFirstElement:(JOYElement *)element1 secondElement:(JOYElement *)element2
|
||||||
|
{
|
||||||
|
self = [super init];
|
||||||
|
if (!self) return self;
|
||||||
|
|
||||||
|
_element1 = element1;
|
||||||
|
_element2 = element2;
|
||||||
|
|
||||||
|
|
||||||
|
if (element1.usagePage == kHIDPage_GenericDesktop) {
|
||||||
|
uint16_t usage = element1.usage;
|
||||||
|
_usage = JOYAxes2DUsageGeneric0 + usage - kHIDUsage_GD_X + 1;
|
||||||
|
}
|
||||||
|
initialX = [_element1 value];
|
||||||
|
initialY = [_element2 value];
|
||||||
|
minX = element1.max;
|
||||||
|
minY = element2.max;
|
||||||
|
maxX = element1.min;
|
||||||
|
maxY = element2.min;
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSPoint)value
|
||||||
|
{
|
||||||
|
return NSMakePoint(_state1, _state2);
|
||||||
|
}
|
||||||
|
|
||||||
|
-(int32_t) effectiveMinX
|
||||||
|
{
|
||||||
|
int32_t rawMin = _element1.min;
|
||||||
|
int32_t rawMax = _element1.max;
|
||||||
|
if (initialX == 0) return rawMin;
|
||||||
|
if (minX <= (rawMin * 2 + initialX) / 3 && maxX >= (rawMax * 2 + initialX) / 3 ) return minX;
|
||||||
|
if ((initialX - rawMin) < (rawMax - initialX)) return rawMin;
|
||||||
|
return initialX - (rawMax - initialX);
|
||||||
|
}
|
||||||
|
|
||||||
|
-(int32_t) effectiveMinY
|
||||||
|
{
|
||||||
|
int32_t rawMin = _element2.min;
|
||||||
|
int32_t rawMax = _element2.max;
|
||||||
|
if (initialY == 0) return rawMin;
|
||||||
|
if (minX <= (rawMin * 2 + initialY) / 3 && maxY >= (rawMax * 2 + initialY) / 3 ) return minY;
|
||||||
|
if ((initialY - rawMin) < (rawMax - initialY)) return rawMin;
|
||||||
|
return initialY - (rawMax - initialY);
|
||||||
|
}
|
||||||
|
|
||||||
|
-(int32_t) effectiveMaxX
|
||||||
|
{
|
||||||
|
int32_t rawMin = _element1.min;
|
||||||
|
int32_t rawMax = _element1.max;
|
||||||
|
if (initialX == 0) return rawMax;
|
||||||
|
if (minX <= (rawMin * 2 + initialX) / 3 && maxX >= (rawMax * 2 + initialX) / 3 ) return maxX;
|
||||||
|
if ((initialX - rawMin) > (rawMax - initialX)) return rawMax;
|
||||||
|
return initialX + (initialX - rawMin);
|
||||||
|
}
|
||||||
|
|
||||||
|
-(int32_t) effectiveMaxY
|
||||||
|
{
|
||||||
|
int32_t rawMin = _element2.min;
|
||||||
|
int32_t rawMax = _element2.max;
|
||||||
|
if (initialY == 0) return rawMax;
|
||||||
|
if (minX <= (rawMin * 2 + initialY) / 3 && maxY >= (rawMax * 2 + initialY) / 3 ) return maxY;
|
||||||
|
if ((initialY - rawMin) > (rawMax - initialY)) return rawMax;
|
||||||
|
return initialY + (initialY - rawMin);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (bool)updateState
|
||||||
|
{
|
||||||
|
int32_t x = [_element1 value];
|
||||||
|
int32_t y = [_element2 value];
|
||||||
|
if (x == 0 && y == 0) return false;
|
||||||
|
|
||||||
|
if (initialX == 0 && initialY == 0) {
|
||||||
|
initialX = x;
|
||||||
|
initialY = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
double old1 = _state1, old2 = _state2;
|
||||||
|
{
|
||||||
|
double min = [self effectiveMinX];
|
||||||
|
double max = [self effectiveMaxX];
|
||||||
|
if (min == max) return false;
|
||||||
|
int32_t value = x;
|
||||||
|
|
||||||
|
if (initialX != 0) {
|
||||||
|
minX = MIN(value, minX);
|
||||||
|
maxX = MAX(value, maxX);
|
||||||
|
}
|
||||||
|
|
||||||
|
_state1 = (value - min) / (max - min) * 2 - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
double min = [self effectiveMinY];
|
||||||
|
double max = [self effectiveMaxY];
|
||||||
|
if (min == max) return false;
|
||||||
|
int32_t value = y;
|
||||||
|
|
||||||
|
if (initialY != 0) {
|
||||||
|
minY = MIN(value, minY);
|
||||||
|
maxY = MAX(value, maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
_state2 = (value - min) / (max - min) * 2 - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return old1 != _state1 || old2 != _state2;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (double)distance
|
||||||
|
{
|
||||||
|
return MIN(sqrt(_state1 * _state1 + _state2 * _state2), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (double)angle {
|
||||||
|
double temp = atan2(_state2, _state1) * 180 / M_PI;
|
||||||
|
if (temp >= 0) return temp;
|
||||||
|
return temp + 360;
|
||||||
|
}
|
||||||
|
@end
|
29
JoyKit/JOYAxis.h
Normal file
29
JoyKit/JOYAxis.h
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
JOYAxisUsageNone,
|
||||||
|
JOYAxisUsageL1,
|
||||||
|
JOYAxisUsageL2,
|
||||||
|
JOYAxisUsageL3,
|
||||||
|
JOYAxisUsageR1,
|
||||||
|
JOYAxisUsageR2,
|
||||||
|
JOYAxisUsageR3,
|
||||||
|
JOYAxisUsageWheel,
|
||||||
|
JOYAxisUsageRudder,
|
||||||
|
JOYAxisUsageThrottle,
|
||||||
|
JOYAxisUsageAccelerator,
|
||||||
|
JOYAxisUsageBrake,
|
||||||
|
JOYAxisUsageNonGenericMax,
|
||||||
|
|
||||||
|
JOYAxisUsageGeneric0 = 0x10000,
|
||||||
|
} JOYAxisUsage;
|
||||||
|
|
||||||
|
@interface JOYAxis : NSObject
|
||||||
|
- (NSString *)usageString;
|
||||||
|
+ (NSString *)usageToString: (JOYAxisUsage) usage;
|
||||||
|
- (uint64_t)uniqueID;
|
||||||
|
- (double)value;
|
||||||
|
@property JOYAxisUsage usage;
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
90
JoyKit/JOYAxis.m
Normal file
90
JoyKit/JOYAxis.m
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
#import "JOYAxis.h"
|
||||||
|
#import "JOYElement.h"
|
||||||
|
|
||||||
|
@implementation JOYAxis
|
||||||
|
{
|
||||||
|
JOYElement *_element;
|
||||||
|
double _state;
|
||||||
|
double _min;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (NSString *)usageToString: (JOYAxisUsage) usage
|
||||||
|
{
|
||||||
|
if (usage < JOYAxisUsageNonGenericMax) {
|
||||||
|
return (NSString *[]) {
|
||||||
|
@"None",
|
||||||
|
@"Analog L1",
|
||||||
|
@"Analog L2",
|
||||||
|
@"Analog L3",
|
||||||
|
@"Analog R1",
|
||||||
|
@"Analog R2",
|
||||||
|
@"Analog R3",
|
||||||
|
@"Wheel",
|
||||||
|
@"Rudder",
|
||||||
|
@"Throttle",
|
||||||
|
@"Accelerator",
|
||||||
|
@"Brake",
|
||||||
|
}[usage];
|
||||||
|
}
|
||||||
|
if (usage >= JOYAxisUsageGeneric0) {
|
||||||
|
return [NSString stringWithFormat:@"Generic Analog Control %d", usage - JOYAxisUsageGeneric0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [NSString stringWithFormat:@"Unknown Usage Axis %d", usage];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)usageString
|
||||||
|
{
|
||||||
|
return [self.class usageToString:_usage];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (uint64_t)uniqueID
|
||||||
|
{
|
||||||
|
return _element.uniqueID;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)description
|
||||||
|
{
|
||||||
|
return [NSString stringWithFormat:@"<%@: %p, %@ (%llu); State: %f%%>", self.className, self, self.usageString, self.uniqueID, _state * 100];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithElement:(JOYElement *)element
|
||||||
|
{
|
||||||
|
self = [super init];
|
||||||
|
if (!self) return self;
|
||||||
|
|
||||||
|
_element = element;
|
||||||
|
|
||||||
|
|
||||||
|
if (element.usagePage == kHIDPage_GenericDesktop) {
|
||||||
|
uint16_t usage = element.usage;
|
||||||
|
_usage = JOYAxisUsageGeneric0 + usage - kHIDUsage_GD_X + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_min = 1.0;
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (double) value
|
||||||
|
{
|
||||||
|
return _state;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (bool)updateState
|
||||||
|
{
|
||||||
|
double min = _element.min;
|
||||||
|
double max = _element.max;
|
||||||
|
if (min == max) return false;
|
||||||
|
double old = _state;
|
||||||
|
double unnormalized = ([_element value] - min) / (max - min);
|
||||||
|
if (unnormalized < _min) {
|
||||||
|
_min = unnormalized;
|
||||||
|
}
|
||||||
|
if (_min != 1) {
|
||||||
|
_state = (unnormalized - _min) / (1 - _min);
|
||||||
|
}
|
||||||
|
return old != _state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
42
JoyKit/JOYButton.h
Normal file
42
JoyKit/JOYButton.h
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
JOYButtonUsageNone,
|
||||||
|
JOYButtonUsageA,
|
||||||
|
JOYButtonUsageB,
|
||||||
|
JOYButtonUsageC,
|
||||||
|
JOYButtonUsageX,
|
||||||
|
JOYButtonUsageY,
|
||||||
|
JOYButtonUsageZ,
|
||||||
|
JOYButtonUsageStart,
|
||||||
|
JOYButtonUsageSelect,
|
||||||
|
JOYButtonUsageHome,
|
||||||
|
JOYButtonUsageMisc,
|
||||||
|
JOYButtonUsageLStick,
|
||||||
|
JOYButtonUsageRStick,
|
||||||
|
JOYButtonUsageL1,
|
||||||
|
JOYButtonUsageL2,
|
||||||
|
JOYButtonUsageL3,
|
||||||
|
JOYButtonUsageR1,
|
||||||
|
JOYButtonUsageR2,
|
||||||
|
JOYButtonUsageR3,
|
||||||
|
JOYButtonUsageDPadLeft,
|
||||||
|
JOYButtonUsageDPadRight,
|
||||||
|
JOYButtonUsageDPadUp,
|
||||||
|
JOYButtonUsageDPadDown,
|
||||||
|
JOYButtonUsageNonGenericMax,
|
||||||
|
|
||||||
|
JOYButtonUsageGeneric0 = 0x10000,
|
||||||
|
} JOYButtonUsage;
|
||||||
|
|
||||||
|
@interface JOYButton : NSObject
|
||||||
|
- (NSString *)usageString;
|
||||||
|
+ (NSString *)usageToString: (JOYButtonUsage) usage;
|
||||||
|
- (uint64_t)uniqueID;
|
||||||
|
- (bool) isPressed;
|
||||||
|
@property JOYButtonUsage usage;
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
102
JoyKit/JOYButton.m
Normal file
102
JoyKit/JOYButton.m
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
#import "JOYButton.h"
|
||||||
|
#import "JOYElement.h"
|
||||||
|
|
||||||
|
@implementation JOYButton
|
||||||
|
{
|
||||||
|
JOYElement *_element;
|
||||||
|
bool _state;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (NSString *)usageToString: (JOYButtonUsage) usage
|
||||||
|
{
|
||||||
|
if (usage < JOYButtonUsageNonGenericMax) {
|
||||||
|
return (NSString *[]) {
|
||||||
|
@"None",
|
||||||
|
@"A",
|
||||||
|
@"B",
|
||||||
|
@"C",
|
||||||
|
@"X",
|
||||||
|
@"Y",
|
||||||
|
@"Z",
|
||||||
|
@"Start",
|
||||||
|
@"Select",
|
||||||
|
@"Home",
|
||||||
|
@"Misc",
|
||||||
|
@"Left Stick",
|
||||||
|
@"Right Stick",
|
||||||
|
@"L1",
|
||||||
|
@"L2",
|
||||||
|
@"L3",
|
||||||
|
@"R1",
|
||||||
|
@"R2",
|
||||||
|
@"R3",
|
||||||
|
@"D-Pad Left",
|
||||||
|
@"D-Pad Right",
|
||||||
|
@"D-Pad Up",
|
||||||
|
@"D-Pad Down",
|
||||||
|
}[usage];
|
||||||
|
}
|
||||||
|
if (usage >= JOYButtonUsageGeneric0) {
|
||||||
|
return [NSString stringWithFormat:@"Generic Button %d", usage - JOYButtonUsageGeneric0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [NSString stringWithFormat:@"Unknown Usage Button %d", usage];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)usageString
|
||||||
|
{
|
||||||
|
return [self.class usageToString:_usage];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (uint64_t)uniqueID
|
||||||
|
{
|
||||||
|
return _element.uniqueID;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)description
|
||||||
|
{
|
||||||
|
return [NSString stringWithFormat:@"<%@: %p, %@ (%llu); State: %s>", self.className, self, self.usageString, self.uniqueID, _state? "Presssed" : "Released"];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithElement:(JOYElement *)element
|
||||||
|
{
|
||||||
|
self = [super init];
|
||||||
|
if (!self) return self;
|
||||||
|
|
||||||
|
_element = element;
|
||||||
|
|
||||||
|
if (element.usagePage == kHIDPage_Button) {
|
||||||
|
uint16_t usage = element.usage;
|
||||||
|
_usage = JOYButtonUsageGeneric0 + usage;
|
||||||
|
}
|
||||||
|
else if (element.usagePage == kHIDPage_GenericDesktop) {
|
||||||
|
switch (element.usage) {
|
||||||
|
case kHIDUsage_GD_DPadUp: _usage = JOYButtonUsageDPadUp; break;
|
||||||
|
case kHIDUsage_GD_DPadDown: _usage = JOYButtonUsageDPadDown; break;
|
||||||
|
case kHIDUsage_GD_DPadRight: _usage = JOYButtonUsageDPadRight; break;
|
||||||
|
case kHIDUsage_GD_DPadLeft: _usage = JOYButtonUsageDPadLeft; break;
|
||||||
|
case kHIDUsage_GD_Start: _usage = JOYButtonUsageStart; break;
|
||||||
|
case kHIDUsage_GD_Select: _usage = JOYButtonUsageSelect; break;
|
||||||
|
case kHIDUsage_GD_SystemMainMenu: _usage = JOYButtonUsageHome; break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (bool) isPressed
|
||||||
|
{
|
||||||
|
return _state;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (bool)updateState
|
||||||
|
{
|
||||||
|
bool state = [_element value];
|
||||||
|
if (_state != state) {
|
||||||
|
_state = state;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
41
JoyKit/JOYController.h
Normal file
41
JoyKit/JOYController.h
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import "JOYButton.h"
|
||||||
|
#import "JOYAxis.h"
|
||||||
|
#import "JOYAxes2D.h"
|
||||||
|
#import "JOYHat.h"
|
||||||
|
|
||||||
|
static NSString const *JOYAxesEmulateButtonsKey = @"JOYAxesEmulateButtons";
|
||||||
|
static NSString const *JOYAxes2DEmulateButtonsKey = @"JOYAxes2DEmulateButtons";
|
||||||
|
static NSString const *JOYHatsEmulateButtonsKey = @"JOYHatsEmulateButtons";
|
||||||
|
|
||||||
|
@class JOYController;
|
||||||
|
|
||||||
|
@protocol JOYListener <NSObject>
|
||||||
|
|
||||||
|
@optional
|
||||||
|
-(void) controllerConnected:(JOYController *)controller;
|
||||||
|
-(void) controllerDisconnected:(JOYController *)controller;
|
||||||
|
-(void) controller:(JOYController *)controller buttonChangedState:(JOYButton *)button;
|
||||||
|
-(void) controller:(JOYController *)controller movedAxis:(JOYAxis *)axis;
|
||||||
|
-(void) controller:(JOYController *)controller movedAxes2D:(JOYAxes2D *)axes;
|
||||||
|
-(void) controller:(JOYController *)controller movedHat:(JOYHat *)hat;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface JOYController : NSObject
|
||||||
|
+ (void)startOnRunLoop:(NSRunLoop *)runloop withOptions: (NSDictionary *)options;
|
||||||
|
+ (NSArray<JOYController *> *) allControllers;
|
||||||
|
+ (void) registerListener:(id<JOYListener>)listener;
|
||||||
|
+ (void) unregisterListener:(id<JOYListener>)listener;
|
||||||
|
- (NSString *)deviceName;
|
||||||
|
- (NSString *)uniqueID;
|
||||||
|
- (NSArray<JOYButton *> *) buttons;
|
||||||
|
- (NSArray<JOYAxis *> *) axes;
|
||||||
|
- (NSArray<JOYAxes2D *> *) axes2D;
|
||||||
|
- (NSArray<JOYHat *> *) hats;
|
||||||
|
- (void)setRumbleAmplitude:(double)amp;
|
||||||
|
- (void)setPlayerLEDs:(uint8_t)mask;
|
||||||
|
@property (readonly, getter=isConnected) bool connected;
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
760
JoyKit/JOYController.m
Normal file
760
JoyKit/JOYController.m
Normal file
@ -0,0 +1,760 @@
|
|||||||
|
#import "JOYController.h"
|
||||||
|
#import "JOYMultiplayerController.h"
|
||||||
|
#import "JOYElement.h"
|
||||||
|
#import "JOYSubElement.h"
|
||||||
|
#import "JOYEmulatedButton.h"
|
||||||
|
#include <IOKit/hid/IOHIDLib.h>
|
||||||
|
|
||||||
|
static NSString const *JOYAxisGroups = @"JOYAxisGroups";
|
||||||
|
static NSString const *JOYReportIDFilters = @"JOYReportIDFilters";
|
||||||
|
static NSString const *JOYButtonUsageMapping = @"JOYButtonUsageMapping";
|
||||||
|
static NSString const *JOYAxisUsageMapping = @"JOYAxisUsageMapping";
|
||||||
|
static NSString const *JOYAxes2DUsageMapping = @"JOYAxes2DUsageMapping";
|
||||||
|
static NSString const *JOYSubElementStructs = @"JOYSubElementStructs";
|
||||||
|
static NSString const *JOYIsSwitch = @"JOYIsSwitch";
|
||||||
|
static NSString const *JOYRumbleUsage = @"JOYRumbleUsage";
|
||||||
|
static NSString const *JOYRumbleUsagePage = @"JOYRumbleUsagePage";
|
||||||
|
static NSString const *JOYConnectedUsage = @"JOYConnectedUsage";
|
||||||
|
static NSString const *JOYConnectedUsagePage = @"JOYConnectedUsagePage";
|
||||||
|
static NSString const *JOYRumbleMin = @"JOYRumbleMin";
|
||||||
|
static NSString const *JOYRumbleMax = @"JOYRumbleMax";
|
||||||
|
static NSString const *JOYSwapZRz = @"JOYSwapZRz";
|
||||||
|
|
||||||
|
|
||||||
|
static NSMutableDictionary<id, JOYController *> *controllers; // Physical controllers
|
||||||
|
static NSMutableArray<JOYController *> *exposedControllers; // Logical controllers
|
||||||
|
|
||||||
|
static NSDictionary *hacksByName = nil;
|
||||||
|
static NSDictionary *hacksByManufacturer = nil;
|
||||||
|
|
||||||
|
static NSMutableSet<id<JOYListener>> *listeners = nil;
|
||||||
|
|
||||||
|
static bool axesEmulateButtons = false;
|
||||||
|
static bool axes2DEmulateButtons = false;
|
||||||
|
static bool hatsEmulateButtons = false;
|
||||||
|
|
||||||
|
@interface JOYController ()
|
||||||
|
+ (void)controllerAdded:(IOHIDDeviceRef) device;
|
||||||
|
+ (void)controllerRemoved:(IOHIDDeviceRef) device;
|
||||||
|
- (void)elementChanged:(IOHIDElementRef) element;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface JOYButton ()
|
||||||
|
- (instancetype)initWithElement:(JOYElement *)element;
|
||||||
|
- (bool)updateState;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface JOYAxis ()
|
||||||
|
- (instancetype)initWithElement:(JOYElement *)element;
|
||||||
|
- (bool)updateState;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface JOYHat ()
|
||||||
|
- (instancetype)initWithElement:(JOYElement *)element;
|
||||||
|
- (bool)updateState;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@interface JOYAxes2D ()
|
||||||
|
- (instancetype)initWithFirstElement:(JOYElement *)element1 secondElement:(JOYElement *)element2;
|
||||||
|
- (bool)updateState;
|
||||||
|
@end
|
||||||
|
|
||||||
|
static NSDictionary *CreateHIDDeviceMatchDictionary(const UInt32 page, const UInt32 usage)
|
||||||
|
{
|
||||||
|
return @{
|
||||||
|
@kIOHIDDeviceUsagePageKey: @(page),
|
||||||
|
@kIOHIDDeviceUsageKey: @(usage),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static void HIDDeviceAdded(void *context, IOReturn result, void *sender, IOHIDDeviceRef device)
|
||||||
|
{
|
||||||
|
[JOYController controllerAdded:device];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void HIDDeviceRemoved(void *context, IOReturn result, void *sender, IOHIDDeviceRef device)
|
||||||
|
{
|
||||||
|
[JOYController controllerRemoved:device];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void HIDInput(void *context, IOReturn result, void *sender, IOHIDValueRef value)
|
||||||
|
{
|
||||||
|
[(__bridge JOYController *)context elementChanged:IOHIDValueGetElement(value)];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct __attribute__((packed)) {
|
||||||
|
uint8_t reportID;
|
||||||
|
uint8_t sequence;
|
||||||
|
uint8_t rumbleData[8];
|
||||||
|
uint8_t command;
|
||||||
|
uint8_t commandData[26];
|
||||||
|
} JOYSwitchPacket;
|
||||||
|
|
||||||
|
@implementation JOYController
|
||||||
|
{
|
||||||
|
IOHIDDeviceRef _device;
|
||||||
|
NSMutableDictionary<JOYElement *, JOYButton *> *_buttons;
|
||||||
|
NSMutableDictionary<JOYElement *, JOYAxis *> *_axes;
|
||||||
|
NSMutableDictionary<JOYElement *, JOYAxes2D *> *_axes2D;
|
||||||
|
NSMutableDictionary<JOYElement *, JOYHat *> *_hats;
|
||||||
|
NSMutableDictionary<JOYElement *, NSArray<JOYElement *> *> *_multiElements;
|
||||||
|
|
||||||
|
// Button emulation
|
||||||
|
NSMutableDictionary<NSNumber *, JOYEmulatedButton *> *_axisEmulatedButtons;
|
||||||
|
NSMutableDictionary<NSNumber *, NSArray <JOYEmulatedButton *> *> *_axes2DEmulatedButtons;
|
||||||
|
NSMutableDictionary<NSNumber *, NSArray <JOYEmulatedButton *> *> *_hatEmulatedButtons;
|
||||||
|
|
||||||
|
JOYElement *_rumbleElement;
|
||||||
|
JOYElement *_connectedElement;
|
||||||
|
NSMutableDictionary<NSValue *, JOYElement *> *_iokitToJOY;
|
||||||
|
NSString *_serialSuffix;
|
||||||
|
bool _isSwitch; // Does thie controller use the Switch protocol?
|
||||||
|
JOYSwitchPacket _lastSwitchPacket;
|
||||||
|
NSCondition *_rumblePWMThreadLock;
|
||||||
|
volatile double _rumblePWMRatio;
|
||||||
|
bool _physicallyConnected;
|
||||||
|
bool _logicallyConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithDevice:(IOHIDDeviceRef) device
|
||||||
|
{
|
||||||
|
return [self initWithDevice:device reportIDFilter:nil serialSuffix:nil];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithDevice:(IOHIDDeviceRef)device reportIDFilter:(NSArray <NSNumber *> *) filter serialSuffix:(NSString *)suffix
|
||||||
|
{
|
||||||
|
self = [super init];
|
||||||
|
if (!self) return self;
|
||||||
|
|
||||||
|
_physicallyConnected = true;
|
||||||
|
_logicallyConnected = true;
|
||||||
|
_device = device;
|
||||||
|
_serialSuffix = suffix;
|
||||||
|
|
||||||
|
IOHIDDeviceRegisterInputValueCallback(device, HIDInput, (void *)self);
|
||||||
|
IOHIDDeviceScheduleWithRunLoop(device, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
|
||||||
|
|
||||||
|
NSArray *array = CFBridgingRelease(IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone));
|
||||||
|
_buttons = [NSMutableDictionary dictionary];
|
||||||
|
_axes = [NSMutableDictionary dictionary];
|
||||||
|
_axes2D = [NSMutableDictionary dictionary];
|
||||||
|
_hats = [NSMutableDictionary dictionary];
|
||||||
|
_axisEmulatedButtons = [NSMutableDictionary dictionary];
|
||||||
|
_axes2DEmulatedButtons = [NSMutableDictionary dictionary];
|
||||||
|
_hatEmulatedButtons = [NSMutableDictionary dictionary];
|
||||||
|
_multiElements = [NSMutableDictionary dictionary];
|
||||||
|
_iokitToJOY = [NSMutableDictionary dictionary];
|
||||||
|
_rumblePWMThreadLock = [[NSCondition alloc] init];
|
||||||
|
|
||||||
|
|
||||||
|
//NSMutableArray *axes3d = [NSMutableArray array];
|
||||||
|
|
||||||
|
NSDictionary *axisGroups = @{
|
||||||
|
@(kHIDUsage_GD_X): @(0),
|
||||||
|
@(kHIDUsage_GD_Y): @(0),
|
||||||
|
@(kHIDUsage_GD_Z): @(1),
|
||||||
|
@(kHIDUsage_GD_Rx): @(2),
|
||||||
|
@(kHIDUsage_GD_Ry): @(2),
|
||||||
|
@(kHIDUsage_GD_Rz): @(1),
|
||||||
|
};
|
||||||
|
NSString *name = (__bridge NSString *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey));
|
||||||
|
NSDictionary *hacks = hacksByName[name];
|
||||||
|
if (!hacks) {
|
||||||
|
hacks = hacksByManufacturer[(__bridge NSNumber *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey))];
|
||||||
|
}
|
||||||
|
axisGroups = hacks[JOYAxisGroups] ?: axisGroups;
|
||||||
|
_isSwitch = [hacks[JOYIsSwitch] boolValue];
|
||||||
|
uint16_t rumbleUsagePage = (uint16_t)[hacks[JOYRumbleUsagePage] unsignedIntValue];
|
||||||
|
uint16_t rumbleUsage = (uint16_t)[hacks[JOYRumbleUsage] unsignedIntValue];
|
||||||
|
uint16_t connectedUsagePage = (uint16_t)[hacks[JOYConnectedUsagePage] unsignedIntValue];
|
||||||
|
uint16_t connectedUsage = (uint16_t)[hacks[JOYConnectedUsage] unsignedIntValue];
|
||||||
|
|
||||||
|
JOYElement *previousAxisElement = nil;
|
||||||
|
id previous = nil;
|
||||||
|
for (id _element in array) {
|
||||||
|
if (_element == previous) continue; // Some elements are reported twice for some reason
|
||||||
|
previous = _element;
|
||||||
|
NSArray *elements = nil;
|
||||||
|
JOYElement *element = [[JOYElement alloc] initWithElement:(__bridge IOHIDElementRef)_element];
|
||||||
|
|
||||||
|
NSArray<NSDictionary <NSString *,NSNumber *>*> *subElementDefs = hacks[JOYSubElementStructs][@(element.uniqueID)];
|
||||||
|
|
||||||
|
bool isOutput = false;
|
||||||
|
if (subElementDefs && element.uniqueID != element.parentID) {
|
||||||
|
elements = [NSMutableArray array];
|
||||||
|
for (NSDictionary<NSString *,NSNumber *> *virtualInput in subElementDefs) {
|
||||||
|
if (filter && virtualInput[@"reportID"] && ![filter containsObject:virtualInput[@"reportID"]]) continue;
|
||||||
|
[(NSMutableArray *)elements addObject:[[JOYSubElement alloc] initWithRealElement:element
|
||||||
|
size:virtualInput[@"size"].unsignedLongValue
|
||||||
|
offset:virtualInput[@"offset"].unsignedLongValue
|
||||||
|
usagePage:virtualInput[@"usagePage"].unsignedLongValue
|
||||||
|
usage:virtualInput[@"usage"].unsignedLongValue
|
||||||
|
min:virtualInput[@"min"].unsignedIntValue
|
||||||
|
max:virtualInput[@"max"].unsignedIntValue]];
|
||||||
|
}
|
||||||
|
isOutput = IOHIDElementGetType((__bridge IOHIDElementRef)_element) == kIOHIDElementTypeOutput;
|
||||||
|
[_multiElements setObject:elements forKey:element];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (filter && ![filter containsObject:@(element.reportID)]) continue;
|
||||||
|
|
||||||
|
switch (IOHIDElementGetType((__bridge IOHIDElementRef)_element)) {
|
||||||
|
/* Handled */
|
||||||
|
case kIOHIDElementTypeInput_Misc:
|
||||||
|
case kIOHIDElementTypeInput_Button:
|
||||||
|
case kIOHIDElementTypeInput_Axis:
|
||||||
|
break;
|
||||||
|
case kIOHIDElementTypeOutput:
|
||||||
|
isOutput = true;
|
||||||
|
break;
|
||||||
|
/* Ignored */
|
||||||
|
default:
|
||||||
|
case kIOHIDElementTypeInput_ScanCodes:
|
||||||
|
case kIOHIDElementTypeInput_NULL:
|
||||||
|
case kIOHIDElementTypeFeature:
|
||||||
|
case kIOHIDElementTypeCollection:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (IOHIDElementIsArray((__bridge IOHIDElementRef)_element)) continue;
|
||||||
|
|
||||||
|
elements = @[element];
|
||||||
|
}
|
||||||
|
|
||||||
|
_iokitToJOY[@(IOHIDElementGetCookie((__bridge IOHIDElementRef)_element))] = element;
|
||||||
|
|
||||||
|
for (JOYElement *element in elements) {
|
||||||
|
if (isOutput) {
|
||||||
|
if (!_rumbleElement && rumbleUsage && rumbleUsagePage && element.usage == rumbleUsage && element.usagePage == rumbleUsagePage) {
|
||||||
|
if (hacks[JOYRumbleMin]) {
|
||||||
|
element.min = [hacks[JOYRumbleMin] unsignedIntValue];
|
||||||
|
}
|
||||||
|
if (hacks[JOYRumbleMax]) {
|
||||||
|
element.max = [hacks[JOYRumbleMax] unsignedIntValue];
|
||||||
|
}
|
||||||
|
_rumbleElement = element;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!_connectedElement && connectedUsage && connectedUsagePage && element.usage == connectedUsage && element.usagePage == connectedUsagePage) {
|
||||||
|
_connectedElement = element;
|
||||||
|
_logicallyConnected = element.value != element.min;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.usagePage == kHIDPage_Button) {
|
||||||
|
button: {
|
||||||
|
JOYButton *button = [[JOYButton alloc] initWithElement: element];
|
||||||
|
[_buttons setObject:button forKey:element];
|
||||||
|
NSNumber *replacementUsage = hacks[JOYButtonUsageMapping][@(button.usage)];
|
||||||
|
if (replacementUsage) {
|
||||||
|
button.usage = [replacementUsage unsignedIntValue];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (element.usagePage == kHIDPage_GenericDesktop) {
|
||||||
|
switch (element.usage) {
|
||||||
|
case kHIDUsage_GD_X:
|
||||||
|
case kHIDUsage_GD_Y:
|
||||||
|
case kHIDUsage_GD_Z:
|
||||||
|
case kHIDUsage_GD_Rx:
|
||||||
|
case kHIDUsage_GD_Ry:
|
||||||
|
case kHIDUsage_GD_Rz: {
|
||||||
|
|
||||||
|
JOYElement *other = previousAxisElement;
|
||||||
|
previousAxisElement = element;
|
||||||
|
if (!other) goto single;
|
||||||
|
if (other.usage >= element.usage) goto single;
|
||||||
|
if (other.reportID != element.reportID) goto single;
|
||||||
|
if (![axisGroups[@(other.usage)] isEqualTo: axisGroups[@(element.usage)]]) goto single;
|
||||||
|
if (other.parentID != element.parentID) goto single;
|
||||||
|
|
||||||
|
JOYAxes2D *axes = nil;
|
||||||
|
if (other.usage == kHIDUsage_GD_Z && element.usage == kHIDUsage_GD_Rz && [hacks[JOYSwapZRz] boolValue]) {
|
||||||
|
axes = [[JOYAxes2D alloc] initWithFirstElement:element secondElement:other];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
axes = [[JOYAxes2D alloc] initWithFirstElement:other secondElement:element];
|
||||||
|
}
|
||||||
|
NSNumber *replacementUsage = hacks[JOYAxes2DUsageMapping][@(axes.usage)];
|
||||||
|
if (replacementUsage) {
|
||||||
|
axes.usage = [replacementUsage unsignedIntValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
[_axisEmulatedButtons removeObjectForKey:@(_axes[other].uniqueID)];
|
||||||
|
[_axes removeObjectForKey:other];
|
||||||
|
previousAxisElement = nil;
|
||||||
|
_axes2D[other] = axes;
|
||||||
|
_axes2D[element] = axes;
|
||||||
|
|
||||||
|
if (axes2DEmulateButtons) {
|
||||||
|
_axes2DEmulatedButtons[@(axes.uniqueID)] = @[
|
||||||
|
[[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadLeft uniqueID:axes.uniqueID | 0x100000000L],
|
||||||
|
[[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadRight uniqueID:axes.uniqueID | 0x200000000L],
|
||||||
|
[[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadUp uniqueID:axes.uniqueID | 0x300000000L],
|
||||||
|
[[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadDown uniqueID:axes.uniqueID | 0x400000000L],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
for (NSArray *group in axes2d) {
|
||||||
|
break;
|
||||||
|
IOHIDElementRef first = (__bridge IOHIDElementRef)group[0];
|
||||||
|
IOHIDElementRef second = (__bridge IOHIDElementRef)group[1];
|
||||||
|
if (IOHIDElementGetUsage(first) > element.usage) continue;
|
||||||
|
if (IOHIDElementGetUsage(second) > element.usage) continue;
|
||||||
|
if (IOHIDElementGetReportID(first) != IOHIDElementGetReportID(element)) continue;
|
||||||
|
if ((IOHIDElementGetUsage(first) - kHIDUsage_GD_X) / 3 != (element.usage - kHIDUsage_GD_X) / 3) continue;
|
||||||
|
if (IOHIDElementGetParent(first) != IOHIDElementGetParent(element)) continue;
|
||||||
|
|
||||||
|
[axes2d removeObject:group];
|
||||||
|
[axes3d addObject:@[(__bridge id)first, (__bridge id)second, _element]];
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}*/
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
single:
|
||||||
|
case kHIDUsage_GD_Slider:
|
||||||
|
case kHIDUsage_GD_Dial:
|
||||||
|
case kHIDUsage_GD_Wheel: {
|
||||||
|
JOYAxis *axis = [[JOYAxis alloc] initWithElement: element];
|
||||||
|
[_axes setObject:axis forKey:element];
|
||||||
|
|
||||||
|
NSNumber *replacementUsage = hacks[JOYAxisUsageMapping][@(axis.usage)];
|
||||||
|
if (replacementUsage) {
|
||||||
|
axis.usage = [replacementUsage unsignedIntValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (axesEmulateButtons && axis.usage >= JOYAxisUsageL1 && axis.usage <= JOYAxisUsageR3) {
|
||||||
|
_axisEmulatedButtons[@(axis.uniqueID)] =
|
||||||
|
[[JOYEmulatedButton alloc] initWithUsage:axis.usage - JOYAxisUsageL1 + JOYButtonUsageL1 uniqueID:axis.uniqueID];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (axesEmulateButtons && axis.usage >= JOYAxisUsageGeneric0) {
|
||||||
|
_axisEmulatedButtons[@(axis.uniqueID)] =
|
||||||
|
[[JOYEmulatedButton alloc] initWithUsage:axis.usage - JOYAxisUsageGeneric0 + JOYButtonUsageGeneric0 uniqueID:axis.uniqueID];
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case kHIDUsage_GD_DPadUp:
|
||||||
|
case kHIDUsage_GD_DPadDown:
|
||||||
|
case kHIDUsage_GD_DPadRight:
|
||||||
|
case kHIDUsage_GD_DPadLeft:
|
||||||
|
case kHIDUsage_GD_Start:
|
||||||
|
case kHIDUsage_GD_Select:
|
||||||
|
case kHIDUsage_GD_SystemMainMenu:
|
||||||
|
goto button;
|
||||||
|
|
||||||
|
case kHIDUsage_GD_Hatswitch: {
|
||||||
|
JOYHat *hat = [[JOYHat alloc] initWithElement: element];
|
||||||
|
[_hats setObject:hat forKey:element];
|
||||||
|
if (hatsEmulateButtons) {
|
||||||
|
_hatEmulatedButtons[@(hat.uniqueID)] = @[
|
||||||
|
[[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadLeft uniqueID:hat.uniqueID | 0x100000000L],
|
||||||
|
[[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadRight uniqueID:hat.uniqueID | 0x200000000L],
|
||||||
|
[[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadUp uniqueID:hat.uniqueID | 0x300000000L],
|
||||||
|
[[JOYEmulatedButton alloc] initWithUsage:JOYButtonUsageDPadDown uniqueID:hat.uniqueID | 0x400000000L],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[exposedControllers addObject:self];
|
||||||
|
if (_logicallyConnected) {
|
||||||
|
for (id<JOYListener> listener in listeners) {
|
||||||
|
if ([listener respondsToSelector:@selector(controllerConnected:)]) {
|
||||||
|
[listener controllerConnected:self];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)deviceName
|
||||||
|
{
|
||||||
|
return IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDProductKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)uniqueID
|
||||||
|
{
|
||||||
|
NSString *serial = (__bridge NSString *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDSerialNumberKey));
|
||||||
|
if (!serial || [(__bridge NSString *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDTransportKey)) isEqualToString:@"USB"]) {
|
||||||
|
serial = [NSString stringWithFormat:@"%04x%04x%08x",
|
||||||
|
[(__bridge NSNumber *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDVendorIDKey)) unsignedIntValue],
|
||||||
|
[(__bridge NSNumber *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDProductIDKey)) unsignedIntValue],
|
||||||
|
[(__bridge NSNumber *)IOHIDDeviceGetProperty(_device, CFSTR(kIOHIDLocationIDKey)) unsignedIntValue]];
|
||||||
|
}
|
||||||
|
if (_serialSuffix) {
|
||||||
|
return [NSString stringWithFormat:@"%@-%@", serial, _serialSuffix];
|
||||||
|
}
|
||||||
|
return serial;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)description
|
||||||
|
{
|
||||||
|
return [NSString stringWithFormat:@"<%@: %p, %@, %@>", self.className, self, self.deviceName, self.uniqueID];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<JOYButton *> *)buttons
|
||||||
|
{
|
||||||
|
NSMutableArray *ret = [[_buttons allValues] mutableCopy];
|
||||||
|
[ret addObjectsFromArray:_axisEmulatedButtons.allValues];
|
||||||
|
for (NSArray *array in _axes2DEmulatedButtons.allValues) {
|
||||||
|
[ret addObjectsFromArray:array];
|
||||||
|
}
|
||||||
|
for (NSArray *array in _hatEmulatedButtons.allValues) {
|
||||||
|
[ret addObjectsFromArray:array];
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<JOYAxis *> *)axes
|
||||||
|
{
|
||||||
|
return [_axes allValues];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<JOYAxes2D *> *)axes2D
|
||||||
|
{
|
||||||
|
return [[NSSet setWithArray:[_axes2D allValues]] allObjects];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<JOYHat *> *)hats
|
||||||
|
{
|
||||||
|
return [_hats allValues];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)elementChanged:(IOHIDElementRef)element
|
||||||
|
{
|
||||||
|
JOYElement *_element = _iokitToJOY[@(IOHIDElementGetCookie(element))];
|
||||||
|
if (_element) {
|
||||||
|
[self _elementChanged:_element];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//NSLog(@"Unhandled usage %x (Cookie: %x, Usage: %x)", IOHIDElementGetUsage(element), IOHIDElementGetCookie(element), IOHIDElementGetUsage(element));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)_elementChanged:(JOYElement *)element
|
||||||
|
{
|
||||||
|
if (element == _connectedElement) {
|
||||||
|
bool old = self.connected;
|
||||||
|
_logicallyConnected = _connectedElement.value != _connectedElement.min;
|
||||||
|
if (!old && self.connected) {
|
||||||
|
for (id<JOYListener> listener in listeners) {
|
||||||
|
if ([listener respondsToSelector:@selector(controllerConnected:)]) {
|
||||||
|
[listener controllerConnected:self];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (old && !self.connected) {
|
||||||
|
for (id<JOYListener> listener in listeners) {
|
||||||
|
if ([listener respondsToSelector:@selector(controllerDisconnected:)]) {
|
||||||
|
[listener controllerDisconnected:self];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
NSArray<JOYElement *> *subElements = _multiElements[element];
|
||||||
|
if (subElements) {
|
||||||
|
for (JOYElement *subElement in subElements) {
|
||||||
|
[self _elementChanged:subElement];
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self.connected) return;
|
||||||
|
{
|
||||||
|
JOYButton *button = _buttons[element];
|
||||||
|
if (button) {
|
||||||
|
if ([button updateState]) {
|
||||||
|
for (id<JOYListener> listener in listeners) {
|
||||||
|
if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) {
|
||||||
|
[listener controller:self buttonChangedState:button];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
JOYAxis *axis = _axes[element];
|
||||||
|
if (axis) {
|
||||||
|
if ([axis updateState]) {
|
||||||
|
for (id<JOYListener> listener in listeners) {
|
||||||
|
if ([listener respondsToSelector:@selector(controller:movedAxis:)]) {
|
||||||
|
[listener controller:self movedAxis:axis];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JOYEmulatedButton *button = _axisEmulatedButtons[@(axis.uniqueID)];
|
||||||
|
if ([button updateStateFromAxis:axis]) {
|
||||||
|
for (id<JOYListener> listener in listeners) {
|
||||||
|
if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) {
|
||||||
|
[listener controller:self buttonChangedState:button];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
JOYAxes2D *axes = _axes2D[element];
|
||||||
|
if (axes) {
|
||||||
|
if ([axes updateState]) {
|
||||||
|
for (id<JOYListener> listener in listeners) {
|
||||||
|
if ([listener respondsToSelector:@selector(controller:movedAxes2D:)]) {
|
||||||
|
[listener controller:self movedAxes2D:axes];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NSArray <JOYEmulatedButton *> *buttons = _axes2DEmulatedButtons[@(axes.uniqueID)];
|
||||||
|
for (JOYEmulatedButton *button in buttons) {
|
||||||
|
if ([button updateStateFromAxes2D:axes]) {
|
||||||
|
for (id<JOYListener> listener in listeners) {
|
||||||
|
if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) {
|
||||||
|
[listener controller:self buttonChangedState:button];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
JOYHat *hat = _hats[element];
|
||||||
|
if (hat) {
|
||||||
|
if ([hat updateState]) {
|
||||||
|
for (id<JOYListener> listener in listeners) {
|
||||||
|
if ([listener respondsToSelector:@selector(controller:movedHat:)]) {
|
||||||
|
[listener controller:self movedHat:hat];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray <JOYEmulatedButton *> *buttons = _hatEmulatedButtons[@(hat.uniqueID)];
|
||||||
|
for (JOYEmulatedButton *button in buttons) {
|
||||||
|
if ([button updateStateFromHat:hat]) {
|
||||||
|
for (id<JOYListener> listener in listeners) {
|
||||||
|
if ([listener respondsToSelector:@selector(controller:buttonChangedState:)]) {
|
||||||
|
[listener controller:self buttonChangedState:button];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)disconnected
|
||||||
|
{
|
||||||
|
if (_logicallyConnected && [exposedControllers containsObject:self]) {
|
||||||
|
for (id<JOYListener> listener in listeners) {
|
||||||
|
if ([listener respondsToSelector:@selector(controllerDisconnected:)]) {
|
||||||
|
[listener controllerDisconnected:self];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_physicallyConnected = false;
|
||||||
|
[self setRumbleAmplitude:0]; // Stop the rumble thread.
|
||||||
|
[exposedControllers removeObject:self];
|
||||||
|
_device = nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)sendReport:(NSData *)report
|
||||||
|
{
|
||||||
|
if (!report.length) return;
|
||||||
|
IOHIDDeviceSetReport(_device, kIOHIDReportTypeOutput, *(uint8_t *)report.bytes, report.bytes, report.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setPlayerLEDs:(uint8_t)mask
|
||||||
|
{
|
||||||
|
mask &= 0xF;
|
||||||
|
if (_isSwitch) {
|
||||||
|
_lastSwitchPacket.reportID = 0x1; // Rumble and LEDs
|
||||||
|
_lastSwitchPacket.sequence++;
|
||||||
|
_lastSwitchPacket.sequence &= 0xF;
|
||||||
|
_lastSwitchPacket.command = 0x30; // LED
|
||||||
|
_lastSwitchPacket.commandData[0] = mask;
|
||||||
|
[self sendReport:[NSData dataWithBytes:&_lastSwitchPacket length:sizeof(_lastSwitchPacket)]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)pwmThread
|
||||||
|
{
|
||||||
|
while (_rumblePWMRatio != 0) {
|
||||||
|
[_rumbleElement setValue:1];
|
||||||
|
[NSThread sleepForTimeInterval:_rumblePWMRatio / 10];
|
||||||
|
[_rumbleElement setValue:0];
|
||||||
|
[NSThread sleepForTimeInterval:(1 - _rumblePWMRatio) / 10];
|
||||||
|
}
|
||||||
|
[_rumblePWMThreadLock lock];
|
||||||
|
[_rumblePWMThreadLock signal];
|
||||||
|
[_rumblePWMThreadLock unlock];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setRumbleAmplitude:(double)amp /* andFrequency: (double)frequency */
|
||||||
|
{
|
||||||
|
double frequency = 144; // I have no idea what I'm doing.
|
||||||
|
|
||||||
|
if (amp < 0) amp = 0;
|
||||||
|
if (amp > 1) amp = 1;
|
||||||
|
if (_isSwitch) {
|
||||||
|
if (amp == 0) {
|
||||||
|
amp = 1;
|
||||||
|
frequency = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t highAmp = amp * 0x64;
|
||||||
|
uint8_t lowAmp = amp * 0x32 + 0x40;
|
||||||
|
if (frequency < 0) frequency = 0;
|
||||||
|
if (frequency > 1252) frequency = 1252;
|
||||||
|
uint8_t encodedFrequency = (uint8_t)round(log2(frequency / 10.0) * 32.0);
|
||||||
|
|
||||||
|
uint16_t highFreq = (encodedFrequency - 0x60) * 4;
|
||||||
|
uint8_t lowFreq = encodedFrequency - 0x40;
|
||||||
|
|
||||||
|
//if (frequency < 82 || frequency > 312) {
|
||||||
|
if (amp) {
|
||||||
|
highAmp = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frequency < 40 || frequency > 626) {
|
||||||
|
lowAmp = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastSwitchPacket.rumbleData[0] = _lastSwitchPacket.rumbleData[4] = highFreq & 0xFF;
|
||||||
|
_lastSwitchPacket.rumbleData[1] = _lastSwitchPacket.rumbleData[5] = (highAmp << 1) + ((highFreq >> 8) & 0x1);
|
||||||
|
_lastSwitchPacket.rumbleData[2] = _lastSwitchPacket.rumbleData[6] = lowFreq;
|
||||||
|
_lastSwitchPacket.rumbleData[3] = _lastSwitchPacket.rumbleData[7] = lowAmp;
|
||||||
|
|
||||||
|
|
||||||
|
_lastSwitchPacket.reportID = 0x10; // Rumble only
|
||||||
|
_lastSwitchPacket.sequence++;
|
||||||
|
_lastSwitchPacket.sequence &= 0xF;
|
||||||
|
_lastSwitchPacket.command = 0; // LED
|
||||||
|
[self sendReport:[NSData dataWithBytes:&_lastSwitchPacket length:sizeof(_lastSwitchPacket)]];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (_rumbleElement.max == 1 && _rumbleElement.min == 0) {
|
||||||
|
if (_rumblePWMRatio == 0) { // PWM thread not running, start it.
|
||||||
|
if (amp != 0) {
|
||||||
|
_rumblePWMRatio = amp;
|
||||||
|
[self performSelectorInBackground:@selector(pwmThread) withObject:nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (amp == 0) { // Thread is running, signal it to stop
|
||||||
|
[_rumblePWMThreadLock lock];
|
||||||
|
_rumblePWMRatio = 0;
|
||||||
|
[_rumblePWMThreadLock wait];
|
||||||
|
[_rumblePWMThreadLock unlock];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
_rumblePWMRatio = amp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[_rumbleElement setValue:amp * (_rumbleElement.max - _rumbleElement.min) + _rumbleElement.min];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (bool)isConnected
|
||||||
|
{
|
||||||
|
return _logicallyConnected && _physicallyConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (void)controllerAdded:(IOHIDDeviceRef) device
|
||||||
|
{
|
||||||
|
NSString *name = (__bridge NSString *)IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey));
|
||||||
|
NSDictionary *hacks = hacksByName[name];
|
||||||
|
NSArray *filters = hacks[JOYReportIDFilters];
|
||||||
|
if (filters) {
|
||||||
|
JOYController *controller = [[JOYMultiplayerController alloc] initWithDevice:device
|
||||||
|
reportIDFilters:filters];
|
||||||
|
[controllers setObject:controller forKey:[NSValue valueWithPointer:device]];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[controllers setObject:[[JOYController alloc] initWithDevice:device] forKey:[NSValue valueWithPointer:device]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (void)controllerRemoved:(IOHIDDeviceRef) device
|
||||||
|
{
|
||||||
|
[[controllers objectForKey:[NSValue valueWithPointer:device]] disconnected];
|
||||||
|
[controllers removeObjectForKey:[NSValue valueWithPointer:device]];
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (NSArray<JOYController *> *)allControllers
|
||||||
|
{
|
||||||
|
return exposedControllers;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (void)load
|
||||||
|
{
|
||||||
|
#include "ControllerConfiguration.inc"
|
||||||
|
}
|
||||||
|
|
||||||
|
+(void)registerListener:(id<JOYListener>)listener
|
||||||
|
{
|
||||||
|
[listeners addObject:listener];
|
||||||
|
}
|
||||||
|
|
||||||
|
+(void)unregisterListener:(id<JOYListener>)listener
|
||||||
|
{
|
||||||
|
[listeners removeObject:listener];
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (void)startOnRunLoop:(NSRunLoop *)runloop withOptions: (NSDictionary *)options
|
||||||
|
{
|
||||||
|
axesEmulateButtons = [options[JOYAxesEmulateButtonsKey] boolValue];
|
||||||
|
axes2DEmulateButtons = [options[JOYAxes2DEmulateButtonsKey] boolValue];
|
||||||
|
hatsEmulateButtons = [options[JOYHatsEmulateButtonsKey] boolValue];
|
||||||
|
|
||||||
|
controllers = [NSMutableDictionary dictionary];
|
||||||
|
exposedControllers = [NSMutableArray array];
|
||||||
|
NSArray *array = @[
|
||||||
|
CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick),
|
||||||
|
CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad),
|
||||||
|
CreateHIDDeviceMatchDictionary(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController),
|
||||||
|
@{@kIOHIDDeviceUsagePageKey: @(kHIDPage_Game)},
|
||||||
|
];
|
||||||
|
|
||||||
|
listeners = [NSMutableSet set];
|
||||||
|
static IOHIDManagerRef manager = nil;
|
||||||
|
if (manager) {
|
||||||
|
CFRelease(manager); // Stop the previous session
|
||||||
|
}
|
||||||
|
manager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
|
||||||
|
|
||||||
|
if (!manager) return;
|
||||||
|
if (IOHIDManagerOpen(manager, kIOHIDOptionsTypeNone)) {
|
||||||
|
CFRelease(manager);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IOHIDManagerSetDeviceMatchingMultiple(manager, (__bridge CFArrayRef)array);
|
||||||
|
IOHIDManagerRegisterDeviceMatchingCallback(manager, HIDDeviceAdded, NULL);
|
||||||
|
IOHIDManagerRegisterDeviceRemovalCallback(manager, HIDDeviceRemoved, NULL);
|
||||||
|
IOHIDManagerScheduleWithRunLoop(manager, [runloop getCFRunLoop], kCFRunLoopDefaultMode);
|
||||||
|
}
|
||||||
|
@end
|
20
JoyKit/JOYElement.h
Normal file
20
JoyKit/JOYElement.h
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#include <IOKit/hid/IOHIDLib.h>
|
||||||
|
|
||||||
|
@interface JOYElement : NSObject<NSCopying>
|
||||||
|
- (instancetype)initWithElement:(IOHIDElementRef)element;
|
||||||
|
- (int32_t)value;
|
||||||
|
- (NSData *)dataValue;
|
||||||
|
- (void)setValue:(uint32_t)value;
|
||||||
|
- (void)setDataValue:(NSData *)value;
|
||||||
|
@property (readonly) uint16_t usage;
|
||||||
|
@property (readonly) uint16_t usagePage;
|
||||||
|
@property (readonly) uint32_t uniqueID;
|
||||||
|
@property int32_t min;
|
||||||
|
@property int32_t max;
|
||||||
|
@property (readonly) int32_t reportID;
|
||||||
|
@property (readonly) int32_t parentID;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
96
JoyKit/JOYElement.m
Normal file
96
JoyKit/JOYElement.m
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
#import "JOYElement.h"
|
||||||
|
#include <IOKit/hid/IOHIDLib.h>
|
||||||
|
|
||||||
|
@implementation JOYElement
|
||||||
|
{
|
||||||
|
id _element;
|
||||||
|
IOHIDDeviceRef _device;
|
||||||
|
int32_t _min, _max;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (int32_t)min
|
||||||
|
{
|
||||||
|
return MIN(_min, _max);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (int32_t)max
|
||||||
|
{
|
||||||
|
return MAX(_max, _min);
|
||||||
|
}
|
||||||
|
|
||||||
|
-(void)setMin:(int32_t)min
|
||||||
|
{
|
||||||
|
_min = min;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setMax:(int32_t)max
|
||||||
|
{
|
||||||
|
_max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithElement:(IOHIDElementRef)element
|
||||||
|
{
|
||||||
|
if ((self = [super init])) {
|
||||||
|
_element = (__bridge id)element;
|
||||||
|
_usage = IOHIDElementGetUsage(element);
|
||||||
|
_usagePage = IOHIDElementGetUsagePage(element);
|
||||||
|
_uniqueID = (uint32_t)IOHIDElementGetCookie(element);
|
||||||
|
_min = (int32_t) IOHIDElementGetLogicalMin(element);
|
||||||
|
_max = (int32_t) IOHIDElementGetLogicalMax(element);
|
||||||
|
_reportID = IOHIDElementGetReportID(element);
|
||||||
|
IOHIDElementRef parent = IOHIDElementGetParent(element);
|
||||||
|
_parentID = parent? (uint32_t)IOHIDElementGetCookie(parent) : -1;
|
||||||
|
_device = IOHIDElementGetDevice(element);
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (int32_t)value
|
||||||
|
{
|
||||||
|
IOHIDValueRef value = NULL;
|
||||||
|
IOHIDDeviceGetValue(_device, (__bridge IOHIDElementRef)_element, &value);
|
||||||
|
if (!value) return 0;
|
||||||
|
CFRelease(CFRetain(value)); // For some reason, this is required to prevent leaks.
|
||||||
|
return (int32_t)IOHIDValueGetIntegerValue(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSData *)dataValue
|
||||||
|
{
|
||||||
|
IOHIDValueRef value = NULL;
|
||||||
|
IOHIDDeviceGetValue(_device, (__bridge IOHIDElementRef)_element, &value);
|
||||||
|
if (!value) return 0;
|
||||||
|
CFRelease(CFRetain(value)); // For some reason, this is required to prevent leaks.
|
||||||
|
return [NSData dataWithBytes:IOHIDValueGetBytePtr(value) length:IOHIDValueGetLength(value)];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setValue:(uint32_t)value
|
||||||
|
{
|
||||||
|
IOHIDValueRef ivalue = IOHIDValueCreateWithIntegerValue(NULL, (__bridge IOHIDElementRef)_element, 0, value);
|
||||||
|
IOHIDDeviceSetValue(_device, (__bridge IOHIDElementRef)_element, ivalue);
|
||||||
|
CFRelease(ivalue);
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setDataValue:(NSData *)value
|
||||||
|
{
|
||||||
|
IOHIDValueRef ivalue = IOHIDValueCreateWithBytes(NULL, (__bridge IOHIDElementRef)_element, 0, value.bytes, value.length);
|
||||||
|
IOHIDDeviceSetValue(_device, (__bridge IOHIDElementRef)_element, ivalue);
|
||||||
|
CFRelease(ivalue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* For use as a dictionary key */
|
||||||
|
|
||||||
|
- (NSUInteger)hash
|
||||||
|
{
|
||||||
|
return self.uniqueID;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isEqual:(id)object
|
||||||
|
{
|
||||||
|
return self->_element == object;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (id)copyWithZone:(nullable NSZone *)zone;
|
||||||
|
{
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
@end
|
11
JoyKit/JOYEmulatedButton.h
Normal file
11
JoyKit/JOYEmulatedButton.h
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#import "JOYButton.h"
|
||||||
|
#import "JOYAxis.h"
|
||||||
|
#import "JOYAxes2D.h"
|
||||||
|
#import "JOYHat.h"
|
||||||
|
|
||||||
|
@interface JOYEmulatedButton : JOYButton
|
||||||
|
- (instancetype)initWithUsage:(JOYButtonUsage)usage uniqueID:(uint64_t)uniqueID;
|
||||||
|
- (bool)updateStateFromAxis:(JOYAxis *)axis;
|
||||||
|
- (bool)updateStateFromAxes2D:(JOYAxes2D *)axes;
|
||||||
|
- (bool)updateStateFromHat:(JOYHat *)hat;
|
||||||
|
@end
|
91
JoyKit/JOYEmulatedButton.m
Normal file
91
JoyKit/JOYEmulatedButton.m
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
#import "JOYEmulatedButton.h"
|
||||||
|
|
||||||
|
@interface JOYButton ()
|
||||||
|
{
|
||||||
|
@public bool _state;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation JOYEmulatedButton
|
||||||
|
{
|
||||||
|
uint64_t _uniqueID;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithUsage:(JOYButtonUsage)usage uniqueID:(uint64_t)uniqueID;
|
||||||
|
{
|
||||||
|
self = [super init];
|
||||||
|
self.usage = usage;
|
||||||
|
_uniqueID = uniqueID;
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (uint64_t)uniqueID
|
||||||
|
{
|
||||||
|
return _uniqueID;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (bool)updateStateFromAxis:(JOYAxis *)axis
|
||||||
|
{
|
||||||
|
bool old = _state;
|
||||||
|
_state = [axis value] > 0.5;
|
||||||
|
return _state != old;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (bool)updateStateFromAxes2D:(JOYAxes2D *)axes
|
||||||
|
{
|
||||||
|
bool old = _state;
|
||||||
|
if (axes.distance < 0.5) {
|
||||||
|
_state = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
unsigned direction = ((unsigned)round(axes.angle / 360 * 8)) & 7;
|
||||||
|
switch (self.usage) {
|
||||||
|
case JOYButtonUsageDPadLeft:
|
||||||
|
_state = direction >= 3 && direction <= 5;
|
||||||
|
break;
|
||||||
|
case JOYButtonUsageDPadRight:
|
||||||
|
_state = direction <= 1 || direction == 7;
|
||||||
|
break;
|
||||||
|
case JOYButtonUsageDPadUp:
|
||||||
|
_state = direction >= 5;
|
||||||
|
break;
|
||||||
|
case JOYButtonUsageDPadDown:
|
||||||
|
_state = direction <= 3 && direction >= 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _state != old;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (bool)updateStateFromHat:(JOYHat *)hat
|
||||||
|
{
|
||||||
|
bool old = _state;
|
||||||
|
if (!hat.pressed) {
|
||||||
|
_state = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
unsigned direction = ((unsigned)round(hat.angle / 360 * 8)) & 7;
|
||||||
|
switch (self.usage) {
|
||||||
|
case JOYButtonUsageDPadLeft:
|
||||||
|
_state = direction >= 3 && direction <= 5;
|
||||||
|
break;
|
||||||
|
case JOYButtonUsageDPadRight:
|
||||||
|
_state = direction <= 1 || direction == 7;
|
||||||
|
break;
|
||||||
|
case JOYButtonUsageDPadUp:
|
||||||
|
_state = direction >= 5;
|
||||||
|
break;
|
||||||
|
case JOYButtonUsageDPadDown:
|
||||||
|
_state = direction <= 3 && direction >= 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _state != old;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
11
JoyKit/JOYHat.h
Normal file
11
JoyKit/JOYHat.h
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
@interface JOYHat : NSObject
|
||||||
|
- (uint64_t)uniqueID;
|
||||||
|
- (double)angle;
|
||||||
|
- (unsigned)resolution;
|
||||||
|
@property (readonly, getter=isPressed) bool pressed;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
60
JoyKit/JOYHat.m
Normal file
60
JoyKit/JOYHat.m
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#import "JOYHat.h"
|
||||||
|
#import "JOYElement.h"
|
||||||
|
|
||||||
|
@implementation JOYHat
|
||||||
|
{
|
||||||
|
JOYElement *_element;
|
||||||
|
double _state;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (uint64_t)uniqueID
|
||||||
|
{
|
||||||
|
return _element.uniqueID;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSString *)description
|
||||||
|
{
|
||||||
|
if (self.isPressed) {
|
||||||
|
return [NSString stringWithFormat:@"<%@: %p (%llu); State: %f degrees>", self.className, self, self.uniqueID, self.angle];
|
||||||
|
}
|
||||||
|
return [NSString stringWithFormat:@"<%@: %p (%llu); State: released>", self.className, self, self.uniqueID];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithElement:(JOYElement *)element
|
||||||
|
{
|
||||||
|
self = [super init];
|
||||||
|
if (!self) return self;
|
||||||
|
|
||||||
|
_element = element;
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (bool)isPressed
|
||||||
|
{
|
||||||
|
return _state >= 0 && _state < 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (double)angle
|
||||||
|
{
|
||||||
|
if (self.isPressed) return fmod((_state + 270), 360);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (unsigned)resolution
|
||||||
|
{
|
||||||
|
return _element.max - _element.min + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (bool)updateState
|
||||||
|
{
|
||||||
|
unsigned state = ([_element value] - _element.min) * 360.0 / self.resolution;
|
||||||
|
if (_state != state) {
|
||||||
|
_state = state;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
8
JoyKit/JOYMultiplayerController.h
Normal file
8
JoyKit/JOYMultiplayerController.h
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#import "JOYController.h"
|
||||||
|
#include <IOKit/hid/IOHIDLib.h>
|
||||||
|
|
||||||
|
@interface JOYMultiplayerController : JOYController
|
||||||
|
- (instancetype)initWithDevice:(IOHIDDeviceRef) device reportIDFilters:(NSArray <NSArray <NSNumber *> *>*) reportIDFilters;
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
44
JoyKit/JOYMultiplayerController.m
Normal file
44
JoyKit/JOYMultiplayerController.m
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#import "JOYMultiplayerController.h"
|
||||||
|
|
||||||
|
@interface JOYController ()
|
||||||
|
- (instancetype)initWithDevice:(IOHIDDeviceRef)device reportIDFilter:(NSArray <NSNumber *> *) filter serialSuffix:(NSString *)suffix;
|
||||||
|
- (void)elementChanged:(IOHIDElementRef) element toValue:(IOHIDValueRef) value;
|
||||||
|
- (void)disconnected;
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation JOYMultiplayerController
|
||||||
|
{
|
||||||
|
NSMutableArray <JOYController *> *_children;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithDevice:(IOHIDDeviceRef) device reportIDFilters:(NSArray <NSArray <NSNumber *> *>*) reportIDFilters
|
||||||
|
{
|
||||||
|
self = [super init];
|
||||||
|
if (!self) return self;
|
||||||
|
|
||||||
|
_children = [NSMutableArray array];
|
||||||
|
|
||||||
|
unsigned index = 1;
|
||||||
|
for (NSArray *filter in reportIDFilters) {
|
||||||
|
JOYController *controller = [[JOYController alloc] initWithDevice:device reportIDFilter:filter serialSuffix:[NSString stringWithFormat:@"%d", index]];
|
||||||
|
[_children addObject:controller];
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)elementChanged:(IOHIDElementRef) element toValue:(IOHIDValueRef) value
|
||||||
|
{
|
||||||
|
for (JOYController *child in _children) {
|
||||||
|
[child elementChanged:element toValue:value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)disconnected
|
||||||
|
{
|
||||||
|
for (JOYController *child in _children) {
|
||||||
|
[child disconnected];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
14
JoyKit/JOYSubElement.h
Normal file
14
JoyKit/JOYSubElement.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#import "JOYElement.h"
|
||||||
|
|
||||||
|
@interface JOYSubElement : JOYElement
|
||||||
|
- (instancetype)initWithRealElement:(JOYElement *)element
|
||||||
|
size:(size_t) size // in bits
|
||||||
|
offset:(size_t) offset // in bits
|
||||||
|
usagePage:(uint16_t)usagePage
|
||||||
|
usage:(uint16_t)usage
|
||||||
|
min:(int32_t)min
|
||||||
|
max:(int32_t)max;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
|
99
JoyKit/JOYSubElement.m
Normal file
99
JoyKit/JOYSubElement.m
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
#import "JOYSubElement.h"
|
||||||
|
|
||||||
|
@interface JOYElement ()
|
||||||
|
{
|
||||||
|
@public uint16_t _usage;
|
||||||
|
@public uint16_t _usagePage;
|
||||||
|
@public uint32_t _uniqueID;
|
||||||
|
@public int32_t _min;
|
||||||
|
@public int32_t _max;
|
||||||
|
@public int32_t _reportID;
|
||||||
|
@public int32_t _parentID;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation JOYSubElement
|
||||||
|
{
|
||||||
|
JOYElement *_parent;
|
||||||
|
size_t _size; // in bits
|
||||||
|
size_t _offset; // in bits
|
||||||
|
}
|
||||||
|
|
||||||
|
- (instancetype)initWithRealElement:(JOYElement *)element
|
||||||
|
size:(size_t) size // in bits
|
||||||
|
offset:(size_t) offset // in bits
|
||||||
|
usagePage:(uint16_t)usagePage
|
||||||
|
usage:(uint16_t)usage
|
||||||
|
min:(int32_t)min
|
||||||
|
max:(int32_t)max
|
||||||
|
{
|
||||||
|
if ((self = [super init])) {
|
||||||
|
_parent = element;
|
||||||
|
_size = size;
|
||||||
|
_offset = offset;
|
||||||
|
_usage = usage;
|
||||||
|
_usagePage = usagePage;
|
||||||
|
_uniqueID = (uint32_t)((_parent.uniqueID << 16) | offset);
|
||||||
|
_min = min;
|
||||||
|
_max = max;
|
||||||
|
_reportID = _parent.reportID;
|
||||||
|
_parentID = _parent.parentID;
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (int32_t)value
|
||||||
|
{
|
||||||
|
NSData *parentValue = [_parent dataValue];
|
||||||
|
if (!parentValue) return 0;
|
||||||
|
if (_size > 32) return 0;
|
||||||
|
if (_size + (_offset % 8) > 32) return 0;
|
||||||
|
size_t parentLength = parentValue.length;
|
||||||
|
if (_size > parentLength * 8) return 0;
|
||||||
|
if (_size + _offset >= parentLength * 8) return 0;
|
||||||
|
const uint8_t *bytes = parentValue.bytes;
|
||||||
|
|
||||||
|
uint8_t temp[4] = {0,};
|
||||||
|
memcpy(temp, bytes + _offset / 8, (_offset + _size - 1) / 8 - _offset / 8 + 1);
|
||||||
|
uint32_t ret = (*(uint32_t *)temp) >> (_offset % 8);
|
||||||
|
ret &= (1 << _size) - 1;
|
||||||
|
|
||||||
|
if (_max < _min) {
|
||||||
|
return _max + _min - ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setValue: (uint32_t) value
|
||||||
|
{
|
||||||
|
NSMutableData *dataValue = [[_parent dataValue] mutableCopy];
|
||||||
|
if (!dataValue) return;
|
||||||
|
if (_size > 32) return;
|
||||||
|
if (_size + (_offset % 8) > 32) return;
|
||||||
|
size_t parentLength = dataValue.length;
|
||||||
|
if (_size > parentLength * 8) return;
|
||||||
|
if (_size + _offset >= parentLength * 8) return;
|
||||||
|
uint8_t *bytes = dataValue.mutableBytes;
|
||||||
|
|
||||||
|
uint8_t temp[4] = {0,};
|
||||||
|
memcpy(temp, bytes + _offset / 8, (_offset + _size - 1) / 8 - _offset / 8 + 1);
|
||||||
|
(*(uint32_t *)temp) &= ~((1 << (_size - 1)) << (_offset % 8));
|
||||||
|
(*(uint32_t *)temp) |= (value) << (_offset % 8);
|
||||||
|
memcpy(bytes + _offset / 8, temp, (_offset + _size - 1) / 8 - _offset / 8 + 1);
|
||||||
|
[_parent setDataValue:dataValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSData *)dataValue
|
||||||
|
{
|
||||||
|
[self doesNotRecognizeSelector:_cmd];
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setDataValue:(NSData *)data
|
||||||
|
{
|
||||||
|
[self doesNotRecognizeSelector:_cmd];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@end
|
6
JoyKit/JoyKit.h
Normal file
6
JoyKit/JoyKit.h
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#ifndef JoyKit_h
|
||||||
|
#define JoyKit_h
|
||||||
|
|
||||||
|
#include "JOYController.h"
|
||||||
|
|
||||||
|
#endif
|
2
Makefile
2
Makefile
@ -140,7 +140,7 @@ SDL_SOURCES := $(shell ls SDL/*.c) $(OPEN_DIALOG)
|
|||||||
TESTER_SOURCES := $(shell ls Tester/*.c)
|
TESTER_SOURCES := $(shell ls Tester/*.c)
|
||||||
|
|
||||||
ifeq ($(PLATFORM),Darwin)
|
ifeq ($(PLATFORM),Darwin)
|
||||||
COCOA_SOURCES := $(shell ls Cocoa/*.m) $(shell ls HexFiend/*.m)
|
COCOA_SOURCES := $(shell ls Cocoa/*.m) $(shell ls HexFiend/*.m) $(shell ls JoyKit/*.m)
|
||||||
QUICKLOOK_SOURCES := $(shell ls QuickLook/*.m) $(shell ls QuickLook/*.c)
|
QUICKLOOK_SOURCES := $(shell ls QuickLook/*.m) $(shell ls QuickLook/*.c)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
@ -141,12 +141,17 @@ static void GB_update_keys_status(GB_gameboy_t *gb, unsigned port)
|
|||||||
GB_set_key_state_for_player(gb, GB_KEY_START, emulated_devices == 1 ? port : 0,
|
GB_set_key_state_for_player(gb, GB_KEY_START, emulated_devices == 1 ? port : 0,
|
||||||
input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START));
|
input_state_cb(port, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START));
|
||||||
|
|
||||||
if (gb->rumble_state)
|
|
||||||
rumble.set_rumble_state(port, RETRO_RUMBLE_STRONG, 65535);
|
|
||||||
else
|
|
||||||
rumble.set_rumble_state(port, RETRO_RUMBLE_STRONG, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void rumble_callback(GB_gameboy_t *gb, double amplitude)
|
||||||
|
{
|
||||||
|
if (gb == &gameboy[0]) {
|
||||||
|
rumble.set_rumble_state(0, RETRO_RUMBLE_STRONG, 65535 * amplitude);
|
||||||
|
}
|
||||||
|
else if (gb == &gameboy[1]) {
|
||||||
|
rumble.set_rumble_state(1, RETRO_RUMBLE_STRONG, 65535 * amplitude);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void audio_callback(GB_gameboy_t *gb, GB_sample_t *sample)
|
static void audio_callback(GB_gameboy_t *gb, GB_sample_t *sample)
|
||||||
{
|
{
|
||||||
@ -370,6 +375,8 @@ static void init_for_current_model(unsigned id)
|
|||||||
GB_set_rgb_encode_callback(&gameboy[i], rgb_encode);
|
GB_set_rgb_encode_callback(&gameboy[i], rgb_encode);
|
||||||
GB_set_sample_rate(&gameboy[i], AUDIO_FREQUENCY);
|
GB_set_sample_rate(&gameboy[i], AUDIO_FREQUENCY);
|
||||||
GB_apu_set_sample_callback(&gameboy[i], audio_callback);
|
GB_apu_set_sample_callback(&gameboy[i], audio_callback);
|
||||||
|
GB_set_rumble_callback(&gameboy[i], rumble_callback);
|
||||||
|
|
||||||
|
|
||||||
/* todo: attempt to make these more generic */
|
/* todo: attempt to make these more generic */
|
||||||
GB_set_vblank_callback(&gameboy[0], (GB_vblank_callback_t) vblank1);
|
GB_set_vblank_callback(&gameboy[0], (GB_vblank_callback_t) vblank1);
|
||||||
|
Loading…
Reference in New Issue
Block a user