#include "mungal4.h" #pragma hdrstop #include "l4padrio.h" #include "l4keylight.h" #include "l4joy.h" #include #pragma comment(lib, "xinput9_1_0.lib") //######################################################################## // Input helpers; the binding tables live in bindings.txt now // (l4padbindings.cpp writes and parses the vRIO-format profile) //######################################################################## namespace { Scalar StickValue(int raw, int dead_zone) { if (raw > -dead_zone && raw < dead_zone) { return (Scalar) 0; } Scalar value = (raw > 0) ? (Scalar)(raw - dead_zone) / (Scalar)(32767 - dead_zone) : (Scalar)(raw + dead_zone) / (Scalar)(32768 - dead_zone); if (value > 1.0f) value = 1.0f; if (value < -1.0f) value = -1.0f; return value; } Scalar Clamp01(Scalar value) { if (value < 0.0f) return 0.0f; if (value > 1.0f) return 1.0f; return value; } Logical KeyDown(int virtual_key) { return (GetAsyncKeyState(virtual_key) & 0x8000) != 0; } // // A generic stick axis is already normalized -1..1, so the deadzone // is a plain cut about centre with the remainder rescaled - press // just past the edge and you get just past zero, not a step. // Scalar JoyAxisValue(Scalar raw, Scalar deadzone) { if (deadzone <= 0.0f) { return raw; } if (raw > -deadzone && raw < deadzone) { return (Scalar) 0; } Scalar value = (raw > 0.0f) ? (raw - deadzone) / (1.0f - deadzone) : (raw + deadzone) / (1.0f - deadzone); if (value > 1.0f) value = 1.0f; if (value < -1.0f) value = -1.0f; return value; } // // A POV hat reports centidegrees clockwise from up, or -1 centered. // The 45-degree window each way is what makes the diagonals press // both of their neighbours, which is how a four-way hat is read. // Logical JoyHatHeld(int centidegrees, int direction) { if (centidegrees < 0) { return False; } int degrees = (centidegrees / 100) % 360; switch (direction) { case 0: return (degrees >= 315 || degrees <= 45) ? True : False; case 1: return (degrees >= 45 && degrees <= 135) ? True : False; case 2: return (degrees >= 135 && degrees <= 225) ? True : False; case 3: return (degrees >= 225 && degrees <= 315) ? True : False; } return False; } void KeyLightLog(const char *line) { DEBUG_STREAM << line << "\n" << std::flush; } } //######################################################################## //############################### PadRIO ################################# //######################################################################## PadRIO *PadRIO::activeInstance = NULL; void PadRIO::SetScreenButton(int unit, Logical pressed) { if (activeInstance != NULL && unit >= 0 && unit < buttonUnits) { activeInstance->screenButton[unit] = pressed ? 1 : 0; } } int PadRIO::GetLampState(int unit) { if (activeInstance != NULL && unit >= 0 && unit < lampCount) { return activeInstance->lampState[unit]; } return 0; } PadRIO::PadRIO() { Check_Pointer(this); queueHead = 0; queueTail = 0; lastPollTick = GetTickCount(); lastPadCheckTick = 0; padIndex = -1; padReported = False; analogRequested = False; throttleAccum = (Scalar) 0; sentThrottle = sentLeftPedal = sentRightPedal = (Scalar) 0; sentJoystickX = sentJoystickY = (Scalar) 0; memset(buttonDown, 0, sizeof(buttonDown)); memset(keypadDown, 0, sizeof(keypadDown)); memset(lampState, 0, sizeof(lampState)); memset(screenButton, 0, sizeof(screenButton)); PadBindings_Load(&profile); // // RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound // to lamp addresses glow with the panel. Yellow = the Secondary / // Screen columns (0x10-0x1F), red = everything else, exactly like // the physical panel and vRIO. RP412KEYLIGHT=0 opts out. // keyLightActive = False; const char *keylight = getenv("RP412KEYLIGHT"); if (keylight == NULL || atoi(keylight) != 0) { int light_keys[PadBindingProfile::maxKeyButtons]; int light_addresses[PadBindingProfile::maxKeyButtons]; unsigned char light_yellow[PadBindingProfile::maxKeyButtons]; int light_count = 0; for (int i = 0; i < profile.keyButtonCount; ++i) { int address = profile.keyButtons[i].address; if (address >= buttonUnits) { continue; // keypads have no lamps } Logical duplicate = False; for (int j = 0; j < light_count; ++j) { if (light_keys[j] == profile.keyButtons[i].virtualKey) { duplicate = True; // first binding wins break; } } if (duplicate) { continue; } light_keys[light_count] = profile.keyButtons[i].virtualKey; light_addresses[light_count] = address; light_yellow[light_count] = (address >= 0x10 && address <= 0x1F) ? 1 : 0; ++light_count; } if (light_count > 0) { KeyLight_SetLogger(&KeyLightLog); KeyLight_SetMap(light_keys, light_addresses, light_yellow, light_count); KeyLight_Start(); keyLightActive = True; } } invertX = False; invertY = False; const char *flip = getenv("L4PADFLIP"); if (flip != NULL) { if (strchr(flip, 'X') || strchr(flip, 'x')) { invertX = True; } if (strchr(flip, 'Y') || strchr(flip, 'y')) { invertY = True; } } // Report as a v4.2 board, like vRIO does MajorRevision = 4; MinorRevision = 2; activeInstance = this; DEBUG_STREAM << "PadRIO: virtual RIO active (XInput pad + keyboard)\n" << std::flush; // // Only open DirectInput when the profile actually asks for it. A // player on keyboard and pad should not pay for an enumeration of // every HID on the machine, and joyconfig.bat is what writes the // rows that turn this on. // if (profile.joyAxisCount > 0 || profile.joyButtonCount > 0 || profile.joyHatCount > 0) { int found = RPJoyInit(); DEBUG_STREAM << "PadRIO: joystick bindings present, " << found << " generic device(s) attached\n" << std::flush; for (int d = 0; d < found; ++d) { const RPJoyDeviceState *state = RPJoyDevice(d); if (state != NULL) { DEBUG_STREAM << "PadRIO: [" << d << "] " << state->name << "\n" << std::flush; } } } } PadRIO::~PadRIO() { Check_Pointer(this); if (keyLightActive) { KeyLight_Stop(); keyLightActive = False; } if (activeInstance == this) { activeInstance = NULL; } } Logical PadRIO::TestInstance() const { return True; } //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // The controls manager drains events every frame; sampling lives here so // button latency does not depend on the analog request cadence (which is // 15 s outside of missions). //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Logical PadRIO::GetNextEvent(RIOEvent *destinationPointer) { Check_Pointer(this); Check_Pointer(destinationPointer); PollInputs(); if (queueTail == queueHead) { return False; } *destinationPointer = eventQueue[queueTail]; queueTail = (queueTail + 1) % queueSize; return True; } void PadRIO::RequestAnalogUpdate() { Check_Pointer(this); analogRequested = True; } void PadRIO::GeneralReset() { Check_Pointer(this); throttleAccum = (Scalar) 0; Throttle = (Scalar) 0; LeftPedal = (Scalar) 0; RightPedal = (Scalar) 0; JoystickX = (Scalar) 0; JoystickY = (Scalar) 0; analogRequested = True; memset(lampState, 0, sizeof(lampState)); if (keyLightActive) { KeyLight_UpdateLamps(lampState, lampCount); } memset(keypadDown, 0, sizeof(keypadDown)); for (int i = 0; i < profile.keyButtonCount; ++i) { profile.keyButtons[i].latched = False; profile.keyButtons[i].wasDown = False; } for (int i = 0; i < profile.padButtonCount; ++i) { profile.padButtons[i].latched = False; profile.padButtons[i].wasDown = False; } for (int i = 0; i < profile.joyButtonCount; ++i) { profile.joyButtons[i].latched = False; profile.joyButtons[i].wasDown = False; } } void PadRIO::ResetThrottle() { Check_Pointer(this); throttleAccum = (Scalar) 0; Throttle = (Scalar) 0; analogRequested = True; } void PadRIO::SetLamp(int lampNumber, int state) { Check_Pointer(this); if (lampNumber >= 0 && lampNumber < lampCount) { lampState[lampNumber] = (unsigned char) state; if (keyLightActive) { KeyLight_UpdateLamps(lampState, lampCount); } } } void PadRIO::QueueEvent(const RIOEvent &an_event) { int next = (queueHead + 1) % queueSize; if (next == queueTail) { // full: drop the oldest event queueTail = (queueTail + 1) % queueSize; } eventQueue[queueHead] = an_event; queueHead = next; } void PadRIO::PollInputs() { unsigned long now = GetTickCount(); if (now - lastPollTick < 10) { return; } Scalar delta_t = (Scalar)(now - lastPollTick) / 1000.0f; if (delta_t > 0.25f) { delta_t = 0.25f; } lastPollTick = now; //--------------------------------------------------------------- // Find / keep the XInput pad. Probing empty slots is slow, so an // absent pad is only re-probed every 3 seconds. //--------------------------------------------------------------- XINPUT_STATE pad; memset(&pad, 0, sizeof(pad)); Logical pad_live = False; if (padIndex >= 0) { pad_live = (XInputGetState((DWORD) padIndex, &pad) == ERROR_SUCCESS); if (!pad_live) { DEBUG_STREAM << "PadRIO: controller " << padIndex << " disconnected\n" << std::flush; padIndex = -1; } } if (padIndex < 0 && (now - lastPadCheckTick) >= 3000) { lastPadCheckTick = now; for (DWORD i = 0; i < 4; ++i) { if (XInputGetState(i, &pad) == ERROR_SUCCESS) { padIndex = (int) i; pad_live = True; DEBUG_STREAM << "PadRIO: controller " << padIndex << " connected\n" << std::flush; break; } } if (padIndex < 0 && !padReported) { padReported = True; DEBUG_STREAM << "PadRIO: no controller found - keyboard only\n" << std::flush; } } //--------------------------------------------------------------- // Buttons: build the desired state from the binding profile // (keyboard + pad, with toggle latches), merge the on-screen // cockpit buttons, then diff against what we last reported. // Keypad addresses (0x50-0x6F) collect separately - they become // arcade KeyEvents, not button events. //--------------------------------------------------------------- unsigned char desired[buttonUnits]; unsigned char keypadDesired[keypadUnits]; memset(desired, 0, sizeof(desired)); memset(keypadDesired, 0, sizeof(keypadDesired)); for (int i = 0; i < profile.keyButtonCount; ++i) { PadKeyButtonBinding *binding = &profile.keyButtons[i]; Logical down = KeyDown(binding->virtualKey); if (binding->toggle && down && !binding->wasDown) { binding->latched = !binding->latched; } binding->wasDown = down; if (binding->toggle ? binding->latched : down) { if (binding->address < buttonUnits) { desired[binding->address] = 1; } else if (binding->address >= 0x50 && binding->address < 0x50 + keypadUnits) { keypadDesired[binding->address - 0x50] = 1; } } } for (int i = 0; i < profile.padButtonCount; ++i) { PadPadButtonBinding *binding = &profile.padButtons[i]; Logical down = pad_live && (pad.Gamepad.wButtons & binding->padMask) != 0; if (binding->toggle && down && !binding->wasDown) { binding->latched = !binding->latched; } binding->wasDown = down; if (binding->toggle ? binding->latched : down) { if (binding->address < buttonUnits) { desired[binding->address] = 1; } else if (binding->address >= 0x50 && binding->address < 0x50 + keypadUnits) { keypadDesired[binding->address - 0x50] = 1; } } } //--------------------------------------------------------------- // Generic joysticks. The slots are resolved every poll rather than // cached, so a stick unplugged mid-race simply stops answering and // one plugged back in picks up where it left off. //--------------------------------------------------------------- int joyDevice[BindJoyDeviceSlots]; Logical joyLive = False; for (int slot = 0; slot < BindJoyDeviceSlots; ++slot) { joyDevice[slot] = -1; } if (profile.joyAxisCount > 0 || profile.joyButtonCount > 0 || profile.joyHatCount > 0) { RPJoyPoll(); for (int slot = 0; slot < BindJoyDeviceSlots; ++slot) { joyDevice[slot] = (profile.joyDeviceMatch[slot][0] != '\0') ? RPJoyFindDevice(profile.joyDeviceMatch[slot]) : ((RPJoyDevice(slot) != NULL) ? slot : -1); if (joyDevice[slot] >= 0) { joyLive = True; } } } for (int i = 0; i < profile.joyButtonCount; ++i) { PadJoyButtonBinding *binding = &profile.joyButtons[i]; const RPJoyDeviceState *state = (binding->device >= 0 && binding->device < BindJoyDeviceSlots) ? RPJoyDevice(joyDevice[binding->device]) : NULL; Logical down = (state != NULL) && (state->buttons & (1u << binding->button)) != 0; if (binding->toggle && down && !binding->wasDown) { binding->latched = !binding->latched; } binding->wasDown = down; if (binding->toggle ? binding->latched : down) { if (binding->address < buttonUnits) { desired[binding->address] = 1; } else if (binding->address >= 0x50 && binding->address < 0x50 + keypadUnits) { keypadDesired[binding->address - 0x50] = 1; } } } for (int i = 0; i < profile.joyHatCount; ++i) { const PadJoyHatBinding *binding = &profile.joyHats[i]; const RPJoyDeviceState *state = (binding->device >= 0 && binding->device < BindJoyDeviceSlots) ? RPJoyDevice(joyDevice[binding->device]) : NULL; if (state != NULL && JoyHatHeld(state->hat[binding->hat], binding->direction)) { if (binding->address < buttonUnits) { desired[binding->address] = 1; } else if (binding->address >= 0x50 && binding->address < 0x50 + keypadUnits) { keypadDesired[binding->address - 0x50] = 1; } } } for (int i = 0; i < buttonUnits; ++i) { if (screenButton[i]) { desired[i] = 1; } } for (int unit = 0; unit < buttonUnits; ++unit) { if (desired[unit] != buttonDown[unit]) { buttonDown[unit] = desired[unit]; RIOEvent an_event; an_event.Type = desired[unit] ? ButtonPressedEvent : ButtonReleasedEvent; an_event.Data.Unit = unit; QueueEvent(an_event); } } //--------------------------------------------------------------- // Keypads: presses become the arcade RIO KeyEvents. Unit 0 is the // pilot's internal keypad (0x50-0x5F), unit 1 the external // operator keypad (0x60-0x6F); the key is the hex digit 0-15. //--------------------------------------------------------------- for (int pad_key = 0; pad_key < keypadUnits; ++pad_key) { if (keypadDesired[pad_key] != keypadDown[pad_key]) { keypadDown[pad_key] = keypadDesired[pad_key]; if (keypadDesired[pad_key]) { RIOEvent an_event; an_event.Type = KeyEvent; an_event.Data.Keyboard.Unit = (pad_key >= 0x10) ? 1 : 0; an_event.Data.Keyboard.Key = pad_key & 0x0F; QueueEvent(an_event); } } } //--------------------------------------------------------------- // Axes, from the profile. 'deflect' sources sum into a springy // position; 'rate' sources integrate the throttle (the pod's only // sticky axis) by value per second. //--------------------------------------------------------------- Scalar deflect[BindAxisCount]; Scalar rate[BindAxisCount]; memset(deflect, 0, sizeof(deflect)); memset(rate, 0, sizeof(rate)); for (int i = 0; i < profile.keyAxisCount; ++i) { const PadKeyAxisBinding *binding = &profile.keyAxes[i]; if (KeyDown(binding->virtualKey)) { if (binding->mode == BindKeyRate) { rate[binding->axis] += binding->value; } else { deflect[binding->axis] += binding->value; } } } if (pad_live) { for (int i = 0; i < profile.padAxisCount; ++i) { const PadPadAxisBinding *binding = &profile.padAxes[i]; Scalar value = (Scalar) 0; switch (binding->source) { case BindPadLeftStickX: value = StickValue(pad.Gamepad.sThumbLX, (int)(binding->deadzone * 32767.0f)); break; case BindPadLeftStickY: value = StickValue(pad.Gamepad.sThumbLY, (int)(binding->deadzone * 32767.0f)); break; case BindPadRightStickX: value = StickValue(pad.Gamepad.sThumbRX, (int)(binding->deadzone * 32767.0f)); break; case BindPadRightStickY: value = StickValue(pad.Gamepad.sThumbRY, (int)(binding->deadzone * 32767.0f)); break; case BindPadLeftTrigger: value = (Scalar)(pad.Gamepad.bLeftTrigger) / 255.0f; if (value <= binding->deadzone) value = (Scalar) 0; break; case BindPadRightTrigger: value = (Scalar)(pad.Gamepad.bRightTrigger) / 255.0f; if (value <= binding->deadzone) value = (Scalar) 0; break; } if (binding->invert) { value = -value; } if (binding->rate > 0.0f) { rate[binding->axis] += value * binding->rate; } else { deflect[binding->axis] += value; } } } //--------------------------------------------------------------- // Joystick axes. A physical throttle lever is the one source that // does not add into the pile: it has an absolute position, so its // full travel IS the channel and it takes ownership rather than // nudging an accumulator that a spring-centred pad stick has to. //--------------------------------------------------------------- Logical throttleLever = False; Scalar throttleLeverValue = (Scalar) 0; if (joyLive) { for (int i = 0; i < profile.joyAxisCount; ++i) { const PadJoyAxisBinding *binding = &profile.joyAxes[i]; if (binding->device < 0 || binding->device >= BindJoyDeviceSlots) { continue; } const RPJoyDeviceState *state = RPJoyDevice(joyDevice[binding->device]); if (state == NULL) { continue; } Scalar raw = (Scalar) state->axis[binding->source]; if (binding->invert) { raw = -raw; } if (binding->axis == BindAxisThrottle && binding->rate == 0.0f) { // -1..1 of lever travel onto the 0..1 the pod runs on throttleLeverValue = (raw + 1.0f) * 0.5f; throttleLever = True; continue; } Scalar value = JoyAxisValue(raw, binding->deadzone); if (binding->rate > 0.0f) { rate[binding->axis] += value * binding->rate; } else { deflect[binding->axis] += value; } } } // // The composite pedal axis becomes the pair the pod actually has. // One signed source presses one pedal or the other, never both, // which is what a rudder bar or a twist grip does. // Scalar pedals = deflect[BindAxisPedals]; if (pedals > 0.0f) { deflect[BindAxisRightPedal] += pedals; } else if (pedals < 0.0f) { deflect[BindAxisLeftPedal] += -pedals; } throttleAccum = Clamp01(throttleAccum + rate[BindAxisThrottle] * delta_t); Scalar x = deflect[BindAxisJoystickX]; Scalar y = deflect[BindAxisJoystickY]; if (x > 1.0f) x = 1.0f; if (x < -1.0f) x = -1.0f; if (y > 1.0f) y = 1.0f; if (y < -1.0f) y = -1.0f; Throttle = throttleLever ? Clamp01(throttleLeverValue) : Clamp01(throttleAccum + deflect[BindAxisThrottle]); LeftPedal = Clamp01(deflect[BindAxisLeftPedal]); RightPedal = Clamp01(deflect[BindAxisRightPedal]); // The profile encodes the pod's stick sign convention; L4PADFLIP // flips on top of it per axis. JoystickX = invertX ? -x : x; JoystickY = invertY ? -y : y; //--------------------------------------------------------------- // Emit an analog event when asked to, or when anything moved //--------------------------------------------------------------- Logical changed = (Throttle != sentThrottle) || (LeftPedal != sentLeftPedal) || (RightPedal != sentRightPedal) || (JoystickX != sentJoystickX) || (JoystickY != sentJoystickY); if (analogRequested || changed) { analogRequested = False; sentThrottle = Throttle; sentLeftPedal = LeftPedal; sentRightPedal = RightPedal; sentJoystickX = JoystickX; sentJoystickY = JoystickY; RIOEvent an_event; an_event.Type = AnalogEvent; an_event.Data.Unit = 0; QueueEvent(an_event); } }