From 7ab2439955e5413cd45ed917d1e5b4cdd1bcf75f Mon Sep 17 00:00:00 2001 From: Wither OrNot Date: Thu, 29 Jan 2026 16:56:50 -0500 Subject: [PATCH] soul: Add initial code by Dniel97 Co-authored-by: Dniel97 --- Package.mk | 17 ++++ dist/soul/config_hook.json | 7 ++ dist/soul/launch.bat | 13 +++ dist/soul/segatools.ini | 178 ++++++++++++++++++++++++++++++++++++ games/soulhook/config.c | 109 ++++++++++++++++++++++ games/soulhook/config.h | 32 +++++++ games/soulhook/dllmain.c | 120 ++++++++++++++++++++++++ games/soulhook/io4.c | 89 ++++++++++++++++++ games/soulhook/io4.h | 7 ++ games/soulhook/meson.build | 30 ++++++ games/soulhook/soul-dll.c | 115 +++++++++++++++++++++++ games/soulhook/soul-dll.h | 23 +++++ games/soulhook/soulhook.def | 22 +++++ games/soulio/config.c | 20 ++++ games/soulio/config.h | 16 ++++ games/soulio/meson.build | 12 +++ games/soulio/soulio.c | 97 ++++++++++++++++++++ games/soulio/soulio.h | 77 ++++++++++++++++ meson.build | 2 + 19 files changed, 986 insertions(+) create mode 100644 dist/soul/config_hook.json create mode 100644 dist/soul/launch.bat create mode 100644 dist/soul/segatools.ini create mode 100644 games/soulhook/config.c create mode 100644 games/soulhook/config.h create mode 100644 games/soulhook/dllmain.c create mode 100644 games/soulhook/io4.c create mode 100644 games/soulhook/io4.h create mode 100644 games/soulhook/meson.build create mode 100644 games/soulhook/soul-dll.c create mode 100644 games/soulhook/soul-dll.h create mode 100644 games/soulhook/soulhook.def create mode 100644 games/soulio/config.c create mode 100644 games/soulio/config.h create mode 100644 games/soulio/meson.build create mode 100644 games/soulio/soulio.c create mode 100644 games/soulio/soulio.h diff --git a/Package.mk b/Package.mk index 3c974e7..51ffbe7 100644 --- a/Package.mk +++ b/Package.mk @@ -256,6 +256,22 @@ $(BUILD_DIR_ZIP)/apm3.zip: $(V)strip $(BUILD_DIR_ZIP)/apm3/*.{exe,dll} $(V)cd $(BUILD_DIR_ZIP)/apm3 ; zip -r ../apm3.zip * +$(BUILD_DIR_ZIP)/soul.zip: + $(V)echo ... $@ + $(V)mkdir -p $(BUILD_DIR_ZIP)/soul + $(V)mkdir -p $(BUILD_DIR_ZIP)/soul/DEVICE + $(V)cp $(BUILD_DIR_64)/subprojects/capnhook/inject/inject.exe \ + $(BUILD_DIR_GAMES_64)/soulhook/soulhook.dll \ + $(DIST_DIR)/soul/segatools.ini \ + $(DIST_DIR)/soul/launch.bat \ + $(DIST_DIR)/soul/config_hook.json \ + $(BUILD_DIR_ZIP)/soul + $(V)cp pki/billing.pub \ + pki/ca.crt \ + $(BUILD_DIR_ZIP)/soul/DEVICE + $(V)strip $(BUILD_DIR_ZIP)/soul/*.{exe,dll} + $(V)cd $(BUILD_DIR_ZIP)/soul ; zip -r ../soul.zip * + $(BUILD_DIR_ZIP)/doc.zip: \ $(DOC_DIR)/config \ $(DOC_DIR)/chunihook.md \ @@ -282,6 +298,7 @@ $(BUILD_DIR_ZIP)/segatools.zip: \ $(BUILD_DIR_ZIP)/fgo.zip \ $(BUILD_DIR_ZIP)/kemono.zip \ $(BUILD_DIR_ZIP)/apm3.zip \ + $(BUILD_DIR_ZIP)/soul.zip CHANGELOG.md \ README.md \ diff --git a/dist/soul/config_hook.json b/dist/soul/config_hook.json new file mode 100644 index 0000000..2f86dea --- /dev/null +++ b/dist/soul/config_hook.json @@ -0,0 +1,7 @@ +{ + "aime": { + "firmware_path": [ + "aime_firm\\update_15396_6728_94.bin" + ] + } +} \ No newline at end of file diff --git a/dist/soul/launch.bat b/dist/soul/launch.bat new file mode 100644 index 0000000..c282757 --- /dev/null +++ b/dist/soul/launch.bat @@ -0,0 +1,13 @@ +@echo off + +pushd %~dp0 + +start "AM Daemon" /min inject -d -k soulhook.dll .\tools\amdaemon\amdaemon.exe -c .\tools\amdaemon\config_common.json .\tools\amdaemon\config_emoney_satellite_pro.json .\tools\amdaemon\config_satellite.json + +inject -d -k soulhook.dll fbb.exe + +taskkill /f /im amdaemon.exe > nul 2>&1 + +echo. +echo Game processes have terminated +pause \ No newline at end of file diff --git a/dist/soul/segatools.ini b/dist/soul/segatools.ini new file mode 100644 index 0000000..4915b41 --- /dev/null +++ b/dist/soul/segatools.ini @@ -0,0 +1,178 @@ +; ----------------------------------------------------------------------------- +; Path settings +; ----------------------------------------------------------------------------- + +[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 Axxx 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= + +; ----------------------------------------------------------------------------- +; Device settings +; ----------------------------------------------------------------------------- + +[aime] +; Enable Aime card reader assembly emulation. Disable to use a real SEGA Aime +; reader. +enable=1 +aimePath=DEVICE\aime.txt + +; 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. Default is the Return key. +scan=0x0D + +[vfd] +; Enable VFD emulation. Disable to use a real VFD +; GP1232A02A FUTABA assembly. +enable=1 + +[led15093] +; 837-15093-06 LED board emulation setting. +enable=1 +; COM port number for the LED board. +portNo1=2 + +; ----------------------------------------------------------------------------- +; Network settings +; ----------------------------------------------------------------------------- + +[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 +; 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`). +addrSuffix=11 + +; ----------------------------------------------------------------------------- +; Board settings +; ----------------------------------------------------------------------------- + +[keychip] +; Keychip serial number. Keychip serials observed in the wild follow this +; pattern: `A\d{2}(E|X)-(01|20)[ABCDU]\d{8}`. +id=A69E-01A88888888 + +; 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.168.0 + +; 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. +; Use `ACA1` for the Server/Client, depending on the dipsw1 +; Use `ACA5` for the Standalone mode +platformId=ACA1 + +[pcbid] +; 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). +serialNo=ACAE01A99999999 + +[system] +; Enable ALLS system settings. +enable=1 + +; Enable freeplay mode. This will disable the coin slot and set the game to +; freeplay. Keep in mind that some game modes (e.g. Freedom/Time Modes) will not +; allow you to start a game in freeplay mode. +freeplay=0 + +; LAN Install: 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 + +; ----------------------------------------------------------------------------- +; Misc. hook settings +; ----------------------------------------------------------------------------- + +[touch] +; WinTouch emulation setting. +enable=1 +remap=1 +cursor=1 + +; ----------------------------------------------------------------------------- +; Custom IO settings +; ----------------------------------------------------------------------------- + +[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= + +[soulio] +; To use a custom Soul IO DLL enter its path here. +; Leave empty if you want to use Segatools built-in gamepad input. +path= + +; ----------------------------------------------------------------------------- +; Input settings +; ----------------------------------------------------------------------------- + +; Keyboard bindings are specified as hexadecimal (prefixed with 0x) or decimal +; (not prefixed with 0x) virtual-key codes, a list of which can be found here: +; +; https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes +; +; This is, admittedly, not the most user-friendly configuration method in the +; world. An improved solution will be provided later. + +[io4] +; Test button virtual-key code. Default is the F1 key. +test=0x70 +; Service button virtual-key code. Default is the F2 key. +service=0x71 +; Keyboard button to increment coin counter. Default is the F3 key. +coin=0x72 + +; Input API selection for IO4 input emulator. +; Set "keyboard" to use keyboard input, "xinput" to use a gamepad, "dinput" to +; use an arcade stick. +mode=keyboard + +[keyboard] +; START button (default: P) +start=0x50 +; HOME button (default: O) +a=0x4a +b=0x4b +c=0x4c + +[xinput] +; Defaults: +; A B LB RB +; X Y LT RT + +; Use the left thumbstick for directional controls on XInput controllers. +analogStickEnabled=1 + +[dinput] +; Name of the DirectInput stick to use (or any text that occurs in its name) +; Example: TE2+ +; +; If this is left blank then the first DirectInput device will be used. +deviceName= +; DirectInput button numbers to map to menu inputs. Note that buttons are +; numbered from 1; some software numbers buttons from 0. +start=9 +home=10 + +a=1 +b=4 +c=6 diff --git a/games/soulhook/config.c b/games/soulhook/config.c new file mode 100644 index 0000000..f1a3430 --- /dev/null +++ b/games/soulhook/config.c @@ -0,0 +1,109 @@ +#include +#include +#include + +#include "board/config.h" + +#include "amex/amex.h" +#include "amex/config.h" + +#include "hooklib/config.h" +#include "hooklib/dvd.h" + +#include "soulhook/config.h" + +#include "platform/config.h" + +void soul_dll_config_load( + struct soul_dll_config *cfg, + const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + GetPrivateProfileStringW( + L"soulio", + L"path", + L"", + cfg->path, + _countof(cfg->path), + filename); +} + +void led15093_config_load(struct led15093_config *cfg, const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + wchar_t tmpstr[16]; + + memset(cfg->board_number, ' ', sizeof(cfg->board_number)); + memset(cfg->chip_number, ' ', sizeof(cfg->chip_number)); + memset(cfg->boot_chip_number, ' ', sizeof(cfg->boot_chip_number)); + + cfg->enable = GetPrivateProfileIntW(L"led15093", L"enable", 1, filename); + cfg->port_no[0] = GetPrivateProfileIntW(L"led15070", L"portNo1", 0, filename); + cfg->port_no[1] = GetPrivateProfileIntW(L"led15070", L"portNo2", 0, filename); + cfg->high_baudrate = GetPrivateProfileIntW(L"led15093", L"highBaud", 0, filename); + cfg->fw_ver = GetPrivateProfileIntW(L"led15093", L"fwVer", 0xA0, filename); + cfg->fw_sum = GetPrivateProfileIntW(L"led15093", L"fwSum", 0xAA53, filename); + + GetPrivateProfileStringW( + L"led15093", + L"boardNumber", + L"15093-06", + tmpstr, + _countof(tmpstr), + filename); + + size_t n = wcstombs(cfg->board_number, tmpstr, sizeof(cfg->board_number)); + for (int i = n; i < sizeof(cfg->board_number); i++) + { + cfg->board_number[i] = ' '; + } + + GetPrivateProfileStringW( + L"led15093", + L"chipNumber", + L"6710A", + tmpstr, + _countof(tmpstr), + filename); + + n = wcstombs(cfg->chip_number, tmpstr, sizeof(cfg->chip_number)); + for (int i = n; i < sizeof(cfg->chip_number); i++) + { + cfg->chip_number[i] = ' '; + } + + GetPrivateProfileStringW( + L"led15093", + L"bootChipNumber", + L"6709 ", + tmpstr, + _countof(tmpstr), + filename); + + n = wcstombs(cfg->boot_chip_number, tmpstr, sizeof(cfg->boot_chip_number)); + for (int i = n; i < sizeof(cfg->boot_chip_number); i++) + { + cfg->boot_chip_number[i] = ' '; + } +} + +void soul_hook_config_load( + struct soul_hook_config *cfg, + const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + platform_config_load(&cfg->platform, filename); + io4_config_load(&cfg->io4, filename); + aime_config_load(&cfg->aime, filename); + dvd_config_load(&cfg->dvd, filename); + vfd_config_load(&cfg->vfd, filename); + touch_screen_config_load(&cfg->touch, filename); + led15093_config_load(&cfg->led15093, filename); + soul_dll_config_load(&cfg->dll, filename); +} diff --git a/games/soulhook/config.h b/games/soulhook/config.h new file mode 100644 index 0000000..5cb44fd --- /dev/null +++ b/games/soulhook/config.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include "board/config.h" +#include "board/led15093.h" + +#include "hooklib/dvd.h" +#include "hooklib/touch.h" + +#include "soulhook/soul-dll.h" + +#include "platform/config.h" + +struct soul_hook_config { + struct platform_config platform; + struct io4_config io4; + struct aime_config aime; + struct dvd_config dvd; + struct vfd_config vfd; + struct touch_screen_config touch; + struct led15093_config led15093; + struct soul_dll_config dll; +}; + +void soul_dll_config_load( + struct soul_dll_config *cfg, + const wchar_t *filename); + +void soul_hook_config_load( + struct soul_hook_config *cfg, + const wchar_t *filename); diff --git a/games/soulhook/dllmain.c b/games/soulhook/dllmain.c new file mode 100644 index 0000000..41fb266 --- /dev/null +++ b/games/soulhook/dllmain.c @@ -0,0 +1,120 @@ +#include + +#include + +#include "board/io4.h" +#include "board/sg-reader.h" +#include "board/vfd.h" + +#include "hook/process.h" + +#include "hooklib/dvd.h" +#include "hooklib/touch.h" +#include "hooklib/serial.h" +#include "hooklib/spike.h" + +#include "soulhook/config.h" +#include "soulhook/io4.h" +#include "soulhook/soul-dll.h" + +#include "platform/platform.h" + +#include "util/dprintf.h" +#include "util/env.h" + +static HMODULE soul_hook_mod; +static process_entry_t soul_startup; +static struct soul_hook_config soul_hook_cfg; + +static DWORD CALLBACK soul_pre_startup(void) +{ + HRESULT hr; + + dprintf("--- Begin soul_pre_startup ---\n"); + + /* Load config */ + + soul_hook_config_load(&soul_hook_cfg, get_config_path()); + + /* Hook Win32 APIs */ + + dvd_hook_init(&soul_hook_cfg.dvd, soul_hook_mod); + touch_screen_hook_init(&soul_hook_cfg.touch, soul_hook_mod); + serial_hook_init(); + + /* Initialize emulation hooks */ + + hr = platform_hook_init( + &soul_hook_cfg.platform, + "SDDP", + "ACA2", + soul_hook_mod); + + if (FAILED(hr)) { + goto fail; + } + + hr = soul_dll_init(&soul_hook_cfg.dll, soul_hook_mod); + + if (FAILED(hr)) { + goto fail; + } + + hr = sg_reader_hook_init(&soul_hook_cfg.aime, 3, 3, soul_hook_mod); + + if (FAILED(hr)) { + goto fail; + } + + hr = vfd_hook_init(&soul_hook_cfg.vfd, 1); + + if (FAILED(hr)) { + goto fail; + } + + hr = soul_io4_hook_init(&soul_hook_cfg.io4); + + if (FAILED(hr)) { + goto fail; + } + + unsigned int led_port_no[2] = {2, 0}; + hr = led15093_hook_init(&soul_hook_cfg.led15093, + soul_dll.led_init, soul_dll.led_set_leds, led_port_no); + + if (FAILED(hr)) { + goto fail; + } + + /* Initialize debug helpers */ + + spike_hook_init(get_config_path()); + + dprintf("--- End soul_pre_startup ---\n"); + + /* Jump to EXE start address */ + + return soul_startup(); + +fail: + ExitProcess(EXIT_FAILURE); +} + +BOOL WINAPI DllMain(HMODULE mod, DWORD cause, void *ctx) +{ + HRESULT hr; + + if (cause != DLL_PROCESS_ATTACH) { + return TRUE; + } + + soul_hook_mod = mod; + + hr = process_hijack_startup(soul_pre_startup, &soul_startup); + + if (!SUCCEEDED(hr)) { + dprintf("Failed to hijack process startup: %x\n", (int) hr); + } + + return SUCCEEDED(hr); +} diff --git a/games/soulhook/io4.c b/games/soulhook/io4.c new file mode 100644 index 0000000..0f0e8f9 --- /dev/null +++ b/games/soulhook/io4.c @@ -0,0 +1,89 @@ +#include + +#include +#include +#include + +#include "board/io4.h" + +#include "soulhook/soul-dll.h" + +#include "util/dprintf.h" + +static HRESULT soul_io4_poll(void *ctx, struct io4_state *state); +static uint16_t coins; + +static const struct io4_ops soul_io4_ops = { + .poll = soul_io4_poll, +}; + +HRESULT soul_io4_hook_init(const struct io4_config *cfg) +{ + HRESULT hr; + + assert(soul_dll.init != NULL); + + hr = io4_hook_init(cfg, &soul_io4_ops, NULL, L"Soul Reverse", false); + + if (FAILED(hr)) { + return hr; + } + + return soul_dll.init(); +} + +static HRESULT soul_io4_poll(void *ctx, struct io4_state *state) +{ + uint8_t opbtn; + uint8_t left; + uint8_t right; + int16_t lever; + HRESULT hr; + + assert(soul_dll.poll != NULL); + assert(soul_dll.get_opbtns != NULL); + assert(soul_dll.get_gamebtns != NULL); + + memset(state, 0, sizeof(*state)); + + hr = soul_dll.poll(); + + if (FAILED(hr)) { + return hr; + } + + opbtn = 0; + left = 0; + right = 0; + lever = 0; + + soul_dll.get_opbtns(&opbtn); + soul_dll.get_gamebtns(&left, &right); + + if (opbtn & SOUL_IO_OPBTN_TEST) { + state->buttons[0] |= IO4_BUTTON_TEST; + } + + if (opbtn & SOUL_IO_OPBTN_SERVICE) { + state->buttons[0] |= IO4_BUTTON_SERVICE; + } + + if (opbtn & SOUL_IO_OPBTN_COIN) { + coins++; + } + state->chutes[0] = coins << 8; + + if (left & SOUL_IO_GAMEBTN_A) { + state->buttons[0] |= 1 << 0; + } + + if (left & SOUL_IO_GAMEBTN_B) { + state->buttons[0] |= 1 << 5; + } + + if (left & SOUL_IO_GAMEBTN_C) { + state->buttons[0] |= 1 << 7; + } + + return S_OK; +} diff --git a/games/soulhook/io4.h b/games/soulhook/io4.h new file mode 100644 index 0000000..230d349 --- /dev/null +++ b/games/soulhook/io4.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include "board/io4.h" + +HRESULT soul_io4_hook_init(const struct io4_config *cfg); diff --git a/games/soulhook/meson.build b/games/soulhook/meson.build new file mode 100644 index 0000000..49dfed8 --- /dev/null +++ b/games/soulhook/meson.build @@ -0,0 +1,30 @@ +shared_library( + 'soulhook', + name_prefix : '', + include_directories : inc, + implicit_include_directories : false, + vs_module_defs : 'soulhook.def', + dependencies : [ + capnhook.get_variable('hook_dep'), + capnhook.get_variable('hooklib_dep'), + xinput_lib, + ], + link_with : [ + aimeio_lib, + amex_lib, + board_lib, + hooklib_lib, + soulio_lib, + platform_lib, + util_lib, + ], + sources : [ + 'config.c', + 'config.h', + 'dllmain.c', + 'io4.c', + 'io4.h', + 'soul-dll.c', + 'soul-dll.h', + ], +) diff --git a/games/soulhook/soul-dll.c b/games/soulhook/soul-dll.c new file mode 100644 index 0000000..62668c8 --- /dev/null +++ b/games/soulhook/soul-dll.c @@ -0,0 +1,115 @@ +#include + +#include +#include + +#include "soulhook/soul-dll.h" + +#include "util/dll-bind.h" +#include "util/dprintf.h" + +const struct dll_bind_sym soul_dll_syms[] = { + { + .sym = "soul_io_init", + .off = offsetof(struct soul_dll, init), + }, { + .sym = "soul_io_poll", + .off = offsetof(struct soul_dll, poll), + }, { + .sym = "soul_io_get_opbtns", + .off = offsetof(struct soul_dll, get_opbtns), + }, { + .sym = "soul_io_get_gamebtns", + .off = offsetof(struct soul_dll, get_gamebtns), + }, { + .sym = "soul_io_led_init", + .off = offsetof(struct soul_dll, led_init), + }, { + .sym = "soul_io_led_set_colors", + .off = offsetof(struct soul_dll, led_set_leds), + } +}; + +struct soul_dll soul_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 soul_dll_init(const struct soul_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("Soul Reverse IO: Failed to load IO DLL: %lx: %S\n", + hr, + cfg->path); + + goto end; + } + + dprintf("Soul Reverse IO: Using custom IO DLL: %S\n", cfg->path); + src = owned; + } else { + owned = NULL; + src = self; + } + + get_api_version = (void *) GetProcAddress(src, "soul_io_get_api_version"); + + if (get_api_version != NULL) { + soul_dll.api_version = get_api_version(); + } else { + soul_dll.api_version = 0x0100; + dprintf("Custom IO DLL does not expose soul_io_get_api_version, " + "assuming API version 1.0.\n" + "Please ask the developer to update their DLL.\n"); + } + + if (soul_dll.api_version >= 0x0200) { + hr = E_NOTIMPL; + dprintf("Soul Reverse IO: Custom IO DLL implements an unsupported " + "API version (%#04x). Please update Segatools.\n", + soul_dll.api_version); + + goto end; + } + + sym = soul_dll_syms; + hr = dll_bind(&soul_dll, src, &sym, _countof(soul_dll_syms)); + + if (FAILED(hr)) { + if (src != self) { + dprintf("Soul Reverse 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/games/soulhook/soul-dll.h b/games/soulhook/soul-dll.h new file mode 100644 index 0000000..1a55e51 --- /dev/null +++ b/games/soulhook/soul-dll.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "soulio/soulio.h" + +struct soul_dll { + uint16_t api_version; + HRESULT (*init)(void); + HRESULT (*poll)(void); + void (*get_opbtns)(uint8_t *opbtn); + void (*get_gamebtns)(uint8_t *left, uint8_t *right); + HRESULT (*led_init)(void); + void (*led_set_leds)(uint8_t board, uint8_t *rgb); +}; + +struct soul_dll_config { + wchar_t path[MAX_PATH]; +}; + +extern struct soul_dll soul_dll; + +HRESULT soul_dll_init(const struct soul_dll_config *cfg, HINSTANCE self); diff --git a/games/soulhook/soulhook.def b/games/soulhook/soulhook.def new file mode 100644 index 0000000..8ebe4be --- /dev/null +++ b/games/soulhook/soulhook.def @@ -0,0 +1,22 @@ +LIBRARY soulhook + +EXPORTS + aime_io_get_api_version + aime_io_init + aime_io_led_set_color + aime_io_vfd_set_text + aime_io_vfd_set_state + aime_io_nfc_get_aime_id + aime_io_nfc_get_felica_id + aime_io_nfc_poll + amDllVideoClose @2 + amDllVideoGetVBiosVersion @4 + amDllVideoOpen @1 + amDllVideoSetResolution @3 + soul_io_get_api_version + soul_io_get_gamebtns + soul_io_get_opbtns + soul_io_init + soul_io_poll + soul_io_led_init + soul_io_led_set_colors diff --git a/games/soulio/config.c b/games/soulio/config.c new file mode 100644 index 0000000..2cb5ff1 --- /dev/null +++ b/games/soulio/config.c @@ -0,0 +1,20 @@ +#include + +#include +#include +#include +#include + +#include "soulio/config.h" + +void soul_io_config_load( + struct soul_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); +} diff --git a/games/soulio/config.h b/games/soulio/config.h new file mode 100644 index 0000000..abb7aee --- /dev/null +++ b/games/soulio/config.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +#include + +struct soul_io_config { + uint8_t vk_test; + uint8_t vk_service; + uint8_t vk_coin; +}; + +void soul_io_config_load( + struct soul_io_config *cfg, + const wchar_t *filename); diff --git a/games/soulio/meson.build b/games/soulio/meson.build new file mode 100644 index 0000000..5c66d0d --- /dev/null +++ b/games/soulio/meson.build @@ -0,0 +1,12 @@ +soulio_lib = static_library( + 'soulio', + name_prefix : '', + include_directories : inc, + implicit_include_directories : false, + sources : [ + 'soulio.c', + 'soulio.h', + 'config.c', + 'config.h', + ], +) diff --git a/games/soulio/soulio.c b/games/soulio/soulio.c new file mode 100644 index 0000000..7e0e43e --- /dev/null +++ b/games/soulio/soulio.c @@ -0,0 +1,97 @@ +#include +#include + +#include +#include + +#include "soulio/soulio.h" +#include "soulio/config.h" + +#include "util/dprintf.h" +#include "util/env.h" + +static uint8_t soul_opbtn; +static uint8_t soul_gamebtn; +static struct soul_io_config soul_io_cfg; +static bool soul_io_coin; + +uint16_t soul_io_get_api_version(void) +{ + return 0x0100; +} + +HRESULT soul_io_init(void) +{ + soul_io_config_load(&soul_io_cfg, get_config_path()); + return S_OK; +} + +HRESULT soul_io_poll(void) +{ + int lever; + int xlever; + XINPUT_STATE xi; + WORD xb; + + soul_opbtn = 0; + soul_gamebtn = 0; + + if (GetAsyncKeyState(soul_io_cfg.vk_test) & 0x8000) { + soul_opbtn |= SOUL_IO_OPBTN_TEST; + } + + if (GetAsyncKeyState(soul_io_cfg.vk_service) & 0x8000) { + soul_opbtn |= SOUL_IO_OPBTN_SERVICE; + } + + if (GetAsyncKeyState(soul_io_cfg.vk_coin) & 0x8000) { + if (!soul_io_coin) { + soul_io_coin = true; + soul_opbtn |= SOUL_IO_OPBTN_COIN; + } + } else { + soul_io_coin = false; + } + + memset(&xi, 0, sizeof(xi)); + XInputGetState(0, &xi); + xb = xi.Gamepad.wButtons; + + if (xb & XINPUT_GAMEPAD_X) { + soul_gamebtn |= SOUL_IO_GAMEBTN_A; + } + + if (xb & XINPUT_GAMEPAD_A) { + soul_gamebtn |= SOUL_IO_GAMEBTN_B; + } + + if (xb & XINPUT_GAMEPAD_B) { + soul_gamebtn |= SOUL_IO_GAMEBTN_C; + } + + return S_OK; +} + +void soul_io_get_opbtns(uint8_t *opbtn) +{ + if (opbtn != NULL) { + *opbtn = soul_opbtn; + } +} + +void soul_io_get_gamebtns(uint8_t *gamebtn) +{ + if (gamebtn != NULL) { + *gamebtn = soul_gamebtn; + } +} + +HRESULT soul_io_led_init(void) +{ + return S_OK; +} + +void soul_io_led_set_colors(uint8_t board, uint8_t *rgb) +{ + return; +} diff --git a/games/soulio/soulio.h b/games/soulio/soulio.h new file mode 100644 index 0000000..b293794 --- /dev/null +++ b/games/soulio/soulio.h @@ -0,0 +1,77 @@ +#pragma once + +#include + +#include + +enum { + SOUL_IO_OPBTN_TEST = 0x01, + SOUL_IO_OPBTN_SERVICE = 0x02, + SOUL_IO_OPBTN_COIN = 0x04, +}; + +enum { + SOUL_IO_GAMEBTN_A = 0x01, + SOUL_IO_GAMEBTN_B = 0x02, + SOUL_IO_GAMEBTN_C = 0x04, +}; + +/* Get the version of the Soul Reverse 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 soul_io_get_api_version(void); + +/* Initialize the IO DLL. This is the second function that will be called on + your DLL, after soul_io_get_api_version. + + All subsequent calls to this API may originate from arbitrary threads. + + Minimum API version: 0x0100 */ + +HRESULT soul_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 soul_io_poll(void); + +/* Get the state of the cabinet's operator buttons as of the last poll. See + SOUL_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 soul_io_get_opbtns(uint8_t *opbtn); + +/* Get the state of the cabinet's gameplay buttons as of the last poll. See + SOUL_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 soul_io_get_gamebtns(uint8_t *gamebtn); + +/* Initialize LED emulation. This function will be called before any + other soul_io_led_*() function calls. + + All subsequent calls may originate from arbitrary threads and some may + overlap with each other. Ensuring synchronization inside your IO DLL is + your responsibility. */ + +HRESULT soul_io_led_init(void); + +/* Update the RGB LEDs. + + Exact layout is TBD. */ + +void soul_io_led_set_colors(uint8_t board, uint8_t *rgb); diff --git a/meson.build b/meson.build index b4376bb..38e9d3a 100644 --- a/meson.build +++ b/meson.build @@ -128,6 +128,7 @@ subdir('games/tokyoio') subdir('games/fgoio') subdir('games/kemonoio') subdir('games/apm3io') +subdir('games/soulio') subdir('games/chunihook') subdir('games/divahook') @@ -145,3 +146,4 @@ subdir('games/tokyohook') subdir('games/fgohook') subdir('games/kemonohook') subdir('games/apm3hook') +subdir('games/soulhook')