spicetools/games/sdvx/sdvx.cpp

467 lines
18 KiB
C++

#include "sdvx.h"
#include <external/robin_hood.h>
#include "avs/game.h"
#include "games/shared/lcdhandle.h"
#include "hooks/audio/audio.h"
#include "hooks/graphics/graphics.h"
#include "hooks/devicehook.h"
#include "hooks/libraryhook.h"
#include "hooks/graphics/nvapi_hook.h"
#include "hooks/powrprof.h"
#include "hooks/sleephook.h"
#include "hooks/winuser.h"
#include "touch/touch.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/sigscan.h"
#include "util/libutils.h"
#include "misc/wintouchemu.h"
#include "misc/eamuse.h"
#include "bi2x_hook.h"
#include "camera.h"
#include "io.h"
#include "acioemu/handle.h"
#include "cfg/configurator.h"
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
namespace games::sdvx {
// constants
const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x3001);
const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x3002);
const char *ORIGINAL_ASIO_DEVICE_NAME = "XONAR SOUND CARD(64)";
// settings
bool DISABLECAMS = false;
bool NATIVETOUCH = false;
uint8_t DIGITAL_KNOB_SENS = 16;
SdvxOverlayPosition OVERLAY_POS = SDVX_OVERLAY_BOTTOM;
bool ENABLE_COM_PORT_SCAN_HOOK = false;
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
std::optional<std::string> ASIO_DRIVER = std::nullopt;
// states
static HKEY real_asio_reg_handle = nullptr;
static HKEY real_asio_device_reg_handle = nullptr;
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
if (lpSubKey != nullptr && phkResult != nullptr) {
if (hKey == HKEY_LOCAL_MACHINE &&
ASIO_DRIVER.has_value() &&
_stricmp(lpSubKey, "software\\asio") == 0)
{
*phkResult = PARENT_ASIO_REG_HANDLE;
return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle);
}
}
return RegOpenKeyA_orig(hKey, lpSubKey, phkResult);
}
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
PHKEY phkResult)
{
// ASIO hook
if (lpSubKey != nullptr && phkResult != nullptr) {
if (hKey == PARENT_ASIO_REG_HANDLE &&
ASIO_DRIVER.has_value() &&
_stricmp(lpSubKey, ORIGINAL_ASIO_DEVICE_NAME) == 0)
{
*phkResult = DEVICE_ASIO_REG_HANDLE;
log_info("sdvx::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
return RegOpenKeyExA_orig(real_asio_reg_handle, ASIO_DRIVER.value().c_str(), ulOptions, samDesired,
&real_asio_device_reg_handle);
}
}
// COM hook
if (ENABLE_COM_PORT_SCAN_HOOK &&
lpSubKey != nullptr && phkResult != nullptr &&
_stricmp(lpSubKey, "HARDWARE\\DEVICEMAP\\SERIALCOMM") == 0) {
log_info("sdvx::io", "failing HKLM\\HARDWARE\\DEVICEMAP\\SERIALCOMM to force COM1 ICCA");
return 2; //ERROR_FILE_NOT_FOUND
}
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) {
auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName);
if (ret == ERROR_SUCCESS && lpName != nullptr) {
log_info("sdvx::asio", "stubbing '{}' with '{}'", lpName, ORIGINAL_ASIO_DEVICE_NAME);
strncpy(lpName, ORIGINAL_ASIO_DEVICE_NAME, cchName);
}
return ret;
} else {
return ERROR_NO_MORE_ITEMS;
}
}
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
}
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCTSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
LPBYTE lpData, LPDWORD lpcbData)
{
if (lpValueName != nullptr && lpData != nullptr && lpcbData != nullptr) {
if (hKey == DEVICE_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
log_info("sdvx::asio", "RegQueryValueExA({}, \"{}\")", fmt::ptr((void *) hKey), lpValueName);
if (_stricmp(lpValueName, "Description") == 0) {
// sdvx does a comparison against hardcoded string "XONAR SOUND CARD(64)" (same as iidx31)
// so what's in the registry must be overridden with "XONAR SOUND CARD(64)"
// otherwise you end up with this error: M:BMSoundLib: ASIODriver: No such driver
memcpy(lpData, ORIGINAL_ASIO_DEVICE_NAME, strlen(ORIGINAL_ASIO_DEVICE_NAME) + 1);
return ERROR_SUCCESS;
} else {
hKey = real_asio_device_reg_handle;
}
}
}
// fallback
return RegQueryValueExA_orig(hKey, lpValueName, lpReserved, lpType, lpData, lpcbData);
}
static LONG WINAPI RegCloseKey_hook(HKEY hKey) {
if (hKey == PARENT_ASIO_REG_HANDLE || hKey == DEVICE_ASIO_REG_HANDLE) {
return ERROR_SUCCESS;
}
return RegCloseKey_orig(hKey);
}
SDVXGame::SDVXGame() : Game("Sound Voltex") {
}
static LPWSTR __stdcall GetCommandLineW_hook() {
static std::wstring lp_args = L"bootstrap.exe prop\\bootstrap.xml";
return lp_args.data();
}
#ifdef SPICE64
static bool sdvx64_spam_remover(void *user, const std::string &data, logger::Style style, std::string &out) {
if (data.empty() || data[0] != '[') {
return false;
}
if (data.find("W:afpu-package: XE592acd000040 texture id invalid") != std::string::npos) {
out = "";
return true;
}
if (data.find("W:afpu-package: XE592acd000042 texture id invalid") != std::string::npos) {
out = "";
return true;
}
if (data.find("W:CTexture: no such texture: id 0") != std::string::npos) {
out = "";
return true;
}
if (data.find("M:autoDj: DEF phrase ") != std::string::npos) {
out = "";
return true;
}
if (data.find("W:afp-access: afp_mc_deep_goto_play frame no error") != std::string::npos) {
out = "";
return true;
}
if (data.find("W:afputils: CDirectX::SetRenderState") != std::string::npos) {
out = "";
return true;
}
if (data.find("W:CameraTexture: Camera error was detected. (err,detail) = (0,0)") != std::string::npos) {
out = "";
return true;
}
// M:AppConfig: [env/APPDATA]=C:\Users\username\AppData\Roaming
if (data.find("M:AppConfig: [env/") != std::string::npos) {
out = "";
return true;
}
return false;
}
typedef void **(__fastcall *volume_set_t)(uint64_t, uint64_t, uint64_t);
static volume_set_t volume_set_orig = nullptr;
static void **__fastcall volume_set_hook(uint64_t vol_sound, uint64_t vol_woofer, uint64_t vol_headphone) {
// volume level conversion tables
static uint8_t SOUND_VOLUMES[] = {
4, 55, 57, 59, 61, 63, 65, 67, 69, 71,
73, 75, 77, 78, 79, 80, 81, 82, 83, 84,
85, 86, 87, 88, 89, 90, 91, 92, 93, 95, 96,
};
static uint8_t WOOFER_VOLUMES[] = {
4, 70, 72, 73, 74, 75, 76, 77, 79, 80,
81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 100,
};
static uint8_t HEADPHONE_VOLUMES[] = {
4, 60, 62, 64, 66, 68, 70, 72, 76, 78,
80, 82, 83, 84, 85, 86, 87, 88, 89, 90,
91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 100,
};
// apply volumes
auto &format = hooks::audio::FORMAT.Format;
auto &lights = games::sdvx::get_lights();
if (format.nChannels == 6 || vol_sound != 30) {
if (vol_sound < std::size(SOUND_VOLUMES) && vol_sound != 30) {
float value = (float) SOUND_VOLUMES[vol_sound] * 0.01f;
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::VOLUME_SOUND], value);
}
}
if (vol_woofer < std::size(WOOFER_VOLUMES)) {
float value = (float) WOOFER_VOLUMES[vol_woofer] * 0.01f;
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::VOLUME_WOOFER], value);
}
if (vol_headphone < std::size(HEADPHONE_VOLUMES)) {
float value = (float) HEADPHONE_VOLUMES[vol_headphone] * 0.01f;
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::VOLUME_HEADPHONE], value);
}
// call original function to set volumes for the 6ch mode
return volume_set_orig(format.nChannels == 6 ? vol_sound : 30, vol_woofer, vol_headphone);
}
#endif
void SDVXGame::pre_attach() {
// for whatever reason, sdvx latches onto cards for much longer than other games
// needed because the game waits forever on the game over screen until a card is not detected
AUTO_INSERT_CARD_COOLDOWN = 15.f;
// check bad model name
if (!cfg::CONFIGURATOR_STANDALONE && avs::game::is_model("UFC")) {
log_fatal(
"sdvx",
"BAD MODEL NAME ERROR\n\n\n"
"!!! model name set to UFC, this is WRONG and will break your game !!!\n"
"!!! !!!\n"
"!!! If you are trying to boot Valkyrie Model, !!!\n"
"!!! change <spec> from F to G. !!!\n"
"!!! !!!\n"
"!!! model name set to UFC, this is WRONG and will break your game !!!\n\n\n"
);
}
}
void SDVXGame::attach() {
Game::attach();
#ifdef SPICE64 // SDVX5+ specific code
bool isValkyrieCabinetMode = avs::game::SPEC[0] == 'G' || avs::game::SPEC[0] == 'H';
// LCD handle
if (!isValkyrieCabinetMode) {
devicehook_init();
devicehook_add(new games::shared::LCDHandle());
}
#else
devicehook_init();
devicehook_add(new games::shared::LCDHandle());
#endif
hooks::sleep::init(1000, 1);
// hooks for chinese SDVX
if (libutils::try_module("unisintr.dll")) {
detour::iat_try("GetCommandLineW", GetCommandLineW_hook);
// skip 30 second timeout after NETWORK DEVICE check
replace_pattern(
avs::game::DLL_INSTANCE,
"89F528003D????0000",
"89F528003D01000000",
0, 0);
}
#ifdef SPICE64 // SDVX5+ specific code
// check for new I/O DLL
auto aio = libutils::try_library("libaio.dll");
if (aio != nullptr) {
// enable 9on12 for AMD
if (!libutils::try_library("nvapi64.dll")) {
log_info(
"sdvx",
"nvapi64.dll not found; for non-NVIDIA GPUs, requesting 9on12 to be enabled");
GRAPHICS_9_ON_12_REQUESTED_BY_GAME = true;
} else {
// don't let nvapi mess with display settings
nvapi_hook::initialize(avs::game::DLL_INSTANCE);
}
// check for Valkyrie cabinet mode
if (isValkyrieCabinetMode) {
// hook touch window
// in windowed mode, game can accept mouse input on the second screen
if (!NATIVETOUCH && !GRAPHICS_WINDOWED) {
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends(
"SOUND VOLTEX",
"Main Screen",
avs::game::DLL_INSTANCE);
}
// insert BI2X hooks
bi2x_hook_init();
// add card readers
devicehook_init(aio);
devicehook_add(new acioemu::ACIOHandle(L"COM1"));
// this is needed because on some newer versions of SDVX6, soundvoltex.dll will open
// HKLM\HARDWARE\DEVICEMAP\SERIALCOMM, go through some of the keys, and depending on
// what is present, pick a port other than COM1 (seemingly the highest port
// available). We want the game to pick COM1 still, so a workaround is needed to
// fool the game.
ENABLE_COM_PORT_SCAN_HOOK = true;
}
}
#endif
// ASIO device hook
RegCloseKey_orig = detour::iat_try(
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
RegEnumKeyA_orig = detour::iat_try(
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyA_orig = detour::iat_try(
"RegOpenKeyA", RegOpenKeyA_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);
#ifdef SPICE64
powrprof_hook_init(avs::game::DLL_INSTANCE);
winuser_hook_init(avs::game::DLL_INSTANCE);
// hook camera
if (!DISABLECAMS) {
camera_init();
}
// RGB CAMERA error ignore
if (!replace_pattern(
avs::game::DLL_INSTANCE,
"418D480484C074218D51FD",
"????????????9090??????",
0, 0)) {
log_warning("sdvx", "failed to insert camera error fix");
}
// remove log spam
logger::hook_add(sdvx64_spam_remover, nullptr);
#endif
}
void SDVXGame::post_attach() {
Game::post_attach();
#ifdef SPICE64 // SDVX5+ specific code
/*
* Volume Hook
*
* How to find the correct RVA:
*
* Method 1 (older versions):
* Search for byte sequence 48 8B C4 48 81 EC 88 00 00 00 80 3D
*
* Method 2 (older versions):
* 1. search for ac_io_bi2a_set_amp_volume
* 2. move one function up (function where it does some calculations, that's ours)
* 3. take the *file offset* of the *first* instruction of this function
*
* Method 3:
* Search for the function with the 3 arguments which is being called from the sound options.
* It is pretty obvious which one it is because it checks all 3 args to be <= 30.
*/
static const robin_hood::unordered_map<std::string, intptr_t> VOLUME_HOOKS {
{ "2019100800", 0x414ED0 },
{ "2020011500", 0x417090 },
{ "2020022700", 0x4281A0 },
{ "2020122200", 0x40C030 },
{ "2021042800", 0x096EB0 },
{ "2021051802", 0x097930 },
{ "2021083100", 0x096E20 },
{ "2021102000", 0x097230 },
{ "2021121400", 0x09AD00 },
};
bool volume_hook_found = false;
for (auto &[datecode, rva] : VOLUME_HOOKS) {
if (avs::game::is_ext(datecode.c_str())) {
// calculate target RVA
auto volume_set_rva = libutils::offset2rva(MODULE_PATH / avs::game::DLL_NAME, rva);
if (volume_set_rva == -1) {
log_warning("sdvx", "failed to insert set volume hook (convert rva {})", rva);
break;
}
// convert RVA to real target
auto *volume_set_ptr = reinterpret_cast<uint16_t *>(
reinterpret_cast<intptr_t>(avs::game::DLL_INSTANCE) + volume_set_rva);
if (volume_set_ptr[0] != 0x8B48) {
log_warning("sdvx", "failed to insert set volume hook (invalid target)");
break;
}
// insert trampoline
if (!detour::trampoline(
reinterpret_cast<volume_set_t>(volume_set_ptr),
volume_set_hook,
&volume_set_orig))
{
log_warning("sdvx", "failed to insert set volume hook (insert trampoline)");
}
// success
volume_hook_found = true;
break;
}
}
// check if version not found
if (!volume_hook_found) {
log_warning("sdvx", "volume hook unavailable for this game version");
// set volumes to sdvx 4 defaults
auto &lights = games::sdvx::get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights[games::sdvx::Lights::VOLUME_SOUND],
(100 - 15) / 100.f);
GameAPI::Lights::writeLight(RI_MGR, lights[games::sdvx::Lights::VOLUME_HEADPHONE],
(100 - 9) / 100.f);
GameAPI::Lights::writeLight(RI_MGR, lights[games::sdvx::Lights::VOLUME_EXTERNAL],
(100 - 96) / 100.f);
GameAPI::Lights::writeLight(RI_MGR, lights[games::sdvx::Lights::VOLUME_WOOFER],
(100 - 9) / 100.f);
}
#endif
}
void SDVXGame::detach() {
Game::detach();
devicehook_dispose();
}
}