FGO: add keyboard input (#61)

Probably self-explanatory :p

Reviewed-on: TeamTofuShop/segatools#61
Co-authored-by: kyoubate-haruka <46010460+kyoubate-haruka@users.noreply.github.com>
Co-committed-by: kyoubate-haruka <46010460+kyoubate-haruka@users.noreply.github.com>
This commit is contained in:
2025-04-05 15:22:14 +00:00
committed by Dniel97
parent 61f95c3f2e
commit 39711a994a
12 changed files with 375 additions and 90 deletions

View File

@ -158,8 +158,11 @@ coin=0x72
; : : AIME. : : ; : : AIME. : :
; '·:..............................................:·' ; '·:..............................................:·'
; ;
; Only XInput is currently supported.
; Select the input mode. "xinput" for controller, "keyboard" for keyboard.
mode=xinput
[xinput]
; XInput bindings ; XInput bindings
; ;
; Left Stick Joystick ; Left Stick Joystick
@ -168,3 +171,27 @@ coin=0x72
; Left Shoulder Switch Target ; Left Shoulder Switch Target
; A/B Attack ; A/B Attack
; X/Y Noble Phantasm ; 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

View File

@ -87,7 +87,7 @@ static HRESULT fgo_io4_poll(void *ctx, struct io4_state *state)
state->buttons[0] |= 1 << 1; state->buttons[0] |= 1 << 1;
} }
if (gamebtn & FGO_IO_GAMEBTN_NOBLE_PHANTASHM) { if (gamebtn & FGO_IO_GAMEBTN_NOBLE_PHANTASM) {
state->buttons[0] |= 1 << 0; state->buttons[0] |= 1 << 0;
} }

10
fgoio/backend.h Normal file
View File

@ -0,0 +1,10 @@
#pragma once
#include <stdint.h>
#include "fgoio/fgoio.h"
struct fgo_io_backend {
void (*get_gamebtns)(uint8_t *gamebtn);
void (*get_analogs)(int16_t *x, int16_t *y);
};

View File

@ -1,11 +1,38 @@
#include <windows.h> #include <windows.h>
#include <assert.h> #include <assert.h>
#include <stddef.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include "fgoio/config.h" #include "fgoio/config.h"
#include <xinput.h>
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( void fgo_io_config_load(
struct fgo_io_config *cfg, 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_test = GetPrivateProfileIntW(L"io4", L"test", VK_F1, filename);
cfg->vk_service = GetPrivateProfileIntW(L"io4", L"service", VK_F2, filename); cfg->vk_service = GetPrivateProfileIntW(L"io4", L"service", VK_F2, filename);
cfg->vk_coin = GetPrivateProfileIntW(L"io4", L"coin", VK_F3, 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);
} }

View File

