diff --git a/Package.mk b/Package.mk index 850f9eea..3e6a8871 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 00000000..c6660ca0 --- /dev/null +++ b/dist/idac/segatools.ini @@ -0,0 +1,136 @@ +[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. +; If you disable netenv then you must set this to your LAN's IP subnet, and +; that subnet must start with 192.168. Set it to your LAN's subnet if you +; want to play head-to-head using netenv=1. +subnet=192.168.100.0 + +; 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. +; 1: JPN: Japan, 4: EXP: Export (for Asian markets) +region=4 + +[gpio] +; ALLS DIP switches. + +; 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 +; 0 is the DZero CVT cab and 1 is the SWDC CVT cab. +dipsw2=0 +; Enable the Single Seat mode, always requires dipsw1=1. +dipsw3=0 +; The next two dip switches are the seat settings in bits, where +; 00 = Seat 1, 10 = Seat 2, 01 = Seat 3 and 11 = Seat 4 +dipsw4=0 +dipsw5=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= + +[idacio] +; To use a custom Initial D The Arcade IO 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 +; 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=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= +; 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 00000000..4b2d8e9c --- /dev/null +++ b/dist/idac/start.bat @@ -0,0 +1,28 @@ +@echo off + +pushd %~dp0 + +REM set the APP_DIR to the Y drive +set APP_DIR=Y:\SDGT + +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%\SDGT folder + +start /min inject -d -k idachook.dll amdaemon.exe -f -c config_aime_high_ex.json config_aime_high_jp.json config_aime_normal_ex.json config_aime_normal_jp.json config_common.json config_ex.json config_jp.json config_laninstall_client_ex.json config_laninstall_client_jp.json config_laninstall_server_ex.json config_laninstall_server_jp.json config_seat_1_ex.json config_seat_1_jp.json config_seat_2_ex.json config_seat_2_jp.json config_seat_3_ex.json config_seat_3_jp.json config_seat_4_ex.json config_seat_4_jp.json config_seat_single_ex.json config_seat_single_jp.json +inject -d -k idachook.dll ..\WindowsNoEditor\GameProject.exe -culture=en launch=Cabinet ABSLOG="..\..\..\..\..\Userdata\GameProject.log" -Master -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/docker-build.bat b/docker-build.bat index 969e0325..822dc9bc 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 00000000..5452bf64 --- /dev/null +++ b/idachook/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 "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"idacio", + 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); + aime_config_load(&cfg->aime, filename); + idac_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/idachook/config.h b/idachook/config.h new file mode 100644 index 00000000..9c7dfd77 --- /dev/null +++ b/idachook/config.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "board/config.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 aime_config aime; + struct dvd_config dvd; + struct io4_config io4; + 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 00000000..5d6f695d --- /dev/null +++ b/idachook/dllmain.c @@ -0,0 +1,105 @@ +#include +#include + +#include + +#include "board/sg-reader.h" +#include "board/io4.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/io4.h" +#include "idachook/zinput.h" + +#include "platform/platform.h" + +#include "util/dprintf.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) +{ + HRESULT hr; + + dprintf("--- Begin idac_pre_startup ---\n"); + + /* Config load */ + + idac_hook_config_load(&idac_hook_cfg, L".\\segatools.ini"); + + /* Hook Win32 APIs */ + + serial_hook_init(); + 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", + "ACA4", + idac_hook_mod); + + if (FAILED(hr)) { + goto fail; + } + + hr = sg_reader_hook_init(&idac_hook_cfg.aime, 3, 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 = idac_io4_hook_init(&idac_hook_cfg.io4); + + 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 00000000..03a6a3ee --- /dev/null +++ b/idachook/idac-dll.c @@ -0,0 +1,115 @@ +#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_init", + .off = offsetof(struct idac_dll, init), + }, { + .sym = "idac_io_poll", + .off = offsetof(struct idac_dll, poll), + }, { + .sym = "idac_io_get_opbtns", + .off = offsetof(struct idac_dll, get_opbtns), + }, { + .sym = "idac_io_get_gamebtns", + .off = offsetof(struct idac_dll, get_gamebtns), + }, { + .sym = "idac_io_get_shifter", + .off = offsetof(struct idac_dll, get_shifter), + }, { + .sym = "idac_io_get_analogs", + .off = offsetof(struct idac_dll, get_analogs), + } +}; + +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("IDAC IO: Failed to load IO DLL: %lx: %S\n", + hr, + cfg->path); + + goto end; + } + + dprintf("IDAC 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("IDAC 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("IDAC 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 00000000..7db5639e --- /dev/null +++ b/idachook/idac-dll.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "idacio/idacio.h" + +struct idac_dll { + uint16_t api_version; + HRESULT (*init)(void); + HRESULT (*poll)(void); + void (*get_opbtns)(uint8_t *opbtn); + void (*get_gamebtns)(uint8_t *gamebtn); + void (*get_shifter)(uint8_t *gear); + void (*get_analogs)(struct idac_io_analog_state *out); +}; + +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.def b/idachook/idachook.def new file mode 100644 index 00000000..4c944fd2 --- /dev/null +++ b/idachook/idachook.def @@ -0,0 +1,20 @@ +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_init + idac_io_poll + idac_io_get_opbtns + idac_io_get_gamebtns + idac_io_get_shifter + idac_io_get_analogs diff --git a/idachook/io4.c b/idachook/io4.c new file mode 100644 index 00000000..3d2eec55 --- /dev/null +++ b/idachook/io4.c @@ -0,0 +1,138 @@ +#include + +#include +#include +#include + +#include "board/io4.h" + +#include "idachook/idac-dll.h" + +#include "util/dprintf.h" + +static HRESULT idac_io4_poll(void *ctx, struct io4_state *state); +static uint16_t coins; + +static const struct io4_ops idac_io4_ops = { + .poll = idac_io4_poll, +}; + +static const uint16_t idac_gear_signals[] = { + /* Neutral */ + 0x0000, + /* 1: Left|Up */ + 0x0028, + /* 2: Left|Down */ + 0x0018, + /* 3: Up */ + 0x0020, + /* 4: Down */ + 0x0010, + /* 5: Right|Up */ + 0x0024, + /* 6: Right|Down */ + 0x0014, +}; + +HRESULT idac_io4_hook_init(const struct io4_config *cfg) +{ + HRESULT hr; + + assert(idac_dll.init != NULL); + + hr = io4_hook_init(cfg, &idac_io4_ops, NULL); + + if (FAILED(hr)) { + return hr; + } + + return idac_dll.init(); +} + +static HRESULT idac_io4_poll(void *ctx, struct io4_state *state) +{ + uint8_t opbtn; + uint8_t gamebtn; + uint8_t gear; + struct idac_io_analog_state analog_state; + HRESULT hr; + + assert(idac_dll.poll != NULL); + assert(idac_dll.get_opbtns != NULL); + assert(idac_dll.get_gamebtns != NULL); + assert(idac_dll.get_analogs != NULL); + assert(idac_dll.get_shifter != NULL); + + memset(state, 0, sizeof(*state)); + memset(&analog_state, 0, sizeof(analog_state)); + + hr = idac_dll.poll(); + + if (FAILED(hr)) { + return hr; + } + + opbtn = 0; + gamebtn = 0; + gear = 0; + + idac_dll.get_opbtns(&opbtn); + idac_dll.get_gamebtns(&gamebtn); + idac_dll.get_shifter(&gear); + idac_dll.get_analogs(&analog_state); + + if (opbtn & IDAC_IO_OPBTN_TEST) { + state->buttons[0] |= IO4_BUTTON_TEST; + } + + if (opbtn & IDAC_IO_OPBTN_SERVICE) { + state->buttons[0] |= IO4_BUTTON_SERVICE; + } + + if (opbtn & IDAC_IO_OPBTN_COIN) { + coins++; + } + state->chutes[0] = coins << 8; + + if (gamebtn & IDAC_IO_GAMEBTN_START) { + state->buttons[0] |= 1 << 7; + } + + if (gamebtn & IDAC_IO_GAMEBTN_VIEW_CHANGE) { + state->buttons[0] |= 1 << 1; + } + + if (gamebtn & IDAC_IO_GAMEBTN_UP) { + state->buttons[0] |= 1 << 5; + } + + if (gamebtn & IDAC_IO_GAMEBTN_DOWN) { + state->buttons[0] |= 1 << 4; + } + + if (gamebtn & IDAC_IO_GAMEBTN_LEFT) { + state->buttons[0] |= 1 << 3; + } + + if (gamebtn & IDAC_IO_GAMEBTN_RIGHT) { + state->buttons[0] |= 1 << 2; + } + + /* Update simulated six-speed shifter */ + + if (gear > 6) { + gear = 6; + } + + state->buttons[1] = idac_gear_signals[gear]; + + /* 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/idachook/io4.h b/idachook/io4.h new file mode 100644 index 00000000..af2ded19 --- /dev/null +++ b/idachook/io4.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include "board/io4.h" + +HRESULT idac_io4_hook_init(const struct io4_config *cfg); diff --git a/idachook/meson.build b/idachook/meson.build new file mode 100644 index 00000000..72214acc --- /dev/null +++ b/idachook/meson.build @@ -0,0 +1,32 @@ +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'), + xinput_lib, + ], + link_with : [ + aimeio_lib, + board_lib, + hooklib_lib, + idacio_lib, + platform_lib, + util_lib, + ], + sources : [ + 'config.c', + 'config.h', + 'dllmain.c', + 'idac-dll.c', + 'idac-dll.h', + 'io4.c', + 'io4.h', + 'zinput.c', + 'zinput.h', + ], +) diff --git a/idachook/zinput.c b/idachook/zinput.c new file mode 100644 index 00000000..59c77b50 --- /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 00000000..13a46cd1 --- /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 00000000..b9833a13 --- /dev/null +++ b/idacio/backend.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include "idacio/idacio.h" + +struct idac_io_backend { + void (*get_opbtns)(uint8_t *opbtn); + void (*get_gamebtns)(uint8_t *gamebtn); + void (*get_shifter)(uint8_t *gear); + void (*get_analogs)(struct idac_io_analog_state *state); +}; diff --git a/idacio/config.c b/idacio/config.c new file mode 100644 index 00000000..700f18b2 --- /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"io4", + 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"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); + + 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"io4", + L"autoNeutral", + 0, + filename); +} diff --git a/idacio/config.h b/idacio/config.h new file mode 100644 index 00000000..6e5563de --- /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 00000000..c77584bd --- /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 00000000..efbe39e3 --- /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 00000000..110a2edf --- /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_get_buttons(uint8_t *gamebtn_out); +static uint8_t idac_di_decode_pov(DWORD pov); +static void idac_di_get_shifter(uint8_t *gear); +static void idac_di_get_shifter_pos(uint8_t *gear); +static void idac_di_get_shifter_virt(uint8_t *gear); +static void idac_di_get_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 = { + .get_gamebtns = idac_di_get_buttons, + .get_shifter = idac_di_get_shifter, + .get_analogs = idac_di_get_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. idachook shorts this out by redirecting dinput8.dll + to a no-op implementation of DirectInput. However, idacio 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_get_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_get_shifter(uint8_t *gear) +{ + assert(gear != NULL); + + if (idac_di_shifter != NULL) { + idac_di_get_shifter_pos(gear); + } else { + idac_di_get_shifter_virt(gear); + } +} + +static void idac_di_get_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_get_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_get_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 00000000..fc03e218 --- /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 00000000..f524139d --- /dev/null +++ b/idacio/dllmain.c @@ -0,0 +1,120 @@ +#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; + +uint16_t idac_io_get_api_version(void) +{ + return 0x0100; +} + +HRESULT idac_io_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("IDAC IO: Invalid IO mode \"%S\", use dinput or xinput\n", + idac_io_cfg.mode); + } + + return hr; +} + +void idac_io_get_opbtns(uint8_t *opbtn_out) +{ + uint8_t opbtn; + + assert(idac_io_backend != NULL); + assert(opbtn_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; + } + + if (GetAsyncKeyState(idac_io_cfg.vk_coin) & 0x8000) { + if (!idac_io_coin) { + idac_io_coin = true; + opbtn |= IDAC_IO_OPBTN_COIN; + } + } else { + idac_io_coin = false; + } + + *opbtn_out = opbtn; +} + + +void idac_io_get_gamebtns(uint8_t *gamebtn_out) +{ + assert(idac_io_backend != NULL); + assert(gamebtn_out != NULL); + + idac_io_backend->get_gamebtns(gamebtn_out); +} + +void idac_io_get_shifter(uint8_t *gear) +{ + assert(gear != NULL); + assert(idac_io_backend != NULL); + + idac_io_backend->get_shifter(gear); +} + +void idac_io_get_analogs(struct idac_io_analog_state *out) +{ + struct idac_io_analog_state tmp; + + assert(out != NULL); + assert(idac_io_backend != NULL); + + idac_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 * idac_io_cfg.restrict_) / 128; + out->accel = tmp.accel; + out->brake = tmp.brake; +} diff --git a/idacio/idacio.def b/idacio/idacio.def new file mode 100644 index 00000000..18839b22 --- /dev/null +++ b/idacio/idacio.def @@ -0,0 +1,9 @@ +LIBRARY idacio + +EXPORTS + idac_io_init + idac_io_poll + idac_io_get_opbtns + idac_io_get_gamebtns + idac_io_get_shifter + idac_io_get_analogs diff --git a/idacio/idacio.h b/idacio/idacio.h new file mode 100644 index 00000000..035c5c4e --- /dev/null +++ b/idacio/idacio.h @@ -0,0 +1,102 @@ +#pragma once + +#include + +#include + +enum { + IDAC_IO_OPBTN_TEST = 0x01, + IDAC_IO_OPBTN_SERVICE = 0x02, + IDAC_IO_OPBTN_COIN = 0x04, +}; + +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 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 idac_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 idac_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 idac_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 idac_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 idac_io_get_gamebtns(uint8_t *gamebtn); + +/* Poll the current state of the cabinet's JVS analog inputs. See structure + definition above for details. + + Minimum API version: 0x0100 */ + +void idac_io_get_analogs(struct idac_io_analog_state *out); + +/* 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_get_shifter(uint8_t *gear); \ No newline at end of file diff --git a/idacio/meson.build b/idacio/meson.build new file mode 100644 index 00000000..7c7edddc --- /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 00000000..55de837a --- /dev/null +++ b/idacio/shifter.c @@ -0,0 +1,32 @@ +#include +#include + +#include "idacio/shifter.h" + +static bool idac_shifter_shifting; +static uint8_t idac_shifter_gear; + +void idac_shifter_set(uint8_t gear) +{ + idac_shifter_gear = gear; +} + +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 00000000..c32dcf12 --- /dev/null +++ b/idacio/shifter.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +void idac_shifter_set(uint8_t gear); +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 00000000..68720b44 --- /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"IDACIO"; + + atom = RegisterClassExW(&wcx); + + if (atom == 0) { + hr = HRESULT_FROM_WIN32(GetLastError()); + dprintf("IDACIO: 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("IDACIO: 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 00000000..21a5a8bb --- /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 00000000..87f3fbdf --- /dev/null +++ b/idacio/xi.c @@ -0,0 +1,186 @@ +#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_get_gamebtns(uint8_t *gamebtn_out); +static void idac_xi_get_shifter(uint8_t *gear); +static void idac_xi_get_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 = { + .get_gamebtns = idac_xi_get_gamebtns, + .get_shifter = idac_xi_get_shifter, + .get_analogs = idac_xi_get_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; +} + +HRESULT idac_io_poll(void) +{ + 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_get_gamebtns(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_get_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_set(0); + } + + /* + // Alternative shifting mode + if (xb & XINPUT_GAMEPAD_X) { + // Set to Gear 2 when X is pressed + idac_shifter_set(2); + } + + if (xb & XINPUT_GAMEPAD_Y) { + // Set to Gear 3 when Y is pressed + idac_shifter_set(3); + } + + shift_dn = xb & XINPUT_GAMEPAD_LEFT_SHOULDER; + shift_up = xb & XINPUT_GAMEPAD_RIGHT_SHOULDER; + */ + + 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_get_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 00000000..16cdea5a --- /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 b5a3d5f2..abbc4a6a 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/platform/config.c b/platform/config.c index 1c6be62c..62f27073 100644 --- a/platform/config.c +++ b/platform/config.c @@ -21,6 +21,7 @@ #include "platform/pcbid.h" #include "platform/platform.h" #include "platform/vfs.h" +#include "platform/dipsw.h" void platform_config_load(struct platform_config *cfg, const wchar_t *filename) { @@ -37,6 +38,7 @@ void platform_config_load(struct platform_config *cfg, const wchar_t *filename) netenv_config_load(&cfg->netenv, filename); nusec_config_load(&cfg->nusec, filename); vfs_config_load(&cfg->vfs, filename); + dipsw_config_load(&cfg->dipsw, filename); } void amvideo_config_load(struct amvideo_config *cfg, const wchar_t *filename) @@ -317,3 +319,21 @@ void vfs_config_load(struct vfs_config *cfg, const wchar_t *filename) filename); } +void dipsw_config_load(struct dipsw_config *cfg, const wchar_t *filename) +{ + wchar_t name[7]; + size_t i; + + assert(cfg != NULL); + assert(filename != NULL); + + cfg->enable = GetPrivateProfileIntW(L"gpio", L"enable", 1, filename); + + wcscpy_s(name, _countof(name), L"dipsw0"); + + for (i = 0 ; i < 8 ; i++) { + name[5] = L'1' + i; + cfg->dipsw[i] = GetPrivateProfileIntW(L"gpio", name, 0, filename); + } +} + diff --git a/platform/config.h b/platform/config.h index 7ece41dc..1e56d432 100644 --- a/platform/config.h +++ b/platform/config.h @@ -17,6 +17,7 @@ #include "platform/pcbid.h" #include "platform/platform.h" #include "platform/vfs.h" +#include "platform/dipsw.h" void platform_config_load( struct platform_config *cfg, @@ -32,3 +33,4 @@ void netenv_config_load(struct netenv_config *cfg, const wchar_t *filename); void nusec_config_load(struct nusec_config *cfg, const wchar_t *filename); void pcbid_config_load(struct pcbid_config *cfg, const wchar_t *filename); void vfs_config_load(struct vfs_config *cfg, const wchar_t *filename); +void dipsw_config_load(struct dipsw_config *cfg, const wchar_t *filename); diff --git a/platform/dipsw.c b/platform/dipsw.c new file mode 100644 index 00000000..e6af6102 --- /dev/null +++ b/platform/dipsw.c @@ -0,0 +1,153 @@ +#include +#include + +#include +#include +// #include + +#include "platform/dipsw.h" +#include "platform/vfs.h" + +#include "util/dprintf.h" +#include "util/str.h" +#include "util/crc.h" + +#define DATA_SIZE 503 +#define BLOCK_SIZE (sizeof(uint32_t) + 4 + 1 + DATA_SIZE) + +#pragma pack(push, 1) + +typedef struct +{ + uint32_t checksum; + char padding_1[4]; + uint8_t dip_switches; + char data[DATA_SIZE]; +} DipSwitchBlock; + +typedef struct +{ + DipSwitchBlock dip_switch_block; + char *data; +} DipSwitches; + +#pragma pack(pop) + +static DipSwitches dip_switches; + +static struct dipsw_config dipsw_config; +static struct vfs_config vfs_config; + +static void dipsw_read_sysfile(const wchar_t *sys_file); +static void dipsw_save_sysfile(const wchar_t *sys_file); + +HRESULT dipsw_init(const struct dipsw_config *cfg, const struct vfs_config *vfs_cfg) +{ + HRESULT hr; + wchar_t sys_file_path[MAX_PATH]; + + assert(cfg != NULL); + assert(vfs_cfg != NULL); + + if (!cfg->enable) + { + return S_FALSE; + } + + memcpy(&dipsw_config, cfg, sizeof(*cfg)); + + sys_file_path[0] = L'\0'; + // concatenate vfs_config.amfs with L"sysfile.dat" + wcsncpy(sys_file_path, vfs_cfg->amfs, MAX_PATH); + wcsncat(sys_file_path, L"\\sysfile.dat", MAX_PATH); + + dipsw_read_sysfile(sys_file_path); + + // now write the dipsw_config.dipsw to the dip_switch_block + dipsw_save_sysfile(sys_file_path); + + return S_OK; +} + +static void dipsw_read_sysfile(const wchar_t *sys_file) +{ + FILE *f = _wfopen(sys_file, L"r"); + + if (f == NULL) + { + dprintf("First run detected, DipSw settings can only be applied AFTER the first run\n"); + return; + } + + fseek(f, 0, SEEK_END); + long file_size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (file_size != 0x6000) + { + dprintf("Invalid sysfile.dat file size\n"); + fclose(f); + + return; + } + + dip_switches.data = malloc(file_size); + fread(dip_switches.data, 1, file_size, f); + fclose(f); + + // memcpy(dip_switches.dip_switch_block, dip_switches.data + 0x2800, BLOCK_SIZE); + memcpy(&dip_switches.dip_switch_block, dip_switches.data + 0x2800, BLOCK_SIZE); +} + +static void dipsw_save_sysfile(const wchar_t *sys_file) +{ + uint8_t dipsw = 0; + // open the sysfile.dat for writing in bytes mode + FILE *f = _wfopen(sys_file, L"rb+"); + + if (f == NULL) + { + return; + } + + // write the dipsw_config.dipsw to the dip_switch_block + for (int i = 0; i < 8; i++) + { + if (dipsw_config.dipsw[i]) + { + // print which dipsw is enabled + dprintf("DipSw: DipSw%d=1 set\n", i + 1); + dipsw |= (1 << i); + } + } + + dip_switches.dip_switch_block.dip_switches = dipsw; + + // calculate the new checksum, skip the old crc32 value + // which is at the beginning of the block, thats's why the +4 + // conver the struct to chars in order for the crc32 calculation to work + dip_switches.dip_switch_block.checksum = crc32( + (char *)&dip_switches.dip_switch_block + 4, BLOCK_SIZE - 4, 0); + + // build the new dip switch block + char block[BLOCK_SIZE]; + memcpy(block, (char *)&dip_switches.dip_switch_block, BLOCK_SIZE); + + // replace the old block with the new one + memcpy(dip_switches.data + 0x2800, block, BLOCK_SIZE); + memcpy(dip_switches.data + 0x5800, block, BLOCK_SIZE); + + // print the dip_switch_block in hex + /* + dprintf("DipSw Block: "); + for (size_t i = 0; i < BLOCK_SIZE; i++) + { + dprintf("%02X ", ((uint8_t *)&dip_switches.dip_switch_block)[i]); + } + dprintf("\n"); + */ + + fwrite(dip_switches.data, 1, 0x6000, f); + + fclose(f); +} diff --git a/platform/dipsw.h b/platform/dipsw.h new file mode 100644 index 00000000..a4bfdb21 --- /dev/null +++ b/platform/dipsw.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include +#include + +#include "platform/vfs.h" + +struct dipsw_config { + bool enable; + bool dipsw[8]; +}; + +HRESULT dipsw_init(const struct dipsw_config *cfg, const struct vfs_config *vfs_cfg); diff --git a/platform/dns.c b/platform/dns.c index 5486d429..e2c59055 100644 --- a/platform/dns.c +++ b/platform/dns.c @@ -79,5 +79,13 @@ HRESULT dns_platform_hook_init(const struct dns_config *cfg) return hr; } + // Disable api/polling to the original servers + + hr = dns_hook_push(L"amlog.sys-all.net", NULL); + + if (FAILED(hr)) { + return hr; + } + return S_OK; } diff --git a/platform/meson.build b/platform/meson.build index 4f0fbc98..0de228c3 100644 --- a/platform/meson.build +++ b/platform/meson.build @@ -32,5 +32,7 @@ platform_lib = static_library( 'platform.h', 'vfs.c', 'vfs.h', + 'dipsw.c', + 'dipsw.h', ], ) diff --git a/platform/platform.c b/platform/platform.c index 218204c4..35d2ba7f 100644 --- a/platform/platform.c +++ b/platform/platform.c @@ -12,6 +12,7 @@ #include "platform/pcbid.h" #include "platform/platform.h" #include "platform/vfs.h" +#include "platform/dipsw.h" HRESULT platform_hook_init( const struct platform_config *cfg, @@ -80,5 +81,11 @@ HRESULT platform_hook_init( return hr; } + hr = dipsw_init(&cfg->dipsw, &cfg->vfs); + + if (FAILED(hr)) { + return hr; + } + return S_OK; } diff --git a/platform/platform.h b/platform/platform.h index 69c65e26..ba06567a 100644 --- a/platform/platform.h +++ b/platform/platform.h @@ -12,6 +12,7 @@ #include "platform/nusec.h" #include "platform/pcbid.h" #include "platform/vfs.h" +#include "platform/dipsw.h" struct platform_config { struct amvideo_config amvideo; @@ -24,6 +25,7 @@ struct platform_config { struct netenv_config netenv; struct nusec_config nusec; struct vfs_config vfs; + struct dipsw_config dipsw; }; HRESULT platform_hook_init( diff --git a/platform/vfs.c b/platform/vfs.c index 7bd29533..9f620d86 100644 --- a/platform/vfs.c +++ b/platform/vfs.c @@ -28,7 +28,9 @@ static HRESULT vfs_reg_read_amfs(void *bytes, uint32_t *nbytes); static HRESULT vfs_reg_read_appdata(void *bytes, uint32_t *nbytes); static wchar_t vfs_nthome_real[MAX_PATH]; -static const wchar_t vfs_nthome[] = L"C:\\Documents and Settings\\AppUser"; +// new home for ALLS +static const wchar_t vfs_nthome[] = L"C:\\Users\\AppUser"; +// static const wchar_t vfs_nthome[] = L"C:\\Documents and Settings\\AppUser"; static const size_t vfs_nthome_len = _countof(vfs_nthome) - 1; static const wchar_t vfs_option[] = L"C:\\Mount\\Option"; @@ -273,8 +275,8 @@ static HRESULT vfs_path_hook(const wchar_t *src, wchar_t *dest, size_t *count) } switch (src[0]) { - case L'D': // later AMDaemon versions default to D: for AMFS if it can't find it - case L'd': + // case L'D': // later AMDaemon versions default to D: for AMFS if it can't find it + // case L'd': case L'e': case L'E': redir = vfs_config.amfs; diff --git a/segatools.md b/segatools.md new file mode 100644 index 00000000..4dcaf432 --- /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.