Files
spice2x-r3d/games/sdvx/sdvx.cpp
T
2023-09-20 02:09:17 +09:00

420 lines
15 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/powrprof.h"
#include "hooks/sleephook.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"
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;
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)
{
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);
}
}
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 "XONAR SOUND CARD(64)"
// otherwise you end up with this error:
// M:BMSoundLib: ASIODriver: No such driver: XONAR SOUND CARD(64)
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 sdvx5_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;
}
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::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, force enabling 9on12");
GRAPHICS_9_ON_12 = true;
}
// check for Valkyrie cabinet mode
if (isValkyrieCabinetMode) {
// hook touch window
if (!NATIVETOUCH) {
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"));
} else {
// don't let nvapi mess with display settings (force failure)
libraryhook_hook_proc("nvapi_QueryInterface", static_cast<void *>(nullptr));
libraryhook_enable(avs::game::DLL_INSTANCE);
}
}
#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);
// 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(sdvx5_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();
}
}