From ec072667b3e8eee702871fcc87519a90331ba5d2 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Fri, 14 Jul 2023 00:52:50 +0200 Subject: [PATCH] swdc: first segatools added --- Package.mk | 16 ++ README.md | 5 +- dist/swdc/segatools.ini | 99 ++++++++++ dist/swdc/start.bat | 29 +++ meson.build | 2 + swdchook/config.c | 53 +++++ swdchook/config.h | 32 +++ swdchook/dllmain.c | 112 +++++++++++ swdchook/io4.c | 137 +++++++++++++ swdchook/io4.h | 7 + swdchook/meson.build | 32 +++ swdchook/swdc-dll.c | 112 +++++++++++ swdchook/swdc-dll.h | 22 +++ swdchook/swdchook.def | 19 ++ swdchook/zinput.c | 186 ++++++++++++++++++ swdchook/zinput.h | 11 ++ swdcio/backend.h | 11 ++ swdcio/config.c | 111 +++++++++++ swdcio/config.h | 47 +++++ swdcio/di-dev.c | 163 ++++++++++++++++ swdcio/di-dev.h | 19 ++ swdcio/di.c | 420 ++++++++++++++++++++++++++++++++++++++++ swdcio/di.h | 9 + swdcio/dllmain.c | 112 +++++++++++ swdcio/meson.build | 30 +++ swdcio/swdcio.def | 8 + swdcio/swdcio.h | 98 ++++++++++ swdcio/wnd.c | 86 ++++++++ swdcio/wnd.h | 5 + swdcio/xi.c | 165 ++++++++++++++++ swdcio/xi.h | 10 + 31 files changed, 2167 insertions(+), 1 deletion(-) create mode 100644 dist/swdc/segatools.ini create mode 100644 dist/swdc/start.bat create mode 100644 swdchook/config.c create mode 100644 swdchook/config.h create mode 100644 swdchook/dllmain.c create mode 100644 swdchook/io4.c create mode 100644 swdchook/io4.h create mode 100644 swdchook/meson.build create mode 100644 swdchook/swdc-dll.c create mode 100644 swdchook/swdc-dll.h create mode 100644 swdchook/swdchook.def create mode 100644 swdchook/zinput.c create mode 100644 swdchook/zinput.h create mode 100644 swdcio/backend.h create mode 100644 swdcio/config.c create mode 100644 swdcio/config.h create mode 100644 swdcio/di-dev.c create mode 100644 swdcio/di-dev.h create mode 100644 swdcio/di.c create mode 100644 swdcio/di.h create mode 100644 swdcio/dllmain.c create mode 100644 swdcio/meson.build create mode 100644 swdcio/swdcio.def create mode 100644 swdcio/swdcio.h create mode 100644 swdcio/wnd.c create mode 100644 swdcio/wnd.h create mode 100644 swdcio/xi.c create mode 100644 swdcio/xi.h diff --git a/Package.mk b/Package.mk index 3e6a887..4562bf1 100644 --- a/Package.mk +++ b/Package.mk @@ -88,6 +88,21 @@ $(BUILD_DIR_ZIP)/idac.zip: $(V)strip $(BUILD_DIR_ZIP)/idac/*.{exe,dll} $(V)cd $(BUILD_DIR_ZIP)/idac ; zip -r ../idac.zip * +$(BUILD_DIR_ZIP)/swdc.zip: + $(V)echo ... $@ + $(V)mkdir -p $(BUILD_DIR_ZIP)/swdc + $(V)mkdir -p $(BUILD_DIR_ZIP)/swdc/DEVICE + $(V)cp $(BUILD_DIR_64)/subprojects/capnhook/inject/inject.exe \ + $(BUILD_DIR_64)/swdchook/swdchook.dll \ + $(DIST_DIR)/swdc/segatools.ini \ + $(DIST_DIR)/swdc/start.bat \ + $(BUILD_DIR_ZIP)/swdc + $(V)cp pki/billing.pub \ + pki/ca.crt \ + $(BUILD_DIR_ZIP)/swdc/DEVICE + $(V)strip $(BUILD_DIR_ZIP)/swdc/*.{exe,dll} + $(V)cd $(BUILD_DIR_ZIP)/swdc ; zip -r ../swdc.zip * + $(BUILD_DIR_ZIP)/mercury.zip: $(V)echo ... $@ $(V)mkdir -p $(BUILD_DIR_ZIP)/mercury @@ -135,6 +150,7 @@ $(BUILD_DIR_ZIP)/segatools.zip: \ $(BUILD_DIR_ZIP)/doc.zip \ $(BUILD_DIR_ZIP)/idz.zip \ $(BUILD_DIR_ZIP)/idac.zip \ + $(BUILD_DIR_ZIP)/swdc.zip \ $(BUILD_DIR_ZIP)/mercury.zip \ $(BUILD_DIR_ZIP)/mu3.zip \ CHANGELOG.md \ diff --git a/README.md b/README.md index a9c53d7..4f48d8d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Segatools -Version: `v005` +Version: `2023-07-14` Loaders and hardware emulators for SEGA games that run on the Nu and ALLS platforms. @@ -14,6 +14,9 @@ Loaders and hardware emulators for SEGA games that run on the Nu and ALLS platfo * [Chunithm Crystal (Plus)](doc/chunihook.md) * Initial D * [Initial D Arcade Stage Zero](doc/idzhook.md) + * Initial D The Arcade +* SEGA World Drivers Championship + * SEGA World Drivers Championship 2019 * Wacca * Wacca Lilly R (WIP) diff --git a/dist/swdc/segatools.ini b/dist/swdc/segatools.ini new file mode 100644 index 0000000..dc0c003 --- /dev/null +++ b/dist/swdc/segatools.ini @@ -0,0 +1,99 @@ +[vfs] +; Insert the path to the game AMFS directory here (contains ICF1 and ICF2) +amfs= +; Insert the path to the game Option directory here (contains OPxx directories) +option= +; Create an empty directory somewhere and insert the path here. +; This directory may be shared between multiple SEGA games. +; NOTE: This has nothing to do with Windows %APPDATA%. +appdata= + +[aime] +; Controls emulation of the Aime card reader assembly. +enable=1 +aimePath=DEVICE\aime.txt +felicaGen=0 + +[dns] +; Insert the hostname or IP address of the server you wish to use here. +; Note that 127.0.0.1, localhost etc are specifically rejected. +default=127.0.0.1 + +[netenv] +; Simulate an ideal LAN environment. This may interfere with head-to-head play. +; SEGA games are somewhat picky about their LAN environment, so leaving this +; setting enabled is recommended. +enable=1 + +[keychip] +; The /24 LAN subnet that the emulated keychip will tell the game to expect. +; You must set this to your LAN's IP subnet, and that subnet must start with 192.168. +subnet=192.168.100.0 + +[aimeio] +; To use a custom card reader IO DLL enter its path here. +; Leave empty if you want to use Segatools built-in keyboard input. +path= + +[swdcio] +; To use a custom SEGA World Drivers Championship DLL enter its path here. +; Leave empty if you want to use Segatools built-in gamepad/wheel input. +path= + +[io4] +; Test button virtual-key code. Default is the 1 key. +test=0x31 +; Service button virtual-key code. Default is the 2 key. +service=0x32 +; Keyboard button to increment coin counter. Default is the 3 key. +coin=0x33 +; Input API selection for IO4 input emulator. +; Set "xinput" to use a gamepad and "dinput" to use a steering wheel. +mode=xinput +; Use the left thumbstick for steering instead of both on XInput Controllers. +; Not recommended as it will not give you the precision needed for this game +singleStickSteering=1 +; Adjust scaling for steering wheel input. +; +; This setting scales the steering wheel input so that the maximum positive +; and minimum negative steering inputs reported in the operator menu's input +; test screen do not exceed the value below. The maximum possible value is 128, +; and the value that matches the input range of a real cabinet is 97. +; +; NOTE: This is not the same thing as DirectInput steering wheel movement +; range! Segatools cannot control the maximum angle of your physical steering +; wheel controller, this setting is vendor-specific and can only be adjusted +; in the Control Panel. +restrict=97 + +[dinput] +; Name of the DirectInput wheel to use (or any text that occurs in its name) +; Example: TMX +; +; If this is left blank then the first DirectInput device will be used. +deviceName= +; Pedal mappings. Valid axis names are: +; +; X, Y, Z, RX, RY, RZ, U, V +; +; (U and V are old names for Slider 1 and Slider 2). +; The examples below are valid for a Thrustmaster TMX. +brakeAxis=RZ +accelAxis=Y +; DirectInput button numbers to map to menu inputs. Note that buttons are +; numbered from 1; some software numbers buttons from 0. +start=3 +viewChg=10 +; Button mappings for the steering wheel paddles. Note shiftDn is the +; left paddle and shiftUp is the right paddle. +shiftDn=1 +shiftUp=2 +; Button mappings for the steering wheel buttons. +wheelGreen=4 +wheelRed=5 +wheelBlue=6 +wheelYellow=7 +; Invert the accelerator and or brake axis +; (Needed when using DirectInput for the Dualshock 4 for example) +reverseAccelAxis=0 +reverseBrakeAxis=0 diff --git a/dist/swdc/start.bat b/dist/swdc/start.bat new file mode 100644 index 0000000..ae33ae9 --- /dev/null +++ b/dist/swdc/start.bat @@ -0,0 +1,29 @@ +@echo off + +pushd %~dp0 + +REM set the APP_DIR to the Y drive +set APP_DIR=Y:\SWDC + +REM create the APP_DIR if it doesn't exist and redirect it to the TEMP folder +if not exist "%APP_DIR%" ( + subst Y: %TEMP% + REM timeout /t 1 + if not exist "%APP_DIR%" ( + mkdir "%APP_DIR%" + ) +) + +echo Mounted the Y:\ drive to the %TEMP%\SWDC folder + +REM start /min inject -d -k swdchook.dll amdaemon.exe -f -c config.json config_LanClient.json config_MiniCabinet.json +start /min inject -d -k swdchook.dll amdaemon.exe -f -c config.json config_LanServer.json config_MiniCabinet.json +inject -d -k swdchook.dll ..\Todoroki\Binaries\Win64\Todoroki-Win64-Shipping.exe -launch=MiniCabinet -ABSLOG="..\..\..\..\..\Userdata\GameProject.log" -UserDir="..\..\..\Userdata" -NotInstalled -UNATTENDED +taskkill /f /im amdaemon.exe > nul 2>&1 + +REM unmount the APP_DIR +subst Y: /d > nul 2>&1 + +echo. +echo Game processes have terminated +pause \ No newline at end of file diff --git a/meson.build b/meson.build index abbc4a6..e346cea 100644 --- a/meson.build +++ b/meson.build @@ -59,6 +59,7 @@ subdir('divaio') subdir('carolio') subdir('idzio') subdir('idacio') +subdir('swdcio') subdir('mu3io') subdir('mercuryio') subdir('cxbio') @@ -68,6 +69,7 @@ subdir('divahook') subdir('carolhook') subdir('idzhook') subdir('idachook') +subdir('swdchook') subdir('minihook') subdir('mu3hook') subdir('mercuryhook') diff --git a/swdchook/config.c b/swdchook/config.c new file mode 100644 index 0000000..ba4d34a --- /dev/null +++ b/swdchook/config.c @@ -0,0 +1,53 @@ +#include +#include + +#include "board/config.h" +#include "board/sg-reader.h" + +#include "hooklib/config.h" +#include "hooklib/dvd.h" + +#include "swdchook/config.h" +#include "swdchook/swdc-dll.h" + +#include "platform/config.h" +#include "platform/platform.h" + +void swdc_dll_config_load( + struct swdc_dll_config *cfg, + const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + GetPrivateProfileStringW( + L"swdcio", + L"path", + L"", + cfg->path, + _countof(cfg->path), + filename); +} + +void swdc_hook_config_load( + struct swdc_hook_config *cfg, + const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + platform_config_load(&cfg->platform, filename); + aime_config_load(&cfg->aime, filename); + swdc_dll_config_load(&cfg->dll, filename); + zinput_config_load(&cfg->zinput, filename); + dvd_config_load(&cfg->dvd, filename); + io4_config_load(&cfg->io4, filename); +} + +void zinput_config_load(struct zinput_config *cfg, const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + cfg->enable = GetPrivateProfileIntW(L"zinput", L"enable", 1, filename); +} diff --git a/swdchook/config.h b/swdchook/config.h new file mode 100644 index 0000000..5237b9f --- /dev/null +++ b/swdchook/config.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "board/config.h" + +#include "hooklib/dvd.h" + +#include "swdchook/swdc-dll.h" +#include "swdchook/zinput.h" + +#include "platform/platform.h" + +struct swdc_hook_config { + struct platform_config platform; + struct aime_config aime; + struct dvd_config dvd; + struct io4_config io4; + struct swdc_dll_config dll; + struct zinput_config zinput; +}; + +void swdc_dll_config_load( + struct swdc_dll_config *cfg, + const wchar_t *filename); + +void swdc_hook_config_load( + struct swdc_hook_config *cfg, + const wchar_t *filename); + +void zinput_config_load(struct zinput_config *cfg, const wchar_t *filename); diff --git a/swdchook/dllmain.c b/swdchook/dllmain.c new file mode 100644 index 0000000..a37c3b0 --- /dev/null +++ b/swdchook/dllmain.c @@ -0,0 +1,112 @@ +#include +#include + +#include + +#include "board/sg-reader.h" +#include "board/io4.h" +#include "board/vfd.h" + +#include "hook/process.h" + +#include "hooklib/dvd.h" +#include "hooklib/serial.h" +#include "hooklib/spike.h" + +#include "swdchook/config.h" +#include "swdchook/swdc-dll.h" +#include "swdchook/io4.h" +#include "swdchook/zinput.h" + +#include "platform/platform.h" + +#include "util/dprintf.h" + +static HMODULE swdc_hook_mod; +static process_entry_t swdc_startup; +static struct swdc_hook_config swdc_hook_cfg; + +static DWORD CALLBACK swdc_pre_startup(void) +{ + HRESULT hr; + + dprintf("--- Begin swdc_pre_startup ---\n"); + + /* Config load */ + + swdc_hook_config_load(&swdc_hook_cfg, L".\\segatools.ini"); + + /* Hook Win32 APIs */ + + serial_hook_init(); + zinput_hook_init(&swdc_hook_cfg.zinput); + dvd_hook_init(&swdc_hook_cfg.dvd, swdc_hook_mod); + + /* Initialize emulation hooks */ + + hr = platform_hook_init( + &swdc_hook_cfg.platform, + "SDDS", + "ACA4", + swdc_hook_mod); + + if (FAILED(hr)) { + goto fail; + } + + hr = sg_reader_hook_init(&swdc_hook_cfg.aime, 3, swdc_hook_mod); + + if (FAILED(hr)) { + goto fail; + } + + hr = vfd_hook_init(4); + + if (FAILED(hr)) { + return hr; + } + + hr = swdc_dll_init(&swdc_hook_cfg.dll, swdc_hook_mod); + + if (FAILED(hr)) { + goto fail; + } + + hr = swdc_io4_hook_init(&swdc_hook_cfg.io4); + + if (FAILED(hr)) { + goto fail; + } + + /* Initialize debug helpers */ + + spike_hook_init(L".\\segatools.ini"); + + dprintf("--- End swdc_pre_startup ---\n"); + + /* Jump to EXE start address */ + + return swdc_startup(); + +fail: + ExitProcess(EXIT_FAILURE); +} + +BOOL WINAPI DllMain(HMODULE mod, DWORD cause, void *ctx) +{ + HRESULT hr; + + if (cause != DLL_PROCESS_ATTACH) { + return TRUE; + } + + swdc_hook_mod = mod; + + hr = process_hijack_startup(swdc_pre_startup, &swdc_startup); + + if (!SUCCEEDED(hr)) { + dprintf("Failed to hijack process startup: %x\n", (int) hr); + } + + return SUCCEEDED(hr); +} diff --git a/swdchook/io4.c b/swdchook/io4.c new file mode 100644 index 0000000..10736ef --- /dev/null +++ b/swdchook/io4.c @@ -0,0 +1,137 @@ +#include + +#include +#include +#include + +#include "board/io4.h" + +#include "swdchook/swdc-dll.h" + +#include "util/dprintf.h" + +static HRESULT swdc_io4_poll(void *ctx, struct io4_state *state); +static uint16_t coins; + +static const struct io4_ops swdc_io4_ops = { + .poll = swdc_io4_poll, +}; + +HRESULT swdc_io4_hook_init(const struct io4_config *cfg) +{ + HRESULT hr; + + assert(swdc_dll.init != NULL); + + hr = io4_hook_init(cfg, &swdc_io4_ops, NULL); + + if (FAILED(hr)) { + return hr; + } + + return swdc_dll.init(); +} + +static HRESULT swdc_io4_poll(void *ctx, struct io4_state *state) +{ + uint8_t opbtn; + uint16_t gamebtn; + struct swdc_io_analog_state analog_state; + HRESULT hr; + + assert(swdc_dll.poll != NULL); + assert(swdc_dll.get_opbtns != NULL); + assert(swdc_dll.get_gamebtns != NULL); + assert(swdc_dll.get_analogs != NULL); + + memset(state, 0, sizeof(*state)); + memset(&analog_state, 0, sizeof(analog_state)); + + hr = swdc_dll.poll(); + + if (FAILED(hr)) { + return hr; + } + + opbtn = 0; + gamebtn = 0; + + swdc_dll.get_opbtns(&opbtn); + swdc_dll.get_gamebtns(&gamebtn); + swdc_dll.get_analogs(&analog_state); + + if (opbtn & SWDC_IO_OPBTN_TEST) { + state->buttons[0] |= IO4_BUTTON_TEST; + } + + if (opbtn & SWDC_IO_OPBTN_SERVICE) { + state->buttons[0] |= IO4_BUTTON_SERVICE; + } + + if (opbtn & SWDC_IO_OPBTN_COIN) { + coins++; + } + state->chutes[0] = coins << 8; + + /* Update Cabinet buttons */ + + if (gamebtn & SWDC_IO_GAMEBTN_START) { + state->buttons[0] |= 1 << 7; + } + + if (gamebtn & SWDC_IO_GAMEBTN_VIEW_CHANGE) { + state->buttons[0] |= 1 << 1; + } + + if (gamebtn & SWDC_IO_GAMEBTN_UP) { + state->buttons[0] |= 1 << 5; + } + + if (gamebtn & SWDC_IO_GAMEBTN_DOWN) { + state->buttons[0] |= 1 << 4; + } + + if (gamebtn & SWDC_IO_GAMEBTN_LEFT) { + state->buttons[0] |= 1 << 3; + } + + if (gamebtn & SWDC_IO_GAMEBTN_RIGHT) { + state->buttons[0] |= 1 << 2; + } + + /* Update steering wheel buttons */ + + if (gamebtn & SWDC_IO_GAMEBTN_STEERING_BLUE) { + state->buttons[1] |= 1 << 15; + } + + if (gamebtn & SWDC_IO_GAMEBTN_STEERING_GREEN) { + state->buttons[1] |= 1 << 14; + } + + if (gamebtn & SWDC_IO_GAMEBTN_STEERING_RED) { + state->buttons[1] |= 1 << 13; + } + + if (gamebtn & SWDC_IO_GAMEBTN_STEERING_YELLOW) { + state->buttons[1] |= 1 << 12; + } + + if (gamebtn & SWDC_IO_GAMEBTN_STEERING_PADDLE_LEFT) { + state->buttons[1] |= 1 << 1; + } + + if (gamebtn & SWDC_IO_GAMEBTN_STEERING_PADDLE_RIGHT) { + state->buttons[1] |= 1 << 0; + } + + /* Steering wheel increases left-to-right. + + Use 0x8000 as the center point. */ + + state->adcs[0] = 0x8000 + analog_state.wheel; + state->adcs[1] = analog_state.accel; + state->adcs[2] = analog_state.brake; + + return S_OK; +} diff --git a/swdchook/io4.h b/swdchook/io4.h new file mode 100644 index 0000000..69580d1 --- /dev/null +++ b/swdchook/io4.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include "board/io4.h" + +HRESULT swdc_io4_hook_init(const struct io4_config *cfg); diff --git a/swdchook/meson.build b/swdchook/meson.build new file mode 100644 index 0000000..c312ed4 --- /dev/null +++ b/swdchook/meson.build @@ -0,0 +1,32 @@ +shared_library( + 'swdchook', + name_prefix : '', + include_directories : inc, + implicit_include_directories : false, + vs_module_defs : 'swdchook.def', + c_pch : '../precompiled.h', + dependencies : [ + capnhook.get_variable('hook_dep'), + capnhook.get_variable('hooklib_dep'), + xinput_lib, + ], + link_with : [ + aimeio_lib, + board_lib, + hooklib_lib, + swdcio_lib, + platform_lib, + util_lib, + ], + sources : [ + 'config.c', + 'config.h', + 'dllmain.c', + 'swdc-dll.c', + 'swdc-dll.h', + 'io4.c', + 'io4.h', + 'zinput.c', + 'zinput.h', + ], +) diff --git a/swdchook/swdc-dll.c b/swdchook/swdc-dll.c new file mode 100644 index 0000000..47120ee --- /dev/null +++ b/swdchook/swdc-dll.c @@ -0,0 +1,112 @@ +#include + +#include +#include + +#include "swdchook/swdc-dll.h" + +#include "util/dll-bind.h" +#include "util/dprintf.h" + +const struct dll_bind_sym swdc_dll_syms[] = { + { + .sym = "swdc_io_init", + .off = offsetof(struct swdc_dll, init), + }, { + .sym = "swdc_io_poll", + .off = offsetof(struct swdc_dll, poll), + }, { + .sym = "swdc_io_get_opbtns", + .off = offsetof(struct swdc_dll, get_opbtns), + }, { + .sym = "swdc_io_get_gamebtns", + .off = offsetof(struct swdc_dll, get_gamebtns), + }, { + .sym = "swdc_io_get_analogs", + .off = offsetof(struct swdc_dll, get_analogs), + } +}; + +struct swdc_dll swdc_dll; + +// Copypasta DLL binding and diagnostic message boilerplate. +// Not much of this lends itself to being easily factored out. Also there +// will be a lot of API-specific branching code here eventually as new API +// versions get defined, so even though these functions all look the same +// now this won't remain the case forever. + +HRESULT swdc_dll_init(const struct swdc_dll_config *cfg, HINSTANCE self) +{ + uint16_t (*get_api_version)(void); + const struct dll_bind_sym *sym; + HINSTANCE owned; + HINSTANCE src; + HRESULT hr; + + assert(cfg != NULL); + assert(self != NULL); + + if (cfg->path[0] != L'\0') { + owned = LoadLibraryW(cfg->path); + + if (owned == NULL) { + hr = HRESULT_FROM_WIN32(GetLastError()); + dprintf("SWDC IO: Failed to load IO DLL: %lx: %S\n", + hr, + cfg->path); + + goto end; + } + + dprintf("SWDC IO: Using custom IO DLL: %S\n", cfg->path); + src = owned; + } else { + owned = NULL; + src = self; + } + + get_api_version = (void *) GetProcAddress(src, "swdc_io_get_api_version"); + + if (get_api_version != NULL) { + swdc_dll.api_version = get_api_version(); + } else { + swdc_dll.api_version = 0x0100; + dprintf("Custom IO DLL does not expose swdc_io_get_api_version, " + "assuming API version 1.0.\n" + "Please ask the developer to update their DLL.\n"); + } + + if (swdc_dll.api_version >= 0x0200) { + hr = E_NOTIMPL; + dprintf("SWDC IO: Custom IO DLL implements an unsupported " + "API version (%#04x). Please update Segatools.\n", + swdc_dll.api_version); + + goto end; + } + + sym = swdc_dll_syms; + hr = dll_bind(&swdc_dll, src, &sym, _countof(swdc_dll_syms)); + + if (FAILED(hr)) { + if (src != self) { + dprintf("SWDC IO: Custom IO DLL does not provide function " + "\"%s\". Please contact your IO DLL's developer for " + "further assistance.\n", + sym->sym); + + goto end; + } else { + dprintf("Internal error: could not reflect \"%s\"\n", sym->sym); + } + } + + owned = NULL; + +end: + if (owned != NULL) { + FreeLibrary(owned); + } + + return hr; +} diff --git a/swdchook/swdc-dll.h b/swdchook/swdc-dll.h new file mode 100644 index 0000000..8e13a2c --- /dev/null +++ b/swdchook/swdc-dll.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "swdcio/swdcio.h" + +struct swdc_dll { + uint16_t api_version; + HRESULT (*init)(void); + HRESULT (*poll)(void); + void (*get_opbtns)(uint8_t *opbtn); + void (*get_gamebtns)(uint16_t *gamebtn); + void (*get_analogs)(struct swdc_io_analog_state *out); +}; + +struct swdc_dll_config { + wchar_t path[MAX_PATH]; +}; + +extern struct swdc_dll swdc_dll; + +HRESULT swdc_dll_init(const struct swdc_dll_config *cfg, HINSTANCE self); diff --git a/swdchook/swdchook.def b/swdchook/swdchook.def new file mode 100644 index 0000000..37b47bd --- /dev/null +++ b/swdchook/swdchook.def @@ -0,0 +1,19 @@ +LIBRARY swdchook + +EXPORTS + aime_io_get_api_version + aime_io_init + aime_io_led_set_color + aime_io_nfc_get_aime_id + aime_io_nfc_get_felica_id + aime_io_nfc_poll + amDllVideoClose @2 + amDllVideoGetVBiosVersion @4 + amDllVideoOpen @1 + amDllVideoSetResolution @3 + swdc_io_get_api_version + swdc_io_init + swdc_io_poll + swdc_io_get_opbtns + swdc_io_get_gamebtns + swdc_io_get_analogs diff --git a/swdchook/zinput.c b/swdchook/zinput.c new file mode 100644 index 0000000..bd18392 --- /dev/null +++ b/swdchook/zinput.c @@ -0,0 +1,186 @@ +#include +#include + +#include +#include +#include + +#include "swdchook/config.h" +#include "swdchook/zinput.h" + +#include "hook/table.h" + +#include "util/dprintf.h" + +HRESULT WINAPI hook_DirectInput8Create( + HINSTANCE hinst, + DWORD dwVersion, + REFIID riidltf, + LPVOID *ppvOut, + LPUNKNOWN punkOuter); + +static unsigned long WINAPI hook_AddRef(IUnknown *self); + +static unsigned long WINAPI hook_Release(IUnknown *self); + +static HRESULT WINAPI hook_CreateDevice( + IDirectInput8W *self, + REFGUID rguid, + LPDIRECTINPUTDEVICE8W * lplpDirectInputDevice, + LPUNKNOWN pUnkOuter); + +static HRESULT WINAPI hook_EnumDevices( + IDirectInput8W *self, + DWORD dwDevType, + LPDIENUMDEVICESCALLBACKW lpCallback, + LPVOID pvRef, + DWORD dwFlags); + +static HRESULT WINAPI hook_SetDataFormat( + IDirectInputDevice8W *self, + LPCDIDATAFORMAT lpdf); + +static HRESULT WINAPI hook_SetCooperativeLevel( + IDirectInputDevice8W *self, + HWND hwnd, + DWORD flags); + +static HRESULT WINAPI hook_Acquire(IDirectInputDevice8W *self); + +static HRESULT WINAPI hook_Unacquire(IDirectInputDevice8W *self); + +static HRESULT WINAPI hook_GetDeviceState( + IDirectInputDevice8W *self, + DWORD cbData, + LPVOID lpvData); + +static const IDirectInput8WVtbl api_vtbl = { + .AddRef = (void *) hook_AddRef, + .Release = (void *) hook_Release, + .CreateDevice = hook_CreateDevice, + .EnumDevices = hook_EnumDevices, +}; + +static const IDirectInput8W api = { (void *) &api_vtbl }; + +static const IDirectInputDevice8WVtbl dev_vtbl = { + .AddRef = (void *) hook_AddRef, + .Release = (void *) hook_Release, + .SetDataFormat = hook_SetDataFormat, + .SetCooperativeLevel= hook_SetCooperativeLevel, + .Acquire = hook_Acquire, + .Unacquire = hook_Unacquire, + .GetDeviceState = hook_GetDeviceState, +}; + +static const IDirectInputDevice8W dev = { (void *) &dev_vtbl }; + +static const struct hook_symbol zinput_hook_syms[] = { + { + .name = "DirectInput8Create", + .patch = hook_DirectInput8Create, + } +}; + +HRESULT zinput_hook_init(struct zinput_config *cfg) +{ + assert(cfg != NULL); + + if (!cfg->enable) { + return S_FALSE; + } + + hook_table_apply( + NULL, + "dinput8.dll", + zinput_hook_syms, + _countof(zinput_hook_syms)); + + return S_OK; +} + +HRESULT WINAPI hook_DirectInput8Create( + HINSTANCE hinst, + DWORD dwVersion, + REFIID riidltf, + LPVOID *ppvOut, + LPUNKNOWN punkOuter) +{ + dprintf("ZInput: Blocking built-in DirectInput support\n"); + *ppvOut = (void *) &api; + + return S_OK; +} + +static unsigned long WINAPI hook_AddRef(IUnknown *self) +{ + return 1; +} + +static unsigned long WINAPI hook_Release(IUnknown *self) +{ + return 1; +} + +static HRESULT WINAPI hook_CreateDevice( + IDirectInput8W *self, + REFGUID rguid, + LPDIRECTINPUTDEVICE8W *lplpDirectInputDevice, + LPUNKNOWN pUnkOuter) +{ + dprintf("ZInput: %s\n", __func__); + *lplpDirectInputDevice = (void *) &dev; + + return S_OK; +} + +static HRESULT WINAPI hook_EnumDevices( + IDirectInput8W *self, + DWORD dwDevType, + LPDIENUMDEVICESCALLBACKW lpCallback, + LPVOID pvRef, + DWORD dwFlags) +{ + dprintf("ZInput: %s\n", __func__); + + return S_OK; +} + +static HRESULT WINAPI hook_SetDataFormat( + IDirectInputDevice8W *self, + LPCDIDATAFORMAT lpdf) +{ + dprintf("ZInput: %s\n", __func__); + + return S_OK; +} + +static HRESULT WINAPI hook_SetCooperativeLevel( + IDirectInputDevice8W *self, + HWND hwnd, + DWORD flags) +{ + dprintf("ZInput: %s\n", __func__); + + return S_OK; +} + +static HRESULT WINAPI hook_Acquire(IDirectInputDevice8W *self) +{ + return S_OK; +} + +static HRESULT WINAPI hook_Unacquire(IDirectInputDevice8W *self) +{ + return S_OK; +} + +static HRESULT WINAPI hook_GetDeviceState( + IDirectInputDevice8W *self, + DWORD cbData, + LPVOID lpvData) +{ + memset(lpvData, 0, cbData); + + return S_OK; +} diff --git a/swdchook/zinput.h b/swdchook/zinput.h new file mode 100644 index 0000000..13a46cd --- /dev/null +++ b/swdchook/zinput.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include + +struct zinput_config { + bool enable; +}; + +HRESULT zinput_hook_init(struct zinput_config *cfg); diff --git a/swdcio/backend.h b/swdcio/backend.h new file mode 100644 index 0000000..d30645b --- /dev/null +++ b/swdcio/backend.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "swdcio/swdcio.h" + +struct swdc_io_backend { + void (*get_opbtns)(uint8_t *opbtn); + void (*get_gamebtns)(uint16_t *gamebtn); + void (*get_analogs)(struct swdc_io_analog_state *state); +}; diff --git a/swdcio/config.c b/swdcio/config.c new file mode 100644 index 0000000..7396338 --- /dev/null +++ b/swdcio/config.c @@ -0,0 +1,111 @@ +#include + +#include +#include +#include +#include +#include + +#include "swdcio/config.h" + +void swdc_di_config_load(struct swdc_di_config *cfg, const wchar_t *filename) +{ + wchar_t key[8]; + int i; + + assert(cfg != NULL); + assert(filename != NULL); + + GetPrivateProfileStringW( + L"dinput", + L"deviceName", + L"", + cfg->device_name, + _countof(cfg->device_name), + filename); + + GetPrivateProfileStringW( + L"dinput", + L"brakeAxis", + L"RZ", + cfg->brake_axis, + _countof(cfg->brake_axis), + filename); + + GetPrivateProfileStringW( + L"dinput", + L"accelAxis", + L"Y", + cfg->accel_axis, + _countof(cfg->accel_axis), + filename); + + cfg->start = GetPrivateProfileIntW(L"dinput", L"start", 0, filename); + cfg->view_chg = GetPrivateProfileIntW(L"dinput", L"viewChg", 0, filename); + cfg->shift_dn = GetPrivateProfileIntW(L"dinput", L"shiftDn", 0, filename); + cfg->shift_up = GetPrivateProfileIntW(L"dinput", L"shiftUp", 0, filename); + cfg->wheel_green = GetPrivateProfileIntW(L"dinput", L"wheelGreen", 0, filename); + cfg->wheel_red = GetPrivateProfileIntW(L"dinput", L"wheelRed", 0, filename); + cfg->wheel_blue = GetPrivateProfileIntW(L"dinput", L"wheelBlue", 0, filename); + cfg->wheel_yellow = GetPrivateProfileIntW(L"dinput", L"wheelYellow", 0, filename); + + cfg->reverse_brake_axis = GetPrivateProfileIntW( + L"dinput", + L"reverseBrakeAxis", + 0, + filename); + cfg->reverse_accel_axis = GetPrivateProfileIntW( + L"dinput", + L"reverseAccelAxis", + 0, + filename); +} + +void swdc_xi_config_load(struct swdc_xi_config *cfg, const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + cfg->single_stick_steering = GetPrivateProfileIntW( + L"io4", + L"singleStickSteering", + 0, + filename); +} + +void swdc_io_config_load(struct swdc_io_config *cfg, const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + cfg->vk_test = GetPrivateProfileIntW(L"io4", L"test", '1', filename); + cfg->vk_service = GetPrivateProfileIntW(L"io4", L"service", '2', filename); + cfg->vk_coin = GetPrivateProfileIntW(L"io4", L"coin", '3', filename); + cfg->restrict_ = GetPrivateProfileIntW(L"io4", L"restrict", 97, filename); + + GetPrivateProfileStringW( + L"io4", + L"mode", + L"xinput", + cfg->mode, + _countof(cfg->mode), + filename); + + swdc_shifter_config_load(&cfg->shifter, filename); + swdc_di_config_load(&cfg->di, filename); + swdc_xi_config_load(&cfg->xi, filename); +} + +void swdc_shifter_config_load( + struct swdc_shifter_config *cfg, + const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + cfg->auto_neutral = GetPrivateProfileIntW( + L"io4", + L"autoNeutral", + 0, + filename); +} diff --git a/swdcio/config.h b/swdcio/config.h new file mode 100644 index 0000000..4fa482f --- /dev/null +++ b/swdcio/config.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include + +struct swdc_shifter_config { + bool auto_neutral; +}; + +struct swdc_di_config { + wchar_t device_name[64]; + wchar_t brake_axis[16]; + wchar_t accel_axis[16]; + uint8_t start; + uint8_t view_chg; + uint8_t shift_dn; + uint8_t shift_up; + uint8_t wheel_green; + uint8_t wheel_red; + uint8_t wheel_blue; + uint8_t wheel_yellow; + bool reverse_brake_axis; + bool reverse_accel_axis; +}; + +struct swdc_xi_config { + bool single_stick_steering; +}; + +struct swdc_io_config { + uint8_t vk_test; + uint8_t vk_service; + uint8_t vk_coin; + wchar_t mode[8]; + int restrict_; + struct swdc_shifter_config shifter; + struct swdc_di_config di; + struct swdc_xi_config xi; +}; + +void swdc_di_config_load(struct swdc_di_config *cfg, const wchar_t *filename); +void swdc_xi_config_load(struct swdc_xi_config *cfg, const wchar_t *filename); +void swdc_io_config_load(struct swdc_io_config *cfg, const wchar_t *filename); +void swdc_shifter_config_load( + struct swdc_shifter_config *cfg, + const wchar_t *filename); diff --git a/swdcio/di-dev.c b/swdcio/di-dev.c new file mode 100644 index 0000000..116e529 --- /dev/null +++ b/swdcio/di-dev.c @@ -0,0 +1,163 @@ +#include +#include + +#include + +#include "swdcio/di-dev.h" + +#include "util/dprintf.h" + +HRESULT swdc_di_dev_start(IDirectInputDevice8W *dev, HWND wnd) +{ + HRESULT hr; + + assert(dev != NULL); + assert(wnd != NULL); + + hr = IDirectInputDevice8_SetCooperativeLevel( + dev, + wnd, + DISCL_BACKGROUND | DISCL_EXCLUSIVE); + + if (FAILED(hr)) { + dprintf("DirectInput: SetCooperativeLevel failed: %08x\n", (int) hr); + + return hr; + } + + hr = IDirectInputDevice8_SetDataFormat(dev, &c_dfDIJoystick); + + if (FAILED(hr)) { + dprintf("DirectInput: SetDataFormat failed: %08x\n", (int) hr); + + return hr; + } + + hr = IDirectInputDevice8_Acquire(dev); + + if (FAILED(hr)) { + dprintf("DirectInput: Acquire failed: %08x\n", (int) hr); + + return hr; + } + + return hr; +} + +void swdc_di_dev_start_fx(IDirectInputDevice8W *dev, IDirectInputEffect **out) +{ + /* Set up force-feedback on devices that support it. This is just a stub + for the time being, since we don't yet know how the serial port force + feedback protocol works. + + I'm currently developing with an Xbox One Thrustmaster TMX wheel, if + we don't perform at least some perfunctory FFB initialization of this + nature (or indeed if no DirectInput application is running) then the + wheel exhibits considerable resistance, similar to that of a stationary + car. Changing cf.lMagnitude to a nonzero value does cause the wheel to + continuously turn in the given direction with the given force as one + would expect (max magnitude per DirectInput docs is +/- 10000). + + Failure here is non-fatal, we log any errors and move on. + + https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ee416353(v=vs.85) + */ + + IDirectInputEffect *obj; + DWORD axis; + LONG direction; + DIEFFECT fx; + DICONSTANTFORCE cf; + HRESULT hr; + + assert(dev != NULL); + assert(out != NULL); + + *out = NULL; + + dprintf("DirectInput: Starting force feedback (may take a sec)\n"); + + axis = DIJOFS_X; + direction = 0; + + memset(&cf, 0, sizeof(cf)); + cf.lMagnitude = 0; + + memset(&fx, 0, sizeof(fx)); + fx.dwSize = sizeof(fx); + fx.dwFlags = DIEFF_CARTESIAN | DIEFF_OBJECTOFFSETS; + fx.dwDuration = INFINITE; + fx.dwGain = DI_FFNOMINALMAX; + fx.dwTriggerButton = DIEB_NOTRIGGER; + fx.dwTriggerRepeatInterval = INFINITE; + fx.cAxes = 1; + fx.rgdwAxes = &axis; + fx.rglDirection = &direction; + fx.cbTypeSpecificParams = sizeof(cf); + fx.lpvTypeSpecificParams = &cf; + + hr = IDirectInputDevice8_CreateEffect( + dev, + &GUID_ConstantForce, + &fx, + &obj, + NULL); + + if (FAILED(hr)) { + dprintf("DirectInput: DirectInput force feedback unavailable: %08x\n", + (int) hr); + + return; + } + + hr = IDirectInputEffect_Start(obj, INFINITE, 0); + + if (FAILED(hr)) { + IDirectInputEffect_Release(obj); + dprintf("DirectInput: DirectInput force feedback start failed: %08x\n", + (int) hr); + + return; + } + + *out = obj; + + dprintf("DirectInput: Force feedback initialized and set to zero\n"); +} + +HRESULT swdc_di_dev_poll( + IDirectInputDevice8W *dev, + HWND wnd, + union swdc_di_state *out) +{ + HRESULT hr; + MSG msg; + + assert(dev != NULL); + assert(wnd != NULL); + assert(out != NULL); + + memset(out, 0, sizeof(*out)); + + /* Pump our dummy window's message queue just in case DirectInput or an + IHV DirectInput driver somehow relies on it */ + + while (PeekMessageW(&msg, wnd, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + hr = IDirectInputDevice8_GetDeviceState( + dev, + sizeof(out->st), + &out->st); + + if (FAILED(hr)) { + dprintf("DirectInput: GetDeviceState error: %08x\n", (int) hr); + } + + /* JVS lacks a protocol for reporting hardware errors from poll command + responses, so this ends up returning zeroed input state instead. */ + + return hr; +} diff --git a/swdcio/di-dev.h b/swdcio/di-dev.h new file mode 100644 index 0000000..82ceef0 --- /dev/null +++ b/swdcio/di-dev.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +#include + +union swdc_di_state { + DIJOYSTATE st; + uint8_t bytes[sizeof(DIJOYSTATE)]; +}; + +HRESULT swdc_di_dev_start(IDirectInputDevice8W *dev, HWND wnd); +void swdc_di_dev_start_fx(IDirectInputDevice8W *dev, IDirectInputEffect **out); +HRESULT swdc_di_dev_poll( + IDirectInputDevice8W *dev, + HWND wnd, + union swdc_di_state *out); + diff --git a/swdcio/di.c b/swdcio/di.c new file mode 100644 index 0000000..f75aa6b --- /dev/null +++ b/swdcio/di.c @@ -0,0 +1,420 @@ +#include +#include + +#include +#include +#include + +#include "swdcio/backend.h" +#include "swdcio/config.h" +#include "swdcio/di.h" +#include "swdcio/di-dev.h" +#include "swdcio/swdcio.h" +#include "swdcio/wnd.h" + +#include "util/dprintf.h" +#include "util/str.h" + +struct swdc_di_axis { + wchar_t name[4]; + size_t off; +}; + +static HRESULT swdc_di_config_apply(const struct swdc_di_config *cfg); +static const struct swdc_di_axis *swdc_di_get_axis(const wchar_t *name); +static BOOL CALLBACK swdc_di_enum_callback( + const DIDEVICEINSTANCEW *dev, + void *ctx); +static BOOL CALLBACK swdc_di_enum_callback_shifter( + const DIDEVICEINSTANCEW *dev, + void *ctx); +static void swdc_di_get_buttons(uint16_t *gamebtn_out); +static uint8_t swdc_di_decode_pov(DWORD pov); +static void swdc_di_get_analogs(struct swdc_io_analog_state *out); + +static const struct swdc_di_axis swdc_di_axes[] = { + /* Just map DIJOYSTATE for now, we can map DIJOYSTATE2 later if needed */ + { .name = L"X", .off = DIJOFS_X }, + { .name = L"Y", .off = DIJOFS_Y }, + { .name = L"Z", .off = DIJOFS_Z }, + { .name = L"RX", .off = DIJOFS_RX }, + { .name = L"RY", .off = DIJOFS_RY }, + { .name = L"RZ", .off = DIJOFS_RZ }, + { .name = L"U", .off = DIJOFS_SLIDER(0) }, + { .name = L"V", .off = DIJOFS_SLIDER(1) }, +}; + +static const struct swdc_io_backend swdc_di_backend = { + .get_gamebtns = swdc_di_get_buttons, + .get_analogs = swdc_di_get_analogs, +}; + +static HWND swdc_di_wnd; +static IDirectInput8W *swdc_di_api; +static IDirectInputDevice8W *swdc_di_dev; +static IDirectInputEffect *swdc_di_fx; +static size_t swdc_di_off_brake; +static size_t swdc_di_off_accel; +static uint8_t swdc_di_shift_dn; +static uint8_t swdc_di_shift_up; +static uint8_t swdc_di_view_chg; +static uint8_t swdc_di_start; +static uint8_t swdc_di_wheel_green; +static uint8_t swdc_di_wheel_red; +static uint8_t swdc_di_wheel_blue; +static uint8_t swdc_di_wheel_yellow; +static bool swdc_di_reverse_brake_axis; +static bool swdc_di_reverse_accel_axis; + +HRESULT swdc_di_init( + const struct swdc_di_config *cfg, + HINSTANCE inst, + const struct swdc_io_backend **backend) +{ + HRESULT hr; + HMODULE dinput8; + HRESULT (WINAPI *api_entry)(HINSTANCE,DWORD,REFIID,LPVOID *,LPUNKNOWN); + wchar_t dll_path[MAX_PATH]; + UINT path_pos; + + assert(cfg != NULL); + assert(backend != NULL); + + *backend = NULL; + + hr = swdc_di_config_apply(cfg); + + if (FAILED(hr)) { + return hr; + } + + hr = swdc_io_wnd_create(inst, &swdc_di_wnd); + + if (FAILED(hr)) { + return hr; + } + + /* SWDC has some built-in DirectInput support that is not + particularly useful. swdchook shorts this out by redirecting dinput8.dll + to a no-op implementation of DirectInput. However, swdcio does need to + talk to the real operating system implementation of DirectInput without + the stub DLL interfering, so build a path to + C:\Windows\System32\dinput.dll here. */ + + dll_path[0] = L'\0'; + path_pos = GetSystemDirectoryW(dll_path, _countof(dll_path)); + wcscat_s( + dll_path + path_pos, + _countof(dll_path) - path_pos, + L"\\dinput8.dll"); + + dinput8 = LoadLibraryW(dll_path); + + if (dinput8 == NULL) { + hr = HRESULT_FROM_WIN32(GetLastError()); + dprintf("DirectInput: LoadLibrary failed: %08x\n", (int) hr); + + return hr; + } + + api_entry = (void *) GetProcAddress(dinput8, "DirectInput8Create"); + + if (api_entry == NULL) { + dprintf("DirectInput: GetProcAddress failed\n"); + + return E_FAIL; + } + + hr = api_entry( + inst, + DIRECTINPUT_VERSION, + &IID_IDirectInput8W, + (void **) &swdc_di_api, + NULL); + + if (FAILED(hr)) { + dprintf("DirectInput: API create failed: %08x\n", (int) hr); + + return hr; + } + + hr = IDirectInput8_EnumDevices( + swdc_di_api, + DI8DEVCLASS_GAMECTRL, + swdc_di_enum_callback, + (void *) cfg, + DIEDFL_ATTACHEDONLY); + + if (FAILED(hr)) { + dprintf("DirectInput: EnumDevices failed: %08x\n", (int) hr); + + return hr; + } + + if (swdc_di_dev == NULL) { + dprintf("Wheel: Controller not found\n"); + + return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); + } + + hr = swdc_di_dev_start(swdc_di_dev, swdc_di_wnd); + + if (FAILED(hr)) { + return hr; + } + + swdc_di_dev_start_fx(swdc_di_dev, &swdc_di_fx); + + dprintf("DirectInput: Controller initialized\n"); + + *backend = &swdc_di_backend; + + return S_OK; +} + +static HRESULT swdc_di_config_apply(const struct swdc_di_config *cfg) +{ + const struct swdc_di_axis *brake_axis; + const struct swdc_di_axis *accel_axis; + int i; + + brake_axis = swdc_di_get_axis(cfg->brake_axis); + accel_axis = swdc_di_get_axis(cfg->accel_axis); + + if (brake_axis == NULL) { + dprintf("Wheel: Invalid brake axis: %S\n", cfg->brake_axis); + + return E_INVALIDARG; + } + + if (accel_axis == NULL) { + dprintf("Wheel: Invalid accel axis: %S\n", cfg->accel_axis); + + return E_INVALIDARG; + } + + if (cfg->start > 32) { + dprintf("Wheel: Invalid start button: %i\n", cfg->start); + + return E_INVALIDARG; + } + + if (cfg->view_chg > 32) { + dprintf("Wheel: Invalid view change button: %i\n", cfg->view_chg); + + return E_INVALIDARG; + } + + if (cfg->shift_dn > 32) { + dprintf("Wheel: Invalid shift down button: %i\n", cfg->shift_dn); + + return E_INVALIDARG; + } + + if (cfg->shift_up > 32) { + dprintf("Wheel: Invalid shift up button: %i\n", cfg->shift_up); + + return E_INVALIDARG; + } + + if (cfg->wheel_green > 32) { + dprintf("Wheel: Invalid steering wheel green button: %i\n", cfg->wheel_green); + + return E_INVALIDARG; + } + + if (cfg->wheel_red > 32) { + dprintf("Wheel: Invalid steering wheel red button: %i\n", cfg->wheel_red); + + return E_INVALIDARG; + } + + if (cfg->wheel_blue > 32) { + dprintf("Wheel: Invalid steering wheel blue button: %i\n", cfg->wheel_blue); + + return E_INVALIDARG; + } + + if (cfg->wheel_yellow > 32) { + dprintf("Wheel: Invalid steering wheel yellow button: %i\n", cfg->wheel_yellow); + + return E_INVALIDARG; + } + + /* Print some debug output to make sure config works... */ + + dprintf("Wheel: --- Begin configuration ---\n"); + dprintf("Wheel: Device name . . . . : Contains \"%S\"\n", + cfg->device_name); + dprintf("Wheel: Brake axis . . . . . . : %S\n", accel_axis->name); + dprintf("Wheel: Accelerator axis . . . : %S\n", brake_axis->name); + dprintf("Wheel: Start button . . . . . : %i\n", cfg->start); + dprintf("Wheel: View Change button . . : %i\n", cfg->view_chg); + dprintf("Wheel: Shift Down button . . : %i\n", cfg->shift_dn); + dprintf("Wheel: Shift Up button . . . : %i\n", cfg->shift_up); + dprintf("Wheel: Steering Green button : %i\n", cfg->wheel_green); + dprintf("Wheel: Steering Red button . : %i\n", cfg->wheel_red); + dprintf("Wheel: Steering Blue button . : %i\n", cfg->wheel_blue); + dprintf("Wheel: Steering Yellow button : %i\n", cfg->wheel_yellow); + dprintf("Wheel: Reverse Brake Axis . . : %i\n", cfg->reverse_brake_axis); + dprintf("Wheel: Reverse Accel Axis . . : %i\n", cfg->reverse_accel_axis); + dprintf("Wheel: --- End configuration ---\n"); + + swdc_di_off_brake = accel_axis->off; + swdc_di_off_accel = brake_axis->off; + swdc_di_start = cfg->start; + swdc_di_view_chg = cfg->view_chg; + swdc_di_shift_dn = cfg->shift_dn; + swdc_di_shift_up = cfg->shift_up; + swdc_di_wheel_green = cfg->wheel_green; + swdc_di_wheel_red = cfg->wheel_red; + swdc_di_wheel_blue = cfg->wheel_blue; + swdc_di_wheel_yellow = cfg->wheel_yellow; + swdc_di_reverse_brake_axis = cfg->reverse_brake_axis; + swdc_di_reverse_accel_axis = cfg->reverse_accel_axis; + + return S_OK; +} + +static const struct swdc_di_axis *swdc_di_get_axis(const wchar_t *name) +{ + const struct swdc_di_axis *axis; + size_t i; + + for (i = 0 ; i < _countof(swdc_di_axes) ; i++) { + axis = &swdc_di_axes[i]; + + if (wstr_ieq(name, axis->name)) { + return axis; + } + } + + return NULL; +} + +static BOOL CALLBACK swdc_di_enum_callback( + const DIDEVICEINSTANCEW *dev, + void *ctx) +{ + const struct swdc_di_config *cfg; + HRESULT hr; + + cfg = ctx; + + if (wcsstr(dev->tszProductName, cfg->device_name) == NULL) { + return DIENUM_CONTINUE; + } + + dprintf("Wheel: Using DirectInput device \"%S\"\n", dev->tszProductName); + + hr = IDirectInput8_CreateDevice( + swdc_di_api, + &dev->guidInstance, + &swdc_di_dev, + NULL); + + if (FAILED(hr)) { + dprintf("Wheel: CreateDevice failed: %08x\n", (int) hr); + } + + return DIENUM_STOP; +} + +static void swdc_di_get_buttons(uint16_t *gamebtn_out) +{ + union swdc_di_state state; + uint8_t gamebtn; + HRESULT hr; + + assert(gamebtn_out != NULL); + + hr = swdc_di_dev_poll(swdc_di_dev, swdc_di_wnd, &state); + + if (FAILED(hr)) { + return; + } + + gamebtn = swdc_di_decode_pov(state.st.rgdwPOV[0]); + + if (swdc_di_start && state.st.rgbButtons[swdc_di_start - 1]) { + gamebtn |= SWDC_IO_GAMEBTN_START; + } + + if (swdc_di_view_chg && state.st.rgbButtons[swdc_di_view_chg - 1]) { + gamebtn |= SWDC_IO_GAMEBTN_VIEW_CHANGE; + } + + if (swdc_di_shift_dn && state.st.rgbButtons[swdc_di_shift_dn - 1]) { + gamebtn |= SWDC_IO_GAMEBTN_STEERING_PADDLE_LEFT; + } + + if (swdc_di_shift_up && state.st.rgbButtons[swdc_di_shift_up - 1]) { + gamebtn |= SWDC_IO_GAMEBTN_STEERING_PADDLE_RIGHT; + } + + if (swdc_di_wheel_green && state.st.rgbButtons[swdc_di_wheel_green - 1]) { + gamebtn |= SWDC_IO_GAMEBTN_STEERING_GREEN; + } + + if (swdc_di_wheel_red && state.st.rgbButtons[swdc_di_wheel_red - 1]) { + gamebtn |= SWDC_IO_GAMEBTN_STEERING_RED; + } + + if (swdc_di_wheel_blue && state.st.rgbButtons[swdc_di_wheel_blue - 1]) { + gamebtn |= SWDC_IO_GAMEBTN_STEERING_BLUE; + } + + if (swdc_di_wheel_yellow && state.st.rgbButtons[swdc_di_wheel_yellow - 1]) { + gamebtn |= SWDC_IO_GAMEBTN_STEERING_YELLOW; + } + + *gamebtn_out = gamebtn; +} + +static uint8_t swdc_di_decode_pov(DWORD pov) +{ + switch (pov) { + case 0: return SWDC_IO_GAMEBTN_UP; + case 4500: return SWDC_IO_GAMEBTN_UP | SWDC_IO_GAMEBTN_RIGHT; + case 9000: return SWDC_IO_GAMEBTN_RIGHT; + case 13500: return SWDC_IO_GAMEBTN_RIGHT | SWDC_IO_GAMEBTN_DOWN; + case 18000: return SWDC_IO_GAMEBTN_DOWN; + case 22500: return SWDC_IO_GAMEBTN_DOWN | SWDC_IO_GAMEBTN_RIGHT; + case 27000: return SWDC_IO_GAMEBTN_LEFT; + case 31500: return SWDC_IO_GAMEBTN_LEFT | SWDC_IO_GAMEBTN_UP; + default: return 0; + } +} + +static void swdc_di_get_analogs(struct swdc_io_analog_state *out) +{ + union swdc_di_state state; + const LONG *brake; + const LONG *accel; + HRESULT hr; + + assert(out != NULL); + + hr = swdc_di_dev_poll(swdc_di_dev, swdc_di_wnd, &state); + + if (FAILED(hr)) { + return; + } + + brake = (LONG *) &state.bytes[swdc_di_off_brake]; + accel = (LONG *) &state.bytes[swdc_di_off_accel]; + + out->wheel = state.st.lX - 32768; + + if (swdc_di_reverse_brake_axis) { + out->brake = *brake; + } else { + out->brake = 65535 - *brake; + } + + if (swdc_di_reverse_accel_axis) { + out->accel = *accel; + } else { + out->accel = 65535 - *accel; + } +} diff --git a/swdcio/di.h b/swdcio/di.h new file mode 100644 index 0000000..6e4b32c --- /dev/null +++ b/swdcio/di.h @@ -0,0 +1,9 @@ +#pragma once + +#include "swdcio/backend.h" +#include "swdcio/config.h" + +HRESULT swdc_di_init( + const struct swdc_di_config *cfg, + HINSTANCE inst, + const struct swdc_io_backend **backend); diff --git a/swdcio/dllmain.c b/swdcio/dllmain.c new file mode 100644 index 0000000..ead174c --- /dev/null +++ b/swdcio/dllmain.c @@ -0,0 +1,112 @@ +#include + +#include +#include +#include + +#include "swdcio/backend.h" +#include "swdcio/config.h" +#include "swdcio/di.h" +#include "swdcio/swdcio.h" +#include "swdcio/xi.h" + +#include "util/dprintf.h" +#include "util/str.h" + +static struct swdc_io_config swdc_io_cfg; +static const struct swdc_io_backend *swdc_io_backend; +static bool swdc_io_coin; + +uint16_t swdc_io_get_api_version(void) +{ + return 0x0100; +} + +HRESULT swdc_io_init(void) +{ + HINSTANCE inst; + HRESULT hr; + + assert(swdc_io_backend == NULL); + + inst = GetModuleHandleW(NULL); + + if (inst == NULL) { + hr = HRESULT_FROM_WIN32(GetLastError()); + dprintf("GetModuleHandleW failed: %lx\n", hr); + + return hr; + } + + swdc_io_config_load(&swdc_io_cfg, L".\\segatools.ini"); + + if (wstr_ieq(swdc_io_cfg.mode, L"dinput")) { + hr = swdc_di_init(&swdc_io_cfg.di, inst, &swdc_io_backend); + } else if (wstr_ieq(swdc_io_cfg.mode, L"xinput")) { + hr = swdc_xi_init(&swdc_io_cfg.xi, &swdc_io_backend); + } else { + hr = E_INVALIDARG; + dprintf("swdc IO: Invalid IO mode \"%S\", use dinput or xinput\n", + swdc_io_cfg.mode); + } + + return hr; +} + +void swdc_io_get_opbtns(uint8_t *opbtn_out) +{ + uint8_t opbtn; + + assert(swdc_io_backend != NULL); + assert(opbtn_out != NULL); + + opbtn = 0; + + if (GetAsyncKeyState(swdc_io_cfg.vk_test) & 0x8000) { + opbtn |= SWDC_IO_OPBTN_TEST; + } + + if (GetAsyncKeyState(swdc_io_cfg.vk_service) & 0x8000) { + opbtn |= SWDC_IO_OPBTN_SERVICE; + } + + if (GetAsyncKeyState(swdc_io_cfg.vk_coin) & 0x8000) { + if (!swdc_io_coin) { + swdc_io_coin = true; + opbtn |= SWDC_IO_OPBTN_COIN; + } + } else { + swdc_io_coin = false; + } + + *opbtn_out = opbtn; +} + + +void swdc_io_get_gamebtns(uint16_t *gamebtn_out) +{ + assert(swdc_io_backend != NULL); + assert(gamebtn_out != NULL); + + swdc_io_backend->get_gamebtns(gamebtn_out); +} + +void swdc_io_get_analogs(struct swdc_io_analog_state *out) +{ + struct swdc_io_analog_state tmp; + + assert(out != NULL); + assert(swdc_io_backend != NULL); + + swdc_io_backend->get_analogs(&tmp); + + /* Apply steering wheel restriction. Real cabs only report about 77% of + the IO-3's max ADC output value when the wheel is turned to either of + its maximum positions. To match this behavior we set the default value + for the wheel restriction config parameter to 97 (out of 128). This + scaling factor is applied using fixed-point arithmetic below. */ + + out->wheel = (tmp.wheel * swdc_io_cfg.restrict_) / 128; + out->accel = tmp.accel; + out->brake = tmp.brake; +} diff --git a/swdcio/meson.build b/swdcio/meson.build new file mode 100644 index 0000000..71e018a --- /dev/null +++ b/swdcio/meson.build @@ -0,0 +1,30 @@ +swdcio_lib = static_library( + 'swdccio', + name_prefix : '', + include_directories : inc, + implicit_include_directories : false, + c_pch : '../precompiled.h', + dependencies : [ + dinput8_lib, + dxguid_lib, + xinput_lib, + ], + link_with : [ + util_lib, + ], + sources : [ + 'backend.h', + 'config.c', + 'config.h', + 'di.c', + 'di.h', + 'di-dev.c', + 'di-dev.h', + 'dllmain.c', + 'swdcio.h', + 'wnd.c', + 'wnd.h', + 'xi.c', + 'xi.h', + ], +) diff --git a/swdcio/swdcio.def b/swdcio/swdcio.def new file mode 100644 index 0000000..c2cb0ce --- /dev/null +++ b/swdcio/swdcio.def @@ -0,0 +1,8 @@ +LIBRARY swdcio + +EXPORTS + swdc_io_init + swdc_io_poll + swdc_io_get_opbtns + swdc_io_get_gamebtns + swdc_io_get_analogs diff --git a/swdcio/swdcio.h b/swdcio/swdcio.h new file mode 100644 index 0000000..5ce593c --- /dev/null +++ b/swdcio/swdcio.h @@ -0,0 +1,98 @@ +#pragma once + +#include + +#include + +enum { + SWDC_IO_OPBTN_TEST = 0x01, + SWDC_IO_OPBTN_SERVICE = 0x02, + SWDC_IO_OPBTN_COIN = 0x04, +}; + +enum { + SWDC_IO_GAMEBTN_UP = 0x01, + SWDC_IO_GAMEBTN_DOWN = 0x02, + SWDC_IO_GAMEBTN_LEFT = 0x04, + SWDC_IO_GAMEBTN_RIGHT = 0x08, + SWDC_IO_GAMEBTN_START = 0x10, + SWDC_IO_GAMEBTN_VIEW_CHANGE = 0x20, + + SWDC_IO_GAMEBTN_STEERING_BLUE = 0x40, + SWDC_IO_GAMEBTN_STEERING_GREEN = 0x80, + SWDC_IO_GAMEBTN_STEERING_RED = 0x100, + SWDC_IO_GAMEBTN_STEERING_YELLOW = 0x200, + SWDC_IO_GAMEBTN_STEERING_PADDLE_LEFT = 0x400, + SWDC_IO_GAMEBTN_STEERING_PADDLE_RIGHT = 0x800, +}; + +struct swdc_io_analog_state { + /* Current steering wheel position, where zero is the centered position. + + The game will accept any signed 16-bit position value, however a real + cabinet will report a value of approximately +/- 25230 when the wheel + is at full lock. Steering wheel positions of a magnitude greater than + this value are not possible on a real cabinet. */ + + int16_t wheel; + + /* Current position of the accelerator pedal, where 0 is released. */ + + uint16_t accel; + + /* Current position of the brake pedal, where 0 is released. */ + + uint16_t brake; +}; + +/* Get the version of the IDAC IO API that this DLL supports. This + function should return a positive 16-bit integer, where the high byte is + the major version and the low byte is the minor version (as defined by the + Semantic Versioning standard). + + The latest API version as of this writing is 0x0100. */ + +uint16_t swdc_io_get_api_version(void); + +/* Initialize the IO DLL. This is the second function that will be called on + your DLL, after mu3_io_get_api_version. + + All subsequent calls to this API may originate from arbitrary threads. + + Minimum API version: 0x0100 */ + +HRESULT swdc_io_init(void); + +/* Send any queued outputs (of which there are currently none, though this may + change in subsequent API versions) and retrieve any new inputs. + + Minimum API version: 0x0100 */ + +HRESULT swdc_io_poll(void); + +/* Get the state of the cabinet's operator buttons as of the last poll. See + MU3_IO_OPBTN enum above: this contains bit mask definitions for button + states returned in *opbtn. All buttons are active-high. + + Minimum API version: 0x0100 */ + +void swdc_io_get_opbtns(uint8_t *opbtn); + +/* Get the state of the cabinet's gameplay buttons as of the last poll. See + MU3_IO_GAMEBTN enum above for bit mask definitions. Inputs are split into + a left hand side set of inputs and a right hand side set of inputs: the bit + mappings are the same in both cases. + + All buttons are active-high, even though some buttons' electrical signals + on a real cabinet are active-low. + + Minimum API version: 0x0100 */ + +void swdc_io_get_gamebtns(uint16_t *gamebtn); + +/* Poll the current state of the cabinet's JVS analog inputs. See structure + definition above for details. + + Minimum API version: 0x0100 */ + +void swdc_io_get_analogs(struct swdc_io_analog_state *out); diff --git a/swdcio/wnd.c b/swdcio/wnd.c new file mode 100644 index 0000000..66699ee --- /dev/null +++ b/swdcio/wnd.c @@ -0,0 +1,86 @@ +#include + +#include +#include + +#include "util/dprintf.h" + +/* DirectInput requires a window for correct initialization (and also force + feedback), so this source file provides some utilities for creating a + generic message-only window. */ + +static LRESULT WINAPI swdc_io_wnd_proc( + HWND hwnd, + UINT msg, + WPARAM wparam, + LPARAM lparam); + +HRESULT swdc_io_wnd_create(HINSTANCE inst, HWND *out) +{ + HRESULT hr; + WNDCLASSEXW wcx; + ATOM atom; + HWND hwnd; + + assert(inst != NULL); /* We are not an EXE */ + assert(out != NULL); + + *out = NULL; + + memset(&wcx, 0, sizeof(wcx)); + wcx.cbSize = sizeof(wcx); + wcx.lpfnWndProc = swdc_io_wnd_proc; + wcx.hInstance = inst; + wcx.lpszClassName = L"SWDCIO"; + + atom = RegisterClassExW(&wcx); + + if (atom == 0) { + hr = HRESULT_FROM_WIN32(GetLastError()); + dprintf("SWDCIO: RegisterClassExW failed: %08x\n", (int) hr); + + goto fail; + } + + hwnd = CreateWindowExW( + 0, + (wchar_t *) (intptr_t) atom, + L"", + 0, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + HWND_MESSAGE, + NULL, + inst, + NULL); + + if (hwnd == NULL) { + hr = HRESULT_FROM_WIN32(GetLastError()); + dprintf("SWDCIO: CreateWindowExW failed: %08x\n", (int) hr); + + goto fail; + } + + *out = hwnd; + + return S_OK; + +fail: + UnregisterClassW((wchar_t *) (intptr_t) atom, inst); + + return hr; +} + +static LRESULT WINAPI swdc_io_wnd_proc( + HWND hwnd, + UINT msg, + WPARAM wparam, + LPARAM lparam) +{ + switch (msg) { + default: + return DefWindowProcW(hwnd, msg, wparam, lparam); + } +} diff --git a/swdcio/wnd.h b/swdcio/wnd.h new file mode 100644 index 0000000..5ab4188 --- /dev/null +++ b/swdcio/wnd.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +HRESULT swdc_io_wnd_create(HINSTANCE inst, HWND *out); diff --git a/swdcio/xi.c b/swdcio/xi.c new file mode 100644 index 0000000..9be56a2 --- /dev/null +++ b/swdcio/xi.c @@ -0,0 +1,165 @@ +#include +#include + +#include +#include +#include + +#include "swdcio/backend.h" +#include "swdcio/config.h" +#include "swdcio/swdcio.h" +#include "swdcio/xi.h" + +#include "util/dprintf.h" + +static void swdc_xi_get_gamebtns(uint16_t *gamebtn_out); +static void swdc_xi_get_analogs(struct swdc_io_analog_state *out); + +static HRESULT swdc_xi_config_apply(const struct swdc_xi_config *cfg); + +static const struct swdc_io_backend swdc_xi_backend = { + .get_gamebtns = swdc_xi_get_gamebtns, + .get_analogs = swdc_xi_get_analogs, +}; + +static bool swdc_xi_single_stick_steering; + +HRESULT swdc_xi_init(const struct swdc_xi_config *cfg, const struct swdc_io_backend **backend) +{ + HRESULT hr; + assert(cfg != NULL); + assert(backend != NULL); + + hr = swdc_xi_config_apply(cfg); + + if (FAILED(hr)) { + return hr; + } + + dprintf("XInput: Using XInput controller\n"); + *backend = &swdc_xi_backend; + + return S_OK; +} + +HRESULT swdc_io_poll(void) +{ + return S_OK; +} + +static HRESULT swdc_xi_config_apply(const struct swdc_xi_config *cfg) +{ + dprintf("XInput: --- Begin configuration ---\n"); + dprintf("XInput: Single Stick Steering : %i\n", cfg->single_stick_steering); + dprintf("XInput: --- End configuration ---\n"); + + swdc_xi_single_stick_steering = cfg->single_stick_steering; + + return S_OK; +} + +static void swdc_xi_get_gamebtns(uint16_t *gamebtn_out) +{ + uint16_t gamebtn; + XINPUT_STATE xi; + WORD xb; + + assert(gamebtn_out != NULL); + + gamebtn = 0; + + memset(&xi, 0, sizeof(xi)); + XInputGetState(0, &xi); + xb = xi.Gamepad.wButtons; + + if (xb & XINPUT_GAMEPAD_DPAD_UP) { + gamebtn |= SWDC_IO_GAMEBTN_UP; + } + + if (xb & XINPUT_GAMEPAD_DPAD_DOWN) { + gamebtn |= SWDC_IO_GAMEBTN_DOWN; + } + + if (xb & XINPUT_GAMEPAD_DPAD_LEFT) { + gamebtn |= SWDC_IO_GAMEBTN_LEFT; + } + + if (xb & XINPUT_GAMEPAD_DPAD_RIGHT) { + gamebtn |= SWDC_IO_GAMEBTN_RIGHT; + } + + if (xb & XINPUT_GAMEPAD_START) { + gamebtn |= SWDC_IO_GAMEBTN_START; + } + + if (xb & XINPUT_GAMEPAD_BACK) { + gamebtn |= SWDC_IO_GAMEBTN_VIEW_CHANGE; + } + + if (xb & XINPUT_GAMEPAD_A) { + gamebtn |= SWDC_IO_GAMEBTN_STEERING_GREEN; + } + + if (xb & XINPUT_GAMEPAD_B) { + gamebtn |= SWDC_IO_GAMEBTN_STEERING_RED; + } + + if (xb & XINPUT_GAMEPAD_X) { + gamebtn |= SWDC_IO_GAMEBTN_STEERING_BLUE; + } + + if (xb & XINPUT_GAMEPAD_Y) { + gamebtn |= SWDC_IO_GAMEBTN_STEERING_YELLOW; + } + + if (xb & XINPUT_GAMEPAD_LEFT_SHOULDER) { + gamebtn |= SWDC_IO_GAMEBTN_STEERING_PADDLE_LEFT; + } + + if (xb & XINPUT_GAMEPAD_RIGHT_SHOULDER) { + gamebtn |= SWDC_IO_GAMEBTN_STEERING_PADDLE_RIGHT; + } + + *gamebtn_out = gamebtn; +} + +static void swdc_xi_get_analogs(struct swdc_io_analog_state *out) +{ + XINPUT_STATE xi; + int left; + int right; + + assert(out != NULL); + + memset(&xi, 0, sizeof(xi)); + XInputGetState(0, &xi); + + left = xi.Gamepad.sThumbLX; + + if (left < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) { + left += XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE; + } else if (left > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) { + left -= XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE; + } else { + left = 0; + } + + right = xi.Gamepad.sThumbRX; + + if (right < -XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE) { + right += XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE; + } else if (right > XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE) { + right -= XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE; + } else { + right = 0; + } + + if (swdc_xi_single_stick_steering) { + out->wheel = left; + } else { + out->wheel = (left + right) / 2; + } + + out->accel = xi.Gamepad.bRightTrigger << 8; + out->brake = xi.Gamepad.bLeftTrigger << 8; +} diff --git a/swdcio/xi.h b/swdcio/xi.h new file mode 100644 index 0000000..15937ff --- /dev/null +++ b/swdcio/xi.h @@ -0,0 +1,10 @@ +#pragma once + +/* Can't call this xinput.h or it will conflict with */ + +#include + +#include "swdcio/backend.h" +#include "swdcio/config.h" + +HRESULT swdc_xi_init(const struct swdc_xi_config *cfg, const struct swdc_io_backend **backend);