From 555784258a82013ea7d22a4cf135d9fa401f78e3 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 23 Apr 2023 16:13:51 +0200 Subject: [PATCH] idac: test --- Package.mk | 16 ++ dist/idac/segatools.ini | 116 ++++++++ dist/idac/start.bat | 10 + docker-build.bat | 2 +- idachook/config.c | 59 +++++ idachook/config.h | 36 +++ idachook/dllmain.c | 139 ++++++++++ idachook/idac-dll.c | 112 ++++++++ idachook/idac-dll.h | 22 ++ idachook/idachook copy.def | 24 ++ idachook/idachook.def | 19 ++ idachook/jvs.c | 177 +++++++++++++ idachook/jvs.h | 7 + idachook/meson.build | 36 +++ idachook/zinput.c | 186 +++++++++++++ idachook/zinput.h | 11 + idacio/backend.h | 11 + idacio/config.c | 121 +++++++++ idacio/config.h | 45 ++++ idacio/di-dev.c | 163 ++++++++++++ idacio/di-dev.h | 19 ++ idacio/di.c | 523 +++++++++++++++++++++++++++++++++++++ idacio/di.h | 9 + idacio/dllmain.c | 125 +++++++++ idacio/idacio.h | 106 ++++++++ idacio/idzio.def | 8 + idacio/meson.build | 32 +++ idacio/shifter.c | 32 +++ idacio/shifter.h | 8 + idacio/wnd.c | 86 ++++++ idacio/wnd.h | 5 + idacio/xi.c | 165 ++++++++++++ idacio/xi.h | 10 + meson.build | 2 + segatools.md | 456 ++++++++++++++++++++++++++++++++ 35 files changed, 2897 insertions(+), 1 deletion(-) create mode 100644 dist/idac/segatools.ini create mode 100644 dist/idac/start.bat create mode 100644 idachook/config.c create mode 100644 idachook/config.h create mode 100644 idachook/dllmain.c create mode 100644 idachook/idac-dll.c create mode 100644 idachook/idac-dll.h create mode 100644 idachook/idachook copy.def create mode 100644 idachook/idachook.def create mode 100644 idachook/jvs.c create mode 100644 idachook/jvs.h create mode 100644 idachook/meson.build create mode 100644 idachook/zinput.c create mode 100644 idachook/zinput.h create mode 100644 idacio/backend.h create mode 100644 idacio/config.c create mode 100644 idacio/config.h create mode 100644 idacio/di-dev.c create mode 100644 idacio/di-dev.h create mode 100644 idacio/di.c create mode 100644 idacio/di.h create mode 100644 idacio/dllmain.c create mode 100644 idacio/idacio.h create mode 100644 idacio/idzio.def create mode 100644 idacio/meson.build create mode 100644 idacio/shifter.c create mode 100644 idacio/shifter.h create mode 100644 idacio/wnd.c create mode 100644 idacio/wnd.h create mode 100644 idacio/xi.c create mode 100644 idacio/xi.h create mode 100644 segatools.md diff --git a/Package.mk b/Package.mk index 850f9ee..3e6a887 100644 --- a/Package.mk +++ b/Package.mk @@ -73,6 +73,21 @@ $(BUILD_DIR_ZIP)/idz.zip: $(V)strip $(BUILD_DIR_ZIP)/idz/*.{exe,dll} $(V)cd $(BUILD_DIR_ZIP)/idz ; zip -r ../idz.zip * +$(BUILD_DIR_ZIP)/idac.zip: + $(V)echo ... $@ + $(V)mkdir -p $(BUILD_DIR_ZIP)/idac + $(V)mkdir -p $(BUILD_DIR_ZIP)/idac/DEVICE + $(V)cp $(BUILD_DIR_64)/subprojects/capnhook/inject/inject.exe \ + $(BUILD_DIR_64)/idachook/idachook.dll \ + $(DIST_DIR)/idac/segatools.ini \ + $(DIST_DIR)/idac/start.bat \ + $(BUILD_DIR_ZIP)/idac + $(V)cp pki/billing.pub \ + pki/ca.crt \ + $(BUILD_DIR_ZIP)/idac/DEVICE + $(V)strip $(BUILD_DIR_ZIP)/idac/*.{exe,dll} + $(V)cd $(BUILD_DIR_ZIP)/idac ; zip -r ../idac.zip * + $(BUILD_DIR_ZIP)/mercury.zip: $(V)echo ... $@ $(V)mkdir -p $(BUILD_DIR_ZIP)/mercury @@ -119,6 +134,7 @@ $(BUILD_DIR_ZIP)/segatools.zip: \ $(BUILD_DIR_ZIP)/diva.zip \ $(BUILD_DIR_ZIP)/doc.zip \ $(BUILD_DIR_ZIP)/idz.zip \ + $(BUILD_DIR_ZIP)/idac.zip \ $(BUILD_DIR_ZIP)/mercury.zip \ $(BUILD_DIR_ZIP)/mu3.zip \ CHANGELOG.md \ diff --git a/dist/idac/segatools.ini b/dist/idac/segatools.ini new file mode 100644 index 0000000..367d27e --- /dev/null +++ b/dist/idac/segatools.ini @@ -0,0 +1,116 @@ +[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= + +[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 + +[ds] +; Region code on the emulated AMEX board DS EEPROM. +; 1: Japan +; 4: Export (some UI elements in English) +; +; NOTE: Changing this setting causes a factory reset. +region=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. +; If you disable netenv then you must set this to your LAN's IP subnet, and +; that subnet must start with 192.168. +subnet=192.168.100.0 + +[gpio] +; Emulated Nu DIP switch for Distribution Server setting. +; +; If multiple machines are present on the same LAN then set this to 1 on +; exactly one machine and set this to 0 on all others. +dipsw1=1 + +[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= + +[idzio] +; To use a custom Initial D Zero IO DLL enter its path here. +; Leave empty if you want to use Segatools built-in gamepad/wheel input. +path= + +[io3] +; Input API selection for JVS input emulator. +; Set "xinput" to use a gamepad and "dinput" to use a steering wheel. +mode=xinput +; Automatically reset the simulated shifter to Neutral when XInput Start is +; pressed (e.g. when navigating menus between races). +autoNeutral=1 +; 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=0 +; 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= +; Name of the positional shifter to use (or any subset thereof). +; Leave blank if you do not have a positional shifter; a positional shifter +; will be simulated using the configured Shift Down and Shift Up buttons +; in this case. +; +; Can be the same device as the wheel. +; +; Example: T500 +shifterName= +; 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 simulated six-speed shifter. +shiftDn=1 +shiftUp=2 +; Button mappings for the positional shifter, if present. +gear1=1 +gear2=2 +gear3=3 +gear4=4 +gear5=5 +gear6=6 +; 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/idac/start.bat b/dist/idac/start.bat new file mode 100644 index 0000000..61ed927 --- /dev/null +++ b/dist/idac/start.bat @@ -0,0 +1,10 @@ +@echo off + +pushd %~dp0 + +start inject.exe -d -k idachook.dll amdaemon.exe -c config_common.json config_jp.json config_seat_1_jp.json config_seat_2_jp.json config_ex.json config_seat_1_ex.json config_seat_2_ex.json +inject -d -k idachook.dll ../WindowsNoEditor/GameProject/Binaries/Win64/GameProject-Win64-Shipping.exe + +echo. +echo Game processes have terminated +pause \ No newline at end of file diff --git a/docker-build.bat b/docker-build.bat index 969e032..822dc9b 100644 --- a/docker-build.bat +++ b/docker-build.bat @@ -18,7 +18,7 @@ if ERRORLEVEL 1 ( goto failure ) -docker image rm -f %IMAGE_NAME% +:: docker image rm -f %IMAGE_NAME% goto success diff --git a/idachook/config.c b/idachook/config.c new file mode 100644 index 0000000..7e648bf --- /dev/null +++ b/idachook/config.c @@ -0,0 +1,59 @@ +#include +#include + +#include "amex/amex.h" +#include "amex/config.h" + +#include "board/config.h" +#include "board/sg-reader.h" + +#include "gfxhook/config.h" + +#include "hooklib/config.h" +#include "hooklib/dvd.h" + +#include "idachook/config.h" +#include "idachook/idac-dll.h" + +#include "platform/config.h" +#include "platform/platform.h" + +void idac_dll_config_load( + struct idac_dll_config *cfg, + const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + GetPrivateProfileStringW( + L"idzio", + L"path", + L"", + cfg->path, + _countof(cfg->path), + filename); +} + +void idac_hook_config_load( + struct idac_hook_config *cfg, + const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + platform_config_load(&cfg->platform, filename); + amex_config_load(&cfg->amex, filename); + aime_config_load(&cfg->aime, filename); + dvd_config_load(&cfg->dvd, filename); + // gfx_config_load(&cfg->gfx, filename); + idac_dll_config_load(&cfg->dll, filename); + zinput_config_load(&cfg->zinput, 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/idachook/config.h b/idachook/config.h new file mode 100644 index 0000000..674797e --- /dev/null +++ b/idachook/config.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include "amex/amex.h" + +#include "board/sg-reader.h" + +#include "gfxhook/gfx.h" + +#include "hooklib/dvd.h" + +#include "idachook/idac-dll.h" +#include "idachook/zinput.h" + +#include "platform/platform.h" + +struct idac_hook_config { + struct platform_config platform; + struct amex_config amex; + struct aime_config aime; + struct dvd_config dvd; + struct idac_dll_config dll; + struct zinput_config zinput; +}; + +void idac_dll_config_load( + struct idac_dll_config *cfg, + const wchar_t *filename); + +void idac_hook_config_load( + struct idac_hook_config *cfg, + const wchar_t *filename); + +void zinput_config_load(struct zinput_config *cfg, const wchar_t *filename); diff --git a/idachook/dllmain.c b/idachook/dllmain.c new file mode 100644 index 0000000..fcf44d9 --- /dev/null +++ b/idachook/dllmain.c @@ -0,0 +1,139 @@ +#include +#include + +#include +#include +#include + +#include "amex/amex.h" + +#include "board/sg-reader.h" + +// #include "gfxhook/d3d11.h" +// #include "gfxhook/dxgi.h" +// #include "gfxhook/gfx.h" + +#include "hook/process.h" + +#include "hooklib/dvd.h" +#include "hooklib/serial.h" +#include "hooklib/spike.h" + +#include "idachook/config.h" +#include "idachook/idac-dll.h" +#include "idachook/jvs.h" +#include "idachook/zinput.h" + +#include "platform/platform.h" + +#include "util/dprintf.h" +#include "util/lib.h" + +static HMODULE idac_hook_mod; +static process_entry_t idac_startup; +static struct idac_hook_config idac_hook_cfg; + +static DWORD CALLBACK idac_pre_startup(void) +{ + wchar_t *module_path; + wchar_t *file_name; + HRESULT hr; + + dprintf("--- Begin idac_pre_startup ---\n"); + + /* Config load */ + + idac_hook_config_load(&idac_hook_cfg, L".\\segatools.ini"); + + /* + module_path = module_file_name(NULL); + + if (module_path != NULL) { + file_name = PathFindFileNameW(module_path); + + _wcslwr(file_name); + + if (wcsstr(file_name, L"serverbox") != NULL) { + dprintf("Executable filename contains 'ServerBox', disabling full-screen mode\n"); + + idac_hook_cfg.gfx.windowed = true; + idac_hook_cfg.gfx.framed = true; + } + + free(module_path); + + module_path = NULL; + } + */ + + /* Hook Win32 APIs */ + + serial_hook_init(); + // gfx_hook_init(&idac_hook_cfg.gfx); + // gfx_d3d11_hook_init(&idac_hook_cfg.gfx, idac_hook_mod); + // gfx_dxgi_hook_init(&idac_hook_cfg.gfx, idac_hook_mod); + zinput_hook_init(&idac_hook_cfg.zinput); + dvd_hook_init(&idac_hook_cfg.dvd, idac_hook_mod); + + /* Initialize emulation hooks */ + + hr = platform_hook_init( + &idac_hook_cfg.platform, + "SDGT", + "ACA2", + idac_hook_mod); + + if (FAILED(hr)) { + goto fail; + } + + hr = idac_dll_init(&idac_hook_cfg.dll, idac_hook_mod); + + if (FAILED(hr)) { + goto fail; + } + + hr = amex_hook_init(&idac_hook_cfg.amex, idac_jvs_init); + + if (FAILED(hr)) { + goto fail; + } + + hr = sg_reader_hook_init(&idac_hook_cfg.aime, 10, idac_hook_mod); + + if (FAILED(hr)) { + goto fail; + } + + /* Initialize debug helpers */ + + spike_hook_init(L".\\segatools.ini"); + + dprintf("--- End idac_pre_startup ---\n"); + + /* Jump to EXE start address */ + + return idac_startup(); + +fail: + ExitProcess(EXIT_FAILURE); +} + +BOOL WINAPI DllMain(HMODULE mod, DWORD cause, void *ctx) +{ + HRESULT hr; + + if (cause != DLL_PROCESS_ATTACH) { + return TRUE; + } + + idac_hook_mod = mod; + + hr = process_hijack_startup(idac_pre_startup, &idac_startup); + + if (!SUCCEEDED(hr)) { + dprintf("Failed to hijack process startup: %x\n", (int) hr); + } + + return SUCCEEDED(hr); +} diff --git a/idachook/idac-dll.c b/idachook/idac-dll.c new file mode 100644 index 0000000..6346089 --- /dev/null +++ b/idachook/idac-dll.c @@ -0,0 +1,112 @@ +#include + +#include +#include + +#include "idachook/idac-dll.h" + +#include "util/dll-bind.h" +#include "util/dprintf.h" + +const struct dll_bind_sym idac_dll_syms[] = { + { + .sym = "idac_io_jvs_init", + .off = offsetof(struct idac_dll, jvs_init), + }, { + .sym = "idac_io_jvs_read_analogs", + .off = offsetof(struct idac_dll, jvs_read_analogs), + }, { + .sym = "idac_io_jvs_read_buttons", + .off = offsetof(struct idac_dll, jvs_read_buttons), + }, { + .sym = "idac_io_jvs_read_shifter", + .off = offsetof(struct idac_dll, jvs_read_shifter), + }, { + .sym = "idac_io_jvs_read_coin_counter", + .off = offsetof(struct idac_dll, jvs_read_coin_counter), + } +}; + +struct idac_dll idac_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 idac_dll_init(const struct idac_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("IDZ IO: Failed to load IO DLL: %lx: %S\n", + hr, + cfg->path); + + goto end; + } + + dprintf("IDZ IO: Using custom IO DLL: %S\n", cfg->path); + src = owned; + } else { + owned = NULL; + src = self; + } + + get_api_version = (void *) GetProcAddress(src, "idac_io_get_api_version"); + + if (get_api_version != NULL) { + idac_dll.api_version = get_api_version(); + } else { + idac_dll.api_version = 0x0100; + dprintf("Custom IO DLL does not expose idac_io_get_api_version, " + "assuming API version 1.0.\n" + "Please ask the developer to update their DLL.\n"); + } + + if (idac_dll.api_version >= 0x0200) { + hr = E_NOTIMPL; + dprintf("IDZ IO: Custom IO DLL implements an unsupported " + "API version (%#04x). Please update Segatools.\n", + idac_dll.api_version); + + goto end; + } + + sym = idac_dll_syms; + hr = dll_bind(&idac_dll, src, &sym, _countof(idac_dll_syms)); + + if (FAILED(hr)) { + if (src != self) { + dprintf("IDZ 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/idachook/idac-dll.h b/idachook/idac-dll.h new file mode 100644 index 0000000..88902ed --- /dev/null +++ b/idachook/idac-dll.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "idacio/idacio.h" + +struct idac_dll { + uint16_t api_version; + HRESULT (*jvs_init)(void); + void (*jvs_read_analogs)(struct idac_io_analog_state *out); + void (*jvs_read_buttons)(uint8_t *opbtn, uint8_t *gamebtn); + void (*jvs_read_shifter)(uint8_t *gear); + void (*jvs_read_coin_counter)(uint16_t *total); +}; + +struct idac_dll_config { + wchar_t path[MAX_PATH]; +}; + +extern struct idac_dll idac_dll; + +HRESULT idac_dll_init(const struct idac_dll_config *cfg, HINSTANCE self); diff --git a/idachook/idachook copy.def b/idachook/idachook copy.def new file mode 100644 index 0000000..a46fa06 --- /dev/null +++ b/idachook/idachook copy.def @@ -0,0 +1,24 @@ +LIBRARY idachook + +EXPORTS + CreateDXGIFactory + CreateDXGIFactory1 + CreateDXGIFactory2 + D3D11CreateDevice + D3D11CreateDeviceAndSwapChain + 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 + idac_io_get_api_version + idac_io_jvs_init + idac_io_jvs_read_analogs + idac_io_jvs_read_buttons + idac_io_jvs_read_coin_counter + idac_io_jvs_read_shifter diff --git a/idachook/idachook.def b/idachook/idachook.def new file mode 100644 index 0000000..68ed1d0 --- /dev/null +++ b/idachook/idachook.def @@ -0,0 +1,19 @@ +LIBRARY idachook + +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 + idac_io_get_api_version + idac_io_jvs_init + idac_io_jvs_read_analogs + idac_io_jvs_read_buttons + idac_io_jvs_read_coin_counter + idac_io_jvs_read_shifter diff --git a/idachook/jvs.c b/idachook/jvs.c new file mode 100644 index 0000000..2192c24 --- /dev/null +++ b/idachook/jvs.c @@ -0,0 +1,177 @@ +#include + +#include +#include +#include + +#include "amex/jvs.h" + +#include "board/io3.h" + +#include "idachook/idac-dll.h" +#include "idachook/jvs.h" + +#include "jvs/jvs-bus.h" + +#include "util/dprintf.h" + +static void idac_jvs_read_analogs( + void *ctx, + uint16_t *analogs, + uint8_t nanalogs); +static void idac_jvs_read_switches(void *ctx, struct io3_switch_state *out); +static void idac_jvs_read_coin_counter( + void *ctx, + uint8_t slot_no, + uint16_t *out); + +static const struct io3_ops idac_jvs_io3_ops = { + .read_switches = idac_jvs_read_switches, + .read_analogs = idac_jvs_read_analogs, + .read_coin_counter = idac_jvs_read_coin_counter, +}; + +static const uint16_t idac_jvs_gear_signals[] = { + /* Neutral */ + 0x0000, + /* 1: Left|Up */ + 0x2800, + /* 2: Left|Down */ + 0x1800, + /* 3: Up */ + 0x2000, + /* 4: Down */ + 0x1000, + /* 5: Right|Up */ + 0x2400, + /* 6: Right|Down */ + 0x1400, +}; + +static struct io3 idac_jvs_io3; + +HRESULT idac_jvs_init(struct jvs_node **out) +{ + HRESULT hr; + + assert(out != NULL); + assert(idac_dll.jvs_init != NULL); + + dprintf("JVS I/O: Starting Initial D Zero backend DLL\n"); + hr = idac_dll.jvs_init(); + + if (FAILED(hr)) { + dprintf("JVS I/O: Backend error, I/O disconnected; %x\n", (int) hr); + + return hr; + } + + io3_init(&idac_jvs_io3, NULL, &idac_jvs_io3_ops, NULL); + *out = io3_to_jvs_node(&idac_jvs_io3); + + return S_OK; +} + +static void idac_jvs_read_switches(void *ctx, struct io3_switch_state *out) +{ + uint8_t opbtn; + uint8_t gamebtn; + uint8_t gear; + + assert(out != NULL); + assert(idac_dll.jvs_read_buttons != NULL); + assert(idac_dll.jvs_read_shifter != NULL); + + opbtn = 0; + gamebtn = 0; + gear = 0; + + idac_dll.jvs_read_buttons(&opbtn, &gamebtn); + idac_dll.jvs_read_shifter(&gear); + + /* Update gameplay buttons */ + + if (gamebtn & IDAC_IO_GAMEBTN_UP) { + out->p1 |= 1 << 13; + } + + if (gamebtn & IDAC_IO_GAMEBTN_DOWN) { + out->p1 |= 1 << 12; + } + + if (gamebtn & IDAC_IO_GAMEBTN_LEFT) { + out->p1 |= 1 << 11; + } + + if (gamebtn & IDAC_IO_GAMEBTN_RIGHT) { + out->p1 |= 1 << 10; + } + + if (gamebtn & IDAC_IO_GAMEBTN_START) { + out->p1 |= 1 << 15; + } + + if (gamebtn & IDAC_IO_GAMEBTN_VIEW_CHANGE) { + out->p1 |= 1 << 9; + } + + /* Update simulated six-speed shifter */ + + if (gear > 6) { + gear = 6; + } + + out->p2 = idac_jvs_gear_signals[gear]; + + /* Update test/service buttons */ + + if (opbtn & IDAC_IO_OPBTN_TEST) { + out->system = 0x80; + } else { + out->system = 0; + } + + if (opbtn & IDAC_IO_OPBTN_SERVICE) { + out->p1 |= 1 << 14; + } +} + +static void idac_jvs_read_analogs( + void *ctx, + uint16_t *analogs, + uint8_t nanalogs) +{ + struct idac_io_analog_state state; + + assert(analogs != NULL); + assert(idac_dll.jvs_read_analogs != NULL); + + memset(&state, 0, sizeof(state)); + idac_dll.jvs_read_analogs(&state); + + if (nanalogs > 0) { + analogs[0] = 0x8000 + state.wheel; + } + + if (nanalogs > 1) { + analogs[1] = state.accel; + } + + if (nanalogs > 2) { + analogs[2] = state.brake; + } +} + +static void idac_jvs_read_coin_counter( + void *ctx, + uint8_t slot_no, + uint16_t *out) +{ + assert(idac_dll.jvs_read_coin_counter != NULL); + + if (slot_no > 0) { + return; + } + + idac_dll.jvs_read_coin_counter(out); +} diff --git a/idachook/jvs.h b/idachook/jvs.h new file mode 100644 index 0000000..f0781a4 --- /dev/null +++ b/idachook/jvs.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include "jvs/jvs-bus.h" + +HRESULT idac_jvs_init(struct jvs_node **root); diff --git a/idachook/meson.build b/idachook/meson.build new file mode 100644 index 0000000..3a3369d --- /dev/null +++ b/idachook/meson.build @@ -0,0 +1,36 @@ +shared_library( + 'idachook', + name_prefix : '', + include_directories : inc, + implicit_include_directories : false, + vs_module_defs : 'idachook.def', + c_pch : '../precompiled.h', + dependencies : [ + capnhook.get_variable('hook_dep'), + capnhook.get_variable('hooklib_dep'), + shlwapi_lib, + xinput_lib, + ], + link_with : [ + aimeio_lib, + amex_lib, + board_lib, + # gfxhook_lib, + hooklib_lib, + idacio_lib, + jvs_lib, + platform_lib, + util_lib, + ], + sources : [ + 'config.c', + 'config.h', + 'dllmain.c', + 'idac-dll.c', + 'idac-dll.h', + 'jvs.c', + 'jvs.h', + 'zinput.c', + 'zinput.h', + ], +) diff --git a/idachook/zinput.c b/idachook/zinput.c new file mode 100644 index 0000000..59c77b5 --- /dev/null +++ b/idachook/zinput.c @@ -0,0 +1,186 @@ +#include +#include + +#include +#include +#include + +#include "idachook/config.h" +#include "idachook/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/idachook/zinput.h b/idachook/zinput.h new file mode 100644 index 0000000..13a46cd --- /dev/null +++ b/idachook/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/idacio/backend.h b/idacio/backend.h new file mode 100644 index 0000000..46daa0b --- /dev/null +++ b/idacio/backend.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "idacio/idacio.h" + +struct idac_io_backend { + void (*jvs_read_buttons)(uint8_t *gamebtn); + void (*jvs_read_shifter)(uint8_t *gear); + void (*jvs_read_analogs)(struct idac_io_analog_state *state); +}; diff --git a/idacio/config.c b/idacio/config.c new file mode 100644 index 0000000..5df7971 --- /dev/null +++ b/idacio/config.c @@ -0,0 +1,121 @@ +#include + +#include +#include +#include +#include +#include + +#include "idacio/config.h" + +void idac_di_config_load(struct idac_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"shifterName", + L"", + cfg->shifter_name, + _countof(cfg->shifter_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->reverse_brake_axis = GetPrivateProfileIntW( + L"dinput", + L"reverseBrakeAxis", + 0, + filename); + cfg->reverse_accel_axis = GetPrivateProfileIntW( + L"dinput", + L"reverseAccelAxis", + 0, + filename); + + for (i = 0 ; i < 6 ; i++) { + swprintf_s(key, _countof(key), L"gear%i", i + 1); + cfg->gear[i] = GetPrivateProfileIntW(L"dinput", key, i + 1, filename); + } + +} + +void idac_xi_config_load(struct idac_xi_config *cfg, const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + cfg->single_stick_steering = GetPrivateProfileIntW( + L"io3", + L"singleStickSteering", + 0, + filename); +} + +void idac_io_config_load(struct idac_io_config *cfg, const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + cfg->vk_test = GetPrivateProfileIntW(L"io3", L"test", '1', filename); + cfg->vk_service = GetPrivateProfileIntW(L"io3", L"service", '2', filename); + cfg->vk_coin = GetPrivateProfileIntW(L"io3", L"coin", '3', filename); + cfg->restrict_ = GetPrivateProfileIntW(L"io3", L"restrict", 97, filename); + + GetPrivateProfileStringW( + L"io3", + L"mode", + L"xinput", + cfg->mode, + _countof(cfg->mode), + filename); + + idac_shifter_config_load(&cfg->shifter, filename); + idac_di_config_load(&cfg->di, filename); + idac_xi_config_load(&cfg->xi, filename); +} + +void idac_shifter_config_load( + struct idac_shifter_config *cfg, + const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + cfg->auto_neutral = GetPrivateProfileIntW( + L"io3", + L"autoNeutral", + 0, + filename); +} diff --git a/idacio/config.h b/idacio/config.h new file mode 100644 index 0000000..6e5563d --- /dev/null +++ b/idacio/config.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +struct idac_shifter_config { + bool auto_neutral; +}; + +struct idac_di_config { + wchar_t device_name[64]; + wchar_t shifter_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 gear[6]; + bool reverse_brake_axis; + bool reverse_accel_axis; +}; + +struct idac_xi_config { + bool single_stick_steering; +}; + +struct idac_io_config { + uint8_t vk_test; + uint8_t vk_service; + uint8_t vk_coin; + wchar_t mode[8]; + int restrict_; + struct idac_shifter_config shifter; + struct idac_di_config di; + struct idac_xi_config xi; +}; + +void idac_di_config_load(struct idac_di_config *cfg, const wchar_t *filename); +void idac_xi_config_load(struct idac_xi_config *cfg, const wchar_t *filename); +void idac_io_config_load(struct idac_io_config *cfg, const wchar_t *filename); +void idac_shifter_config_load( + struct idac_shifter_config *cfg, + const wchar_t *filename); diff --git a/idacio/di-dev.c b/idacio/di-dev.c new file mode 100644 index 0000000..c77584b --- /dev/null +++ b/idacio/di-dev.c @@ -0,0 +1,163 @@ +#include +#include + +#include + +#include "idacio/di-dev.h" + +#include "util/dprintf.h" + +HRESULT idac_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 idac_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 idac_di_dev_poll( + IDirectInputDevice8W *dev, + HWND wnd, + union idac_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/idacio/di-dev.h b/idacio/di-dev.h new file mode 100644 index 0000000..efbe39e --- /dev/null +++ b/idacio/di-dev.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +#include + +union idac_di_state { + DIJOYSTATE st; + uint8_t bytes[sizeof(DIJOYSTATE)]; +}; + +HRESULT idac_di_dev_start(IDirectInputDevice8W *dev, HWND wnd); +void idac_di_dev_start_fx(IDirectInputDevice8W *dev, IDirectInputEffect **out); +HRESULT idac_di_dev_poll( + IDirectInputDevice8W *dev, + HWND wnd, + union idac_di_state *out); + diff --git a/idacio/di.c b/idacio/di.c new file mode 100644 index 0000000..b2d0d3d --- /dev/null +++ b/idacio/di.c @@ -0,0 +1,523 @@ +#include +#include + +#include +#include +#include + +#include "idacio/backend.h" +#include "idacio/config.h" +#include "idacio/di.h" +#include "idacio/di-dev.h" +#include "idacio/idacio.h" +#include "idacio/shifter.h" +#include "idacio/wnd.h" + +#include "util/dprintf.h" +#include "util/str.h" + +struct idac_di_axis { + wchar_t name[4]; + size_t off; +}; + +static HRESULT idac_di_config_apply(const struct idac_di_config *cfg); +static const struct idac_di_axis *idac_di_get_axis(const wchar_t *name); +static BOOL CALLBACK idac_di_enum_callback( + const DIDEVICEINSTANCEW *dev, + void *ctx); +static BOOL CALLBACK idac_di_enum_callback_shifter( + const DIDEVICEINSTANCEW *dev, + void *ctx); +static void idac_di_jvs_read_buttons(uint8_t *gamebtn_out); +static uint8_t idac_di_decode_pov(DWORD pov); +static void idac_di_jvs_read_shifter(uint8_t *gear); +static void idac_di_jvs_read_shifter_pos(uint8_t *gear); +static void idac_di_jvs_read_shifter_virt(uint8_t *gear); +static void idac_di_jvs_read_analogs(struct idac_io_analog_state *out); + +static const struct idac_di_axis idac_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 idac_io_backend idac_di_backend = { + .jvs_read_buttons = idac_di_jvs_read_buttons, + .jvs_read_shifter = idac_di_jvs_read_shifter, + .jvs_read_analogs = idac_di_jvs_read_analogs, +}; + +static HWND idac_di_wnd; +static IDirectInput8W *idac_di_api; +static IDirectInputDevice8W *idac_di_dev; +static IDirectInputDevice8W *idac_di_shifter; +static IDirectInputEffect *idac_di_fx; +static size_t idac_di_off_brake; +static size_t idac_di_off_accel; +static uint8_t idac_di_shift_dn; +static uint8_t idac_di_shift_up; +static uint8_t idac_di_view_chg; +static uint8_t idac_di_start; +static uint8_t idac_di_gear[6]; +static bool idac_di_reverse_brake_axis; +static bool idac_di_reverse_accel_axis; + +HRESULT idac_di_init( + const struct idac_di_config *cfg, + HINSTANCE inst, + const struct idac_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 = idac_di_config_apply(cfg); + + if (FAILED(hr)) { + return hr; + } + + hr = idac_io_wnd_create(inst, &idac_di_wnd); + + if (FAILED(hr)) { + return hr; + } + + /* Initial D Zero has some built-in DirectInput support that is not + particularly useful. idzhook shorts this out by redirecting dinput8.dll + to a no-op implementation of DirectInput. However, idzio 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 **) &idac_di_api, + NULL); + + if (FAILED(hr)) { + dprintf("DirectInput: API create failed: %08x\n", (int) hr); + + return hr; + } + + hr = IDirectInput8_EnumDevices( + idac_di_api, + DI8DEVCLASS_GAMECTRL, + idac_di_enum_callback, + (void *) cfg, + DIEDFL_ATTACHEDONLY); + + if (FAILED(hr)) { + dprintf("DirectInput: EnumDevices failed: %08x\n", (int) hr); + + return hr; + } + + if (idac_di_dev == NULL) { + dprintf("Wheel: Controller not found\n"); + + return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); + } + + hr = idac_di_dev_start(idac_di_dev, idac_di_wnd); + + if (FAILED(hr)) { + return hr; + } + + idac_di_dev_start_fx(idac_di_dev, &idac_di_fx); + + if (cfg->shifter_name[0] != L'\0') { + hr = IDirectInput8_EnumDevices( + idac_di_api, + DI8DEVCLASS_GAMECTRL, + idac_di_enum_callback_shifter, + (void *) cfg, + DIEDFL_ATTACHEDONLY); + + if (FAILED(hr)) { + dprintf("DirectInput: EnumDevices failed: %08x\n", (int) hr); + + return hr; + } + + if (idac_di_dev == NULL) { + dprintf("Shifter: Controller not found\n"); + + return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); + } + + hr = idac_di_dev_start(idac_di_shifter, idac_di_wnd); + + if (FAILED(hr)) { + return hr; + } + } + + dprintf("DirectInput: Controller initialized\n"); + + *backend = &idac_di_backend; + + return S_OK; +} + +static HRESULT idac_di_config_apply(const struct idac_di_config *cfg) +{ + const struct idac_di_axis *brake_axis; + const struct idac_di_axis *accel_axis; + int i; + + brake_axis = idac_di_get_axis(cfg->brake_axis); + accel_axis = idac_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; + } + + for (i = 0 ; i < 6 ; i++) { + if (cfg->gear[i] > 32) { + dprintf("Shifter: Invalid gear %i button: %i\n", + i + 1, + cfg->gear[i]); + + 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: 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"); + + if (cfg->shifter_name[0] != L'\0') { + dprintf("Shifter: --- Begin configuration ---\n"); + dprintf("Shifter: Device name . . . : Contains \"%S\"\n", + cfg->shifter_name); + dprintf("Shifter: Gear buttons . . : %i %i %i %i %i %i\n", + cfg->gear[0], + cfg->gear[1], + cfg->gear[2], + cfg->gear[3], + cfg->gear[4], + cfg->gear[5]); + dprintf("Shifter: --- End configuration ---\n"); + } + + idac_di_off_brake = accel_axis->off; + idac_di_off_accel = brake_axis->off; + idac_di_start = cfg->start; + idac_di_view_chg = cfg->view_chg; + idac_di_shift_dn = cfg->shift_dn; + idac_di_shift_up = cfg->shift_up; + idac_di_reverse_brake_axis = cfg->reverse_brake_axis; + idac_di_reverse_accel_axis = cfg->reverse_accel_axis; + + for (i = 0 ; i < 6 ; i++) { + idac_di_gear[i] = cfg->gear[i]; + } + + return S_OK; +} + +static const struct idac_di_axis *idac_di_get_axis(const wchar_t *name) +{ + const struct idac_di_axis *axis; + size_t i; + + for (i = 0 ; i < _countof(idac_di_axes) ; i++) { + axis = &idac_di_axes[i]; + + if (wstr_ieq(name, axis->name)) { + return axis; + } + } + + return NULL; +} + +static BOOL CALLBACK idac_di_enum_callback( + const DIDEVICEINSTANCEW *dev, + void *ctx) +{ + const struct idac_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( + idac_di_api, + &dev->guidInstance, + &idac_di_dev, + NULL); + + if (FAILED(hr)) { + dprintf("Wheel: CreateDevice failed: %08x\n", (int) hr); + } + + return DIENUM_STOP; +} + +static BOOL CALLBACK idac_di_enum_callback_shifter( + const DIDEVICEINSTANCEW *dev, + void *ctx) +{ + const struct idac_di_config *cfg; + HRESULT hr; + + cfg = ctx; + + if (wcsstr(dev->tszProductName, cfg->shifter_name) == NULL) { + return DIENUM_CONTINUE; + } + + dprintf("Shifter: Using DirectInput device \"%S\"\n", dev->tszProductName); + + hr = IDirectInput8_CreateDevice( + idac_di_api, + &dev->guidInstance, + &idac_di_shifter, + NULL); + + if (FAILED(hr)) { + dprintf("Shifter: CreateDevice failed: %08x\n", (int) hr); + } + + return DIENUM_STOP; +} + +static void idac_di_jvs_read_buttons(uint8_t *gamebtn_out) +{ + union idac_di_state state; + uint8_t gamebtn; + HRESULT hr; + + assert(gamebtn_out != NULL); + + hr = idac_di_dev_poll(idac_di_dev, idac_di_wnd, &state); + + if (FAILED(hr)) { + return; + } + + gamebtn = idac_di_decode_pov(state.st.rgdwPOV[0]); + + if (idac_di_start && state.st.rgbButtons[idac_di_start - 1]) { + gamebtn |= IDAC_IO_GAMEBTN_START; + } + + if (idac_di_view_chg && state.st.rgbButtons[idac_di_view_chg - 1]) { + gamebtn |= IDAC_IO_GAMEBTN_VIEW_CHANGE; + } + + *gamebtn_out = gamebtn; +} + +static uint8_t idac_di_decode_pov(DWORD pov) +{ + switch (pov) { + case 0: return IDAC_IO_GAMEBTN_UP; + case 4500: return IDAC_IO_GAMEBTN_UP | IDAC_IO_GAMEBTN_RIGHT; + case 9000: return IDAC_IO_GAMEBTN_RIGHT; + case 13500: return IDAC_IO_GAMEBTN_RIGHT | IDAC_IO_GAMEBTN_DOWN; + case 18000: return IDAC_IO_GAMEBTN_DOWN; + case 22500: return IDAC_IO_GAMEBTN_DOWN | IDAC_IO_GAMEBTN_RIGHT; + case 27000: return IDAC_IO_GAMEBTN_LEFT; + case 31500: return IDAC_IO_GAMEBTN_LEFT | IDAC_IO_GAMEBTN_UP; + default: return 0; + } +} + +static void idac_di_jvs_read_shifter(uint8_t *gear) +{ + assert(gear != NULL); + + if (idac_di_shifter != NULL) { + idac_di_jvs_read_shifter_pos(gear); + } else { + idac_di_jvs_read_shifter_virt(gear); + } +} + +static void idac_di_jvs_read_shifter_pos(uint8_t *out) +{ + union idac_di_state state; + uint8_t btn_no; + uint8_t gear; + uint8_t i; + HRESULT hr; + + assert(out != NULL); + assert(idac_di_shifter != NULL); + + hr = idac_di_dev_poll(idac_di_shifter, idac_di_wnd, &state); + + if (FAILED(hr)) { + return; + } + + gear = 0; + + for (i = 0 ; i < 6 ; i++) { + btn_no = idac_di_gear[i]; + + if (btn_no && state.st.rgbButtons[btn_no - 1]) { + gear = i + 1; + } + } + + *out = gear; +} + +static void idac_di_jvs_read_shifter_virt(uint8_t *gear) +{ + union idac_di_state state; + bool shift_dn; + bool shift_up; + HRESULT hr; + + assert(gear != NULL); + + hr = idac_di_dev_poll(idac_di_dev, idac_di_wnd, &state); + + if (FAILED(hr)) { + return; + } + + if (idac_di_shift_dn) { + shift_dn = state.st.rgbButtons[idac_di_shift_dn - 1]; + } else { + shift_dn = false; + } + + if (idac_di_shift_up) { + shift_up = state.st.rgbButtons[idac_di_shift_up - 1]; + } else { + shift_up = false; + } + + idac_shifter_update(shift_dn, shift_up); + + *gear = idac_shifter_current_gear(); +} + +static void idac_di_jvs_read_analogs(struct idac_io_analog_state *out) +{ + union idac_di_state state; + const LONG *brake; + const LONG *accel; + HRESULT hr; + + assert(out != NULL); + + hr = idac_di_dev_poll(idac_di_dev, idac_di_wnd, &state); + + if (FAILED(hr)) { + return; + } + + brake = (LONG *) &state.bytes[idac_di_off_brake]; + accel = (LONG *) &state.bytes[idac_di_off_accel]; + + out->wheel = state.st.lX - 32768; + + if (idac_di_reverse_brake_axis) { + out->brake = *brake; + } else { + out->brake = 65535 - *brake; + } + + if (idac_di_reverse_accel_axis) { + out->accel = *accel; + } else { + out->accel = 65535 - *accel; + } +} diff --git a/idacio/di.h b/idacio/di.h new file mode 100644 index 0000000..fc03e21 --- /dev/null +++ b/idacio/di.h @@ -0,0 +1,9 @@ +#pragma once + +#include "idacio/backend.h" +#include "idacio/config.h" + +HRESULT idac_di_init( + const struct idac_di_config *cfg, + HINSTANCE inst, + const struct idac_io_backend **backend); diff --git a/idacio/dllmain.c b/idacio/dllmain.c new file mode 100644 index 0000000..fdf1004 --- /dev/null +++ b/idacio/dllmain.c @@ -0,0 +1,125 @@ +#include + +#include +#include +#include + +#include "idacio/backend.h" +#include "idacio/config.h" +#include "idacio/di.h" +#include "idacio/idacio.h" +#include "idacio/xi.h" + +#include "util/dprintf.h" +#include "util/str.h" + +static struct idac_io_config idac_io_cfg; +static const struct idac_io_backend *idac_io_backend; +static bool idac_io_coin; +static uint16_t idac_io_coins; + +uint16_t idac_io_get_api_version(void) +{ + return 0x0100; +} + +HRESULT idac_io_jvs_init(void) +{ + HINSTANCE inst; + HRESULT hr; + + assert(idac_io_backend == NULL); + + inst = GetModuleHandleW(NULL); + + if (inst == NULL) { + hr = HRESULT_FROM_WIN32(GetLastError()); + dprintf("GetModuleHandleW failed: %lx\n", hr); + + return hr; + } + + idac_io_config_load(&idac_io_cfg, L".\\segatools.ini"); + + if (wstr_ieq(idac_io_cfg.mode, L"dinput")) { + hr = idac_di_init(&idac_io_cfg.di, inst, &idac_io_backend); + } else if (wstr_ieq(idac_io_cfg.mode, L"xinput")) { + hr = idac_xi_init(&idac_io_cfg.xi, &idac_io_backend); + } else { + hr = E_INVALIDARG; + dprintf("IDZ IO: Invalid IO mode \"%S\", use dinput or xinput\n", + idac_io_cfg.mode); + } + + return hr; +} + +void idac_io_jvs_read_buttons(uint8_t *opbtn_out, uint8_t *gamebtn_out) +{ + uint8_t opbtn; + + assert(idac_io_backend != NULL); + assert(opbtn_out != NULL); + assert(gamebtn_out != NULL); + + opbtn = 0; + + if (GetAsyncKeyState(idac_io_cfg.vk_test) & 0x8000) { + opbtn |= IDAC_IO_OPBTN_TEST; + } + + if (GetAsyncKeyState(idac_io_cfg.vk_service) & 0x8000) { + opbtn |= IDAC_IO_OPBTN_SERVICE; + } + + *opbtn_out = opbtn; + + idac_io_backend->jvs_read_buttons(gamebtn_out); +} + +void idac_io_jvs_read_shifter(uint8_t *gear) +{ + assert(gear != NULL); + assert(idac_io_backend != NULL); + + idac_io_backend->jvs_read_shifter(gear); +} + +void idac_io_jvs_read_analogs(struct idac_io_analog_state *out) +{ + struct idac_io_analog_state tmp; + + assert(out != NULL); + assert(idac_io_backend != NULL); + + idac_io_backend->jvs_read_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 * idac_io_cfg.restrict_) / 128; + out->accel = tmp.accel; + out->brake = tmp.brake; +} + +void idac_io_jvs_read_coin_counter(uint16_t *out) +{ + assert(out != NULL); + + /* Coin counter is not backend-specific */ + + if (idac_io_cfg.vk_coin && + (GetAsyncKeyState(idac_io_cfg.vk_coin) & 0x8000)) { + if (!idac_io_coin) { + idac_io_coin = true; + idac_io_coins++; + } + } else { + idac_io_coin = false; + } + + *out = idac_io_coins; +} diff --git a/idacio/idacio.h b/idacio/idacio.h new file mode 100644 index 0000000..10fff60 --- /dev/null +++ b/idacio/idacio.h @@ -0,0 +1,106 @@ +#pragma once + +/* INITIAL D THE ARCADE CUSTOM IO API + + This API definition allows custom driver DLLs to be defined for the + emulation of Initial D The Arcade cabinets. To be honest, there is very + little reason to want to do this, since driving game controllers are a + mostly-standardized PC peripheral which can be adequately controlled by the + built-in DirectInput and XInput support in idzhook. However, previous + versions of Segatools broke this functionality out into a separate DLL just + like all of the other supported games, so in the interests of maintaining + backwards compatibility we provide the option to load custom IDZIO + implementations as well. */ + +#include + +#include + +enum { + IDAC_IO_OPBTN_TEST = 0x01, + IDAC_IO_OPBTN_SERVICE = 0x02, +}; + +enum { + IDAC_IO_GAMEBTN_UP = 0x01, + IDAC_IO_GAMEBTN_DOWN = 0x02, + IDAC_IO_GAMEBTN_LEFT = 0x04, + IDAC_IO_GAMEBTN_RIGHT = 0x08, + IDAC_IO_GAMEBTN_START = 0x10, + IDAC_IO_GAMEBTN_VIEW_CHANGE = 0x20, +}; + +struct idac_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 IDZ 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 idac_io_get_api_version(void); + +/* Initialize JVS-based input. This function will be called before any other + idac_io_jvs_*() function calls. Errors returned from this function will + manifest as a disconnected JVS bus. + + All subsequent calls may originate from arbitrary threads and some may + overlap with each other. Ensuring synchronization inside your IO DLL is + your responsibility. + + Minimum API version: 0x0100 */ + +HRESULT idac_io_jvs_init(void); + +/* Poll the current state of the cabinet's JVS analog inputs. See structure + definition above for details. + + Minimum API version: 0x0100 */ + +void idac_io_jvs_read_analogs(struct idac_io_analog_state *out); + +/* Poll the current state of the cabinet's JVS input buttons and return them + through the opbtn and gamebtn out parameters. See enum definitions at the + top of this file for a list of bit masks to be used with these out + parameters. + + Minimum API version: 0x0100 */ + +void idac_io_jvs_read_buttons(uint8_t *opbtn, uint8_t *gamebtn); + +/* Poll the current position of the six-speed shifter and return it via the + gear out parameter. Valid values are 0 for neutral and 1-6 for gears 1-6. + + idzhook internally translates this gear position value into the correct + combination of Gear Left, Gear Right, Gear Up, Gear Down buttons that the + game will then interpret as the current position of the gear lever. + + Minimum API version: 0x0100 */ + +void idac_io_jvs_read_shifter(uint8_t *gear); + +/* Read the current state of the coin counter. This value should be incremented + for every coin detected by the coin acceptor mechanism. This count does not + need to persist beyond the lifetime of the process. + + Minimum API version: 0x0100 */ + +void idac_io_jvs_read_coin_counter(uint16_t *total); diff --git a/idacio/idzio.def b/idacio/idzio.def new file mode 100644 index 0000000..b495482 --- /dev/null +++ b/idacio/idzio.def @@ -0,0 +1,8 @@ +LIBRARY idacio + +EXPORTS + idac_io_jvs_init + idac_io_jvs_read_analogs + idac_io_jvs_read_buttons + idac_io_jvs_read_coin_counter + idac_io_jvs_read_shifter diff --git a/idacio/meson.build b/idacio/meson.build new file mode 100644 index 0000000..7c7eddd --- /dev/null +++ b/idacio/meson.build @@ -0,0 +1,32 @@ +idacio_lib = static_library( + 'idacio', + 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', + 'idacio.h', + 'shifter.c', + 'shifter.h', + 'wnd.c', + 'wnd.h', + 'xi.c', + 'xi.h', + ], +) diff --git a/idacio/shifter.c b/idacio/shifter.c new file mode 100644 index 0000000..02d8a54 --- /dev/null +++ b/idacio/shifter.c @@ -0,0 +1,32 @@ +#include +#include + +#include "idzio/shifter.h" + +static bool idac_shifter_shifting; +static uint8_t idac_shifter_gear; + +void idac_shifter_reset(void) +{ + idac_shifter_gear = 0; +} + +void idac_shifter_update(bool shift_dn, bool shift_up) +{ + if (!idac_shifter_shifting) { + if (shift_dn && idac_shifter_gear > 0) { + idac_shifter_gear--; + } + + if (shift_up && idac_shifter_gear < 6) { + idac_shifter_gear++; + } + } + + idac_shifter_shifting = shift_dn || shift_up; +} + +uint8_t idac_shifter_current_gear(void) +{ + return idac_shifter_gear; +} diff --git a/idacio/shifter.h b/idacio/shifter.h new file mode 100644 index 0000000..523a9a6 --- /dev/null +++ b/idacio/shifter.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +void idac_shifter_reset(void); +void idac_shifter_update(bool shift_dn, bool shift_up); +uint8_t idac_shifter_current_gear(void); diff --git a/idacio/wnd.c b/idacio/wnd.c new file mode 100644 index 0000000..7353273 --- /dev/null +++ b/idacio/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 idac_io_wnd_proc( + HWND hwnd, + UINT msg, + WPARAM wparam, + LPARAM lparam); + +HRESULT idac_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 = idac_io_wnd_proc; + wcx.hInstance = inst; + wcx.lpszClassName = L"IDZIO"; + + atom = RegisterClassExW(&wcx); + + if (atom == 0) { + hr = HRESULT_FROM_WIN32(GetLastError()); + dprintf("IDZIO: 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("IDZIO: 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 idac_io_wnd_proc( + HWND hwnd, + UINT msg, + WPARAM wparam, + LPARAM lparam) +{ + switch (msg) { + default: + return DefWindowProcW(hwnd, msg, wparam, lparam); + } +} diff --git a/idacio/wnd.h b/idacio/wnd.h new file mode 100644 index 0000000..21a5a8b --- /dev/null +++ b/idacio/wnd.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +HRESULT idac_io_wnd_create(HINSTANCE inst, HWND *out); diff --git a/idacio/xi.c b/idacio/xi.c new file mode 100644 index 0000000..63e4242 --- /dev/null +++ b/idacio/xi.c @@ -0,0 +1,165 @@ +#include +#include + +#include +#include +#include + +#include "idacio/backend.h" +#include "idacio/config.h" +#include "idacio/idacio.h" +#include "idacio/shifter.h" +#include "idacio/xi.h" + +#include "util/dprintf.h" + +static void idac_xi_jvs_read_buttons(uint8_t *gamebtn_out); +static void idac_xi_jvs_read_shifter(uint8_t *gear); +static void idac_xi_jvs_read_analogs(struct idac_io_analog_state *out); + +static HRESULT idac_xi_config_apply(const struct idac_xi_config *cfg); + +static const struct idac_io_backend idac_xi_backend = { + .jvs_read_buttons = idac_xi_jvs_read_buttons, + .jvs_read_shifter = idac_xi_jvs_read_shifter, + .jvs_read_analogs = idac_xi_jvs_read_analogs, +}; + +static bool idac_xi_single_stick_steering; + +HRESULT idac_xi_init(const struct idac_xi_config *cfg, const struct idac_io_backend **backend) +{ + HRESULT hr; + assert(cfg != NULL); + assert(backend != NULL); + + hr = idac_xi_config_apply(cfg); + + if (FAILED(hr)) { + return hr; + } + + dprintf("XInput: Using XInput controller\n"); + *backend = &idac_xi_backend; + + return S_OK; +} + +static HRESULT idac_xi_config_apply(const struct idac_xi_config *cfg) +{ + dprintf("XInput: --- Begin configuration ---\n"); + dprintf("XInput: Single Stick Steering : %i\n", cfg->single_stick_steering); + dprintf("XInput: --- End configuration ---\n"); + + idac_xi_single_stick_steering = cfg->single_stick_steering; + + return S_OK; +} + +static void idac_xi_jvs_read_buttons(uint8_t *gamebtn_out) +{ + uint8_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 |= IDAC_IO_GAMEBTN_UP; + } + + if (xb & XINPUT_GAMEPAD_DPAD_DOWN) { + gamebtn |= IDAC_IO_GAMEBTN_DOWN; + } + + if (xb & XINPUT_GAMEPAD_DPAD_LEFT) { + gamebtn |= IDAC_IO_GAMEBTN_LEFT; + } + + if (xb & XINPUT_GAMEPAD_DPAD_RIGHT) { + gamebtn |= IDAC_IO_GAMEBTN_RIGHT; + } + + if (xb & (XINPUT_GAMEPAD_START | XINPUT_GAMEPAD_A)) { + gamebtn |= IDAC_IO_GAMEBTN_START; + } + + if (xb & (XINPUT_GAMEPAD_BACK | XINPUT_GAMEPAD_B)) { + gamebtn |= IDAC_IO_GAMEBTN_VIEW_CHANGE; + } + + *gamebtn_out = gamebtn; +} + +static void idac_xi_jvs_read_shifter(uint8_t *gear) +{ + bool shift_dn; + bool shift_up; + XINPUT_STATE xi; + WORD xb; + + assert(gear != NULL); + + memset(&xi, 0, sizeof(xi)); + XInputGetState(0, &xi); + xb = xi.Gamepad.wButtons; + + if (xb & XINPUT_GAMEPAD_START) { + /* Reset to Neutral when start is pressed */ + idac_shifter_reset(); + } + + shift_dn = xb & (XINPUT_GAMEPAD_Y | XINPUT_GAMEPAD_LEFT_SHOULDER); + shift_up = xb & (XINPUT_GAMEPAD_X | XINPUT_GAMEPAD_RIGHT_SHOULDER); + + idac_shifter_update(shift_dn, shift_up); + + *gear = idac_shifter_current_gear(); +} + +static void idac_xi_jvs_read_analogs(struct idac_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(idac_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/idacio/xi.h b/idacio/xi.h new file mode 100644 index 0000000..16cdea5 --- /dev/null +++ b/idacio/xi.h @@ -0,0 +1,10 @@ +#pragma once + +/* Can't call this xinput.h or it will conflict with */ + +#include + +#include "idacio/backend.h" +#include "idacio/config.h" + +HRESULT idac_xi_init(const struct idac_xi_config *cfg, const struct idac_io_backend **backend); diff --git a/meson.build b/meson.build index b5a3d5f..abbc4a6 100644 --- a/meson.build +++ b/meson.build @@ -58,6 +58,7 @@ subdir('chuniio') subdir('divaio') subdir('carolio') subdir('idzio') +subdir('idacio') subdir('mu3io') subdir('mercuryio') subdir('cxbio') @@ -66,6 +67,7 @@ subdir('chunihook') subdir('divahook') subdir('carolhook') subdir('idzhook') +subdir('idachook') subdir('minihook') subdir('mu3hook') subdir('mercuryhook') diff --git a/segatools.md b/segatools.md new file mode 100644 index 0000000..4dcaf43 --- /dev/null +++ b/segatools.md @@ -0,0 +1,456 @@ +# Segatools common configuration settings + +This file describes configuration settings for Segatools that are common to +all games. + +Keyboard binding settings use +[Virtual-Key Codes](https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes). + +## `[aimeio]` + +Controls the card reader driver. + +### `path` + +Specify a path for a third-party card reader driver DLL. Default is empty +(use built-in emulation based on text files and keyboard input). + +In previous versions of Segatools this was accomplished by replacing the +AIMEIO.DLL file that came with Segatools. Segatools no longer ships with a +separate AIMEIO.DLL file (its functionality is now built into the various hook +DLLs). + +## `[aime]` + +Controls emulation of the Aime card reader assembly. + +### `enable` + +Default: `1` + +Enable Aime card reader assembly emulation. Disable to use a real SEGA Aime +reader (COM port number varies by game). + +### `aimePath` + +Default: `DEVICE\aime.txt` + +Path to a text file containing a classic Aime IC card ID. **This does not +currently work**. + +### `felicaPath` + +Default: `DEVICE\felica.txt` + +Path to a text file containing a FeliCa e-cash card IDm serial number. + +### `felicaGen` + +Default: `1` + +Whether to generate a random FeliCa ID if the file at `felicaPath` does not +exist. + +### `scan` + +Default: `0x0D` (`VK_RETURN`) + +Virtual-key code. If this button is **held** then the emulated IC card reader +emulates an IC card in its proximity. A variety of different IC cards can be +emulated; the exact choice of card that is emulated depends on the presence or +absence of the configured card ID files. + +## `[amvideo]` + +Controls the `amvideo.dll` stub built into Segatools. This is a DLL that is +normally present on the SEGA operating system image which is responsible for +changing screen resolution and orientation. + +### `enable` + +Default: `1` + +Enable stub `amvideo.dll`. Disable to use a real `amvideo.dll` build. Note that +you must have the correct registry settings installed and you must use the +version of `amvideo.dll` that matches your GPU vendor (since these DLLs make +use of vendor-specific APIs). + +## `[clock]` + +Controls hooks for Windows time-of-day APIs. + +### `timezone` + +Default: `1` + +Make the system time zone appear to be JST. SEGA games malfunction in strange +ways if the system time zone is not JST. There should not be any reason to +disable this hook other than possible implementation bugs, but the option is +provided if you need it. + +### `timewarp` + +Default: `0` + +Experimental time-of-day warping hook that skips over the hardcoded server +maintenance period. Causes an incorrect in-game time-of-day to be reported. +Better solutions for this problem exist and this feature will probably be +removed soon. + +### `writeable` + +Default: `0` + +Allow game to adjust system clock and time zone settings. This should normally +be left at `0`, but the option is provided if you need it. + +## `[dns]` + +Controls redirection of network server hostname lookups + +### `default` + +Default: `localhost` + +Controls hostname of all of the common network services servers, unless +overriden by a specific setting below. Most users will only need to change this +setting. Also, loopback addresses are specifically checked for and rejected by +the games themselves; this needs to be a LAN or WAN IP (or a hostname that +resolves to one). + +### `router` + +Default: Empty string (i.e. use value from `default` setting) + +Overrides the target of the `tenporouter.loc` and `bbrouter.loc` hostname +lookups. + +### `startup` + +Default: Empty string (i.e. use value from `default` setting) + +Overrides the target of the `naominet.jp` host lookup. + +### `billing` + +Default: Empty string (i.e. use value from `default` setting) + +Overrides the target of the `ib.naominet.jp` host lookup. + +### `aimedb` + +Default: Empty string (i.e. use value from `default` setting) + +Overrides the target of the `aime.naominet.jp` host lookup. + +## `[ds]` + +Controls emulation of the "DS (Dallas Semiconductor) EEPROM" chip on the AMEX +PCIe board. This is a small (32 byte) EEPROM that contains serial number and +region code information. It is not normally written to outside of inital +factory provisioning of a Sega Nu. + +### `enable` + +Default: `1` + +Enable DS EEPROM emulation. Disable to use the DS EEPROM chip on a real AMEX. + +### `region` + +Default: `1` + +AMEX Board region code. This appears to be a bit mask? + +- `1`: Japan +- `2`: USA? (Dead code, not used) +- `4`: Export +- `8`: China + +### `serialNo` + +Default `AAVE-01A99999999` + +"MAIN ID" serial number. First three characters are hardware series: + +- `AAV`: Nu-series +- `AAW`: NuSX-series +- `ACA`: ALLS-series + +## `[eeprom]` + +Controls emulation of the bulk EEPROM on the AMEX PCIe board. This chip stores +status and configuration information. + +### `enable` + +Default: `1` + +Enable bulk EEPROM emulation. Disable to use the bulk EEPROM chip on a real +AMEX. + +### `path` + +Default: `DEVICE\eeprom.bin` + +Path to the storage file for EEPROM emulation. This file is automatically +created and initialized with a suitable number of zero bytes if it does not +already exist. + +## `[gpio]` + +Configure emulation of the AMEX PCIe GPIO (General Purpose Input Output) +controller. + +### `enable` + +Default: `1` + +Enable GPIO emulation. Disable to use the GPIO controller on a real AMEX. + +### `sw1` + +Default `0x70` (`VK_F1`) + +Keyboard binding for Nu chassis SW1 button (alternative Test) + +### `sw2` + +Default `0x71` (`VK_F2`) + +Keyboard binding for Nu chassis SW2 button (alternative Service) + +### `dipsw1` .. `dipsw8` + +Defaults: `1`, `0`, `0`, `0`, `0`, `0`, `0`, `0` + +Nu chassis DIP switch settings: + +- Switch 1: Game-specific, but usually controls the "distribution server" + setting. Exactly one arcade machine on a cabinet router must be set to the + Server setting. + - `0`: Client + - `1`: Server +- Switch 2,3: Game-specific. + - Used by Mario&Sonic to configure cabinet ID, possibly other games. +- Switch 4: Screen orientation. Only used by the Nu system startup program. + - `0`: YOKO/Horizontal + - `1`: TATE/Vertical +- Switch 5,6,7: Screen resolution. Only used by the Nu system startup program. + - `000`: No change + - `100`: 640x480 + - `010`: 1024x600 + - `110`: 1024x768 + - `001`: 1280x720 + - `101`: 1280x1024 + - `110`: 1360x768 + - `111`: 1920x1080 +- Switch 8: Game-specific. Not used in any shipping game. + +## `[hwmon]` + +Configure stub implementation of the platform hardware monitor driver. The +real implementation of this driver monitors CPU temperatures by reading from +Intel Model Specific Registers, which is an action that is only permitted from +kernel mode. + +### `enable` + +Default `1` + +Enable hwmon emulation. Disable to use the real hwmon driver. + +## `[jvs]` + +Configure emulation of the AMEX PCIe JVS *controller* (not IO board!) + +### `enable` + +Default `1` + +Enable JVS port emulation. Disable to use the JVS port on a real AMEX. + +## `[keychip]` + +Configure keychip emulation. + +### `enable` + +Enable keychip emulation. Disable to use a real keychip. + +### `id` + +Default: `A69E-01A88888888` + +Keychip serial number. Keychip serials observed in the wild follow this +pattern: `A6xE-01Ayyyyyyyy`. + +### `gameId` + +Default: (Varies depending on game) + +Override the game's four-character model code. Changing this from the game's +expected value will probably just cause a system error. + +### `platformId` + +Default: (Varies depending on game) + +Override the game's four-character platform code (e.g. `AAV2` for Nu 2). This +is actually supposed to be a separate three-character `platformId` and +integer `modelType` setting, but they are combined here for convenience. Valid +values include: + +- `AAV0`: Nu 1 (Project DIVA) +- `AAV1`: Nu 1.1 (Chunithm) +- `AAV2`: Nu 2 (Initial D Zero) +- `AAW0`: NuSX 1 +- `AAW1`: NuSX 1.1 +- `ACA0`: ALLS UX +- `ACA1`: ALLS HX +- `ACA2`: ALLS UX (without dedicated GPU) +- `ACA4`: ALLS MX + +### `region` + +Default: `1` + +Override the keychip's region code. Most games seem to pay attention to the +DS EEPROM region code and not the keychip region code, and this seems to be +a bit mask that controls which Nu PCB region codes this keychip is authorized +for. So it probably only affects the system software and not the game software. +Bit values are: + +- 1: JPN: Japan +- 2: USA (unused) +- 3: EXP: Export (for Asian markets) +- 4: CHS: China (Simplified Chinese?) + +### `systemFlag` + +Default: `0x64` + +An 8-bit bitfield of unclear meaning. The least significant bit indicates a +developer dongle, I think? Changing this doesn't seem to have any effect on +anything other than Project DIVA. + +Other values observed in the wild: + +- `0x04`: SDCH, SDCA +- `0x20`: SDCA + +### `subnet` + +Default `192.168.100.0` + +The LAN IP range that the game will expect. The prefix length is hardcoded into +the game program: for some games this is `/24`, for others it is `/20`. + +## `[netenv]` + +Configure network environment virtualization. This module helps bypass various +restrictions placed upon the game's LAN environment. + +### `enable` + +Default `1` + +Enable network environment virtualization. You may need to disable this if +you want to do any head-to-head play on your LAN. + +Note: The virtualized LAN IP range is taken from the emulated keychip's +`subnet` setting. + +### `addrSuffix` + +Default: `11` + +The final octet of the local host's IP address on the virtualized subnet (so, +if the keychip subnet is `192.168.32.0` and this value is set to `11`, then the +local host's virtualized LAN IP is `192.168.32.11`). + +### `routerSuffix` + +Default: `1` + +The final octet of the default gateway's IP address on the virtualized subnet. + +### `macAddr` + +Default: `01:02:03:04:05:06` + +The MAC address of the virtualized Ethernet adapter. The exact value shouldn't +ever matter. + +## `[pcbid]` + +Configure Windows host name virtualization. The ALLS-series platform no longer +has an AMEX board, so the MAIN ID serial number is stored in the Windows +hostname. + +### `enable` + +Default: `1` + +Enable Windows host name virtualization. This is only needed for ALLS-platform +games (since the ALLS lacks an AMEX and therefore has no DS EEPROM, so it needs +another way to store the PCB serial), but it does no harm on games that run on +earlier hardware. + +### `serialNo` + +Default: `ACAE01A99999999` + +Set the Windows host name. This should be an ALLS MAIN ID, without the +hyphen (which is not a valid character in a Windows host name). + +## `[sram]` + +Configure emulation of the AMEX PCIe battery-backed SRAM. This stores +bookkeeping state and settings. This file is automatically created and +initialized with a suitable number of zero bytes if it does not already exist. + +### `enable` + +Default `1` + +Enable SRAM emulation. Disable to use the SRAM on a real AMEX. + +### `path` + +Default `DEVICE\sram.bin` + +Path to the storage file for SRAM emulation. + +## `[vfs]` + +Configure Windows path redirection hooks. + +### `enable` + +Default: `1` + +Enable path redirection. + +### `amfs` + +Default: Empty string (causes a startup error) + +Configure the location of the SEGA AMFS volume. Stored on the `E` partition on +real hardware. + +### `appdata` + +Default: Empty string (causes a startup error) + +Configure the location of the SEGA "APPDATA" volume (nothing to do with the +Windows user's `%APPDATA%` directory). Stored on the `Y` partition on real +hardware. + +### `option` + +Default: Empty string + +Configure the location of the "Option" data mount point. This mount point is +optional (hence the name, probably) and contains directories which contain +minor over-the-air content updates.