gitadora: (arena model) asio option (#718)

## Link to GitHub Issue or related Pull Request, if one exists
#717 

## Description of change
Add an option to override the ASIO device.

By default the game will try to use any ASIO driver that contains the
string `XONAR` in it.

## Testing
Tested with FlexASIO.
This commit is contained in:
bicarus
2026-05-30 00:03:06 -07:00
committed by GitHub
parent 0209b80a22
commit 4ea55a61b8
8 changed files with 236 additions and 34 deletions
+1
View File
@@ -416,6 +416,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/nost/io.cpp games/nost/io.cpp
games/nost/poke.cpp games/nost/poke.cpp
games/gitadora/gitadora.cpp games/gitadora/gitadora.cpp
games/gitadora/asio.cpp
games/gitadora/io.cpp games/gitadora/io.cpp
games/gitadora/handle.cpp games/gitadora/handle.cpp
games/gitadora/j32d.cpp games/gitadora/j32d.cpp
+166
View File
@@ -0,0 +1,166 @@
#include "asio.h"
#include <windows.h>
#include <cstring>
#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<HKEY>(0x4001);
static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(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\<configured>
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<DWORD>(len);
return ERROR_MORE_DATA;
}
memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len);
*lpcbData = static_cast<DWORD>(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);
}
}
+12
View File
@@ -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();
}
+5
View File
@@ -1,4 +1,5 @@
#include "gitadora.h" #include "gitadora.h"
#include "asio.h"
#include "handle.h" #include "handle.h"
#include "bi2x_hook.h" #include "bi2x_hook.h"
#include <unordered_map> #include <unordered_map>
@@ -28,6 +29,7 @@ namespace games::gitadora {
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE; std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent; std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt; std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
std::optional<std::string> ASIO_DRIVER = std::nullopt;
/* /*
* Prevent GitaDora from creating folders on F drive * 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("GetDriveTypeA", GetDriveTypeA_hook, avs::game::DLL_INSTANCE);
detour::iat_try("CreateDirectoryA", CreateDirectoryA_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 // volume change prevention
hooks::audio::mme::init(avs::game::DLL_INSTANCE); hooks::audio::mme::init(avs::game::DLL_INSTANCE);
+1
View File
@@ -16,6 +16,7 @@ namespace games::gitadora {
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE; extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
extern std::optional<socd::SocdAlgorithm> PICK_ALGO; extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
extern std::optional<uint8_t> ARENA_WINDOW_COUNT; extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
extern std::optional<std::string> ASIO_DRIVER;
class GitaDoraGame : public games::Game { class GitaDoraGame : public games::Game {
public: public:
+3
View File
@@ -677,6 +677,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::GitaDoraSubOverlaySize].is_active()) { if (options[launcher::Options::GitaDoraSubOverlaySize].is_active()) {
games::gitadora::SUBSCREEN_OVERLAY_SIZE = options[launcher::Options::GitaDoraSubOverlaySize].value_text(); 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()) { if (options[launcher::Options::LoadNostalgiaModule].value_bool()) {
attach_nostalgia = true; attach_nostalgia = true;
} }
+45 -32
View File
@@ -1092,38 +1092,6 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Game Options", .category = "Game Options",
.elements = {{"1", "DX"}, {"2", "SD"}, {"3", "SD2 - white cab"}}, .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 // GitaDoraLefty
.title = "GitaDora Lefty Guitar (for Digital Wailing)", .title = "GitaDora Lefty Guitar (for Digital Wailing)",
@@ -1189,6 +1157,51 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{"large", ""} {"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", .title = "Force Load Jubeat Module",
.name = "jb", .name = "jb",
+3 -2
View File
@@ -109,12 +109,13 @@ namespace launcher {
LoadGitaDoraModule, LoadGitaDoraModule,
GitaDoraTwoChannelAudio, GitaDoraTwoChannelAudio,
GitaDoraCabinetType, GitaDoraCabinetType,
GitaDoraArenaSingleWindow,
GitaDoraArenaWindowLayout,
GitaDoraLefty, GitaDoraLefty,
GitaDoraWailHold, GitaDoraWailHold,
GitaDoraPickAlgo, GitaDoraPickAlgo,
GitaDoraSubOverlaySize, GitaDoraSubOverlaySize,
GitaDoraArenaSingleWindow,
GitaDoraArenaWindowLayout,
GitaDoraArenaAsioDriver,
LoadJubeatModule, LoadJubeatModule,
LoadReflecBeatModule, LoadReflecBeatModule,
LoadShogikaiModule, LoadShogikaiModule,