diff --git a/dist/fgo/segatools.ini b/dist/fgo/segatools.ini index 6ceac0a..4b7ac9f 100644 --- a/dist/fgo/segatools.ini +++ b/dist/fgo/segatools.ini @@ -158,8 +158,11 @@ coin=0x72 ; : : AIME. : : ; '·:..............................................:·' ; -; Only XInput is currently supported. +; Select the input mode. "xinput" for controller, "keyboard" for keyboard. +mode=xinput + +[xinput] ; XInput bindings ; ; Left Stick Joystick @@ -168,3 +171,27 @@ coin=0x72 ; Left Shoulder Switch Target ; A/B Attack ; X/Y Noble Phantasm + +; Configure deadzones for the left thumbsticks. +; The default value for the left stick is 7849, max value is 32767. +stickDeadzone=7849 + +[keyboard] +; Keyboard bindings: + +; Stick controls (default: WASD) +up=0x57 +left=0x41 +down=0x53 +right=0x44 + +; Attack (default: Space) +attack=0x20 +; Dash (default: LSHIFT) +dash=0xa0 +; Change Target (default: J) +target=0x4A +; Re-center camera (default: K) +camera=0x4B +; Noble Phantasm (default: L) +np=0x4C diff --git a/fgohook/io4.c b/fgohook/io4.c index b2deb5f..67dfba4 100644 --- a/fgohook/io4.c +++ b/fgohook/io4.c @@ -87,7 +87,7 @@ static HRESULT fgo_io4_poll(void *ctx, struct io4_state *state) state->buttons[0] |= 1 << 1; } - if (gamebtn & FGO_IO_GAMEBTN_NOBLE_PHANTASHM) { + if (gamebtn & FGO_IO_GAMEBTN_NOBLE_PHANTASM) { state->buttons[0] |= 1 << 0; } diff --git a/fgoio/backend.h b/fgoio/backend.h new file mode 100644 index 0000000..578bda4 --- /dev/null +++ b/fgoio/backend.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +#include "fgoio/fgoio.h" + +struct fgo_io_backend { + void (*get_gamebtns)(uint8_t *gamebtn); + void (*get_analogs)(int16_t *x, int16_t *y); +}; diff --git a/fgoio/config.c b/fgoio/config.c index 6270391..437b7be 100644 --- a/fgoio/config.c +++ b/fgoio/config.c @@ -1,11 +1,38 @@ #include #include -#include +#include #include #include "fgoio/config.h" +#include + + +void fgo_kb_config_load( + struct fgo_kb_config *cfg, + const wchar_t *filename) { + + cfg->vk_attack = GetPrivateProfileIntW(L"keyboard", L"attack", ' ', filename); + cfg->vk_dash = GetPrivateProfileIntW(L"keyboard", L"dash", VK_LSHIFT, filename); + cfg->vk_target = GetPrivateProfileIntW(L"keyboard", L"target", 'J', filename); + cfg->vk_camera = GetPrivateProfileIntW(L"keyboard", L"camera", 'K', filename); + cfg->vk_np = GetPrivateProfileIntW(L"keyboard", L"np", 'L', filename); + + // Standard WASD + cfg->vk_right = GetPrivateProfileIntW(L"keyboard", L"right", 'D', filename); + cfg->vk_left = GetPrivateProfileIntW(L"keyboard", L"left", 'A', filename); + cfg->vk_down = GetPrivateProfileIntW(L"keyboard", L"down", 'S', filename); + cfg->vk_up = GetPrivateProfileIntW(L"keyboard", L"up", 'W', filename); +} + +void fgo_xi_config_load( + struct fgo_xi_config *cfg, + const wchar_t *filename) { + + cfg->stick_deadzone = GetPrivateProfileIntW(L"xinput", L"stickDeadzone", XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, filename); + +} void fgo_io_config_load( struct fgo_io_config *cfg, @@ -17,4 +44,15 @@ void fgo_io_config_load( cfg->vk_test = GetPrivateProfileIntW(L"io4", L"test", VK_F1, filename); cfg->vk_service = GetPrivateProfileIntW(L"io4", L"service", VK_F2, filename); cfg->vk_coin = GetPrivateProfileIntW(L"io4", L"coin", VK_F3, filename); + + GetPrivateProfileStringW( + L"io4", + L"mode", + L"xinput", + cfg->mode, + _countof(cfg->mode), + filename); + + fgo_xi_config_load(&cfg->xi, filename); + fgo_kb_config_load(&cfg->kb, filename); } diff --git a/fgoio/config.h b/fgoio/config.h index 835e5e8..d824ff5 100644 --- a/fgoio/config.h +++ b/fgoio/config.h @@ -5,12 +5,34 @@ #include +struct fgo_kb_config { + uint8_t vk_np; + uint8_t vk_target; + uint8_t vk_dash; + uint8_t vk_attack; + uint8_t vk_camera; + uint8_t vk_right; + uint8_t vk_left; + uint8_t vk_down; + uint8_t vk_up; +}; + +struct fgo_xi_config { + uint16_t stick_deadzone; +}; + struct fgo_io_config { uint8_t vk_test; uint8_t vk_service; uint8_t vk_coin; + + wchar_t mode[12]; + struct fgo_kb_config kb; + struct fgo_xi_config xi; }; +void fgo_kb_config_load(struct fgo_kb_config *cfg, const wchar_t *filename); +void fgo_xi_config_load(struct fgo_xi_config *cfg, const wchar_t *filename); void fgo_io_config_load( struct fgo_io_config *cfg, const wchar_t *filename); diff --git a/fgoio/fgoio.c b/fgoio/fgoio.c index 4a362a0..4817bc0 100644 --- a/fgoio/fgoio.c +++ b/fgoio/fgoio.c @@ -2,37 +2,51 @@ #include #include -#include #include #include "fgoio/fgoio.h" + +#include + +#include "keyboard.h" +#include "xi.h" #include "fgoio/config.h" #include "util/dprintf.h" #include "util/env.h" +#include "util/str.h" static uint8_t fgo_opbtn; static uint8_t fgo_gamebtn; static int16_t fgo_stick_x; static int16_t fgo_stick_y; static struct fgo_io_config fgo_io_cfg; +static const struct fgo_io_backend* fgo_io_backend; static bool fgo_io_coin; -uint16_t fgo_io_get_api_version(void) -{ +uint16_t fgo_io_get_api_version(void) { return 0x0100; } -HRESULT fgo_io_init(void) -{ +HRESULT fgo_io_init(void) { fgo_io_config_load(&fgo_io_cfg, get_config_path()); - return S_OK; + HRESULT hr; + + if (wstr_ieq(fgo_io_cfg.mode, L"keyboard")) { + hr = fgo_kb_init(&fgo_io_cfg.kb, &fgo_io_backend); + } else if (wstr_ieq(fgo_io_cfg.mode, L"xinput")) { + hr = fgo_xi_init(&fgo_io_cfg.xi, &fgo_io_backend); + } else { + hr = E_INVALIDARG; + dprintf("FGO IO: Invalid IO mode \"%S\", use keyboard or xinput\n", + fgo_io_cfg.mode); + } + + return hr; } -HRESULT fgo_io_poll(void) -{ - XINPUT_STATE xi; - WORD xb; +HRESULT fgo_io_poll(void) { + assert(fgo_io_backend != NULL); fgo_opbtn = 0; fgo_gamebtn = 0; @@ -56,97 +70,34 @@ HRESULT fgo_io_poll(void) fgo_io_coin = false; } - memset(&xi, 0, sizeof(xi)); - XInputGetState(0, &xi); - xb = xi.Gamepad.wButtons; - - if (xi.Gamepad.bLeftTrigger > 64) { - fgo_gamebtn |= FGO_IO_GAMEBTN_SPEED_UP; - } - - if (xb & XINPUT_GAMEPAD_LEFT_SHOULDER) { - fgo_gamebtn |= FGO_IO_GAMEBTN_TARGET; - } - - if (xb & XINPUT_GAMEPAD_A || xb & XINPUT_GAMEPAD_B) { - fgo_gamebtn |= FGO_IO_GAMEBTN_ATTACK; - } - - if (xb & XINPUT_GAMEPAD_Y || xb & XINPUT_GAMEPAD_X) { - fgo_gamebtn |= FGO_IO_GAMEBTN_NOBLE_PHANTASHM; - } - - if (xb & XINPUT_GAMEPAD_LEFT_THUMB) { - fgo_gamebtn |= FGO_IO_GAMEBTN_CAMERA; - } - - float LX = xi.Gamepad.sThumbLX; - float LY = xi.Gamepad.sThumbLY; - - // determine how far the controller is pushed - float magnitude = sqrt(LX*LX + LY*LY); - - // determine the direction the controller is pushed - float normalizedLX = LX / magnitude; - float normalizedLY = LY / magnitude; - - float normalizedMagnitude = 0; - - // check if the controller is outside a circular dead zone - if (magnitude > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) - { - // clip the magnitude at its expected maximum value - if (magnitude > 32767) magnitude = 32767; - - // adjust magnitude relative to the end of the dead zone - magnitude -= XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE; - - // optionally normalize the magnitude with respect to its expected range - // giving a magnitude value of 0.0 to 1.0 - normalizedMagnitude = magnitude / (32767 - XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE); - } else // if the controller is in the deadzone zero out the magnitude - { - magnitude = 0.0; - normalizedMagnitude = 0.0; - } - - fgo_stick_x = (int16_t)(normalizedLX * normalizedMagnitude * 32767); - fgo_stick_y = (int16_t)(normalizedLY * normalizedMagnitude * 32767); - return S_OK; } -void fgo_io_get_opbtns(uint8_t *opbtn) -{ +void fgo_io_get_opbtns(uint8_t* opbtn) { if (opbtn != NULL) { *opbtn = fgo_opbtn; } } -void fgo_io_get_gamebtns(uint8_t *btn) -{ - if (btn != NULL) { - *btn = fgo_gamebtn; - } +void fgo_io_get_gamebtns(uint8_t* btn) { + assert(fgo_io_backend != NULL); + assert(btn != NULL); + + fgo_io_backend->get_gamebtns(btn); } -void fgo_io_get_analogs(int16_t *stick_x, int16_t *stick_y) -{ - if (stick_x != NULL) { - *stick_x = fgo_stick_x; - } +void fgo_io_get_analogs(int16_t* stick_x, int16_t* stick_y) { + assert(fgo_io_backend != NULL); + assert(stick_x != NULL); + assert(stick_y != NULL); - if (stick_y != NULL) { - *stick_y = fgo_stick_y; - } + fgo_io_backend->get_analogs(stick_x, stick_y); } -HRESULT fgo_io_led_init(void) -{ +HRESULT fgo_io_led_init(void) { return S_OK; } -void fgo_io_led_set_colors(uint8_t board, uint8_t *rgb) -{ +void fgo_io_led_set_colors(uint8_t board, uint8_t* rgb) { return; } diff --git a/fgoio/fgoio.h b/fgoio/fgoio.h index c5b5fd8..65a11b0 100644 --- a/fgoio/fgoio.h +++ b/fgoio/fgoio.h @@ -14,7 +14,7 @@ enum { FGO_IO_GAMEBTN_SPEED_UP = 0x01, FGO_IO_GAMEBTN_TARGET = 0x02, FGO_IO_GAMEBTN_ATTACK = 0x04, - FGO_IO_GAMEBTN_NOBLE_PHANTASHM = 0x08, + FGO_IO_GAMEBTN_NOBLE_PHANTASM = 0x08, FGO_IO_GAMEBTN_CAMERA = 0x10, }; diff --git a/fgoio/keyboard.c b/fgoio/keyboard.c new file mode 100644 index 0000000..6d403dd --- /dev/null +++ b/fgoio/keyboard.c @@ -0,0 +1,82 @@ +#include + +#include +#include +#include +#include + +#include "fgoio/backend.h" +#include "fgoio/config.h" +#include "fgoio/fgoio.h" +#include "fgoio/keyboard.h" + +#include "util/dprintf.h" + +static void fgo_kb_get_gamebtns(uint8_t* gamebtn_out); +static void fgo_kb_get_analogs(int16_t* x, int16_t* y); + +static const struct fgo_io_backend fgo_kb_backend = { + .get_gamebtns = fgo_kb_get_gamebtns, + .get_analogs = fgo_kb_get_analogs +}; + +static struct fgo_kb_config config; + +HRESULT fgo_kb_init(const struct fgo_kb_config* cfg, const struct fgo_io_backend** backend) { + assert(cfg != NULL); + assert(backend != NULL); + + dprintf("Keyboard: Using keyboard input\n"); + *backend = &fgo_kb_backend; + config = *cfg; + + return S_OK; +} + +static void fgo_kb_get_gamebtns(uint8_t* gamebtn_out) { + assert(gamebtn_out != NULL); + + uint8_t gamebtn = 0; + + if (GetAsyncKeyState(config.vk_np) & 0x8000) { + gamebtn |= FGO_IO_GAMEBTN_NOBLE_PHANTASM; + } + + if (GetAsyncKeyState(config.vk_target) & 0x8000) { + gamebtn |= FGO_IO_GAMEBTN_TARGET; + } + + if (GetAsyncKeyState(config.vk_dash) & 0x8000) { + gamebtn |= FGO_IO_GAMEBTN_SPEED_UP; + } + + if (GetAsyncKeyState(config.vk_attack) & 0x8000) { + gamebtn |= FGO_IO_GAMEBTN_ATTACK; + } + + if (GetAsyncKeyState(config.vk_camera) & 0x8000) { + gamebtn |= FGO_IO_GAMEBTN_CAMERA; + } + + *gamebtn_out = gamebtn; +} + +static void fgo_kb_get_analogs(int16_t* x, int16_t* y) { + assert(x != NULL); + assert(y != NULL); + + if (GetAsyncKeyState(config.vk_left) & 0x8000) { + *x = SHRT_MIN + 1; + } else if (GetAsyncKeyState(config.vk_right) & 0x8000) { + *x = SHRT_MAX - 1; + } else { + *x = 0; + } + if (GetAsyncKeyState(config.vk_down) & 0x8000) { + *y = SHRT_MIN + 1; + } else if (GetAsyncKeyState(config.vk_up) & 0x8000) { + *y = SHRT_MAX - 1; + } else { + *y = 0; + } +} diff --git a/fgoio/keyboard.h b/fgoio/keyboard.h new file mode 100644 index 0000000..1b46a64 --- /dev/null +++ b/fgoio/keyboard.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +#include "fgoio/backend.h" +#include "fgoio/config.h" + +HRESULT fgo_kb_init(const struct fgo_kb_config *cfg, const struct fgo_io_backend **backend); diff --git a/fgoio/meson.build b/fgoio/meson.build index eed2fd3..5847686 100644 --- a/fgoio/meson.build +++ b/fgoio/meson.build @@ -11,5 +11,10 @@ fgoio_lib = static_library( 'fgoio.h', 'config.c', 'config.h', + 'backend.h', + 'keyboard.c', + 'keyboard.h', + 'xi.c', + 'xi.h', ], ) diff --git a/fgoio/xi.c b/fgoio/xi.c new file mode 100644 index 0000000..91a78a8 --- /dev/null +++ b/fgoio/xi.c @@ -0,0 +1,132 @@ +#include +#include + +#include +#include +#include + +#include "fgoio/backend.h" +#include "fgoio/config.h" +#include "fgoio/fgoio.h" +#include "fgoio/xi.h" + +#include "util/dprintf.h" + +static void fgo_xi_get_gamebtns(uint8_t* gamebtn_out); +static void fgo_xi_get_analogs(int16_t* x, int16_t* y); +static HRESULT fgo_xi_config_apply(const struct fgo_xi_config* cfg); + +static const struct fgo_io_backend fgo_xi_backend = { + .get_gamebtns = fgo_xi_get_gamebtns, + .get_analogs = fgo_xi_get_analogs +}; + +static float fgo_xi_stick_deadzone; + +const uint16_t max_stick_value = 32767; + +HRESULT fgo_xi_init(const struct fgo_xi_config* cfg, const struct fgo_io_backend** backend) { + assert(cfg != NULL); + assert(backend != NULL); + + HRESULT hr = fgo_xi_config_apply(cfg); + + if (FAILED(hr)) { + return hr; + } + + dprintf("XInput: Using XInput controller\n"); + *backend = &fgo_xi_backend; + + return S_OK; +} + +static HRESULT fgo_xi_config_apply(const struct fgo_xi_config* cfg) { + /* Deadzone check */ + if (cfg->stick_deadzone > 32767 || cfg->stick_deadzone < 0) { + dprintf("XInput: Stick deadzone is too large or negative\n"); + return E_INVALIDARG; + } + + dprintf("XInput: --- Begin configuration ---\n"); + dprintf("XInput: Left Deadzone . . . . : %i\n", cfg->stick_deadzone); + dprintf("XInput: --- End configuration ---\n"); + + fgo_xi_stick_deadzone = cfg->stick_deadzone; + + return S_OK; +} + +static void fgo_xi_get_gamebtns(uint8_t* gamebtn_out) { + assert(gamebtn_out != NULL); + + XINPUT_STATE xi; + memset(&xi, 0, sizeof(xi)); + XInputGetState(0, &xi); + + uint8_t gamebtn = 0; + WORD xb = xi.Gamepad.wButtons; + + if (xi.Gamepad.bLeftTrigger > 64) { + gamebtn |= FGO_IO_GAMEBTN_SPEED_UP; + } + + if (xb & XINPUT_GAMEPAD_LEFT_SHOULDER) { + gamebtn |= FGO_IO_GAMEBTN_TARGET; + } + + if (xb & XINPUT_GAMEPAD_A || xb & XINPUT_GAMEPAD_B) { + gamebtn |= FGO_IO_GAMEBTN_ATTACK; + } + + if (xb & XINPUT_GAMEPAD_Y || xb & XINPUT_GAMEPAD_X) { + gamebtn |= FGO_IO_GAMEBTN_NOBLE_PHANTASM; + } + + if (xb & XINPUT_GAMEPAD_LEFT_THUMB) { + gamebtn |= FGO_IO_GAMEBTN_CAMERA; + } + + *gamebtn_out = gamebtn; +} + +static void fgo_xi_get_analogs(int16_t* x, int16_t* y) { + + assert(x != NULL); + assert(y != NULL); + + XINPUT_STATE xi; + memset(&xi, 0, sizeof(xi)); + XInputGetState(0, &xi); + + float LX = xi.Gamepad.sThumbLX; + float LY = xi.Gamepad.sThumbLY; + + // determine how far the controller is pushed + float magnitude = sqrt(LX * LX + LY * LY); + + // determine the direction the controller is pushed + float normalizedLX = LX / magnitude; + float normalizedLY = LY / magnitude; + + float normalizedMagnitude = 0; + + // check if the controller is outside a circular dead zone + if (magnitude > fgo_xi_stick_deadzone) { + // clip the magnitude at its expected maximum value + if (magnitude > 32767) magnitude = 32767; + + // adjust magnitude relative to the end of the dead zone + magnitude -= fgo_xi_stick_deadzone; + + // optionally normalize the magnitude with respect to its expected range + // giving a magnitude value of 0.0 to 1.0 + normalizedMagnitude = magnitude / (32767 - fgo_xi_stick_deadzone); + } else // if the controller is in the deadzone zero out the magnitude + { + normalizedMagnitude = 0; + } + + *x = (int16_t) (normalizedLX * normalizedMagnitude * 32767); + *y = (int16_t) (normalizedLY * normalizedMagnitude * 32767); +} diff --git a/fgoio/xi.h b/fgoio/xi.h new file mode 100644 index 0000000..ebb8859 --- /dev/null +++ b/fgoio/xi.h @@ -0,0 +1,10 @@ +#pragma once + +/* Can't call this xinput.h or it will conflict with */ + +#include + +#include "fgoio/backend.h" +#include "fgoio/config.h" + +HRESULT fgo_xi_init(const struct fgo_xi_config *cfg, const struct fgo_io_backend **backend);