From e2e4b37e3f20c42106933cf24a111fdfc3cc1d50 Mon Sep 17 00:00:00 2001 From: kyoubate-haruka <46010460+kyoubate-haruka@users.noreply.github.com> Date: Sun, 20 Jul 2025 09:43:56 +0000 Subject: [PATCH 1/2] APMv3: add hook (#73) This adds support for APMv3 I/O, menus and the launcher. * Added a apm3hook dll and I/O based on the usual layout. * Added C:\Mount\Apm to vfs. * Added the relevant .dlls to unityhook. * Added a hook for apmmount.dll that uses `CreateDosDevice` to mount decrypted data to the locations the launcher and games expect files to be. This will conflict with anything that is already at W:\ and X:\, but I do not have better solutions for this. * `launch.bat` is a bit more involved as it simulates the launcher loop. It can be broken by alt+f4ing or closing the launcher with "X". * An extra export was added, so rundll32 can be used to get rid of the dosdevices after the launcher was killed. * Since all the games do everything via `X:\lib\apm.dll`, no game hooks were needed in testing, therefore, `game.bat` files can be used as is. * Path hooks are applied correctly, so you can go correctly between games, launcher, sub system test mode and game test modes. A setup guide (some stuff specific to my server) can be found here: https://gmg.hopto.org:82/gmg/wiki/index.php/All.Net_P-ras_Multi_Menu Tested with the 2 APM sample apps, Blazblue, Puyo, Guilty Gear and some weird unity puzzle game whose name I forgot. ![Apmv3System_yLRityJVpm.png](/attachments/3d645e71-81e6-42e6-acd4-63c537cda59e) ![puyoe_hJNhnJGFnd.png](/attachments/01664049-71fe-4c38-9c99-39649ab21e56) Reviewed-on: https://gitea.tendokyu.moe/TeamTofuShop/segatools/pulls/73 Co-authored-by: kyoubate-haruka <46010460+kyoubate-haruka@users.noreply.github.com> Co-committed-by: kyoubate-haruka <46010460+kyoubate-haruka@users.noreply.github.com> --- Package.mk | 17 +++ common/hooklib/path.c | 3 +- common/hooklib/path.h | 1 + common/platform/meson.build | 3 + common/platform/security.c | 210 ++++++++++++++++++++++++++++++++++++ common/platform/security.h | 9 ++ common/platform/vfs.c | 59 ++++++++++ common/unityhook/hook.c | 6 +- dist/apm3/config_hook.json | 10 ++ dist/apm3/launch.bat | 51 +++++++++ dist/apm3/segatools.ini | 161 +++++++++++++++++++++++++++ games/apm3hook/apm3-dll.c | 115 ++++++++++++++++++++ games/apm3hook/apm3-dll.h | 23 ++++ games/apm3hook/apm3hook.def | 21 ++++ games/apm3hook/config.c | 113 +++++++++++++++++++ games/apm3hook/config.h | 43 ++++++++ games/apm3hook/dllmain.c | 173 +++++++++++++++++++++++++++++ games/apm3hook/io4.c | 129 ++++++++++++++++++++++ games/apm3hook/io4.h | 7 ++ games/apm3hook/meson.build | 32 ++++++ games/apm3hook/mount.c | 174 ++++++++++++++++++++++++++++++ games/apm3hook/mount.h | 7 ++ games/apm3io/apm3io.c | 128 ++++++++++++++++++++++ games/apm3io/apm3io.h | 84 +++++++++++++++ games/apm3io/config.c | 36 +++++++ games/apm3io/config.h | 28 +++++ games/apm3io/meson.build | 15 +++ meson.build | 3 + 28 files changed, 1658 insertions(+), 3 deletions(-) create mode 100644 common/platform/security.c create mode 100644 common/platform/security.h create mode 100644 dist/apm3/config_hook.json create mode 100644 dist/apm3/launch.bat create mode 100644 dist/apm3/segatools.ini create mode 100644 games/apm3hook/apm3-dll.c create mode 100644 games/apm3hook/apm3-dll.h create mode 100644 games/apm3hook/apm3hook.def create mode 100644 games/apm3hook/config.c create mode 100644 games/apm3hook/config.h create mode 100644 games/apm3hook/dllmain.c create mode 100644 games/apm3hook/io4.c create mode 100644 games/apm3hook/io4.h create mode 100644 games/apm3hook/meson.build create mode 100644 games/apm3hook/mount.c create mode 100644 games/apm3hook/mount.h create mode 100644 games/apm3io/apm3io.c create mode 100644 games/apm3io/apm3io.h create mode 100644 games/apm3io/config.c create mode 100644 games/apm3io/config.h create mode 100644 games/apm3io/meson.build diff --git a/Package.mk b/Package.mk index ae87f73..3c974e7 100644 --- a/Package.mk +++ b/Package.mk @@ -240,6 +240,22 @@ $(BUILD_DIR_ZIP)/kemono.zip: for x in exe dll; do strip $(BUILD_DIR_ZIP)/kemono/*.$$x; done $(V)cd $(BUILD_DIR_ZIP)/kemono ; zip -r ../kemono.zip * +$(BUILD_DIR_ZIP)/apm3.zip: + $(V)echo ... $@ + $(V)mkdir -p $(BUILD_DIR_ZIP)/apm3 + $(V)mkdir -p $(BUILD_DIR_ZIP)/apm3/DEVICE + $(V)cp $(BUILD_DIR_64)/subprojects/capnhook/inject/inject.exe \ + $(BUILD_DIR_GAMES_64)/apm3hook/apm3hook.dll \ + $(DIST_DIR)/apm3/segatools.ini \ + $(DIST_DIR)/apm3/launch.bat \ + $(DIST_DIR)/apm3/config_hook.json \ + $(BUILD_DIR_ZIP)/apm3 + $(V)cp pki/billing.pub \ + pki/ca.crt \ + $(BUILD_DIR_ZIP)/apm3/DEVICE + $(V)strip $(BUILD_DIR_ZIP)/apm3/*.{exe,dll} + $(V)cd $(BUILD_DIR_ZIP)/apm3 ; zip -r ../apm3.zip * + $(BUILD_DIR_ZIP)/doc.zip: \ $(DOC_DIR)/config \ $(DOC_DIR)/chunihook.md \ @@ -265,6 +281,7 @@ $(BUILD_DIR_ZIP)/segatools.zip: \ $(BUILD_DIR_ZIP)/tokyo.zip \ $(BUILD_DIR_ZIP)/fgo.zip \ $(BUILD_DIR_ZIP)/kemono.zip \ + $(BUILD_DIR_ZIP)/apm3.zip \ CHANGELOG.md \ README.md \ diff --git a/common/hooklib/path.c b/common/hooklib/path.c index 1afb8a8..2a77442 100644 --- a/common/hooklib/path.c +++ b/common/hooklib/path.c @@ -17,7 +17,6 @@ static void path_hook_init(void); static BOOL path_transform_a(char **out, const char *src); -static BOOL path_transform_w(wchar_t **out, const wchar_t *src); /* API hooks */ @@ -490,7 +489,7 @@ end: return ok; } -static BOOL path_transform_w(wchar_t **out, const wchar_t *src) +BOOL path_transform_w(wchar_t **out, const wchar_t *src) { BOOL ok; HRESULT hr; diff --git a/common/hooklib/path.h b/common/hooklib/path.h index 3a58bb7..eb55dd7 100644 --- a/common/hooklib/path.h +++ b/common/hooklib/path.h @@ -13,6 +13,7 @@ typedef HRESULT (*path_hook_t)( HRESULT path_hook_push(path_hook_t hook); void path_hook_insert_hooks(HMODULE target); int path_compare_w(const wchar_t *string1, const wchar_t *string2, size_t count); +BOOL path_transform_w(wchar_t **out, const wchar_t *src); static inline bool path_is_separator_w(wchar_t c) { diff --git a/common/platform/meson.build b/common/platform/meson.build index 160f8af..0e54b6c 100644 --- a/common/platform/meson.build +++ b/common/platform/meson.build @@ -5,6 +5,7 @@ platform_lib = static_library( dependencies : [ capnhook.get_variable('hook_dep'), shlwapi_lib, + bcrypt_lib, ], sources : [ 'amvideo.c', @@ -31,6 +32,8 @@ platform_lib = static_library( 'pcbid.h', 'platform.c', 'platform.h', + 'security.c', + 'security.h', 'vfs.c', 'vfs.h', 'system.c', diff --git a/common/platform/security.c b/common/platform/security.c new file mode 100644 index 0000000..b504885 --- /dev/null +++ b/common/platform/security.c @@ -0,0 +1,210 @@ +#include +#include + +#define STATUS_SUCCESS ((NTSTATUS)0x00000000) + +#include +#include + +#include "hook/table.h" + +#include "platform/security.h" + +#include "util/dprintf.h" +#include "util/dump.h" + +static NTSTATUS __stdcall my_BCryptDecrypt( + BCRYPT_KEY_HANDLE hKey, + PUCHAR pbInput, + ULONG cbInput, + void* pPaddingInfo, + PUCHAR pbIV, + ULONG cbIV, + PUCHAR pbOutput, + ULONG cbOutput, + ULONG* pcbResult, + ULONG dwFlags); + +static NTSTATUS (__stdcall *next_BCryptDecrypt)( + BCRYPT_KEY_HANDLE hKey, + PUCHAR pbInput, + ULONG cbInput, + void* pPaddingInfo, + PUCHAR pbIV, + ULONG cbIV, + PUCHAR pbOutput, + ULONG cbOutput, + ULONG* pcbResult, + ULONG dwFlags); + +static NTSTATUS __stdcall my_BCryptEncrypt( + BCRYPT_KEY_HANDLE hKey, + PUCHAR pbInput, + ULONG cbInput, + void* pPaddingInfo, + PUCHAR pbIV, + ULONG cbIV, + PUCHAR pbOutput, + ULONG cbOutput, + ULONG* pcbResult, + ULONG dwFlags); + +static NTSTATUS (__stdcall *next_BCryptEncrypt)( + BCRYPT_KEY_HANDLE hKey, + PUCHAR pbInput, + ULONG cbInput, + void* pPaddingInfo, + PUCHAR pbIV, + ULONG cbIV, + PUCHAR pbOutput, + ULONG cbOutput, + ULONG* pcbResult, + ULONG dwFlags); + +static const struct hook_symbol security_hook_syms[] = { + { + .name = "BCryptDecrypt", + .patch = my_BCryptDecrypt, + .link = (void **) &next_BCryptDecrypt, + }, + { + .name = "BCryptEncrypt", + .patch = my_BCryptEncrypt, + .link = (void **) &next_BCryptEncrypt, + } +}; + +void LogKeyProperties(BCRYPT_KEY_HANDLE hKey) { + NTSTATUS status; + ULONG cbData; + + // Retrieve the algorithm name + WCHAR algorithmName[256]; + ULONG cbAlgorithmName = sizeof(algorithmName); + + status = BCryptGetProperty(hKey, BCRYPT_ALGORITHM_NAME, (PUCHAR) algorithmName, cbAlgorithmName * sizeof(WCHAR), + &cbData, 0); + if (status == STATUS_SUCCESS) { + dprintf("Algorithm: %ls\n", algorithmName); + } else { + dprintf("Algorithm: failed to retrieve algorithm name (0x%08lX)\n", status); + } + + // Export the key value + DWORD keyBlobSize = 0; + status = BCryptExportKey(hKey, NULL, BCRYPT_KEY_DATA_BLOB, NULL, 0, &keyBlobSize, 0); + if (status != STATUS_SUCCESS) { + dprintf("KEY: failed to determine key blob size (0x%08lX)\n", status); + return; + } + + PUCHAR keyBlob = (PUCHAR) malloc(keyBlobSize); + if (!keyBlob) { + dprintf("KEY: failed the memory allocation for key blob.\n"); + return; + } + + status = BCryptExportKey(hKey, NULL, BCRYPT_KEY_DATA_BLOB, keyBlob, keyBlobSize, &keyBlobSize, 0); + if (status == STATUS_SUCCESS) { + int headerSize = sizeof(BCRYPT_KEY_DATA_BLOB_HEADER); + dprintf("KEY (%lu bytes):\n", keyBlobSize - headerSize); + dump(keyBlob + headerSize, keyBlobSize - headerSize); + } else { + dprintf("KEY: failed to export key value (0x%08lX)\n", status); + } + + free(keyBlob); +} + + +static NTSTATUS __stdcall my_BCryptDecrypt( + BCRYPT_KEY_HANDLE hKey, + PUCHAR pbInput, + ULONG cbInput, + void* pPaddingInfo, + PUCHAR pbIV, + ULONG cbIV, + PUCHAR pbOutput, + ULONG cbOutput, + ULONG* pcbResult, + ULONG dwFlags) { + dprintf("BCrypt: Decrypt\n"); + dprintf("FLAGS: %lx\n", dwFlags); + LogKeyProperties(hKey); + dprintf("IV:\n"); + dump(pbIV, cbIV); + dprintf("ENCRYPTED:\n"); + dump(pbInput, cbInput); + NTSTATUS ret = next_BCryptDecrypt(hKey, pbInput, cbInput, pPaddingInfo, pbIV, cbIV, pbOutput, cbOutput, pcbResult, + dwFlags); + dprintf("Operation Result: %lx\n", ret); + if (ret == 0) { + dprintf("Decrypted (%ld, max buf: %ld):\n", *pcbResult, cbOutput); + if (pbOutput != NULL) { + dump(pbOutput, *pcbResult); + } else { + dprintf("pbOutput == NULL!\n"); + } + } + + return ret; +} + + +static NTSTATUS __stdcall my_BCryptEncrypt( + BCRYPT_KEY_HANDLE hKey, + PUCHAR pbInput, + ULONG cbInput, + void* pPaddingInfo, + PUCHAR pbIV, + ULONG cbIV, + PUCHAR pbOutput, + ULONG cbOutput, + ULONG* pcbResult, + ULONG dwFlags) { + dprintf("BCrypt: Encrypt\n"); + dprintf("FLAGS: %lx\n", dwFlags); + LogKeyProperties(hKey); + dprintf("IV:\n"); + dump(pbIV, cbIV); + dprintf("Input data (%ld):\n", cbInput); + dump(pbOutput, *pcbResult); + NTSTATUS ret = next_BCryptEncrypt(hKey, pbInput, cbInput, pPaddingInfo, pbIV, cbIV, pbOutput, cbOutput, pcbResult, + dwFlags); + dprintf("Operation Result: %lx\n", ret); + if (ret == 0) { + dprintf("First 16 bytes of encrypted data (%ld, max buf: %ld):\n", *pcbResult, cbOutput); + if (pbOutput != NULL) { + dump(pbOutput, min(16, *pcbResult)); + } else { + dprintf("pbOutput == NULL!\n"); + } + } + return ret; +} + +HRESULT security_hook_init() { + security_hook_insert_hooks(NULL); + + return S_OK; +} + +void security_hook_insert_hooks(HMODULE mod) { + + char value[8]; + if (!GetEnvironmentVariableA("BCRYPT_DUMP_ENABLED", value, 8)) { + return; + } + + if (value[0] != '1') { + return; + } + + dprintf("BCrypt: dumping enabled\n"); + + hook_table_apply( + mod, + "BCrypt.dll", + security_hook_syms, + _countof(security_hook_syms)); +} diff --git a/common/platform/security.h b/common/platform/security.h new file mode 100644 index 0000000..bd4bb82 --- /dev/null +++ b/common/platform/security.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +#include +#include + +HRESULT security_hook_init(); +void security_hook_insert_hooks(HMODULE mod); diff --git a/common/platform/vfs.c b/common/platform/vfs.c index e16389a..372add1 100644 --- a/common/platform/vfs.c +++ b/common/platform/vfs.c @@ -31,6 +31,10 @@ static HRESULT vfs_path_hook_option( const wchar_t *src, wchar_t *dest, size_t *count); +static HRESULT vfs_path_hook_apm( + const wchar_t *src, + wchar_t *dest, + size_t *count); static HRESULT vfs_reg_read_amfs(void *bytes, uint32_t *nbytes); static HRESULT vfs_reg_read_appdata(void *bytes, uint32_t *nbytes); @@ -64,6 +68,9 @@ static const size_t vfs_w10home_len = _countof(vfs_w10home) - 1; static const wchar_t vfs_option[] = L"C:\\Mount\\Option"; static const size_t vfs_option_len = _countof(vfs_option) - 1; +static const wchar_t vfs_apm3[] = L"C:\\Mount\\Apm"; +static const size_t vfs_apm3_len = _countof(vfs_apm3) - 1; + static const struct reg_hook_val vfs_reg_vals[] = { { .name = L"AMFS", @@ -199,6 +206,11 @@ HRESULT vfs_hook_init(const struct vfs_config *config, const char* game_id) if (vfs_config.option[0] != L'\0') { hr = path_hook_push(vfs_path_hook_option); + if (FAILED(hr)) { + return hr; + } + hr = path_hook_push(vfs_path_hook_apm); + if (FAILED(hr)) { return hr; } @@ -514,6 +526,53 @@ static HRESULT vfs_path_hook_option( return S_OK; } +static HRESULT vfs_path_hook_apm( + const wchar_t *src, + wchar_t *dest, + size_t *count) +{ + size_t required; + size_t redir_len; + size_t shift; + + assert(src != NULL); + assert(count != NULL); + + /* Case-insensitive check to see if src starts with vfs_apm */ + + if (path_compare_w(src, vfs_apm3, vfs_apm3_len) != 0) { + return S_FALSE; + } + + /* Check if the character after vfs_nthome is a separator or the end of + the string */ + + if (!path_is_separator_w(src[vfs_apm3_len]) && + src[vfs_apm3_len] != L'\0') + { + return S_FALSE; + } + + /* Cut off the matched \, add the replaced prefix, count NUL */ + + shift = path_is_separator_w(src[vfs_apm3_len]) ? 1 : 0; + redir_len = wcslen(vfs_config.option); + required = wcslen(src) - vfs_apm3_len - shift + redir_len + 1; + + if (dest != NULL) { + if (required > *count) { + return HRESULT_FROM_WIN32(ERROR_INSUFFICIENT_BUFFER); + } + + wcscpy_s(dest, *count, vfs_config.option); + wcscpy_s(dest + redir_len, *count - redir_len, src + vfs_apm3_len + shift); + } + + *count = required; + + return S_OK; +} + static HRESULT vfs_reg_read_amfs(void *bytes, uint32_t *nbytes) { return reg_hook_read_wstr(bytes, nbytes, L"E:\\"); diff --git a/common/unityhook/hook.c b/common/unityhook/hook.c index 33070a0..94d1a5b 100644 --- a/common/unityhook/hook.c +++ b/common/unityhook/hook.c @@ -34,7 +34,11 @@ static const wchar_t *target_modules[] = { L"C300FWDLusb.dll", L"apmled.dll", L"HKBSys_api.dll", - L"amptw.dll" + L"amptw.dll", + L"apmmount.dll", + L"abaasgs.dll", + L"AVProVideo.dll", + L"Audio360.dll", }; static const size_t target_modules_len = _countof(target_modules); diff --git a/dist/apm3/config_hook.json b/dist/apm3/config_hook.json new file mode 100644 index 0000000..52f2e2b --- /dev/null +++ b/dist/apm3/config_hook.json @@ -0,0 +1,10 @@ +{ + "common": { + "language": "english" + }, + "aime": { + "firmware_path": [ + "aime_firm\\update_15396_6728_94.bin" + ] + } +} \ No newline at end of file diff --git a/dist/apm3/launch.bat b/dist/apm3/launch.bat new file mode 100644 index 0000000..0c8d689 --- /dev/null +++ b/dist/apm3/launch.bat @@ -0,0 +1,51 @@ +@echo off + +cd /d %~dp0 + +set PATH=%~dp0lib;%~dp0;X:\;%PATH% + +:BEGIN +cd /d %~dp0 + +qprocess amdaemon.exe > NUL +IF %ERRORLEVEL% NEQ 0 start /min cmd /C "inject -d -k apm3hook.dll amdaemon.exe -f -c daemon_config\common.json daemon_config\server.json config_hook.json" + +inject -d -k apm3hook.dll APMV3System -screen-fullscreen 0 -screen-width 1920 -screen-height 1080 -popupWindow -logFile output_log.txt + +if exist %tmp%\segaboot ( + del %tmp%\segaboot + goto END +) + +if exist %tmp%\app ( + del %tmp%\app + goto APP +) + +if exist %tmp%\apptest ( + del %tmp%\apptest + goto APPTEST +) + +goto END + +:APP + +call W:\game.bat +taskkill /f /im emoneyUI.exe > nul 2>&1 +goto BEGIN + +:APPTEST +call W:\gametest.bat +goto BEGIN + +:END + +taskkill /f /im emoneyUI.exe > nul 2>&1 +taskkill /f /im amdaemon.exe > nul 2>&1 + +rundll32 apm3hook.dll,UnmountApmDrives + +echo. +echo Game processes have terminated +pause \ No newline at end of file diff --git a/dist/apm3/segatools.ini b/dist/apm3/segatools.ini new file mode 100644 index 0000000..98d2c66 --- /dev/null +++ b/dist/apm3/segatools.ini @@ -0,0 +1,161 @@ +; ----------------------------------------------------------------------------- +; 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=appdata + +; ----------------------------------------------------------------------------- +; Device settings +; ----------------------------------------------------------------------------- + +[aime] +; Enable Aime card reader assembly emulation. Disable to use a real SEGA Aime +; reader. +enable=1 +aimePath=DEVICE\aime.txt + +[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] +; 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 + +[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 + +; ----------------------------------------------------------------------------- +; Misc. hook settings +; ----------------------------------------------------------------------------- + +[touch] +; WinTouch emulation setting. +enable=1 +remap=1 +cursor=1 + +[unity] +; Enable Unity hook. This will allow you to run custom .NET code before the game +enable=1 + +; Path to a .NET DLL that should run before the game. Useful for loading +; modding frameworks such as BepInEx. +targetAssembly= + +[mount] +; Enables W: drive mapping instead of using .vhd files +enable=1 +; Delays the mount operation by a slight bit, so the launch sound effect isn't cut off. +delay=1 + +[epay] +; Enables the Thinca emulation. This will allow you to enable E-Money on compatible servers. +enable=0 + +; ----------------------------------------------------------------------------- +; 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= + +[apmio] +; To use a custom APM 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 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 + +; START button (default: P) +start=0x50 +; HOME button (default: O) +home=0x4F + +; Buttons for the fight stick (default: arrows) +up=0x26 +right=0x27 +down=0x28 +left=0x25 + +; Main buttons +; The arrangement is as following: +; 1 2 3 4 +; 5 6 7 8 +; Defaults: +; Q W E R +; A S D F +button1=0x51 +button2=0x57 +button3=0x45 +button4=0x52 +button5=0x41 +button6=0x53 +button7=0x44 +button8=0x46 diff --git a/games/apm3hook/apm3-dll.c b/games/apm3hook/apm3-dll.c new file mode 100644 index 0000000..b2e9ef6 --- /dev/null +++ b/games/apm3hook/apm3-dll.c @@ -0,0 +1,115 @@ +#include + +#include +#include + +#include "apm3hook/apm3-dll.h" + +#include "util/dll-bind.h" +#include "util/dprintf.h" + +const struct dll_bind_sym apm3_dll_syms[] = { + { + .sym = "apm3_io_init", + .off = offsetof(struct apm3_dll, init), + }, { + .sym = "apm3_io_poll", + .off = offsetof(struct apm3_dll, poll), + }, { + .sym = "apm3_io_get_opbtns", + .off = offsetof(struct apm3_dll, get_opbtns), + }, { + .sym = "apm3_io_get_gamebtns", + .off = offsetof(struct apm3_dll, get_gamebtns), + }, { + .sym = "apm3_io_led_init", + .off = offsetof(struct apm3_dll, led_init), + }, { + .sym = "apm3_io_led_set_colors", + .off = offsetof(struct apm3_dll, led_set_leds), + } +}; + +struct apm3_dll apm3_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 apm3_dll_init(const struct apm3_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("APM IO: Failed to load IO DLL: %lx: %S\n", + hr, + cfg->path); + + goto end; + } + + dprintf("APM IO: Using custom IO DLL: %S\n", cfg->path); + src = owned; + } else { + owned = NULL; + src = self; + } + + get_api_version = (void *) GetProcAddress(src, "apm3_io_get_api_version"); + + if (get_api_version != NULL) { + apm3_dll.api_version = get_api_version(); + } else { + apm3_dll.api_version = 0x0100; + dprintf("Custom IO DLL does not expose apm3_io_get_api_version, " + "assuming API version 1.0.\n" + "Please ask the developer to update their DLL.\n"); + } + + if (apm3_dll.api_version >= 0x0200) { + hr = E_NOTIMPL; + dprintf("APM IO: Custom IO DLL implements an unsupported " + "API version (%#04x). Please update Segatools.\n", + apm3_dll.api_version); + + goto end; + } + + sym = apm3_dll_syms; + hr = dll_bind(&apm3_dll, src, &sym, _countof(apm3_dll_syms)); + + if (FAILED(hr)) { + if (src != self) { + dprintf("APM 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/apm3hook/apm3-dll.h b/games/apm3hook/apm3-dll.h new file mode 100644 index 0000000..cd6d908 --- /dev/null +++ b/games/apm3hook/apm3-dll.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "apm3io/apm3io.h" + +struct apm3_dll { + uint16_t api_version; + HRESULT (*init)(void); + HRESULT (*poll)(void); + void (*get_opbtns)(uint8_t *opbtn); + void (*get_gamebtns)(uint32_t *gamebtn); + HRESULT (*led_init)(void); + void (*led_set_leds)(uint8_t board, uint8_t *rgb); +}; + +struct apm3_dll_config { + wchar_t path[MAX_PATH]; +}; + +extern struct apm3_dll apm3_dll; + +HRESULT apm3_dll_init(const struct apm3_dll_config *cfg, HINSTANCE self); diff --git a/games/apm3hook/apm3hook.def b/games/apm3hook/apm3hook.def new file mode 100644 index 0000000..ec636df --- /dev/null +++ b/games/apm3hook/apm3hook.def @@ -0,0 +1,21 @@ +LIBRARY apm3hook + +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 + apm3_io_get_api_version + apm3_io_get_gamebtns + apm3_io_get_opbtns + apm3_io_init + apm3_io_poll + apm3_io_led_init + apm3_io_led_set_colors + UnmountApmDrives diff --git a/games/apm3hook/config.c b/games/apm3hook/config.c new file mode 100644 index 0000000..af6bd89 --- /dev/null +++ b/games/apm3hook/config.c @@ -0,0 +1,113 @@ +#include +#include +#include + +#include "board/config.h" + +#include "hooklib/config.h" +#include "hooklib/dvd.h" + +#include "apm3hook/config.h" + +#include "platform/config.h" + +void apm3_dll_config_load( + struct apm3_dll_config *cfg, + const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + GetPrivateProfileStringW( + L"apmio", + 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"led15093", L"portNo1", 0, filename); + cfg->port_no[1] = GetPrivateProfileIntW(L"led15093", 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 mount_config_load(struct mount_config *cfg, const wchar_t *filename) { + cfg->enable = GetPrivateProfileIntW(L"mount", L"enable", 1, filename); + cfg->delay = GetPrivateProfileIntW(L"mount", L"delay", 1, filename); +} + +void apm3_hook_config_load( + struct apm3_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); + dvd_config_load(&cfg->dvd, filename); + io4_config_load(&cfg->io4, filename); + vfd_config_load(&cfg->vfd, filename); + touch_screen_config_load(&cfg->touch, filename); + led15093_config_load(&cfg->led15093, filename); + apm3_dll_config_load(&cfg->dll, filename); + unity_config_load(&cfg->unity, filename); + mount_config_load(&cfg->mount, filename); +} diff --git a/games/apm3hook/config.h b/games/apm3hook/config.h new file mode 100644 index 0000000..b336677 --- /dev/null +++ b/games/apm3hook/config.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include "board/config.h" +#include "board/led15093.h" + +#include "hooklib/dvd.h" +#include "hooklib/touch.h" +#include "hooklib/printer.h" + +#include "gfxhook/config.h" + +#include "apm3hook/apm3-dll.h" + +#include "platform/config.h" +#include "unityhook/config.h" + +struct mount_config { + bool enable; + bool delay; +}; + +struct apm3_hook_config { + struct platform_config platform; + struct aime_config aime; + struct dvd_config dvd; + struct io4_config io4; + struct vfd_config vfd; + struct touch_screen_config touch; + struct led15093_config led15093; + struct apm3_dll_config dll; + struct unity_config unity; + struct mount_config mount; +}; + +void apm3_dll_config_load( + struct apm3_dll_config *cfg, + const wchar_t *filename); + +void apm3_hook_config_load( + struct apm3_hook_config *cfg, + const wchar_t *filename); diff --git a/games/apm3hook/dllmain.c b/games/apm3hook/dllmain.c new file mode 100644 index 0000000..4c232e8 --- /dev/null +++ b/games/apm3hook/dllmain.c @@ -0,0 +1,173 @@ +/* +"ALL.Net P-ras multi Ver.3" (apm) hook + + Devices + + USB: 837-15257 "Type 4" I/O Board + COM1: 200-6275 VFD GP1232A02A FUTABA Board + COM2: 837-15093-06 LED Controller Board + COM3: 837-15396 "Gen 3" Aime Reader +*/ + +#include + +#include + +#include "config.h" +#include "io4.h" +#include "mount.h" + +#include "amex/ds.h" + +#include "hook/process.h" +#include "hooklib/serial.h" + +#include "hooklib/spike.h" + +#include "platform/clock.h" +#include "platform/config.h" +#include "platform/nusec.h" +#include "platform/security.h" +#include "unityhook/hook.h" + +#include "util/dprintf.h" +#include "util/env.h" + +static HMODULE apm3_hook_mod; +static process_entry_t apm3_startup; +static struct apm3_hook_config apm3_hook_cfg; + +void unity_hook_callback(HMODULE hmodule, const wchar_t* p) { + dprintf("Unity: Hook callback: %ls\n", p); + + serial_hook_apply_hooks(hmodule); + security_hook_insert_hooks(hmodule); + touch_hook_insert_hooks(hmodule); + //mount_hook_apply_hooks(&apm3_hook_cfg.mount, hmodule); +} + +void apm3_extra_hooks_init(void) { + HMODULE module = LoadLibraryA("Apmv3System_Data/Plugins/x86_64/apmmount.dll"); // HACK?? + if (module != NULL){ + dprintf("APM: Successfully pre-loaded apmmount\n"); + } + module = LoadLibraryA("Apmv3System_Data/Plugins/x86_64/apmled.dll"); // HACK?? + if (module != NULL){ + dprintf("APM: Successfully pre-loaded apmled\n"); + } +} + +static DWORD CALLBACK apm3_pre_startup(void) +{ + HRESULT hr; + + dprintf("--- Begin %s ---\n", __func__); + + /* Load config */ + + apm3_hook_config_load(&apm3_hook_cfg, get_config_path()); + + /* Pin stuff */ + apm3_extra_hooks_init(); + + /* Hook Win32 APIs */ + + dvd_hook_init(&apm3_hook_cfg.dvd, apm3_hook_mod); + touch_screen_hook_init(&apm3_hook_cfg.touch, apm3_hook_mod); + serial_hook_init(); + + /* Initialize emulation hooks */ + + hr = platform_hook_init( + &apm3_hook_cfg.platform, + "SDEM", + "ACA1", + apm3_hook_mod); + + if (FAILED(hr)) { + goto fail; + } + + hr = sg_reader_hook_init(&apm3_hook_cfg.aime, 3, 3, apm3_hook_mod); + + if (FAILED(hr)) { + goto fail; + } + + hr = vfd_hook_init(&apm3_hook_cfg.vfd, 1); + + if (FAILED(hr)) { + goto fail; + } + + hr = apm3_dll_init(&apm3_hook_cfg.dll, apm3_hook_mod); + + if (FAILED(hr)) { + goto fail; + } + + hr = apm3_io4_hook_init(&apm3_hook_cfg.io4); + + if (FAILED(hr)) { + goto fail; + } + + unsigned int led_port_no[2] = {2, 0}; + hr = led15093_hook_init(&apm3_hook_cfg.led15093, + apm3_dll.led_init, apm3_dll.led_set_leds, led_port_no); + + if (FAILED(hr)) { + goto fail; + } + + hr = security_hook_init(); + + if (FAILED(hr)) { + goto fail; + } + + touch_screen_hook_init(&apm3_hook_cfg.touch, apm3_hook_mod); + + mount_hook_init(&apm3_hook_cfg.platform.vfs, &apm3_hook_cfg.mount); + + security_hook_insert_hooks(NULL); + + mount_hook_apply_hooks(NULL); + + unity_hook_init(&apm3_hook_cfg.unity, apm3_hook_mod, unity_hook_callback); + + /* Initialize debug helpers */ + + spike_hook_init(get_config_path()); + + dprintf("--- End %s ---\n", __func__); + + return apm3_startup(); + +fail: + ExitProcess(EXIT_FAILURE); +} + +BOOL WINAPI DllMain(HMODULE mod, DWORD cause, void *ctx) +{ + HRESULT hr; + + if (cause != DLL_PROCESS_ATTACH) { + return TRUE; + } + + wchar_t szFileName[MAX_PATH]; + GetModuleFileNameW(NULL, szFileName, MAX_PATH); + if (wcsstr(szFileName, L"rundll32") != NULL) { + return TRUE; + } + + apm3_hook_mod = mod; + hr = process_hijack_startup(apm3_pre_startup, &apm3_startup); + + if (!SUCCEEDED(hr)) { + dprintf("Failed to hijack process startup: %x\n", (int) hr); + } + + return SUCCEEDED(hr); +} diff --git a/games/apm3hook/io4.c b/games/apm3hook/io4.c new file mode 100644 index 0000000..4d04f97 --- /dev/null +++ b/games/apm3hook/io4.c @@ -0,0 +1,129 @@ +#include + +#include +#include +#include + +#include "board/io4.h" + +#include "apm3hook/apm3-dll.h" + +#include "util/dprintf.h" + +static HRESULT apm3_io4_poll(void *ctx, struct io4_state *state); +static uint16_t coins; + +static const struct io4_ops apm3_io4_ops = { + .poll = apm3_io4_poll, +}; + +HRESULT apm3_io4_hook_init(const struct io4_config *cfg) +{ + HRESULT hr; + + assert(apm3_dll.init != NULL); + + hr = io4_hook_init(cfg, &apm3_io4_ops, NULL); + + if (FAILED(hr)) { + return hr; + } + + return apm3_dll.init(); +} + +static HRESULT apm3_io4_poll(void *ctx, struct io4_state *state) +{ + uint8_t opbtn; + uint32_t gamebtn; + HRESULT hr; + + assert(apm3_dll.poll != NULL); + assert(apm3_dll.get_opbtns != NULL); + assert(apm3_dll.get_gamebtns != NULL); + + memset(state, 0, sizeof(*state)); + + hr = apm3_dll.poll(); + + if (FAILED(hr)) { + return hr; + } + + opbtn = 0; + gamebtn = 0; + + apm3_dll.get_opbtns(&opbtn); + apm3_dll.get_gamebtns(&gamebtn); + + if (opbtn & APM3_IO_OPBTN_TEST) { + state->buttons[0] |= IO4_BUTTON_TEST; + } + + if (opbtn & APM3_IO_OPBTN_SERVICE) { + state->buttons[0] |= IO4_BUTTON_SERVICE; + } + + if (opbtn & APM3_IO_OPBTN_COIN) { + coins++; + } + state->chutes[0] = coins << 8; + + if (gamebtn & APM3_IO_GAMEBTN_HOME) { + state->buttons[1] |= 1 << 12; + } + + if (gamebtn & APM3_IO_GAMEBTN_START) { + state->buttons[0] |= 1 << 7; + } + + if (gamebtn & APM3_IO_GAMEBTN_UP) { + state->buttons[0] |= 1 << 5; + } + + if (gamebtn & APM3_IO_GAMEBTN_RIGHT) { + state->buttons[0] |= 1 << 2; + } + + if (gamebtn & APM3_IO_GAMEBTN_DOWN) { + state->buttons[0] |= 1 << 4; + } + + if (gamebtn & APM3_IO_GAMEBTN_LEFT) { + state->buttons[0] |= 1 << 3; + } + + if (gamebtn & APM3_IO_GAMEBTN_B1) { + state->buttons[0] |= 1 << 1; + } + + if (gamebtn & APM3_IO_GAMEBTN_B2) { + state->buttons[0] |= 1 << 0; + } + + if (gamebtn & APM3_IO_GAMEBTN_B3) { + state->buttons[0] |= 1 << 15; + } + + if (gamebtn & APM3_IO_GAMEBTN_B4) { + state->buttons[0] |= 1 << 14; + } + + if (gamebtn & APM3_IO_GAMEBTN_B5) { + state->buttons[0] |= 1 << 13; + } + + if (gamebtn & APM3_IO_GAMEBTN_B6) { + state->buttons[0] |= 1 << 12; + } + + if (gamebtn & APM3_IO_GAMEBTN_B7) { + state->buttons[0] |= 1 << 11; + } + + if (gamebtn & APM3_IO_GAMEBTN_B8) { + state->buttons[0] |= 1 << 10; + } + + return S_OK; +} diff --git a/games/apm3hook/io4.h b/games/apm3hook/io4.h new file mode 100644 index 0000000..4f52503 --- /dev/null +++ b/games/apm3hook/io4.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include "board/io4.h" + +HRESULT apm3_io4_hook_init(const struct io4_config *cfg); diff --git a/games/apm3hook/meson.build b/games/apm3hook/meson.build new file mode 100644 index 0000000..520b8f2 --- /dev/null +++ b/games/apm3hook/meson.build @@ -0,0 +1,32 @@ +shared_library( + 'apm3hook', + name_prefix : '', + include_directories : inc, + implicit_include_directories : false, + vs_module_defs : 'apm3hook.def', + dependencies : [ + capnhook.get_variable('hook_dep'), + capnhook.get_variable('hooklib_dep'), + ], + link_with : [ + aimeio_lib, + apm3io_lib, + board_lib, + gfxhook_lib, + hooklib_lib, + platform_lib, + unityhook_lib, + util_lib, + ], + sources : [ + 'apm3-dll.c', + 'apm3-dll.h', + 'config.c', + 'config.h', + 'dllmain.c', + 'io4.c', + 'io4.h', + 'mount.c', + 'mount.h', + ], +) diff --git a/games/apm3hook/mount.c b/games/apm3hook/mount.c new file mode 100644 index 0000000..2b894f4 --- /dev/null +++ b/games/apm3hook/mount.c @@ -0,0 +1,174 @@ +#include "board/aime-dll.h" + +#include "config.h" +#include "mount.h" + +#include +#include +#include +#include +#include + +#include "hooklib/path.h" +#include "hook/procaddr.h" +#include "hook/table.h" + +#include "util/dprintf.h" + +/* API hooks */ + +static bool hook_ApmSys_mountVhd(const wchar_t* vhdOriginalPath, const wchar_t* vhdPatchPath, char mountDriveLetter); +static bool hook_ApmSys_mountFscrypt(const wchar_t* fscryptImagePath, const wchar_t* mountFolderPath, const wchar_t* subGameId); +static bool hook_ApmSys_unmountVhd(char mountDriveLetter); +static bool hook_ApmSys_unmountFscrypt(const wchar_t* mountFolderPath); + +static const struct hook_symbol mount_hooks[] = { + { + .name = "ApmSys_mountVhd", + .patch = hook_ApmSys_mountVhd, + .ordinal = 2, + }, + { + .name = "ApmSys_unmountVhd", + .patch = hook_ApmSys_unmountVhd, + .ordinal = 4, + }, + { + .name = "ApmSys_mountFscrypt", + .patch = hook_ApmSys_mountFscrypt, + .ordinal = 1, + }, + { + .name = "ApmSys_unmountFscrypt", + .patch = hook_ApmSys_unmountFscrypt, + .ordinal = 3, + }, +}; + +static struct vfs_config* vcfg; +static struct mount_config* mcfg; + +static wchar_t game_directory[MAX_PATH]; + +void mount_hook_init(struct vfs_config* vfs_cfg, struct mount_config* mount_cfg) { + + assert(vfs_cfg != NULL); + assert(mount_cfg != NULL); + + vcfg = vfs_cfg; + mcfg = mount_cfg; + + if (!mcfg->enable) { + return; + } + + dprintf("Mount: Hook enabled\n"); +} + +void mount_hook_apply_hooks(HMODULE module) { + if (!mcfg->enable) { + return; + } + + proc_addr_table_push(module, "apmmount.dll", mount_hooks, _countof(mount_hooks)); +} + + +bool hook_ApmSys_mountFscrypt(const wchar_t* fscryptImagePath, const wchar_t* mountFolderPath, const wchar_t* subGameId) { + dprintf("Mount: Mount FsCrypt (%ls, %ls, %ls)\n", fscryptImagePath, mountFolderPath, subGameId); + + wcscpy_s(game_directory, MAX_PATH, fscryptImagePath); + PathCchRemoveFileSpec(game_directory, MAX_PATH); + wnsprintfW(game_directory, MAX_PATH, L"%s\\App", game_directory); + + wchar_t *trans; + BOOL ok; + + ok = path_transform_w(&trans, game_directory); + + if (!ok) { + dprintf("Mount: Path transformation error\n"); + return 1; + } + + if (trans != NULL) { + wcscpy_s(game_directory, MAX_PATH, trans); + } + + free(trans); + + dprintf("Mount: Target Path: %ls\n", game_directory); + + return 0; +} + +bool hook_ApmSys_mountVhd(const wchar_t* vhdOriginalPath, const wchar_t* vhdPatchPath, char mountDriveLetter) { + dprintf("Mount: Mount VHD (%ls, %ls, %c)\n", vhdOriginalPath, vhdPatchPath, mountDriveLetter); + + if (game_directory[0] == '\0') { + dprintf("Mount: Target directory is unset!\n"); + return 1; + } + + // Game drive + wchar_t device[3]; + device[0] = mountDriveLetter; + device[1] = ':'; + device[2] = '\0'; + + dprintf("Mount: Mapping %ls to %ls\n", device, game_directory); + if (!DefineDosDeviceW(0, device, game_directory)) { + dprintf("DefineDosDevice failed: %lx\n", GetLastError()); + return 1; + } + + // APM root drive + device[0] = 'X'; + + wchar_t self_directory[MAX_PATH]; + GetCurrentDirectoryW(MAX_PATH, self_directory); + + dprintf("Mount: Mapping %ls to %ls\n", device, self_directory); + if (!DefineDosDeviceW(0, device, self_directory)) { + dprintf("DefineDosDevice failed: %lx\n", GetLastError()); + return 1; + } + + if (mcfg->delay) { + Sleep(1500); + } + return 0; +} +bool hook_ApmSys_unmountVhd(char mountDriveLetter) { + dprintf("Mount: Unmount VHD (%c)\n", mountDriveLetter); + + char device[3]; + device[0] = mountDriveLetter; + device[1] = ':'; + device[2] = '\0'; + + // Game drive + if (!DefineDosDevice(DDD_REMOVE_DEFINITION, device, NULL)) { + dprintf("DefineDosDevice failed: %lx\n", GetLastError()); + return 1; + } + + // APM root drive + device[0] = 'X'; + if (!DefineDosDevice(DDD_REMOVE_DEFINITION, device, NULL)) { + dprintf("DefineDosDevice failed: %lx\n", GetLastError()); + return 1; + } + return 0; +} + +void CALLBACK UnmountApmDrives(HWND hwnd, HINSTANCE hinst, LPSTR lpszCmdLine, int nCmdShow) { + hook_ApmSys_unmountVhd('W'); + hook_ApmSys_unmountVhd('X'); +} + +bool hook_ApmSys_unmountFscrypt(const wchar_t* mountFolderPath) { + dprintf("Mount: Unmount FsCrypt (%ls)\n", mountFolderPath); + memset(game_directory, 0, MAX_PATH * sizeof(wchar_t)); + return 0; +} \ No newline at end of file diff --git a/games/apm3hook/mount.h b/games/apm3hook/mount.h new file mode 100644 index 0000000..6ab2827 --- /dev/null +++ b/games/apm3hook/mount.h @@ -0,0 +1,7 @@ +#pragma once + +#include "board/aime-dll.h" +#include "config.h" + +void mount_hook_apply_hooks(HMODULE module); +void mount_hook_init(struct vfs_config* vfs_cfg, struct mount_config* mount_cfg); diff --git a/games/apm3io/apm3io.c b/games/apm3io/apm3io.c new file mode 100644 index 0000000..bfa4ed9 --- /dev/null +++ b/games/apm3io/apm3io.c @@ -0,0 +1,128 @@ +#include +#include +#include + +#include +#include + +#include "apm3io/apm3io.h" +#include "apm3io/config.h" +#include "util/dprintf.h" +#include "util/env.h" + +static uint8_t apm3_opbtn; +static uint32_t apm3_gamebtn; +static int16_t apm3_stick_x; +static int16_t apm3_stick_y; +static struct apm3_io_config apm3_io_cfg; +static bool apm3_io_coin; + +uint16_t apm3_io_get_api_version(void) { + return 0x0100; +} + +HRESULT apm3_io_init(void) { + apm3_io_config_load(&apm3_io_cfg, get_config_path()); + + return S_OK; +} + +HRESULT apm3_io_poll(void) { + apm3_opbtn = 0; + apm3_gamebtn = 0; + + if (GetAsyncKeyState(apm3_io_cfg.vk_test) & 0x8000) { + apm3_opbtn |= APM3_IO_OPBTN_TEST; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_service) & 0x8000) { + apm3_opbtn |= APM3_IO_OPBTN_SERVICE; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_coin) & 0x8000) { + if (!apm3_io_coin) { + apm3_io_coin = true; + apm3_opbtn |= APM3_IO_OPBTN_COIN; + } + } else { + apm3_io_coin = false; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_home) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_HOME; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_start) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_START; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_up) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_UP; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_right) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_RIGHT; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_down) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_DOWN; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_left) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_LEFT; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[0]) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_B1; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[1]) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_B2; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[2]) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_B3; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[3]) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_B4; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[4]) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_B5; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[5]) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_B6; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[6]) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_B7; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[7]) & 0x8000) { + apm3_gamebtn |= APM3_IO_GAMEBTN_B8; + } + + return S_OK; +} + +void apm3_io_get_opbtns(uint8_t* opbtn) { + if (opbtn != NULL) { + *opbtn = apm3_opbtn; + } +} + +void apm3_io_get_gamebtns(uint32_t* btn) { + if (btn != NULL) { + *btn = apm3_gamebtn; + } +} + +HRESULT apm3_io_led_init(void) { + return S_OK; +} + +void apm3_io_led_set_colors(uint8_t board, uint8_t* rgb) { + return; +} diff --git a/games/apm3io/apm3io.h b/games/apm3io/apm3io.h new file mode 100644 index 0000000..5970335 --- /dev/null +++ b/games/apm3io/apm3io.h @@ -0,0 +1,84 @@ +#pragma once + +#include + +#include + +enum { + APM3_IO_OPBTN_TEST = 0x01, + APM3_IO_OPBTN_SERVICE = 0x02, + APM3_IO_OPBTN_COIN = 0x04, +}; + +enum { + APM3_IO_GAMEBTN_HOME = 0x01, + APM3_IO_GAMEBTN_START = 0x02, + APM3_IO_GAMEBTN_UP = 0x04, + APM3_IO_GAMEBTN_RIGHT = 0x08, + APM3_IO_GAMEBTN_DOWN = 0x10, + APM3_IO_GAMEBTN_LEFT = 0x20, + APM3_IO_GAMEBTN_B1 = 0x40, + APM3_IO_GAMEBTN_B2 = 0x80, + APM3_IO_GAMEBTN_B3 = 0x100, + APM3_IO_GAMEBTN_B4 = 0x200, + APM3_IO_GAMEBTN_B5 = 0x400, + APM3_IO_GAMEBTN_B6 = 0x800, + APM3_IO_GAMEBTN_B7 = 0x1000, + APM3_IO_GAMEBTN_B8 = 0x2000, +}; + +/* Get the version of the APMv3 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 apm3_io_get_api_version(void); + +/* Initialize the IO DLL. This is the second function that will be called on + your DLL, after apm3_io_get_api_version. + + All subsequent calls to this API may originate from arbitrary threads. + + Minimum API version: 0x0100 */ + +HRESULT apm3_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 apm3_io_poll(void); + +/* Get the state of the cabinet's operator buttons as of the last poll. See + APM3_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 apm3_io_get_opbtns(uint8_t *opbtn); + +/* Get the state of the cabinet's gameplay buttons as of the last poll. See + APM3_IO_GAMEBTN enum above: this contains bit mask definitions for button + states returned in *gamebtn. All buttons are active-high. + + Minimum API version: 0x0100 */ + +void apm3_io_get_gamebtns(uint32_t *gamebtn); + +/* Initialize LED emulation. This function will be called before any + other apm3_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 apm3_io_led_init(void); + +/* Update the RGB LEDs. + + Exact layout is TBD. */ + +void apm3_io_led_set_colors(uint8_t board, uint8_t *rgb); diff --git a/games/apm3io/config.c b/games/apm3io/config.c new file mode 100644 index 0000000..37c5b20 --- /dev/null +++ b/games/apm3io/config.c @@ -0,0 +1,36 @@ +#include + +#include +#include +#include + +#include "apm3io/config.h" + +const int BUTTON_DEFAULTS[] = {'Q','W','E','R','A','S','D','F'}; + +void apm3_io_config_load( + struct apm3_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->vk_start = GetPrivateProfileIntW(L"io4", L"start", 'P', filename); + cfg->vk_home = GetPrivateProfileIntW(L"io4", L"home", 'O', filename); + + cfg->vk_up = GetPrivateProfileIntW(L"io4", L"up", VK_UP, filename); + cfg->vk_right = GetPrivateProfileIntW(L"io4", L"right", VK_RIGHT, filename); + cfg->vk_down = GetPrivateProfileIntW(L"io4", L"down", VK_DOWN, filename); + cfg->vk_left = GetPrivateProfileIntW(L"io4", L"left", VK_LEFT, filename); + + wchar_t tmp[16]; + for (int i = 0; i < APM3_BUTTON_COUNT; i++) { + swprintf_s(tmp, 32, L"button%d", i + 1); + cfg->vk_buttons[i] = GetPrivateProfileIntW(L"io4", tmp, BUTTON_DEFAULTS[i], filename); + } + +} diff --git a/games/apm3io/config.h b/games/apm3io/config.h new file mode 100644 index 0000000..cc51050 --- /dev/null +++ b/games/apm3io/config.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include + +#define APM3_BUTTON_COUNT 8 + +struct apm3_io_config { + uint8_t vk_test; + uint8_t vk_service; + uint8_t vk_coin; + + uint8_t vk_start; + uint8_t vk_home; + + uint8_t vk_up; + uint8_t vk_right; + uint8_t vk_down; + uint8_t vk_left; + + uint8_t vk_buttons[APM3_BUTTON_COUNT]; +}; + +void apm3_io_config_load( + struct apm3_io_config *cfg, + const wchar_t *filename); diff --git a/games/apm3io/meson.build b/games/apm3io/meson.build new file mode 100644 index 0000000..e742183 --- /dev/null +++ b/games/apm3io/meson.build @@ -0,0 +1,15 @@ +apm3io_lib = static_library( + 'apm3io', + name_prefix : '', + include_directories : inc, + implicit_include_directories : false, + dependencies : [ + xinput_lib, + ], + sources : [ + 'apm3io.c', + 'apm3io.h', + 'config.c', + 'config.h', + ], +) diff --git a/meson.build b/meson.build index b21d194..76e9577 100644 --- a/meson.build +++ b/meson.build @@ -93,6 +93,7 @@ xinput_lib = cc.find_library('xinput') pathcch_lib = cc.find_library('pathcch') imagehlp_lib = cc.find_library('imagehlp') ws2_32_lib = cc.find_library('ws2_32') +bcrypt_lib = cc.find_library('bcrypt') inc = include_directories('common', 'games') capnhook = subproject('capnhook') @@ -123,6 +124,7 @@ subdir('games/cxbio') subdir('games/tokyoio') subdir('games/fgoio') subdir('games/kemonoio') +subdir('games/apm3io') subdir('games/chunihook') subdir('games/divahook') @@ -139,3 +141,4 @@ subdir('games/cxbhook') subdir('games/tokyohook') subdir('games/fgohook') subdir('games/kemonohook') +subdir('games/apm3hook') From e974a76fe679ebdbe0f170ee7832154b54b8e2e4 Mon Sep 17 00:00:00 2001 From: Dniel97 Date: Sun, 20 Jul 2025 18:11:34 +0200 Subject: [PATCH 2/2] [apm3] add DInput and XInput support --- README.md | 4 +- dist/apm3/segatools.ini | 97 +++++++++++++-- games/apm3hook/config.c | 2 +- games/apm3hook/dllmain.c | 9 +- games/apm3io/apm3io.c | 128 ------------------- games/apm3io/apm3io.h | 3 +- games/apm3io/backend.h | 10 ++ games/apm3io/config.c | 92 +++++++++++--- games/apm3io/config.h | 38 ++++-- games/apm3io/di-dev.c | 83 +++++++++++++ games/apm3io/di-dev.h | 18 +++ games/apm3io/di.c | 257 +++++++++++++++++++++++++++++++++++++++ games/apm3io/di.h | 7 ++ games/apm3io/dllmain.c | 105 ++++++++++++++++ games/apm3io/kb.c | 184 ++++++++++++++++++++++++++++ games/apm3io/kb.h | 9 ++ games/apm3io/meson.build | 18 ++- games/apm3io/wnd.c | 86 +++++++++++++ games/apm3io/wnd.h | 5 + games/apm3io/xi.c | 167 +++++++++++++++++++++++++ games/apm3io/xi.h | 11 ++ 21 files changed, 1157 insertions(+), 176 deletions(-) delete mode 100644 games/apm3io/apm3io.c create mode 100644 games/apm3io/backend.h create mode 100644 games/apm3io/di-dev.c create mode 100644 games/apm3io/di-dev.h create mode 100644 games/apm3io/di.c create mode 100644 games/apm3io/di.h create mode 100644 games/apm3io/dllmain.c create mode 100644 games/apm3io/kb.c create mode 100644 games/apm3io/kb.h create mode 100644 games/apm3io/wnd.c create mode 100644 games/apm3io/wnd.h create mode 100644 games/apm3io/xi.c create mode 100644 games/apm3io/xi.h diff --git a/README.md b/README.md index 6f8211c..532e16a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Segatools -Version: `2024-09-30` +Version: `2025-07-20` Loaders and hardware emulators for SEGA games that run on the Nu and ALLS platforms. @@ -32,6 +32,8 @@ Loaders and hardware emulators for SEGA games that run on the Nu and ALLS platfo * starting from WACCA * Kemono Friends * Kemono Friends 3: Planet Tours +* ALL.Net P-ras MULTI Version 3 + * starting from ALL.Net P-ras MULTI Version 3 1.01 ## End-users diff --git a/dist/apm3/segatools.ini b/dist/apm3/segatools.ini index 98d2c66..258eedc 100644 --- a/dist/apm3/segatools.ini +++ b/dist/apm3/segatools.ini @@ -10,7 +10,7 @@ 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=appdata +appdata= ; ----------------------------------------------------------------------------- ; Device settings @@ -22,6 +22,12 @@ appdata=appdata 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. @@ -57,11 +63,20 @@ addrSuffix=11 ; ----------------------------------------------------------------------------- [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 +[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 @@ -108,8 +123,8 @@ enable=0 ; Leave empty if you want to use Segatools built-in keyboard input. path= -[apmio] -; To use a custom APM IO DLL enter its path here. +[apm3io] +; To use a custom APM3 IO DLL enter its path here. ; Leave empty if you want to use Segatools built-in gamepad input. path= @@ -126,13 +141,45 @@ path= ; world. An improved solution will be provided later. [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 +; 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 + +; Button mappings for the 8 vewlix arcade stick buttons. +; +; :=**#*+-. :+#@@@@#*- -*#@@@@#+: +; =%@@@@@@@@@#: -%@@@@@@@@@@%- -%@@@@@@@@@@%- +; #@@@@@@@@@@@@@+ +@@@@@@@@@@@@@@+ +@@@@@@@@@@@@@@= +; :::: *@@@@@ @@@@@- .@@@@@ @@@@@@. .@@@@@ @@@@@@ +; :+%@@@@@@%*: %@@@@@ [2] @@@@@+ :@@@@@ [3] @@@@@@: :@@@@@ [4] @@@@@@ +; #@@@@@@@@@@@@# +@@@@@ @@@@@: #@@@@ @@@@@# #@@@@ @@@@@* +; #@@@@ @@@@@# *@@@@@@@@@@@@@= *@@@@@@@@@@@@* *@@@@@@@@@@@@* +; @@@@@ [1] @@@@@@: -#@@@@@@@@@*. .+%@@@@@@%+. :*%@@@@@@%+. +; @@@@@ @@@@@@. :=++=-: .::. .::. +; =@@@@@@@@@@@@@@+ +; -%@@@@@@@@@@%- =*%@@@%#=. :+#%@@@%*- .=#@@@@@%*- +; :+#%@@%#+- *@@@@@@@@@@@#. :#@@@@@@@@@@@+ +@@@@@@@@@@@%- +; %@@@@@@@@@@@@@@. -@@@@@@@@@@@@@@% #@@@@@@@@@@@@@@- +; .:::. +@@@@@ @@@@@# %@@@@@ @@@@@- -@@@@@@ @@@@% +; =#@@@@@@@#- +@@@@@ [6] @@@@@# @@@@@@ [7] @@@@@= -@@@@@@ [8] @@@@@ +; =@@@@@@@@@@@@%. .@@@@@ @@@@@- =@@@@@ @@@@@. %@@@@@ @@@@= +; +@@@@@ @@@@@. :%@@@@@@@@@@@@- =@@@@@@@@@@@@#. *@@@@@@@@@@@@= +; @@@@@@ [5] @@@@@+ -*@@@@@@@#= =#@@@@@@%*: :+#@@@@@@*= +; %@@@@@ @@@@@= .:. ..:. .. +; :@@@@@@@@@@@@@@# +; .#@@@@@@@@@@@+ +; .::. + + +[keyboard] ; START button (default: P) start=0x50 ; HOME button (default: O) @@ -144,10 +191,6 @@ right=0x27 down=0x28 left=0x25 -; Main buttons -; The arrangement is as following: -; 1 2 3 4 -; 5 6 7 8 ; Defaults: ; Q W E R ; A S D F @@ -159,3 +202,31 @@ button5=0x41 button6=0x53 button7=0x44 button8=0x46 + +[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 + +button1=1 +button2=4 +button3=6 +button4=5 +button5=2 +button6=3 +button7=8 +button8=7 diff --git a/games/apm3hook/config.c b/games/apm3hook/config.c index af6bd89..06ab9ff 100644 --- a/games/apm3hook/config.c +++ b/games/apm3hook/config.c @@ -19,7 +19,7 @@ void apm3_dll_config_load( assert(filename != NULL); GetPrivateProfileStringW( - L"apmio", + L"apm3io", L"path", L"", cfg->path, diff --git a/games/apm3hook/dllmain.c b/games/apm3hook/dllmain.c index 4c232e8..203bd62 100644 --- a/games/apm3hook/dllmain.c +++ b/games/apm3hook/dllmain.c @@ -1,5 +1,5 @@ /* -"ALL.Net P-ras multi Ver.3" (apm) hook + "ALL.Net P-ras MULTI Version 3" (apm3) hook Devices @@ -17,17 +17,16 @@ #include "io4.h" #include "mount.h" -#include "amex/ds.h" - #include "hook/process.h" -#include "hooklib/serial.h" +#include "hooklib/serial.h" #include "hooklib/spike.h" #include "platform/clock.h" #include "platform/config.h" #include "platform/nusec.h" #include "platform/security.h" + #include "unityhook/hook.h" #include "util/dprintf.h" @@ -43,7 +42,7 @@ void unity_hook_callback(HMODULE hmodule, const wchar_t* p) { serial_hook_apply_hooks(hmodule); security_hook_insert_hooks(hmodule); touch_hook_insert_hooks(hmodule); - //mount_hook_apply_hooks(&apm3_hook_cfg.mount, hmodule); + // mount_hook_apply_hooks(&apm3_hook_cfg.mount, hmodule); } void apm3_extra_hooks_init(void) { diff --git a/games/apm3io/apm3io.c b/games/apm3io/apm3io.c deleted file mode 100644 index bfa4ed9..0000000 --- a/games/apm3io/apm3io.c +++ /dev/null @@ -1,128 +0,0 @@ -#include -#include -#include - -#include -#include - -#include "apm3io/apm3io.h" -#include "apm3io/config.h" -#include "util/dprintf.h" -#include "util/env.h" - -static uint8_t apm3_opbtn; -static uint32_t apm3_gamebtn; -static int16_t apm3_stick_x; -static int16_t apm3_stick_y; -static struct apm3_io_config apm3_io_cfg; -static bool apm3_io_coin; - -uint16_t apm3_io_get_api_version(void) { - return 0x0100; -} - -HRESULT apm3_io_init(void) { - apm3_io_config_load(&apm3_io_cfg, get_config_path()); - - return S_OK; -} - -HRESULT apm3_io_poll(void) { - apm3_opbtn = 0; - apm3_gamebtn = 0; - - if (GetAsyncKeyState(apm3_io_cfg.vk_test) & 0x8000) { - apm3_opbtn |= APM3_IO_OPBTN_TEST; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_service) & 0x8000) { - apm3_opbtn |= APM3_IO_OPBTN_SERVICE; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_coin) & 0x8000) { - if (!apm3_io_coin) { - apm3_io_coin = true; - apm3_opbtn |= APM3_IO_OPBTN_COIN; - } - } else { - apm3_io_coin = false; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_home) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_HOME; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_start) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_START; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_up) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_UP; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_right) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_RIGHT; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_down) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_DOWN; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_left) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_LEFT; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[0]) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_B1; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[1]) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_B2; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[2]) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_B3; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[3]) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_B4; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[4]) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_B5; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[5]) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_B6; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[6]) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_B7; - } - - if (GetAsyncKeyState(apm3_io_cfg.vk_buttons[7]) & 0x8000) { - apm3_gamebtn |= APM3_IO_GAMEBTN_B8; - } - - return S_OK; -} - -void apm3_io_get_opbtns(uint8_t* opbtn) { - if (opbtn != NULL) { - *opbtn = apm3_opbtn; - } -} - -void apm3_io_get_gamebtns(uint32_t* btn) { - if (btn != NULL) { - *btn = apm3_gamebtn; - } -} - -HRESULT apm3_io_led_init(void) { - return S_OK; -} - -void apm3_io_led_set_colors(uint8_t board, uint8_t* rgb) { - return; -} diff --git a/games/apm3io/apm3io.h b/games/apm3io/apm3io.h index 5970335..67d14df 100644 --- a/games/apm3io/apm3io.h +++ b/games/apm3io/apm3io.h @@ -17,6 +17,7 @@ enum { APM3_IO_GAMEBTN_RIGHT = 0x08, APM3_IO_GAMEBTN_DOWN = 0x10, APM3_IO_GAMEBTN_LEFT = 0x20, + APM3_IO_GAMEBTN_B1 = 0x40, APM3_IO_GAMEBTN_B2 = 0x80, APM3_IO_GAMEBTN_B3 = 0x100, @@ -66,7 +67,7 @@ void apm3_io_get_opbtns(uint8_t *opbtn); Minimum API version: 0x0100 */ -void apm3_io_get_gamebtns(uint32_t *gamebtn); +void apm3_io_get_gamebtns(uint16_t *gamebtn); /* Initialize LED emulation. This function will be called before any other apm3_io_led_*() function calls. diff --git a/games/apm3io/backend.h b/games/apm3io/backend.h new file mode 100644 index 0000000..748445c --- /dev/null +++ b/games/apm3io/backend.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +#include "apm3io/apm3io.h" + +struct apm3_io_backend { + void (*get_opbtns)(uint8_t *opbtn); + void (*get_gamebtns)(uint16_t *gamebtn); +}; diff --git a/games/apm3io/config.c b/games/apm3io/config.c index 37c5b20..c487dce 100644 --- a/games/apm3io/config.c +++ b/games/apm3io/config.c @@ -3,11 +3,72 @@ #include #include #include +#include #include "apm3io/config.h" const int BUTTON_DEFAULTS[] = {'Q','W','E','R','A','S','D','F'}; +void apm3_kb_config_load( + struct apm3_kb_config *cfg, + const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + cfg->vk_start = GetPrivateProfileIntW(L"keyboard", L"start", 'P', filename); + cfg->vk_home = GetPrivateProfileIntW(L"keyboard", L"home", 'O', filename); + + cfg->vk_up = GetPrivateProfileIntW(L"keyboard", L"up", VK_UP, filename); + cfg->vk_right = GetPrivateProfileIntW(L"keyboard", L"right", VK_RIGHT, filename); + cfg->vk_down = GetPrivateProfileIntW(L"keyboard", L"down", VK_DOWN, filename); + cfg->vk_left = GetPrivateProfileIntW(L"keyboard", L"left", VK_LEFT, filename); + + wchar_t tmp[16]; + for (int i = 0; i < APM3_BUTTON_COUNT; i++) { + swprintf_s(tmp, 32, L"button%d", i + 1); + cfg->vk_buttons[i] = GetPrivateProfileIntW(L"keyboard", tmp, BUTTON_DEFAULTS[i], filename); + } +} + +void apm3_di_config_load(struct apm3_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); + + cfg->home = GetPrivateProfileIntW(L"dinput", L"home", 0, filename); + cfg->start = GetPrivateProfileIntW(L"dinput", L"start", 0, filename); + + for (i = 0 ; i < 8 ; i++) { + swprintf_s(key, _countof(key), L"button%i", i + 1); + cfg->button[i] = GetPrivateProfileIntW(L"dinput", key, i + 1, filename); + } + +} + +void apm3_xi_config_load(struct apm3_xi_config *cfg, const wchar_t *filename) +{ + assert(cfg != NULL); + assert(filename != NULL); + + cfg->analog_stick_enabled = GetPrivateProfileIntW( + L"xinput", + L"analogStickEnabled", + 1, + filename); +} + void apm3_io_config_load( struct apm3_io_config *cfg, const wchar_t *filename) @@ -15,22 +76,19 @@ void apm3_io_config_load( 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->vk_start = GetPrivateProfileIntW(L"io4", L"start", 'P', filename); - cfg->vk_home = GetPrivateProfileIntW(L"io4", L"home", 'O', filename); - - cfg->vk_up = GetPrivateProfileIntW(L"io4", L"up", VK_UP, filename); - cfg->vk_right = GetPrivateProfileIntW(L"io4", L"right", VK_RIGHT, filename); - cfg->vk_down = GetPrivateProfileIntW(L"io4", L"down", VK_DOWN, filename); - cfg->vk_left = GetPrivateProfileIntW(L"io4", L"left", VK_LEFT, filename); - - wchar_t tmp[16]; - for (int i = 0; i < APM3_BUTTON_COUNT; i++) { - swprintf_s(tmp, 32, L"button%d", i + 1); - cfg->vk_buttons[i] = GetPrivateProfileIntW(L"io4", tmp, BUTTON_DEFAULTS[i], filename); - } + cfg->vk_test = GetPrivateProfileIntW(L"io4", L"test", VK_F1, filename); + cfg->vk_service = GetPrivateProfileIntW(L"io4", L"service", VK_F2, filename); + cfg->vk_coin = GetPrivateProfileIntW(L"io4", L"coin", VK_F3, filename); + GetPrivateProfileStringW( + L"io4", + L"mode", + L"", + cfg->mode, + _countof(cfg->mode), + filename); + + apm3_kb_config_load(&cfg->kb, filename); + apm3_di_config_load(&cfg->di, filename); + apm3_xi_config_load(&cfg->xi, filename); } diff --git a/games/apm3io/config.h b/games/apm3io/config.h index cc51050..72b3a13 100644 --- a/games/apm3io/config.h +++ b/games/apm3io/config.h @@ -1,17 +1,23 @@ #pragma once +#include #include #include -#include - #define APM3_BUTTON_COUNT 8 -struct apm3_io_config { - uint8_t vk_test; - uint8_t vk_service; - uint8_t vk_coin; +struct apm3_di_config { + wchar_t device_name[64]; + uint8_t start; + uint8_t home; + uint8_t button[8]; +}; +struct apm3_xi_config { + bool analog_stick_enabled; +}; + +struct apm3_kb_config { uint8_t vk_start; uint8_t vk_home; @@ -23,6 +29,20 @@ struct apm3_io_config { uint8_t vk_buttons[APM3_BUTTON_COUNT]; }; -void apm3_io_config_load( - struct apm3_io_config *cfg, - const wchar_t *filename); +struct apm3_io_config { + uint8_t vk_test; + uint8_t vk_service; + uint8_t vk_coin; + + wchar_t mode[9]; + + struct apm3_kb_config kb; + struct apm3_di_config di; + struct apm3_xi_config xi; +}; + +void apm3_io_config_load(struct apm3_io_config *cfg, const wchar_t *filename); + +void apm3_kb_config_load(struct apm3_kb_config *cfg, const wchar_t *filename); +void apm3_di_config_load(struct apm3_di_config *cfg, const wchar_t *filename); +void apm3_xi_config_load(struct apm3_xi_config *cfg, const wchar_t *filename); diff --git a/games/apm3io/di-dev.c b/games/apm3io/di-dev.c new file mode 100644 index 0000000..ed16ddc --- /dev/null +++ b/games/apm3io/di-dev.c @@ -0,0 +1,83 @@ +#include +#include + +#include + +#include "apm3io/di-dev.h" + +#include "util/dprintf.h" + +HRESULT apm3_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; +} + + +HRESULT apm3_di_dev_poll( + IDirectInputDevice8W *dev, + HWND wnd, + union apm3_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/games/apm3io/di-dev.h b/games/apm3io/di-dev.h new file mode 100644 index 0000000..1737c93 --- /dev/null +++ b/games/apm3io/di-dev.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +#include + +union apm3_di_state { + DIJOYSTATE st; + uint8_t bytes[sizeof(DIJOYSTATE)]; +}; + +HRESULT apm3_di_dev_start(IDirectInputDevice8W *dev, HWND wnd); +HRESULT apm3_di_dev_poll( + IDirectInputDevice8W *dev, + HWND wnd, + union apm3_di_state *out); + diff --git a/games/apm3io/di.c b/games/apm3io/di.c new file mode 100644 index 0000000..9b6005f --- /dev/null +++ b/games/apm3io/di.c @@ -0,0 +1,257 @@ +#include +#include + +#include +#include +#include +#include + +#include "apm3io/backend.h" +#include "apm3io/config.h" +#include "apm3io/di.h" +#include "apm3io/di-dev.h" +#include "apm3io/apm3io.h" +#include "apm3io/wnd.h" + +#include "util/dprintf.h" +#include "util/str.h" + +struct apm3_di_axis { + wchar_t name[4]; + size_t off; +}; + +static HRESULT apm3_di_config_apply(const struct apm3_di_config *cfg); +static BOOL CALLBACK apm3_di_enum_callback( + const DIDEVICEINSTANCEW *dev, + void *ctx); +static BOOL CALLBACK apm3_di_enum_callback_shifter( + const DIDEVICEINSTANCEW *dev, + void *ctx); +static void apm3_di_get_gamebtns(uint16_t *gamebtn_out); +static uint8_t apm3_di_decode_pov(DWORD pov); + + +static const struct apm3_io_backend apm3_di_backend = { + .get_gamebtns = apm3_di_get_gamebtns, +}; + +static HWND apm3_di_wnd; +static IDirectInput8W *apm3_di_api; +static IDirectInputDevice8W *apm3_di_dev; +static IDirectInputEffect *apm3_di_fx; +static uint8_t apm3_di_home; +static uint8_t apm3_di_start; +static uint8_t apm3_di_button[8]; + +HRESULT apm3_di_init( + const struct apm3_di_config *cfg, + HINSTANCE inst, + const struct apm3_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 = apm3_di_config_apply(cfg); + + if (FAILED(hr)) { + return hr; + } + + hr = apm3_io_wnd_create(inst, &apm3_di_wnd); + + if (FAILED(hr)) { + return hr; + } + + hr = IDirectInput8_EnumDevices( + apm3_di_api, + DI8DEVCLASS_GAMECTRL, + apm3_di_enum_callback, + (void *) cfg, + DIEDFL_ATTACHEDONLY); + + if (FAILED(hr)) { + dprintf("DirectInput: EnumDevices failed: %08x\n", (int) hr); + + return hr; + } + + if (apm3_di_dev == NULL) { + dprintf("Stick: Controller not found\n"); + + return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); + } + + hr = apm3_di_dev_start(apm3_di_dev, apm3_di_wnd); + + if (FAILED(hr)) { + return hr; + } + + dprintf("DirectInput: Controller initialized\n"); + + *backend = &apm3_di_backend; + + return S_OK; +} + +static HRESULT apm3_di_config_apply(const struct apm3_di_config *cfg) +{ + int i; + + if (cfg->start > 32) { + dprintf("Stick: Invalid start button: %i\n", cfg->start); + + return E_INVALIDARG; + } + + if (cfg->home > 32) { + dprintf("Stick: Invalid home button: %i\n", cfg->home); + + return E_INVALIDARG; + } + + /* Check all 8 defined buttons */ + for (i = 0; i < 8; i++) { + if (cfg->button[i] > 32) { + dprintf("Stick: Invalid button %i: %i\n", i, cfg->button[i]); + + return E_INVALIDARG; + } + } + + /* Print some debug output to make sure config works... */ + + dprintf("Stick: --- Begin configuration ---\n"); + dprintf("Stick: Device name . . . . : Contains \"%S\"\n", + cfg->device_name); + dprintf("Stick: Home button . . . : %i\n", cfg->home); + dprintf("Stick: Start button . . . : %i\n", cfg->start); + + /* Print the configuration for all 8 buttons */ + for (i = 0; i < 8; i++) { + dprintf("Stick: Button %i . . . . . : %i\n", i, cfg->button[i]); + } + + dprintf("Stick: --- End configuration ---\n"); + + apm3_di_start = cfg->start; + apm3_di_home = cfg->home; + + for (i = 0; i < 8; i++) { + apm3_di_button[i] = cfg->button[i]; + } + + return S_OK; +} + +static BOOL CALLBACK apm3_di_enum_callback( + const DIDEVICEINSTANCEW *dev, + void *ctx) +{ + const struct apm3_di_config *cfg; + HRESULT hr; + + cfg = ctx; + + if (wcsstr(dev->tszProductName, cfg->device_name) == NULL) { + return DIENUM_CONTINUE; + } + + dprintf("Stick: Using DirectInput device \"%S\"\n", dev->tszProductName); + + hr = IDirectInput8_CreateDevice( + apm3_di_api, + &dev->guidInstance, + &apm3_di_dev, + NULL); + + if (FAILED(hr)) { + dprintf("Stick: CreateDevice failed: %08x\n", (int) hr); + } + + return DIENUM_STOP; +} + +static void apm3_di_get_gamebtns(uint16_t *gamebtn_out) +{ + union apm3_di_state state; + uint16_t gamebtn; + HRESULT hr; + + assert(gamebtn_out != NULL); + + hr = apm3_di_dev_poll(apm3_di_dev, apm3_di_wnd, &state); + + if (FAILED(hr)) { + return; + } + + gamebtn = apm3_di_decode_pov(state.st.rgdwPOV[0]); + + if (apm3_di_start && state.st.rgbButtons[apm3_di_start - 1]) { + gamebtn |= APM3_IO_GAMEBTN_START; + } + + if (apm3_di_home && state.st.rgbButtons[apm3_di_home - 1]) { + gamebtn |= APM3_IO_GAMEBTN_HOME; + } + + if (apm3_di_button[0] && state.st.rgbButtons[apm3_di_button[0] - 1]) { + gamebtn |= APM3_IO_GAMEBTN_B1; + } + + if (apm3_di_button[1] && state.st.rgbButtons[apm3_di_button[1] - 1]) { + gamebtn |= APM3_IO_GAMEBTN_B2; + } + + if (apm3_di_button[2] && state.st.rgbButtons[apm3_di_button[2] - 1]) { + gamebtn |= APM3_IO_GAMEBTN_B3; + } + + if (apm3_di_button[3] && state.st.rgbButtons[apm3_di_button[3] - 1]) { + gamebtn |= APM3_IO_GAMEBTN_B4; + } + + if (apm3_di_button[4] && state.st.rgbButtons[apm3_di_button[4] - 1]) { + gamebtn |= APM3_IO_GAMEBTN_B5; + } + + if (apm3_di_button[5] && state.st.rgbButtons[apm3_di_button[5] - 1]) { + gamebtn |= APM3_IO_GAMEBTN_B6; + } + + if (apm3_di_button[6] && state.st.rgbButtons[apm3_di_button[6] - 1]) { + gamebtn |= APM3_IO_GAMEBTN_B7; + } + + if (apm3_di_button[7] && state.st.rgbButtons[apm3_di_button[7] - 1]) { + gamebtn |= APM3_IO_GAMEBTN_B8; + } + + *gamebtn_out = gamebtn; +} + +static uint8_t apm3_di_decode_pov(DWORD pov) +{ + switch (pov) { + case 0: return APM3_IO_GAMEBTN_UP; + case 4500: return APM3_IO_GAMEBTN_UP | APM3_IO_GAMEBTN_RIGHT; + case 9000: return APM3_IO_GAMEBTN_RIGHT; + case 13500: return APM3_IO_GAMEBTN_RIGHT | APM3_IO_GAMEBTN_DOWN; + case 18000: return APM3_IO_GAMEBTN_DOWN; + case 22500: return APM3_IO_GAMEBTN_DOWN | APM3_IO_GAMEBTN_LEFT; + case 27000: return APM3_IO_GAMEBTN_LEFT; + case 31500: return APM3_IO_GAMEBTN_LEFT | APM3_IO_GAMEBTN_UP; + default: return 0; + } +} diff --git a/games/apm3io/di.h b/games/apm3io/di.h new file mode 100644 index 0000000..5ad1824 --- /dev/null +++ b/games/apm3io/di.h @@ -0,0 +1,7 @@ +#pragma once + +#include "apm3io/backend.h" +#include "apm3io/config.h" + +HRESULT apm3_di_init(const struct apm3_di_config *cfg, HINSTANCE inst, + const struct apm3_io_backend **backend); diff --git a/games/apm3io/dllmain.c b/games/apm3io/dllmain.c new file mode 100644 index 0000000..ee14dd2 --- /dev/null +++ b/games/apm3io/dllmain.c @@ -0,0 +1,105 @@ +#include + +#include +#include +#include + +#include "apm3io/backend.h" +#include "apm3io/config.h" +#include "apm3io/apm3io.h" +#include "apm3io/kb.h" +#include "apm3io/di.h" +#include "apm3io/xi.h" + +#include "util/dprintf.h" +#include "util/str.h" + +static struct apm3_io_config apm3_io_cfg; +static const struct apm3_io_backend *apm3_io_backend; +static bool apm3_io_coin; + +uint16_t apm3_io_get_api_version(void) +{ + return 0x0100; +} + +HRESULT apm3_io_init(void) +{ + HINSTANCE inst; + HRESULT hr; + + assert(apm3_io_backend == NULL); + + inst = GetModuleHandleW(NULL); + + if (inst == NULL) { + hr = HRESULT_FROM_WIN32(GetLastError()); + dprintf("GetModuleHandleW failed: %lx\n", hr); + + return hr; + } + + apm3_io_config_load(&apm3_io_cfg, L".\\segatools.ini"); + + if (wstr_ieq(apm3_io_cfg.mode, L"keyboard")) { + hr = apm3_kb_init(&apm3_io_cfg.kb, &apm3_io_backend); + } else if (wstr_ieq(apm3_io_cfg.mode, L"dinput")) { + hr = apm3_di_init(&apm3_io_cfg.di, inst, &apm3_io_backend); + } else if (wstr_ieq(apm3_io_cfg.mode, L"xinput")) { + hr = apm3_xi_init(&apm3_io_cfg.xi, &apm3_io_backend); + } else { + hr = E_INVALIDARG; + dprintf("APM3 IO: Invalid IO mode \"%S\", use keyboard, dinput or xinput\n", + apm3_io_cfg.mode); + } + + return hr; +} + +void apm3_io_get_opbtns(uint8_t *opbtn_out) +{ + uint8_t opbtn; + + assert(apm3_io_backend != NULL); + assert(opbtn_out != NULL); + + opbtn = 0; + + if (GetAsyncKeyState(apm3_io_cfg.vk_test) & 0x8000) { + opbtn |= APM3_IO_OPBTN_TEST; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_service) & 0x8000) { + opbtn |= APM3_IO_OPBTN_SERVICE; + } + + if (GetAsyncKeyState(apm3_io_cfg.vk_coin) & 0x8000) { + if (!apm3_io_coin) { + apm3_io_coin = true; + opbtn |= APM3_IO_OPBTN_COIN; + } + } else { + apm3_io_coin = false; + } + + *opbtn_out = opbtn; +} + + +void apm3_io_get_gamebtns(uint16_t *gamebtn_out) +{ + assert(apm3_io_backend != NULL); + assert(gamebtn_out != NULL); + + apm3_io_backend->get_gamebtns(gamebtn_out); +} + +HRESULT apm3_io_led_init(void) +{ + return S_OK; +} + +void apm3_io_led_set_colors(uint8_t board, uint8_t *rgb) +{ + return; +} diff --git a/games/apm3io/kb.c b/games/apm3io/kb.c new file mode 100644 index 0000000..04bc58b --- /dev/null +++ b/games/apm3io/kb.c @@ -0,0 +1,184 @@ +#include +#include +#include + +#include +#include +#include + +#include "apm3io/apm3io.h" +#include "apm3io/config.h" +#include "apm3io/kb.h" + +#include "util/dprintf.h" +#include "util/env.h" + +static uint8_t apm3_kb_home; +static uint8_t apm3_kb_start; +static uint8_t apm3_kb_up; +static uint8_t apm3_kb_right; +static uint8_t apm3_kb_down; +static uint8_t apm3_kb_left; +static uint8_t apm3_kb_button[8]; + +static void apm3_kb_get_gamebtns(uint16_t *gamebtn_out); + +static HRESULT apm3_kb_config_apply(const struct apm3_kb_config *cfg); + +static const struct apm3_io_backend apm3_kb_backend = { + .get_gamebtns = apm3_kb_get_gamebtns, +}; + +HRESULT apm3_kb_init(const struct apm3_kb_config *cfg, + const struct apm3_io_backend **backend) { + HRESULT hr; + + assert(cfg != NULL); + assert(backend != NULL); + + *backend = NULL; + + hr = apm3_kb_config_apply(cfg); + + if (FAILED(hr)) { + return hr; + } + + dprintf("Keyboard: Using keyboard input\n"); + *backend = &apm3_kb_backend; + + return S_OK; +} + +static HRESULT apm3_kb_config_apply(const struct apm3_kb_config *cfg) { + int i; + + if (cfg->vk_start > 255) { + dprintf("Keyboard: Invalid start key configuration: %u\n", cfg->vk_start); + return E_INVALIDARG; + } + + if (cfg->vk_home > 255) { + dprintf("Keyboard: Invalid home key configuration: %u\n", cfg->vk_home); + return E_INVALIDARG; + } + + if (cfg->vk_up > 255) { + dprintf("Keyboard: Invalid up key configuration: %u\n", cfg->vk_up); + return E_INVALIDARG; + } + + if (cfg->vk_right > 255) { + dprintf("Keyboard: Invalid right key configuration: %u\n", cfg->vk_right); + return E_INVALIDARG; + } + + if (cfg->vk_down > 255) { + dprintf("Keyboard: Invalid down key configuration: %u\n", cfg->vk_down); + return E_INVALIDARG; + } + + if (cfg->vk_left > 255) { + dprintf("Keyboard: Invalid left key configuration: %u\n", cfg->vk_left); + return E_INVALIDARG; + } + + for (i = 0; i < APM3_BUTTON_COUNT; i++) { + if (cfg->vk_buttons[i] > 255) { + dprintf("Keyboard: Invalid button %i configuration\n", i); + return E_INVALIDARG; + } + apm3_kb_button[i] = cfg->vk_buttons[i]; + } + + /* Apply the configuration */ + apm3_kb_home = cfg->vk_home; + apm3_kb_start = cfg->vk_start; + apm3_kb_up = cfg->vk_up; + apm3_kb_right = cfg->vk_right; + apm3_kb_down = cfg->vk_down; + apm3_kb_left = cfg->vk_left; + + /* Print some debug output to make sure config works... */ + dprintf("Keyboard: --- Begin configuration ---\n"); + dprintf("Keyboard: Home key . . . . : %u\n", apm3_kb_home); + dprintf("Keyboard: Start key . . . : %u\n", apm3_kb_start); + + dprintf("Keyboard: Down key . . . . : %u\n", cfg->vk_down); + dprintf("Keyboard: Left key . . . . : %u\n", cfg->vk_left); + + /* Print the configuration for all 8 buttons */ + for (i = 0; i < APM3_BUTTON_COUNT; i++) { + dprintf("Keyboard: Button %i . . . . : %u\n", i + 1, apm3_kb_button[i]); + } + + dprintf("Keyboard: --- End configuration ---\n"); + + return S_OK; +} + +static void apm3_kb_get_gamebtns(uint16_t* gamebtn_out) { + uint16_t gamebtn; + + assert(gamebtn_out != NULL); + + gamebtn = 0; + + if (GetAsyncKeyState(apm3_kb_home) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_HOME; + } + + if (GetAsyncKeyState(apm3_kb_start) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_START; + } + + if (GetAsyncKeyState(apm3_kb_up) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_UP; + } + + if (GetAsyncKeyState(apm3_kb_right) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_RIGHT; + } + + if (GetAsyncKeyState(apm3_kb_down) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_DOWN; + } + + if (GetAsyncKeyState(apm3_kb_left) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_LEFT; + } + + if (GetAsyncKeyState(apm3_kb_button[0]) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_B1; + } + + if (GetAsyncKeyState(apm3_kb_button[1]) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_B2; + } + + if (GetAsyncKeyState(apm3_kb_button[2]) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_B3; + } + + if (GetAsyncKeyState(apm3_kb_button[3]) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_B4; + } + + if (GetAsyncKeyState(apm3_kb_button[4]) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_B5; + } + + if (GetAsyncKeyState(apm3_kb_button[5]) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_B6; + } + + if (GetAsyncKeyState(apm3_kb_button[6]) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_B7; + } + + if (GetAsyncKeyState(apm3_kb_button[7]) & 0x8000) { + gamebtn |= APM3_IO_GAMEBTN_B8; + } + + *gamebtn_out = gamebtn; +} diff --git a/games/apm3io/kb.h b/games/apm3io/kb.h new file mode 100644 index 0000000..8965e91 --- /dev/null +++ b/games/apm3io/kb.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +#include "apm3io/backend.h" +#include "apm3io/config.h" + +HRESULT apm3_kb_init(const struct apm3_kb_config *cfg, + const struct apm3_io_backend **backend); diff --git a/games/apm3io/meson.build b/games/apm3io/meson.build index e742183..c6735a3 100644 --- a/games/apm3io/meson.build +++ b/games/apm3io/meson.build @@ -4,12 +4,28 @@ apm3io_lib = static_library( include_directories : inc, implicit_include_directories : false, dependencies : [ + dinput8_lib, + dxguid_lib, xinput_lib, ], + link_with : [ + util_lib, + ], sources : [ - 'apm3io.c', 'apm3io.h', + 'backend.h', 'config.c', 'config.h', + 'di.c', + 'di.h', + 'di-dev.c', + 'di-dev.h', + 'dllmain.c', + 'kb.c', + 'kb.h', + 'wnd.c', + 'wnd.h', + 'xi.c', + 'xi.h', ], ) diff --git a/games/apm3io/wnd.c b/games/apm3io/wnd.c new file mode 100644 index 0000000..05db65c --- /dev/null +++ b/games/apm3io/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 apm3_io_wnd_proc( + HWND hwnd, + UINT msg, + WPARAM wparam, + LPARAM lparam); + +HRESULT apm3_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 = apm3_io_wnd_proc; + wcx.hInstance = inst; + wcx.lpszClassName = L"apm3IO"; + + atom = RegisterClassExW(&wcx); + + if (atom == 0) { + hr = HRESULT_FROM_WIN32(GetLastError()); + dprintf("apm3IO: 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("apm3IO: 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 apm3_io_wnd_proc( + HWND hwnd, + UINT msg, + WPARAM wparam, + LPARAM lparam) +{ + switch (msg) { + default: + return DefWindowProcW(hwnd, msg, wparam, lparam); + } +} diff --git a/games/apm3io/wnd.h b/games/apm3io/wnd.h new file mode 100644 index 0000000..6e1fd53 --- /dev/null +++ b/games/apm3io/wnd.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +HRESULT apm3_io_wnd_create(HINSTANCE inst, HWND *out); diff --git a/games/apm3io/xi.c b/games/apm3io/xi.c new file mode 100644 index 0000000..6062a9d --- /dev/null +++ b/games/apm3io/xi.c @@ -0,0 +1,167 @@ +#include +#include + +#include +#include +#include + +#include "apm3io/backend.h" +#include "apm3io/config.h" +#include "apm3io/apm3io.h" +#include "apm3io/xi.h" + +#include "util/dprintf.h" + +static void apm3_xi_get_gamebtns(uint16_t *gamebtn_out); + +static HRESULT apm3_xi_config_apply(const struct apm3_xi_config *cfg); + +static const struct apm3_io_backend apm3_xi_backend = { + .get_gamebtns = apm3_xi_get_gamebtns, +}; + +static bool apm3_xi_analog_stick_enabled; + +HRESULT apm3_xi_init(const struct apm3_xi_config *cfg, const struct apm3_io_backend **backend) +{ + HRESULT hr; + assert(cfg != NULL); + assert(backend != NULL); + + hr = apm3_xi_config_apply(cfg); + + if (FAILED(hr)) { + return hr; + } + + dprintf("XInput: Using XInput controller\n"); + *backend = &apm3_xi_backend; + + return S_OK; +} + +HRESULT apm3_io_poll(void) +{ + return S_OK; +} + +static HRESULT apm3_xi_config_apply(const struct apm3_xi_config *cfg) +{ + dprintf("XInput: --- Begin configuration ---\n"); + dprintf("XInput: Analog Stick : %i\n", cfg->analog_stick_enabled); + dprintf("XInput: --- End configuration ---\n"); + + apm3_xi_analog_stick_enabled = cfg->analog_stick_enabled; + + return S_OK; +} + +static void apm3_xi_get_gamebtns(uint16_t *gamebtn_out) +{ + uint16_t gamebtn; + XINPUT_STATE xi; + WORD xb; + int left_x, left_y; + + assert(gamebtn_out != NULL); + + gamebtn = 0; + + memset(&xi, 0, sizeof(xi)); + XInputGetState(0, &xi); + xb = xi.Gamepad.wButtons; + + /* Use the left analog stick as a D Pad if enabled */ + if (apm3_xi_analog_stick_enabled) { + left_x = xi.Gamepad.sThumbLX; + left_y = xi.Gamepad.sThumbLY; + + if (left_x < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE * 2) { + left_x += XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE * 2; + } else if (left_x > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE * 2) { + left_x -= XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE * 2; + } else { + left_x = 0; + } + + if (left_y < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE * 2) { + left_y += XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE * 2; + } else if (left_y > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE * 2) { + left_y -= XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE * 2; + } else { + left_y = 0; + } + + if (left_y < 0) { + gamebtn |= APM3_IO_GAMEBTN_DOWN; + } else if (left_y > 0) { + gamebtn |= APM3_IO_GAMEBTN_UP; + } + + if (left_x < 0) { + gamebtn |= APM3_IO_GAMEBTN_LEFT; + } else if (left_x > 0) { + gamebtn |= APM3_IO_GAMEBTN_RIGHT; + } + } + + /* Normal game button controls */ + + if (xb & XINPUT_GAMEPAD_DPAD_UP) { + gamebtn |= APM3_IO_GAMEBTN_UP; + } + + if (xb & XINPUT_GAMEPAD_DPAD_DOWN) { + gamebtn |= APM3_IO_GAMEBTN_DOWN; + } + + if (xb & XINPUT_GAMEPAD_DPAD_LEFT) { + gamebtn |= APM3_IO_GAMEBTN_LEFT; + } + + if (xb & XINPUT_GAMEPAD_DPAD_RIGHT) { + gamebtn |= APM3_IO_GAMEBTN_RIGHT; + } + + if (xb & XINPUT_GAMEPAD_START) { + gamebtn |= APM3_IO_GAMEBTN_START; + } + + if (xb & XINPUT_GAMEPAD_BACK) { + gamebtn |= APM3_IO_GAMEBTN_HOME; + } + + if (xb & XINPUT_GAMEPAD_A) { + gamebtn |= APM3_IO_GAMEBTN_B1; + } + + if (xb & XINPUT_GAMEPAD_B) { + gamebtn |= APM3_IO_GAMEBTN_B2; + } + + if (xb & XINPUT_GAMEPAD_X) { + gamebtn |= APM3_IO_GAMEBTN_B5; + } + + if (xb & XINPUT_GAMEPAD_Y) { + gamebtn |= APM3_IO_GAMEBTN_B6; + } + + if (xb & XINPUT_GAMEPAD_LEFT_SHOULDER) { + gamebtn |= APM3_IO_GAMEBTN_B3; + } + + if (xb & XINPUT_GAMEPAD_RIGHT_SHOULDER) { + gamebtn |= APM3_IO_GAMEBTN_B4; + } + + if (xi.Gamepad.bLeftTrigger > 64) { + gamebtn |= APM3_IO_GAMEBTN_B7; + } + + if (xi.Gamepad.bRightTrigger > 64) { + gamebtn |= APM3_IO_GAMEBTN_B8; + } + + *gamebtn_out = gamebtn; +} diff --git a/games/apm3io/xi.h b/games/apm3io/xi.h new file mode 100644 index 0000000..ab61b72 --- /dev/null +++ b/games/apm3io/xi.h @@ -0,0 +1,11 @@ +#pragma once + +/* Can't call this xinput.h or it will conflict with */ + +#include + +#include "apm3io/backend.h" +#include "apm3io/config.h" + +HRESULT apm3_xi_init(const struct apm3_xi_config *cfg, + const struct apm3_io_backend **backend);