@ -5,12 +5,34 @@
#include <stdbool.h> #include <stdbool.h>
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 { struct fgo_io_config {
uint8_t vk_test; uint8_t vk_test;
uint8_t vk_service; uint8_t vk_service;
uint8_t vk_coin; 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( void fgo_io_config_load(
struct fgo_io_config *cfg, struct fgo_io_config *cfg,
const wchar_t *filename); const wchar_t *filename);

View File

@ -2,37 +2,51 @@
#include <xinput.h> #include <xinput.h>
#include <math.h> #include <math.h>
#include <limits.h>
#include <stdint.h> #include <stdint.h>
#include "fgoio/fgoio.h" #include "fgoio/fgoio.h"
#include <assert.h>
#include "keyboard.h"
#include "xi.h"
#include "fgoio/config.h" #include "fgoio/config.h"
#include "util/dprintf.h" #include "util/dprintf.h"
#include "util/env.h" #include "util/env.h"
#include "util/str.h"
static uint8_t fgo_opbtn; static uint8_t fgo_opbtn;
static uint8_t fgo_gamebtn; static uint8_t fgo_gamebtn;
static int16_t fgo_stick_x; static int16_t fgo_stick_x;
static int16_t fgo_stick_y; static int16_t fgo_stick_y;
static struct fgo_io_config fgo_io_cfg; static struct fgo_io_config fgo_io_cfg;
static const struct fgo_io_backend* fgo_io_backend;
static bool fgo_io_coin; static bool fgo_io_coin;
uint16_t fgo_io_get_api_version(void) uint16_t fgo_io_get_api_version(void) {
{
return 0x0100; return 0x0100;
} }
HRESULT fgo_io_init(void) HRESULT fgo_io_init(void) {
{
fgo_io_config_load(&fgo_io_cfg, get_config_path()); 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) HRESULT fgo_io_poll(void) {
{ assert(fgo_io_backend != NULL);
XINPUT_STATE xi;
WORD xb;
fgo_opbtn = 0; fgo_opbtn = 0;
fgo_gamebtn = 0; fgo_gamebtn = 0;
@ -56,97 +70,34 @@ HRESULT fgo_io_poll(void)
fgo_io_coin = false; 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; return S_OK;
} }
void fgo_io_get_opbtns(uint8_t *opbtn) void fgo_io_get_opbtns(uint8_t* opbtn) {
{
if (opbtn != NULL) { if (opbtn != NULL) {
*opbtn = fgo_opbtn; *opbtn = fgo_opbtn;
} }
} }
void fgo_io_get_gamebtns(uint8_t *btn) void fgo_io_get_gamebtns(uint8_t* btn) {
{ assert(fgo_io_backend != NULL);
if (btn != NULL) { assert(btn != NULL);
*btn = fgo_gamebtn;
} fgo_io_backend->get_gamebtns(btn);
} }
void fgo_io_get_analogs(int16_t *stick_x, int16_t *stick_y) void fgo_io_get_analogs(int16_t* stick_x, int16_t* stick_y) {
{ assert(fgo_io_backend != NULL);
if (stick_x != NULL) { assert(stick_x != NULL);
*stick_x = fgo_stick_x; assert(stick_y != NULL);
}
if (stick_y != NULL) { fgo_io_backend->get_analogs(stick_x, stick_y);
*stick_y = fgo_stick_y;
}
} }
HRESULT fgo_io_led_init(void) HRESULT fgo_io_led_init(void) {
{
return S_OK; 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; return;
} }

View File

@ -14,7 +14,7 @@ enum {
FGO_IO_GAMEBTN_SPEED_UP = 0x01, FGO_IO_GAMEBTN_SPEED_UP = 0x01,
FGO_IO_GAMEBTN_TARGET = 0x02, FGO_IO_GAMEBTN_TARGET = 0x02,
FGO_IO_GAMEBTN_ATTACK = 0x04, FGO_IO_GAMEBTN_ATTACK = 0x04,
FGO_IO_GAMEBTN_NOBLE_PHANTASHM = 0x08, FGO_IO_GAMEBTN_NOBLE_PHANTASM = 0x08,
FGO_IO_GAMEBTN_CAMERA = 0x10, FGO_IO_GAMEBTN_CAMERA = 0x10,
}; };

82
fgoio/keyboard.c Normal file
View File

@ -0,0 +1,82 @@
#include <windows.h>
#include <math.h>
#include <assert.h>
#include <stdint.h>
#include <limits.h>
#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;
}
}

8
fgoio/keyboard.h Normal file
View File

@ -0,0 +1,8 @@
#pragma once
#include <windows.h>
#include "fgoio/backend.h"
#include "fgoio/config.h"
HRESULT fgo_kb_init(const struct fgo_kb_config *cfg, const struct fgo_io_backend **backend);

View File

@ -11,5 +11,10 @@ fgoio_lib = static_library(
'fgoio.h', 'fgoio.h',
'config.c', 'config.c',
'config.h', 'config.h',
'backend.h',
'keyboard.c',
'keyboard.h',
'xi.c',
'xi.h',
], ],
) )

132
fgoio/xi.c Normal file
View File

@ -0,0 +1,132 @@
#include <windows.h>
#include <xinput.h>
#include <math.h>
#include <assert.h>
#include <stdint.h>
#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);
}

10
fgoio/xi.h Normal file
View File

@ -0,0 +1,10 @@
#pragma once
/* Can't call this xinput.h or it will conflict with <xinput.h> */
#include <windows.h>
#include "fgoio/backend.h"
#include "fgoio/config.h"
HRESULT fgo_xi_init(const struct fgo_xi_config *cfg, const struct fgo_io_backend **backend);