/* Copyright (c) 2024 Alex Diener 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. Alex Diener alex@ludobloom.com */ #include "inputcontroller/GamepadLogicalInputController.h" #include #include #define stemobject_implementation GamepadLogicalInputController v_begin(); v_func(dispose); v_end(); static Atom buttonDownEventAtom, buttonUpEventAtom, axisActivateEventAtom, axisReleaseEventAtom, axisMotionEventAtom; GamepadLogicalInputController * GamepadLogicalInputController_create(GamepadMap * gamepadMap) { stemobject_create_implementation(init, gamepadMap) } bool GamepadLogicalInputController_init(GamepadLogicalInputController * self, GamepadMap * gamepadMap) { call_super(init, self); self->gamepadMap = gamepadMap; self->eventDispatcher = EventDispatcher_create(); self->analogStickActionActivateThreshold = 0.75f; self->analogStickActionReleaseThreshold = 0.625f; self->analogStickInnerDeadZone = 0.0f; self->analogStickOuterDeadZone = 0.0f; self->analogTriggerActionActivateThreshold = -0.75f; self->analogTriggerActionReleaseThreshold = -0.875f; self->analogTriggerInnerDeadZone = 0.0f; self->analogTriggerOuterDeadZone = 0.0f; return true; } void GamepadLogicalInputController_dispose(GamepadLogicalInputController * self) { EventDispatcher_dispose(self->eventDispatcher); call_super_virtual(dispose, self); } static GamepadElement getEquivalentAxisElement(GamepadElement elementID, int * outDirection) { switch (elementID) { case GAMEPAD_DPAD_LEFT: *outDirection = -1; return GAMEPAD_DPAD_X; case GAMEPAD_DPAD_RIGHT: *outDirection = 1; return GAMEPAD_DPAD_X; case GAMEPAD_DPAD_DOWN: *outDirection = -1; return GAMEPAD_DPAD_Y; case GAMEPAD_DPAD_UP: *outDirection = 1; return GAMEPAD_DPAD_Y; case GAMEPAD_LEFT_STICK_LEFT: *outDirection = -1; return GAMEPAD_LEFT_STICK_X; case GAMEPAD_LEFT_STICK_RIGHT: *outDirection = 1; return GAMEPAD_LEFT_STICK_X; case GAMEPAD_LEFT_STICK_DOWN: *outDirection = -1; return GAMEPAD_LEFT_STICK_Y; case GAMEPAD_LEFT_STICK_UP: *outDirection = 1; return GAMEPAD_LEFT_STICK_Y; case GAMEPAD_RIGHT_STICK_LEFT: *outDirection = -1; return GAMEPAD_RIGHT_STICK_X; case GAMEPAD_RIGHT_STICK_RIGHT: *outDirection = 1; return GAMEPAD_RIGHT_STICK_X; case GAMEPAD_RIGHT_STICK_DOWN: *outDirection = -1; return GAMEPAD_RIGHT_STICK_Y; case GAMEPAD_RIGHT_STICK_UP: *outDirection = 1; return GAMEPAD_RIGHT_STICK_Y; case GAMEPAD_LEFT_BACK_SHOULDER: *outDirection = -1; return GAMEPAD_BOTH_BACK_SHOULDERS; case GAMEPAD_RIGHT_BACK_SHOULDER: *outDirection = 1; return GAMEPAD_BOTH_BACK_SHOULDERS; default: return GAMEPAD_UNKNOWN; } } bool GamepadLogicalInputController_gamepadButtonDown(GamepadLogicalInputController * self, struct Gamepad_device * device, unsigned int buttonID, double timestamp, bool peek) { bool handled = false; GamepadElement elementID = GamepadMap_getElementForHardwareID(self->gamepadMap, device->vendorID, device->productID, GAMEPAD_ELEMENT_BUTTON, buttonID); if (elementID != GAMEPAD_UNKNOWN) { if (buttonDownEventAtom == NULL) { buttonDownEventAtom = ATOM(GAMEPAD_CONTROLLER_EVENT_BUTTON_DOWN); } GamepadLogicalInputController_buttonEvent buttonEventData = {elementID, device, elementID, timestamp, peek}; handled |= EventDispatcher_dispatchEvent(self->eventDispatcher, buttonDownEventAtom, &buttonEventData); int direction = 0; GamepadElement equivalentAxisElement = getEquivalentAxisElement(elementID, &direction); if (equivalentAxisElement != GAMEPAD_UNKNOWN) { GamepadLogicalInputController_axisEvent axisEventData = {equivalentAxisElement, device, UINT_MAX, direction, direction, direction, 0, timestamp, peek}; if (axisActivateEventAtom == NULL) { axisActivateEventAtom = ATOM(GAMEPAD_CONTROLLER_EVENT_AXIS_ACTIVATE); } handled |= EventDispatcher_dispatchEvent(self->eventDispatcher, axisActivateEventAtom, &axisEventData); } } return handled; } bool GamepadLogicalInputController_gamepadButtonUp(GamepadLogicalInputController * self, struct Gamepad_device * device, unsigned int buttonID, double timestamp, bool peek) { bool handled = false; GamepadElement elementID = GamepadMap_getElementForHardwareID(self->gamepadMap, device->vendorID, device->productID, GAMEPAD_ELEMENT_BUTTON, buttonID); if (elementID != GAMEPAD_UNKNOWN) { if (buttonUpEventAtom == NULL) { buttonUpEventAtom = ATOM(GAMEPAD_CONTROLLER_EVENT_BUTTON_UP); } GamepadLogicalInputController_buttonEvent buttonEventData = {elementID, device, elementID, timestamp, peek}; handled |= EventDispatcher_dispatchEvent(self->eventDispatcher, buttonUpEventAtom, &buttonEventData); int direction = 0; GamepadElement equivalentAxisElement = getEquivalentAxisElement(elementID, &direction); if (equivalentAxisElement != GAMEPAD_UNKNOWN) { GamepadLogicalInputController_axisEvent axisEventData = {equivalentAxisElement, device, UINT_MAX, direction, 0, 0, direction, timestamp, peek}; if (axisReleaseEventAtom == NULL) { axisReleaseEventAtom = ATOM(GAMEPAD_CONTROLLER_EVENT_AXIS_RELEASE); } handled |= EventDispatcher_dispatchEvent(self->eventDispatcher, axisReleaseEventAtom, &axisEventData); } } return handled; } static GamepadElement getEquivalentButtonElement(GamepadElement elementID, int direction) { switch (elementID) { case GAMEPAD_DPAD_X: if (direction < 0) { return GAMEPAD_DPAD_LEFT; } return GAMEPAD_DPAD_RIGHT; case GAMEPAD_DPAD_Y: if (direction < 0) { return GAMEPAD_DPAD_DOWN; } return GAMEPAD_DPAD_UP; case GAMEPAD_LEFT_STICK_X: if (direction < 0) { return GAMEPAD_LEFT_STICK_LEFT; } return GAMEPAD_LEFT_STICK_RIGHT; case GAMEPAD_LEFT_STICK_Y: if (direction < 0) { return GAMEPAD_LEFT_STICK_DOWN; } return GAMEPAD_LEFT_STICK_UP; case GAMEPAD_RIGHT_STICK_X: if (direction < 0) { return GAMEPAD_RIGHT_STICK_LEFT; } return GAMEPAD_RIGHT_STICK_RIGHT; case GAMEPAD_RIGHT_STICK_Y: if (direction < 0) { return GAMEPAD_RIGHT_STICK_DOWN; } return GAMEPAD_RIGHT_STICK_UP; case GAMEPAD_BOTH_BACK_SHOULDERS: if (direction < 0) { return GAMEPAD_LEFT_BACK_SHOULDER; } return GAMEPAD_RIGHT_BACK_SHOULDER; default: return GAMEPAD_UNKNOWN; } } static bool isBidirectionalAxisElement(GamepadElement elementID) { switch (elementID) { case GAMEPAD_DPAD_X: case GAMEPAD_DPAD_Y: case GAMEPAD_LEFT_STICK_X: case GAMEPAD_LEFT_STICK_Y: case GAMEPAD_RIGHT_STICK_X: case GAMEPAD_RIGHT_STICK_Y: case GAMEPAD_BOTH_BACK_SHOULDERS: return true; default: return false; } return false; } bool GamepadLogicalInputController_gamepadAxisMoved(GamepadLogicalInputController * self, struct Gamepad_device * device, unsigned int axisID, float value, float lastValue, double timestamp, bool peek) { bool handled = false; GamepadElement elementID = GamepadMap_getElementForHardwareID(self->gamepadMap, device->vendorID, device->productID, GAMEPAD_ELEMENT_AXIS, axisID); if (elementID != GAMEPAD_UNKNOWN) { if (axisActivateEventAtom == NULL) { axisActivateEventAtom = ATOM(GAMEPAD_CONTROLLER_EVENT_AXIS_ACTIVATE); } if (axisReleaseEventAtom == NULL) { axisReleaseEventAtom = ATOM(GAMEPAD_CONTROLLER_EVENT_AXIS_RELEASE); } if (axisMotionEventAtom == NULL) { axisMotionEventAtom = ATOM(GAMEPAD_CONTROLLER_EVENT_AXIS_MOTION); } if (buttonDownEventAtom == NULL) { buttonDownEventAtom = ATOM(GAMEPAD_CONTROLLER_EVENT_BUTTON_DOWN); } if (buttonUpEventAtom == NULL) { buttonUpEventAtom = ATOM(GAMEPAD_CONTROLLER_EVENT_BUTTON_UP); } int sign = GamepadMap_getAxisSignForElement(self->gamepadMap, device->vendorID, device->productID, elementID); float activateThreshold, releaseThreshold, innerDeadZone, outerDeadZone; bool bidirectional = isBidirectionalAxisElement(elementID); if (bidirectional) { activateThreshold = self->analogStickActionActivateThreshold; releaseThreshold = self->analogStickActionReleaseThreshold; innerDeadZone = self->analogStickInnerDeadZone; outerDeadZone = self->analogStickOuterDeadZone; } else { activateThreshold = self->analogTriggerActionActivateThreshold; releaseThreshold = self->analogTriggerActionReleaseThreshold; innerDeadZone = self->analogTriggerInnerDeadZone; outerDeadZone = self->analogTriggerOuterDeadZone; } float deadZoneAdjustedValue = value * sign; if (deadZoneAdjustedValue < 0.0f) { deadZoneAdjustedValue = fmaxf(-1.0f, fminf(0.0f, deadZoneAdjustedValue + innerDeadZone) / (1.0f - outerDeadZone - innerDeadZone)); } else { deadZoneAdjustedValue = fminf(1.0f, fmaxf(0.0f, deadZoneAdjustedValue - innerDeadZone) / (1.0f - outerDeadZone - innerDeadZone)); } GamepadLogicalInputController_axisEvent axisEventData = {elementID, device, axisID, 1, value * sign, deadZoneAdjustedValue, lastValue * sign, timestamp, peek}; if (lastValue * sign < activateThreshold && value * sign >= activateThreshold) { handled |= EventDispatcher_dispatchEvent(self->eventDispatcher, axisActivateEventAtom, &axisEventData); } else if (lastValue * sign > releaseThreshold && value * sign <= releaseThreshold) { handled |= EventDispatcher_dispatchEvent(self->eventDispatcher, axisReleaseEventAtom, &axisEventData); } if (bidirectional) { GamepadLogicalInputController_axisEvent reverseAxisEventData = axisEventData; reverseAxisEventData.sign = -1; if (lastValue * -sign < activateThreshold && value * -sign >= activateThreshold) { handled |= EventDispatcher_dispatchEvent(self->eventDispatcher, axisActivateEventAtom, &reverseAxisEventData); } else if (lastValue * -sign > releaseThreshold && value * -sign <= releaseThreshold) { handled |= EventDispatcher_dispatchEvent(self->eventDispatcher, axisReleaseEventAtom, &reverseAxisEventData); } } handled |= EventDispatcher_dispatchEvent(self->eventDispatcher, axisMotionEventAtom, &axisEventData); for (int direction = -1; direction <= 1; direction += 2) { GamepadElement equivalentButtonElement = getEquivalentButtonElement(elementID, direction); if (equivalentButtonElement != GAMEPAD_UNKNOWN) { if (lastValue * sign * direction < activateThreshold && value * sign * direction >= activateThreshold) { GamepadLogicalInputController_buttonEvent buttonEventData = {equivalentButtonElement, device, UINT_MAX, timestamp, peek}; handled |= EventDispatcher_dispatchEvent(self->eventDispatcher, buttonDownEventAtom, &buttonEventData); } else if (lastValue * sign * direction > releaseThreshold && value * sign * direction <= releaseThreshold) { GamepadLogicalInputController_buttonEvent buttonEventData = {equivalentButtonElement, device, UINT_MAX, timestamp, peek}; handled |= EventDispatcher_dispatchEvent(self->eventDispatcher, buttonUpEventAtom, &buttonEventData); } } } } return handled; }