diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 32cc734..ac59ad5 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -416,6 +416,7 @@ set(SOURCE_FILES ${SOURCE_FILES} games/nost/io.cpp games/nost/poke.cpp games/gitadora/gitadora.cpp + games/gitadora/asio.cpp games/gitadora/io.cpp games/gitadora/handle.cpp games/gitadora/j32d.cpp diff --git a/src/spice2x/games/gitadora/asio.cpp b/src/spice2x/games/gitadora/asio.cpp new file mode 100644 index 0000000..e5045b0 --- /dev/null +++ b/src/spice2x/games/gitadora/asio.cpp @@ -0,0 +1,166 @@ +#include "asio.h" + +#include +#include + +#include "avs/game.h" +#include "gitadora.h" +#include "util/detour.h" +#include "util/logging.h" + +namespace games::gitadora { + + // Redirects the game's hard-coded "XONAR" ASIO driver lookup to the + // driver name in ASIO_DRIVER by intercepting registry calls to + // HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles + // so we can recognise them on subsequent reg* calls. + + static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast(0x4001); + static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast(0x4002); + static const char *FAKE_ASIO_DEVICE_NAME = "XONAR"; + + static decltype(RegCloseKey) *RegCloseKey_orig = nullptr; + static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr; + static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr; + static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr; + + static HKEY real_asio_reg_handle = nullptr; + static HKEY real_asio_device_reg_handle = nullptr; + + static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired, + PHKEY phkResult) + { + // ASIO\XONAR redirect to ASIO\ + if (ASIO_DRIVER.has_value() && + lpSubKey != nullptr && + phkResult != nullptr && + hKey == PARENT_ASIO_REG_HANDLE && + _stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) { + + *phkResult = DEVICE_ASIO_REG_HANDLE; + + log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value()); + const auto result = RegOpenKeyExA_orig( + real_asio_reg_handle, + ASIO_DRIVER.value().c_str(), + ulOptions, + samDesired, + &real_asio_device_reg_handle); + + if (result != ERROR_SUCCESS) { + log_warning( + "gitadora::asio", + "failed to open registry subkey '{}', error=0x{:x}", + ASIO_DRIVER.value(), result); + log_warning( + "gitadora::asio", + "due to improper ASIO setting, audio init will fail"); + } + + return result; + } + + // open of the ASIO root: hand back a sentinel + if (ASIO_DRIVER.has_value() && + lpSubKey != nullptr && + phkResult != nullptr && + hKey == HKEY_LOCAL_MACHINE && + _stricmp(lpSubKey, "software\\asio") == 0) + { + *phkResult = PARENT_ASIO_REG_HANDLE; + return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, &real_asio_reg_handle); + } + + return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult); + } + + static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) { + if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) { + if (dwIndex == 0) { + // forward to real handle just to verify the key exists; we + // overwrite the name with our fake driver string regardless + auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName); + if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) { + log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME); + strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName); + lpName[cchName - 1] = '\0'; + } + return ret; + } else { + return ERROR_NO_MORE_ITEMS; + } + } + + return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName); + } + + static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType, + LPBYTE lpData, LPDWORD lpcbData) + { + HKEY target = hKey; + + if (ASIO_DRIVER.has_value() && + lpValueName != nullptr && + lpData != nullptr && + lpcbData != nullptr && + hKey == DEVICE_ASIO_REG_HANDLE) { + if (_stricmp(lpValueName, "Description") == 0) { + // engine may verify the driver name after open; ensure it still + // sees something containing "XONAR" so the substring check passes + const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1; + if (*lpcbData < len) { + *lpcbData = static_cast(len); + return ERROR_MORE_DATA; + } + memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len); + *lpcbData = static_cast(len); + if (lpType != nullptr) { + *lpType = REG_SZ; + } + return ERROR_SUCCESS; + } + + // for everything else (CLSID etc.) defer to the real driver subkey + target = real_asio_device_reg_handle; + } + + return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData); + } + + static LONG WINAPI RegCloseKey_hook(HKEY hKey) { + if (hKey == PARENT_ASIO_REG_HANDLE) { + if (real_asio_reg_handle != nullptr) { + RegCloseKey_orig(real_asio_reg_handle); + real_asio_reg_handle = nullptr; + } + return ERROR_SUCCESS; + } + + if (hKey == DEVICE_ASIO_REG_HANDLE) { + if (real_asio_device_reg_handle != nullptr) { + RegCloseKey_orig(real_asio_device_reg_handle); + real_asio_device_reg_handle = nullptr; + } + return ERROR_SUCCESS; + } + + return RegCloseKey_orig(hKey); + } + + void asio_hook_init() { + if (!ASIO_DRIVER.has_value()) { + return; + } + + log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value()); + + RegCloseKey_orig = detour::iat_try( + "RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE); + RegEnumKeyA_orig = detour::iat_try( + "RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE); + RegOpenKeyExA_orig = detour::iat_try( + "RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE); + RegQueryValueExA_orig = detour::iat_try( + "RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE); + } +} diff --git a/src/spice2x/games/gitadora/asio.h b/src/spice2x/games/gitadora/asio.h new file mode 100644 index 0000000..f4b5685 --- /dev/null +++ b/src/spice2x/games/gitadora/asio.h @@ -0,0 +1,12 @@ +#pragma once + +namespace games::gitadora { + + // installs IAT registry hooks in gfdm.dll that redirect the game's + // ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen + // driver name read from games::gitadora::ASIO_DRIVER. + // + // safe to call unconditionally; if ASIO_DRIVER is unset the hooks + // forward every call straight through to advapi32. + void asio_hook_init(); +} diff --git a/src/spice2x/games/gitadora/gitadora.cpp b/src/spice2x/games/gitadora/gitadora.cpp index 516024e..11a2e65 100644 --- a/src/spice2x/games/gitadora/gitadora.cpp +++ b/src/spice2x/games/gitadora/gitadora.cpp @@ -1,4 +1,5 @@ #include "gitadora.h" +#include "asio.h" #include "handle.h" #include "bi2x_hook.h" #include @@ -28,6 +29,7 @@ namespace games::gitadora { std::optional SUBSCREEN_OVERLAY_SIZE; std::optional PICK_ALGO = socd::SocdAlgorithm::PreferRecent; std::optional ARENA_WINDOW_COUNT = std::nullopt; + std::optional ASIO_DRIVER = std::nullopt; /* * Prevent GitaDora from creating folders on F drive @@ -612,6 +614,9 @@ namespace games::gitadora { detour::iat_try("GetDriveTypeA", GetDriveTypeA_hook, avs::game::DLL_INSTANCE); detour::iat_try("CreateDirectoryA", CreateDirectoryA_hook, avs::game::DLL_INSTANCE); + // ASIO driver redirect (XONAR -> user-configured driver) + asio_hook_init(); + // volume change prevention hooks::audio::mme::init(avs::game::DLL_INSTANCE); diff --git a/src/spice2x/games/gitadora/gitadora.h b/src/spice2x/games/gitadora/gitadora.h index 451c370..d08d8da 100644 --- a/src/spice2x/games/gitadora/gitadora.h +++ b/src/spice2x/games/gitadora/gitadora.h @@ -16,6 +16,7 @@ namespace games::gitadora { extern std::optional SUBSCREEN_OVERLAY_SIZE; extern std::optional PICK_ALGO; extern std::optional ARENA_WINDOW_COUNT; + extern std::optional ASIO_DRIVER; class GitaDoraGame : public games::Game { public: diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index feb41de..633103b 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -677,6 +677,9 @@ int main_implementation(int argc, char *argv[]) { if (options[launcher::Options::GitaDoraSubOverlaySize].is_active()) { games::gitadora::SUBSCREEN_OVERLAY_SIZE = options[launcher::Options::GitaDoraSubOverlaySize].value_text(); } + if (options[launcher::Options::GitaDoraArenaAsioDriver].is_active()) { + games::gitadora::ASIO_DRIVER = options[launcher::Options::GitaDoraArenaAsioDriver].value_text(); + } if (options[launcher::Options::LoadNostalgiaModule].value_bool()) { attach_nostalgia = true; } diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 48a954f..39f148e 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -1092,38 +1092,6 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Game Options", .elements = {{"1", "DX"}, {"2", "SD"}, {"3", "SD2 - white cab"}}, }, - { - // GitaDoraArenaSingleWindow - .title = "GitaDora Arena Disable Subscreens (DEPRECATED - use -gdalayout)", - .name = "gdaonewindow", - .desc = "DEPRECATED - use -gdalayout.\n\n" - "For Arena Model:\n\n" - "Windowed mode: instead of 4 windows, create 1 window.\n\n" - "Fullscreen mode: instead of requiring 4 monitors, use only the primary monitor. WARNING: requires 4K monitor.\n\n" - "To access the subscreen, use the subscreen overlay.", - .type = OptionType::Bool, - .hidden = true, - .game_name = "GitaDora", - .category = "Game Options", - }, - { - // GitaDoraArenaWindowLayout - .title = "GitaDora Arena Layout (EXPERIMENTAL)", - .name = "gdalayout", - .desc = "For Arena Model: select how many windows to create.\n\n" - "1 window: main only; use the subscreen overlay for SMALL.\n\n" - "2 windows: main + SMALL. Note: currently only works for windowed mode, broken for fullscreen.\n\n" - "4 windows: main + LEFT + RIGHT + SMALL. Fullscreen requires exactly 4 monitors in the " - "right resolution; see wiki for details.", - .type = OptionType::Enum, - .game_name = "GitaDora", - .category = "Game Options", - .elements = { - {"1", "1 window"}, - {"2", "2 windows"}, - {"4", "4 windows"} - }, - }, { // GitaDoraLefty .title = "GitaDora Lefty Guitar (for Digital Wailing)", @@ -1189,6 +1157,51 @@ static const std::vector OPTION_DEFINITIONS = { {"large", ""} }, }, + { + // GitaDoraArenaSingleWindow + .title = "GitaDora Arena Disable Subscreens (DEPRECATED - use -gdalayout)", + .name = "gdaonewindow", + .desc = "DEPRECATED - use -gdalayout.\n\n" + "For Arena Model:\n\n" + "Windowed mode: instead of 4 windows, create 1 window.\n\n" + "Fullscreen mode: instead of requiring 4 monitors, use only the primary monitor. WARNING: requires 4K monitor.\n\n" + "To access the subscreen, use the subscreen overlay.", + .type = OptionType::Bool, + .hidden = true, + .game_name = "GitaDora", + .category = "Game Options", + }, + { + // GitaDoraArenaWindowLayout + .title = "GitaDora Arena Layout (EXPERIMENTAL)", + .name = "gdalayout", + .desc = "For Arena Model: select how many windows to create.\n\n" + "1 window: main only; use the subscreen overlay for SMALL.\n\n" + "2 windows: main + SMALL. Note: currently only works for windowed mode, broken for fullscreen.\n\n" + "4 windows: main + LEFT + RIGHT + SMALL. Fullscreen requires exactly 4 monitors in the " + "right resolution; see wiki for details.", + .type = OptionType::Enum, + .game_name = "GitaDora", + .category = "Game Options", + .elements = { + {"1", "1 window"}, + {"2", "2 windows"}, + {"4", "4 windows"} + }, + }, + { + // GitaDoraArenaAsioDriver + .title = "GitaDora ASIO driver", + .name = "gdaasio", + .desc = "For Arena Model: ASIO driver name to use in place of XONAR. " + "String should match a subkey under HKLM\\SOFTWARE\\ASIO\\\n\n" + "Requires 7.1 @ 48kHz; if the game rejects your ASIO device, it will automatically " + "fall back to using WASAPI.", + .type = OptionType::Text, + .game_name = "GitaDora", + .category = "Game Options", + .picker = OptionPickerType::AsioDriver, + }, { .title = "Force Load Jubeat Module", .name = "jb", diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index 9a0357b..6e86c92 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -109,12 +109,13 @@ namespace launcher { LoadGitaDoraModule, GitaDoraTwoChannelAudio, GitaDoraCabinetType, - GitaDoraArenaSingleWindow, - GitaDoraArenaWindowLayout, GitaDoraLefty, GitaDoraWailHold, GitaDoraPickAlgo, GitaDoraSubOverlaySize, + GitaDoraArenaSingleWindow, + GitaDoraArenaWindowLayout, + GitaDoraArenaAsioDriver, LoadJubeatModule, LoadReflecBeatModule, LoadShogikaiModule,