FGO: add keyboard input (#61)

Probably self-explanatory :p

Reviewed-on: https://gitea.tendokyu.moe/TeamTofuShop/segatools/pulls/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>
pull/35/head^2
kyoubate-haruka 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. : :
; '·:..............................................:·'
;
; 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

View File

@ -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;
}

10
fgoio/backend.h 100644
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 <assert.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.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(
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);
}

View File

@ -5,12 +5,34 @@
#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 {
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);

View File

@ -2,37 +2,51 @@
#include <xinput.h>
#include <math.h>
#include <limits.h>
#include <stdint.h>
#include "fgoio/fgoio.h"
#include <assert.h>
#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;
}

View File

@ -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,
};

82
fgoio/keyboard.c 100644
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 100644
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',
'config.c',
'config.h',
'backend.h',
'keyboard.c',
'keyboard.h',
'xi.c',
'xi.h',
],
)

132
fgoio/xi.c 100644
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 100644
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);