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:
Lior Halphon 2019-10-19 19:26:04 +03:00
parent 7d6cdf3819
commit 0ece21bca7
39 changed files with 2418 additions and 1026 deletions

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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;

View File

@ -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); static const unsigned gb_to_joykit[] = {
[GBRight]=JOYButtonUsageDPadRight,
[GBLeft]=JOYButtonUsageDPadLeft,
[GBUp]=JOYButtonUsageDPadUp,
[GBDown]=JOYButtonUsageDPadDown,
[GBA]=JOYButtonUsageA,
[GBB]=JOYButtonUsageB,
[GBSelect]=JOYButtonUsageSelect,
[GBStart]=JOYButtonUsageStart,
[GBTurbo]=JOYButtonUsageL1,
[GBRewind]=JOYButtonUsageL2,
[GBUnderclock]=JOYButtonUsageR1,
};
all_mappings[joystick_name] = mapping; if (joystick_configuration_state == GBUnderclock) {
[[NSUserDefaults standardUserDefaults] setObject:all_mappings forKey:@"GBJoypadMappings"]; for (JOYAxis *axis in controller.axes) {
[self refreshJoypadMenu:nil]; 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

View File

@ -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

View File

@ -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,123 +261,97 @@
} }
} }
- (void) joystick:(NSString *)joystick_name button: (unsigned)button changedState: (bool) state - (void)setRumble:(bool)on
{ {
unsigned player_count = GB_get_player_count(_gb); [lastController setRumbleAmplitude:(double)on];
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];
for (GBButton i = 0; i < GBButtonCount; i++) {
NSNumber *mapped_button = [mapping objectForKey:GBButtonNames[i]];
if (mapped_button && [mapped_button integerValue] == button) {
switch (i) {
case GBTurbo:
GB_set_turbo_mode(_gb, state, state && self.isRewinding);
break;
case GBRewind:
self.isRewinding = state;
if (state) {
GB_set_turbo_mode(_gb, false, false);
}
break;
case GBUnderclock:
underclockKeyDown = state;
break;
default:
GB_set_key_state_for_player(_gb, (GB_key_t)i, player, state);
break;
}
}
}
}
} }
- (void) joystick:(NSString *)joystick_name axis: (unsigned)axis movedTo: (signed) value - (void)controller:(JOYController *)controller movedAxis:(JOYAxis *)axis
{ {
unsigned player_count = GB_get_player_count(_gb); if (![self.window isMainWindow]) return;
UpdateSystemActivity(UsrActivity); NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID];
for (unsigned player = 0; player < player_count; player++) { if (!mapping) {
NSString *preferred_joypad = [[[NSUserDefaults standardUserDefaults] dictionaryForKey:@"GBDefaultJoypads"] mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName];
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); 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);
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;
} }
assert(state + 1 < 9); [controller setPlayerLEDs:1 << player];
/* - N NE E SE S SW W NW */ NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID];
GB_set_key_state_for_player(_gb, GB_KEY_UP, player, (bool []){0, 1, 1, 0, 0, 0, 0, 0, 1}[state + 1]); if (!mapping) {
GB_set_key_state_for_player(_gb, GB_KEY_RIGHT, player, (bool []){0, 0, 1, 1, 1, 0, 0, 0, 0}[state + 1]); mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName];
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]);
JOYButtonUsage usage = ((JOYButtonUsage)[mapping[n2s(button.uniqueID)] unsignedIntValue]) ?: button.usage;
if (!mapping && usage >= JOYButtonUsageGeneric0) {
usage = (const JOYButtonUsage[]){JOYButtonUsageY, JOYButtonUsageA, JOYButtonUsageB, JOYButtonUsageX}[(usage - JOYButtonUsageGeneric0) & 3];
}
switch (usage) {
case JOYButtonUsageNone: break;
case JOYButtonUsageA: GB_set_key_state_for_player(_gb, GB_KEY_A, player, button.isPressed); break;
case JOYButtonUsageB: GB_set_key_state_for_player(_gb, GB_KEY_B, player, button.isPressed); break;
case JOYButtonUsageC: break;
case JOYButtonUsageStart:
case JOYButtonUsageX: GB_set_key_state_for_player(_gb, GB_KEY_START, player, button.isPressed); 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;
}
} }
} }

View File

@ -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>

View File

@ -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");
}
}

View File

@ -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);
} }

View File

@ -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) {

View File

@ -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);
} }

View File

@ -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;
); );
}; };

View File

@ -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;
} }

View File

@ -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);

View File

@ -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);

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

View 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

View 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
View 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
View 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

View 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

View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
#ifndef JoyKit_h
#define JoyKit_h
#include "JOYController.h"
#endif

View File

@ -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

View File

@ -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);