git.y1.nz

SameBoy

Accurate GB/GBC emulator
download: https://git.y1.nz/archives/sameboy.tar.gz
README | Files | Log | Refs | LICENSE

commit f1d52c53d3775b12aa5f3ef5ce39f83364eadd2c
parent 9096f629c9fab8f279043911d818cd6f3703a003
Author: Lior Halphon <LIJI32@gmail.com>
Date:   Sat, 29 Mar 2025 14:47:10 +0300

Allow using joysticks as faux analog controllers

Diffstat:
MCocoa/GBView.m104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
MCocoa/Preferences.xib49++++++++++++++++++++++++++++++-------------------
MCore/gb.c1+
MCore/gb.h5+++++
MCore/joypad.c104++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
MCore/joypad.h3+++
MSDL/configuration.h1+
MSDL/gui.c11+++++++++++
MSDL/main.c21++++++++++++++++++++-
MiOS/GBBackgroundView.m2++
MiOS/GBSettingsViewController.m16++++++++++++----
MiOS/GBViewController.m6++++++
MiOS/main.m1+
13 files changed, 273 insertions(+), 51 deletions(-)

diff --git a/Cocoa/GBView.m b/Cocoa/GBView.m @@ -431,13 +431,22 @@ static const uint8_t workboy_vk_to_key[] = { if (self.document.partner) { if (player == 0) { GB_set_key_state_for_player(_gb, (GB_key_t)button, 0, true); + if (button <= GB_KEY_DOWN) { + GB_set_use_faux_analog_inputs(_gb, 0, false); + } } else { GB_set_key_state_for_player(self.document.partner.gb, (GB_key_t)button, 0, true); + if (button <= GB_KEY_DOWN) { + GB_set_use_faux_analog_inputs(self.document.partner.gb, 0, false); + } } } else { GB_set_key_state_for_player(_gb, (GB_key_t)button, player, true); + if (button <= GB_KEY_DOWN) { + GB_set_use_faux_analog_inputs(_gb, player, false); + } } break; } @@ -581,12 +590,63 @@ static const uint8_t workboy_vk_to_key[] = { } } +- (bool)controller:(JOYController *)controller applicableForPlayer:(unsigned)player effectivePlayer:(unsigned *)effectivePlayer effectiveGB:(GB_gameboy_t **)effectiveGB +{ + NSDictionary<NSNumber *, JOYController *> *controllerMapping = [self controllerMapping]; + + JOYController *preferredJoypad = controllerMapping[@(player)]; + if (preferredJoypad && preferredJoypad != controller) return false; // The player has a different assigned controller + if (!preferredJoypad && self.playerCount != 1) return false; // The player has no assigned controller in multiplayer mode, prevent controller inputs + + dispatch_async(dispatch_get_main_queue(), ^{ + [controller setPlayerLEDs:[controller LEDMaskForPlayer:player]]; + }); + + *effectiveGB = _gb; + *effectivePlayer = player; + + if (player && self.document.partner) { + *effectiveGB = self.document.partner.gb; + *effectivePlayer = 0; + if (controller != self.document.partner.view->lastController) { + [self setRumble:0]; + self.document.partner.view->lastController = controller; + } + } + else { + if (controller != lastController) { + [self setRumble:0]; + lastController = controller; + } + } + return true; +} + - (void)controller:(JOYController *)controller movedAxes2D:(JOYAxes2D *)axes { if (!_gb) return; - if ([self shouldControllerUseJoystickForMotion:controller]) { - if (!self.mouseControlsActive) { - GB_set_accelerometer_values(_gb, -axes.value.x, -axes.value.y); + /* Always handle only the most dominant 2D input. */ + for (JOYAxes2D *otherAxes in controller.axes2D) { + if (otherAxes == axes) continue; + if (otherAxes.distance > axes.distance) { + return; + } + } + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + + if ([self shouldControllerUseJoystickForMotion:controller] && !self.mouseControlsActive) { + GB_set_accelerometer_values(_gb, -axes.value.x, -axes.value.y); + } + else if ([defaults boolForKey:@"GBFauxAnalogInputs"]) { + unsigned playerCount = self.playerCount; + for (unsigned player = 0; player < playerCount; player++) { + unsigned effectivePlayer; + GB_gameboy_t *effectiveGB; + if (![self controller:controller applicableForPlayer:player effectivePlayer:&effectivePlayer effectiveGB:&effectiveGB]) continue; + + GB_set_use_faux_analog_inputs(effectiveGB, effectivePlayer, true); + NSPoint position = axes.value; + GB_set_faux_analog_inputs(effectiveGB, effectivePlayer, position.x, position.y); } } } @@ -650,18 +710,17 @@ static const uint8_t workboy_vk_to_key[] = { IOPMAssertionDeclareUserActivity(CFSTR(""), kIOPMUserActiveLocal, &assertionID); - NSDictionary<NSNumber *, JOYController *> *controllerMapping = [self controllerMapping]; + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + bool fauxAnalog = [defaults boolForKey:@"GBFauxAnalogInputs"]; + for (unsigned player = 0; player < playerCount; player++) { - JOYController *preferredJoypad = controllerMapping[@(player)]; - if (preferredJoypad && preferredJoypad != controller) continue; // The player has a different assigned controller - if (!preferredJoypad && playerCount != 1) continue; // The player has no assigned controller in multiplayer mode, prevent controller inputs + unsigned effectivePlayer; + GB_gameboy_t *effectiveGB; + if (![self controller:controller applicableForPlayer:player effectivePlayer:&effectivePlayer effectiveGB:&effectiveGB]) continue; - dispatch_async(dispatch_get_main_queue(), ^{ - [controller setPlayerLEDs:[controller LEDMaskForPlayer:player]]; - }); - NSDictionary *mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; + NSDictionary *mapping = [defaults dictionaryForKey:@"JoyKitInstanceMapping"][controller.uniqueID]; if (!mapping) { - mapping = [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName]; + mapping = [defaults dictionaryForKey:@"JoyKitNameMapping"][controller.deviceName]; } JOYButtonUsage usage = ((JOYButtonUsage)[mapping[n2s(button.uniqueID)] unsignedIntValue]) ?: button.usage; @@ -669,21 +728,14 @@ static const uint8_t workboy_vk_to_key[] = { usage = GB_inline_const(JOYButtonUsage[], {JOYButtonUsageY, JOYButtonUsageA, JOYButtonUsageB, JOYButtonUsageX})[(usage - JOYButtonUsageGeneric0) & 3]; } - GB_gameboy_t *effectiveGB = _gb; - unsigned effectivePlayer = player; - - if (player && self.document.partner) { - effectiveGB = self.document.partner.gb; - effectivePlayer = 0; - if (controller != self.document.partner.view->lastController) { - [self setRumble:0]; - self.document.partner.view->lastController = controller; + if (usage >= JOYButtonUsageDPadLeft && usage <= JOYButtonUsageDPadDown) { + if (fauxAnalog && button.type == JOYButtonTypeAxes2DEmulated) { + // This isn't a real button, it's an emulated Axes2D. We want to handle it as an Axes2D instead + continue; } - } - else { - if (controller != lastController) { - [self setRumble:0]; - lastController = controller; + else { + // User used a digital direction input, revert to non-analog inputs + GB_set_use_faux_analog_inputs(effectiveGB, effectivePlayer, false); } } diff --git a/Cocoa/Preferences.xib b/Cocoa/Preferences.xib @@ -751,11 +751,11 @@ <point key="canvasLocation" x="-854" y="626.5"/> </customView> <customView id="8TU-6J-NCg"> - <rect key="frame" x="0.0" y="0.0" width="632" height="381"/> + <rect key="frame" x="0.0" y="0.0" width="632" height="409"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <subviews> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Utu-t4-cLx"> - <rect key="frame" x="18" y="346" width="122" height="17"/> + <rect key="frame" x="18" y="374" width="122" height="17"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Control settings for" id="YqW-Ds-VIC"> <font key="font" metaFont="system"/> @@ -764,7 +764,7 @@ </textFieldCell> </textField> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ReM-uo-H0r"> - <rect key="frame" x="227" y="346" width="8" height="17"/> + <rect key="frame" x="227" y="374" width="8" height="17"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title=":" id="VhO-3T-glt"> <font key="font" metaFont="system"/> @@ -773,7 +773,7 @@ </textFieldCell> </textField> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="gWx-7h-0xq"> - <rect key="frame" x="140" y="339" width="87" height="26"/> + <rect key="frame" x="140" y="367" width="87" height="26"/> <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"> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> @@ -792,7 +792,7 @@ </connections> </popUpButton> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="zYm-Ov-RHL"> - <rect key="frame" x="330" y="343" width="280" height="17"/> + <rect key="frame" x="330" y="371" width="280" height="17"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="When playing motion-control games:" id="sgY-3F-qld"> <font key="font" metaFont="system"/> @@ -801,7 +801,7 @@ </textFieldCell> </textField> <button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="i7F-1r-NkQ" customClass="GBPreferenceButton"> - <rect key="frame" x="342" y="319" width="272" height="18"/> + <rect key="frame" x="342" y="347" width="272" height="18"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/> <buttonCell key="cell" type="check" title="Prefer joysticks over motion controls" bezelStyle="regularSquare" imagePosition="left" lineBreakMode="charWrapping" inset="2" id="Szg-5G-j50"> <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> @@ -812,7 +812,7 @@ </userDefinedRuntimeAttributes> </button> <button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wUE-aB-ub1" customClass="GBPreferenceButton"> - <rect key="frame" x="342" y="299" width="268" height="18"/> + <rect key="frame" x="342" y="327" width="268" height="18"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/> <buttonCell key="cell" type="check" title="Allow mouse controls" bezelStyle="regularSquare" imagePosition="left" lineBreakMode="charWrapping" state="on" inset="2" id="z0R-FZ-LfD"> <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> @@ -823,7 +823,7 @@ </userDefinedRuntimeAttributes> </button> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DZu-ts-deW"> - <rect key="frame" x="330" y="166" width="280" height="17"/> + <rect key="frame" x="330" y="194" width="280" height="17"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Enable rumble:" id="QMX-3p-s1Z"> <font key="font" metaFont="system"/> @@ -832,7 +832,7 @@ </textFieldCell> </textField> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2SB-Bq-Exy"> - <rect key="frame" x="330" y="276" width="280" height="17"/> + <rect key="frame" x="330" y="304" width="280" height="17"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Controller “Hotkey 1†action:" id="GTq-aw-0cU"> <font key="font" metaFont="system"/> @@ -841,7 +841,7 @@ </textFieldCell> </textField> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="qsH-w5-Rja"> - <rect key="frame" x="342" y="243" width="262" height="26"/> + <rect key="frame" x="342" y="271" width="262" height="26"/> <autoresizingMask key="autoresizingMask" flexibleMinX="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="uFa-fO-lTm" id="x4X-Uv-vXf"> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> @@ -903,7 +903,7 @@ </connections> </popUpButton> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8py-43-vaO"> - <rect key="frame" x="330" y="221" width="280" height="17"/> + <rect key="frame" x="330" y="249" width="280" height="17"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Controller “Hotkey 2†action:" id="Gdp-S8-V3q"> <font key="font" metaFont="system"/> @@ -912,7 +912,7 @@ </textFieldCell> </textField> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ryk-mD-c0y"> - <rect key="frame" x="342" y="188" width="262" height="26"/> + <rect key="frame" x="342" y="216" width="262" height="26"/> <autoresizingMask key="autoresizingMask" flexibleMinX="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="wmV-YY-dHY" id="fS9-cS-iTD"> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> @@ -974,7 +974,7 @@ </connections> </popUpButton> <button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="RuW-Db-dzW" customClass="GBPreferenceButton"> - <rect key="frame" x="330" y="112" width="280" height="18"/> + <rect key="frame" x="330" y="140" width="280" height="18"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/> <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"/> @@ -996,7 +996,7 @@ </userDefinedRuntimeAttributes> </button> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ogs-xG-b4b" customClass="GBPreferencePopUpButton"> - <rect key="frame" x="342" y="133" width="262" height="26"/> + <rect key="frame" x="342" y="161" width="262" height="26"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/> <popUpButtonCell key="cell" type="push" title="Never" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="jki-7x-bnM" id="o9b-MH-8kd"> <behavior key="behavior" lightByBackground="YES" lightByGray="YES"/> @@ -1036,7 +1036,7 @@ </connections> </button> <box horizontalHuggingPriority="750" fixedFrame="YES" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="Bm0-C4-acq"> - <rect key="frame" x="314" y="20" width="5" height="344"/> + <rect key="frame" x="314" y="20" width="5" height="372"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" heightSizable="YES"/> </box> <button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uw7-6d-2xu"> @@ -1051,11 +1051,11 @@ </connections> </button> <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="87" width="262" height="249"/> + <rect key="frame" x="32" y="115" width="262" height="249"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <clipView key="contentView" focusRingType="none" drawsBackground="NO" id="AMs-PO-nid"> <rect key="frame" x="1" y="1" width="260" height="247"/> - <autoresizingMask key="autoresizingMask"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> <tableView focusRingType="none" verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnReordering="NO" columnResizing="NO" multipleSelection="NO" emptySelection="NO" autosaveColumns="NO" typeSelect="NO" id="UDd-IJ-fxX"> <rect key="frame" x="0.0" y="0.0" width="260" height="247"/> @@ -1107,7 +1107,7 @@ </scroller> </scrollView> <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="fcF-wc-KwM"> - <rect key="frame" x="30" y="62" width="231" height="17"/> + <rect key="frame" x="30" y="90" width="231" height="17"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Controller for multiplayer games:" id="AJA-9b-VKI"> <font key="font" metaFont="system"/> @@ -1116,7 +1116,7 @@ </textFieldCell> </textField> <popUpButton verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="0Az-0R-oNw"> - <rect key="frame" x="42" y="29" width="255" height="26"/> + <rect key="frame" x="42" y="57" width="255" height="26"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/> <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"/> @@ -1131,6 +1131,17 @@ <action selector="changeDefaultJoypad:" target="QvC-M9-y7g" id="TP2-Ug-Jpy"/> </connections> </popUpButton> + <button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Pk4-h9-QXi" customClass="GBPreferenceButton"> + <rect key="frame" x="330" y="116" width="280" height="18"/> + <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMinY="YES"/> + <buttonCell key="cell" type="check" title="Use joysticks as faux analog controls" bezelStyle="regularSquare" imagePosition="left" lineBreakMode="charWrapping" inset="2" id="JzD-wy-xaW"> + <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/> + <font key="font" metaFont="system"/> + </buttonCell> + <userDefinedRuntimeAttributes> + <userDefinedRuntimeAttribute type="string" keyPath="preferenceName" value="GBFauxAnalogInputs"/> + </userDefinedRuntimeAttributes> + </button> </subviews> <point key="canvasLocation" x="-1010" y="304"/> </customView> diff --git a/Core/gb.c b/Core/gb.c @@ -1205,6 +1205,7 @@ unsigned GB_run(GB_gameboy_t *gb) GB_cpu_run(gb); GB_clear_running_thread(gb); if (unlikely(gb->vblank_just_occured)) { + GB_update_faux_analog(gb); GB_debugger_handle_async_commands(gb); GB_set_running_thread(gb); GB_rewind_push(gb); diff --git a/Core/gb.h b/Core/gb.h @@ -678,6 +678,11 @@ struct GB_gameboy_internal_s { GB_color_correction_mode_t color_correction_mode; double light_temperature; bool keys[4][GB_KEY_MAX]; + bool use_faux_analog[4]; + struct { + int8_t x, y; + } faux_analog_inputs[4]; + uint8_t faux_analog_ticks; double accelerometer_x, accelerometer_y; GB_border_mode_t border_mode; GB_sgb_border_t borrowed_border; diff --git a/Core/joypad.c b/Core/joypad.c @@ -1,5 +1,6 @@ #include "gb.h" #include <assert.h> +#include <math.h> static inline bool should_bounce(GB_gameboy_t *gb) { @@ -21,6 +22,47 @@ static inline uint16_t bounce_for_key(GB_gameboy_t *gb, GB_key_t key) static inline bool get_input(GB_gameboy_t *gb, uint8_t player, GB_key_t key) { + if (gb->use_faux_analog[player] && key <= GB_KEY_DOWN) { + if (gb->keys[player][key]) return true; + uint8_t pattern = 0; + uint8_t index_in_pattern; + switch (key) { + // Most games only sample inputs in 30FPS, so we shift right once. + case GB_KEY_RIGHT: + if (gb->faux_analog_inputs[player].x <= 0) return false; + pattern = gb->faux_analog_inputs[player].x - 1; + index_in_pattern = gb->faux_analog_ticks; + break; + case GB_KEY_LEFT: + if (gb->faux_analog_inputs[player].x >= 0) return false; + pattern = -gb->faux_analog_inputs[player].x - 1; + index_in_pattern = gb->faux_analog_ticks; + break; + case GB_KEY_UP: + if (gb->faux_analog_inputs[player].y >= 0) return false; + pattern = -gb->faux_analog_inputs[player].y - 1; + index_in_pattern = gb->faux_analog_ticks + 2; + break; + case GB_KEY_DOWN: + if (gb->faux_analog_inputs[player].y <= 0) return false; + pattern = gb->faux_analog_inputs[player].y - 1; + index_in_pattern = gb->faux_analog_ticks + 2; + break; + nodefault; + } + if (pattern == 7) return true; + /* Dithering pattern */ + static const uint8_t patterns[] = { + 0x1, + 0x11, + 0x94, + 0x55, + 0x6d, + 0x77, + 0x7f + }; + return patterns[pattern] & (1 << (index_in_pattern & 6)); + } if (player != 0) { return gb->keys[player][key]; } @@ -170,7 +212,7 @@ void GB_set_key_mask_for_player(GB_gameboy_t *gb, GB_key_mask_t mask, unsigned p void GB_joypad_run(GB_gameboy_t *gb, unsigned cycles) { if (gb->joypad_is_stable) return; - bool should_update_joyp = false; + bool should_update_joyp = gb->use_faux_analog[gb->sgb? gb->sgb->current_player : 0]; gb->joypad_is_stable = true; if (gb->joyp_switching_delay) { gb->joypad_is_stable = false; @@ -229,3 +271,63 @@ void GB_set_update_input_hint_callback(GB_gameboy_t *gb, GB_update_input_hint_ca } gb->update_input_hint_callback = callback; } + +void GB_set_use_faux_analog_inputs(GB_gameboy_t *gb, unsigned player, bool use) +{ + if (gb->use_faux_analog[player] == use) return; + gb->use_faux_analog[player] = use; + for (GB_key_t key = GB_KEY_RIGHT; key <= GB_KEY_DOWN; key++) { + gb->keys[player][key] = false; + } + GB_update_joyp(gb); +} + +void GB_set_faux_analog_inputs(GB_gameboy_t *gb, unsigned player, double x, double y) +{ + + if (x > 1) x = 1; + else if (x < -1) x = -1; + if (y > 1) y = 1; + else if (y < -1) y = -1; + + double abs_x = fabs(x), abs_y = fabs(y); + if (abs_x <= 0.1) x = abs_x = 0; + if (abs_y <= 0.1) y = abs_y = 0; + if (!x && !y) { + gb->faux_analog_inputs[player].x = gb->faux_analog_inputs[player].y = 0; + } + else { + if (x) { + abs_x = (abs_x - 0.1) / 0.9; + x = x > 0? abs_x : -abs_x; + } + if (y) { + abs_y = (abs_y - 0.1) / 0.9; + y = y > 0? abs_y : -abs_y; + } + // Square the circle + double distance = MIN(sqrt(x * x + y * y), 1); + double multiplier = 8 * distance / MAX(abs_x, abs_y); + + gb->faux_analog_inputs[player].x = round(x * multiplier); + gb->faux_analog_inputs[player].y = round(y * multiplier); + } + GB_update_joyp(gb); +} + +void GB_update_faux_analog(GB_gameboy_t *gb) +{ + gb->faux_analog_ticks++; + for (unsigned i = 0; i < 4; i++) { + if (!gb->use_faux_analog[i]) continue; + if ((gb->faux_analog_inputs[i].x != 0 && + gb->faux_analog_inputs[i].x != 8 && + gb->faux_analog_inputs[i].x != -8) || + (gb->faux_analog_inputs[i].y != 0 && + gb->faux_analog_inputs[i].y != 8 && + gb->faux_analog_inputs[i].y != -8)) { + gb->joypad_is_stable = false; + return; + } + } +} diff --git a/Core/joypad.h b/Core/joypad.h @@ -37,8 +37,11 @@ void GB_clear_joyp_accessed(GB_gameboy_t *gb); void GB_set_allow_illegal_inputs(GB_gameboy_t *gb, bool allow); void GB_set_emulate_joypad_bouncing(GB_gameboy_t *gb, bool emulate); void GB_set_update_input_hint_callback(GB_gameboy_t *gb, GB_update_input_hint_callback_t callback); +void GB_set_use_faux_analog_inputs(GB_gameboy_t *gb, unsigned player, bool use); +void GB_set_faux_analog_inputs(GB_gameboy_t *gb, unsigned player, double x, double y); #ifdef GB_INTERNAL internal void GB_update_joyp(GB_gameboy_t *gb); internal void GB_joypad_run(GB_gameboy_t *gb, unsigned cycles); +internal void GB_update_faux_analog(GB_gameboy_t *gb); #endif diff --git a/SDL/configuration.h b/SDL/configuration.h @@ -160,6 +160,7 @@ typedef struct { /* v1.0.1 */ bool disable_rounded_corners; // Windows only + bool use_faux_analog_inputs; }; } configuration_t; diff --git a/SDL/gui.c b/SDL/gui.c @@ -1984,6 +1984,16 @@ static const char *current_rumble_mode(unsigned index) [configuration.rumble_mode]; } +static void toggle_use_faux_analog_inputs(unsigned index) +{ + configuration.use_faux_analog_inputs ^= true; +} + +static const char *current_faux_analog_inputs(unsigned index) +{ + return configuration.use_faux_analog_inputs? "Faux Analog" : "Digital"; +} + static void toggle_allow_background_controllers(unsigned index) { configuration.allow_background_controllers ^= true; @@ -2054,6 +2064,7 @@ static const struct menu_item joypad_menu[] = { {"Hotkey 1 Action:", cycle_hotkey, current_hotkey, cycle_hotkey_backwards}, {"Hotkey 2 Action:", cycle_hotkey, current_hotkey, cycle_hotkey_backwards}, {"Rumble Mode:", cycle_rumble_mode, current_rumble_mode, cycle_rumble_mode_backwards}, + {"Analog Stick Behavior:", toggle_use_faux_analog_inputs, current_faux_analog_inputs, toggle_use_faux_analog_inputs}, {"Enable Control:", toggle_allow_background_controllers, current_background_control_mode, toggle_allow_background_controllers}, {"Back", enter_controls_menu}, {NULL,} diff --git a/SDL/main.c b/SDL/main.c @@ -423,19 +423,25 @@ static void handle_events(GB_gameboy_t *gb) case SDL_JOYAXISMOTION: { static bool axis_active[2] = {false, false}; static double accel_values[2] = {0, 0}; + static double axis_values[2] = {0, 0}; joypad_axis_t axis = get_joypad_axis(event.jaxis.axis); if (axis == JOYPAD_AXISES_X) { if (GB_has_accelerometer(gb)) { accel_values[0] = event.jaxis.value / (double)32768.0; GB_set_accelerometer_values(gb, -accel_values[0], -accel_values[1]); } + else if (configuration.use_faux_analog_inputs) { + axis_values[0] = event.jaxis.value / (double)32768.0; + } else if (event.jaxis.value > JOYSTICK_HIGH) { axis_active[0] = true; + GB_set_use_faux_analog_inputs(gb, 0, false); GB_set_key_state(gb, GB_KEY_RIGHT, true); GB_set_key_state(gb, GB_KEY_LEFT, false); } else if (event.jaxis.value < -JOYSTICK_HIGH) { axis_active[0] = true; + GB_set_use_faux_analog_inputs(gb, 0, false); GB_set_key_state(gb, GB_KEY_RIGHT, false); GB_set_key_state(gb, GB_KEY_LEFT, true); } @@ -450,13 +456,18 @@ static void handle_events(GB_gameboy_t *gb) accel_values[1] = event.jaxis.value / (double)32768.0; GB_set_accelerometer_values(gb, -accel_values[0], -accel_values[1]); } + else if (configuration.use_faux_analog_inputs) { + axis_values[1] = event.jaxis.value / (double)32768.0; + } else if (event.jaxis.value > JOYSTICK_HIGH) { axis_active[1] = true; + GB_set_use_faux_analog_inputs(gb, 0, false); GB_set_key_state(gb, GB_KEY_DOWN, true); GB_set_key_state(gb, GB_KEY_UP, false); } else if (event.jaxis.value < -JOYSTICK_HIGH) { axis_active[1] = true; + GB_set_use_faux_analog_inputs(gb, 0, false); GB_set_key_state(gb, GB_KEY_DOWN, false); GB_set_key_state(gb, GB_KEY_UP, true); } @@ -466,8 +477,12 @@ static void handle_events(GB_gameboy_t *gb) GB_set_key_state(gb, GB_KEY_UP, false); } } - } + if (configuration.use_faux_analog_inputs && !GB_has_accelerometer(gb)) { + GB_set_use_faux_analog_inputs(gb, 0, true); + GB_set_faux_analog_inputs(gb, 0, axis_values[0], axis_values[1]); + } break; + } case SDL_JOYHATMOTION: { uint8_t value = event.jhat.value; @@ -476,6 +491,7 @@ static void handle_events(GB_gameboy_t *gb) int8_t leftright = value == SDL_HAT_LEFTUP || value == SDL_HAT_LEFT || value == SDL_HAT_LEFTDOWN ? -1 : (value == SDL_HAT_RIGHTUP || value == SDL_HAT_RIGHT || value == SDL_HAT_RIGHTDOWN ? 1 : 0); + GB_set_use_faux_analog_inputs(gb, 0, false); GB_set_key_state(gb, GB_KEY_LEFT, leftright == -1); GB_set_key_state(gb, GB_KEY_RIGHT, leftright == 1); GB_set_key_state(gb, GB_KEY_UP, updown == -1); @@ -598,6 +614,9 @@ static void handle_events(GB_gameboy_t *gb) else { for (unsigned i = 0; i < GB_KEY_MAX; i++) { if (event.key.keysym.scancode == configuration.keys[i]) { + if (i <= GB_KEY_DOWN) { + GB_set_use_faux_analog_inputs(gb, 0, false); + } GB_set_key_state(gb, i, event.type == SDL_KEYDOWN); } } diff --git a/iOS/GBBackgroundView.m b/iOS/GBBackgroundView.m @@ -320,6 +320,7 @@ static GB_key_mask_t angleToKeyMask(double angle) CGPoint point = [_padTouch locationInView:self]; double squaredDistance = CGPointSquaredDistance(point, _padSwipeOrigin); if (squaredDistance > 16 * 16) { + GB_set_use_faux_analog_inputs(_gbView.gb, 0, false); double angle = CGPointAngle(point, _padSwipeOrigin); mask |= angleToKeyMask(angle); if (squaredDistance > 24 * 24) { @@ -388,6 +389,7 @@ static GB_key_mask_t angleToKeyMask(double angle) fabs(point.y - _layout.dpadLocation.y) <= dpadRadius) ) && (fabs(point.x - _layout.dpadLocation.x) >= dpadRadius / 5 || fabs(point.y - _layout.dpadLocation.y) >= dpadRadius / 5)) { + GB_set_use_faux_analog_inputs(_gbView.gb, 0, false); dpadHandled = true; // Don't handle the dpad twice double angle = CGPointAngle(point, _layout.dpadLocation); mask |= angleToKeyMask(angle); diff --git a/iOS/GBSettingsViewController.m b/iOS/GBSettingsViewController.m @@ -326,17 +326,24 @@ static NSString const *typeLightTemp = @"typeLightTemp"; }, }, @{ + @"header": @"Enable Rumble", + @"items": @[ + @{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"Never", @"value": @(GB_RUMBLE_DISABLED),}, + @{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"For Rumble-Enabled Game Paks", @"value": @(GB_RUMBLE_CARTRIDGE_ONLY),}, + @{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"Always", @"value": @(GB_RUMBLE_ALL_GAMES),}, + ], + }, + @{ @"items": @[ @{@"type": typeCheck, @"pref": @"GBControllersHideInterface", @"title": @"Hide UI While Using a Controller"}, ], @"footer": @"When enabled, the on-screen user interface will be hidden while a game controller is being used." }, @{ - @"header": @"Enable Rumble", + @"header": @"Controller Joystick Behavior", @"items": @[ - @{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"Never", @"value": @(GB_RUMBLE_DISABLED),}, - @{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"For Rumble-Enabled Game Paks", @"value": @(GB_RUMBLE_CARTRIDGE_ONLY),}, - @{@"type": typeRadio, @"pref": @"GBRumbleMode", @"title": @"Always", @"value": @(GB_RUMBLE_ALL_GAMES),}, + @{@"type": typeRadio, @"pref": @"GBFauxAnalogInputs", @"title": @"Digital", @"value": @NO}, + @{@"type": typeRadio, @"pref": @"GBFauxAnalogInputs", @"title": @"Faux Analog", @"value": @YES}, ], }, @{ @@ -349,6 +356,7 @@ static NSString const *typeLightTemp = @"typeLightTemp"; } ], }, + ]; diff --git a/iOS/GBViewController.m b/iOS/GBViewController.m @@ -519,6 +519,7 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) case GBLeft: case GBUp: case GBDown: + GB_set_use_faux_analog_inputs(&_gb, 0, false); case GBA: case GBB: case GBSelect: @@ -590,6 +591,11 @@ static void rumbleCallback(GB_gameboy_t *gb, double amp) _backgroundView.fullScreenMode = true; } + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBFauxAnalogInputs"]) { + GB_set_use_faux_analog_inputs(&_gb, 0, true); + GB_set_faux_analog_inputs(&_gb, 0, axis.right.value - axis.left.value, axis.down.value - axis.up.value); + } + GB_set_key_state(&_gb, GB_KEY_LEFT, left); GB_set_key_state(&_gb, GB_KEY_RIGHT, right); GB_set_key_state(&_gb, GB_KEY_UP, up); diff --git a/iOS/main.m b/iOS/main.m @@ -25,6 +25,7 @@ int main(int argc, char * argv[]) @"GBTurboSpeed": @1, @"GBRewindSpeed": @1, @"GBDynamicSpeed": @NO, + @"GBFauxAnalogInputs": @NO, @"GBInterfaceTheme": @"SameBoy", @"GBControllersHideInterface": @YES,

This webpage is intended to be an accessible preview of this repository. To get a fuller picture, clone it and use the git CLI.