#include "camera.h" #if SPICE64 #include #include "mf_wrappers.h" #include "avs/game.h" #include "external/rapidjson/document.h" #include "external/rapidjson/pointer.h" #include "external/rapidjson/prettywriter.h" #include "games/iidx/iidx.h" #include "games/io.h" #include "launcher/options.h" #include "misc/eamuse.h" #include "util/detour.h" #include "util/fileutils.h" #include "util/libutils.h" #include "util/logging.h" #include "util/sigscan.h" #include "util/utils.h" static std::filesystem::path dll_path; static HMODULE iidx_module; static uintptr_t addr_hook_a = 0; static uintptr_t addr_hook_b = 0; static uintptr_t addr_textures = 0; static uintptr_t addr_device_offset = 0; typedef void **(__fastcall *camera_hook_a_t)(PBYTE); typedef LPDIRECT3DTEXTURE9 (*camera_hook_b_t)(void*, int); static camera_hook_a_t camera_hook_a_orig = nullptr; static camera_hook_b_t camera_hook_b_orig = nullptr; auto static hook_a_init = std::once_flag {}; static LPDIRECT3DDEVICE9EX device; static LPDIRECT3DTEXTURE9 *texture_a = nullptr; static LPDIRECT3DTEXTURE9 *texture_b = nullptr; struct PredefinedHook { std::string pe_identifier; uintptr_t hook_a; uintptr_t hook_b; uintptr_t hook_textures; uintptr_t hook_device_offset; }; PredefinedHook g_predefinedHooks[] = { // 27 003 { "5f713b52_6d4090", 0x005971b0, 0x005fb2c0, 0x04e49898, 0x000000d0 }, // 27 010 { "5f713946_7f52b0", 0x006b89e0, 0x0071c950, 0x04fd08b8, 0x000000d0 }, }; const DWORD g_predefinedHooksLength = ARRAYSIZE(g_predefinedHooks); namespace games::iidx { static IIDXLocalCamera *top_camera = nullptr; // camera id #0 static IIDXLocalCamera *front_camera = nullptr; // camera id #1 std::vector LOCAL_CAMERA_LIST = {}; static IDirect3DDeviceManager9 *s_pD3DManager = nullptr; std::filesystem::path CAMERA_CONFIG_PATH; bool CAMERA_READY = false; bool parse_cmd_params() { const auto remove_spaces = [](const char& c) { return c == ' '; }; log_misc("iidx:camhook", "Using user-supplied addresses (-iidxcamhookoffset)"); auto s = TDJ_CAMERA_OVERRIDE.value(); s.erase(std::remove_if(s.begin(), s.end(), remove_spaces), s.end()); if (sscanf( s.c_str(), "%i,%i,%i,%i", (int*)&addr_hook_a, (int*)&addr_hook_b, (int*)&addr_textures, (int*)&addr_device_offset) == 4) { return true; } else { log_fatal("iidx:camhook", "failed to parse -iidxcamhookoffset"); return false; } } bool find_camera_hooks() { std::string dll_name = avs::game::DLL_NAME; dll_path = MODULE_PATH / dll_name; iidx_module = libutils::try_module(dll_path); if (!iidx_module) { log_warning("iidx:camhook", "failed to hook game DLL"); return FALSE; } uint32_t time_date_stamp = 0; uint32_t address_of_entry_point = 0; // there are three different ways we try to hook the camera entry points // 1) user-provided override via cmd line // 2) predefined offsets using PE header matching (needed for iidx27 and few others) // 3) signature match (should work for most iidx28-31) // method 1: user provided if (TDJ_CAMERA_OVERRIDE.has_value() && parse_cmd_params()) { return TRUE; } // method 2: predefined offsets get_pe_identifier(dll_path, &time_date_stamp, &address_of_entry_point); auto pe = fmt::format("{:x}_{:x}", time_date_stamp, address_of_entry_point); log_info("iidx:camhook", "Locating predefined hook addresses for LDJ-{}", pe); for (DWORD i = 0; i < g_predefinedHooksLength; i++) { if (pe.compare(g_predefinedHooks[i].pe_identifier) == 0) { log_misc("iidx:camhook", "Found predefined addresses"); addr_hook_a = g_predefinedHooks[i].hook_a; addr_hook_b = g_predefinedHooks[i].hook_b; addr_textures = g_predefinedHooks[i].hook_textures; addr_device_offset = g_predefinedHooks[i].hook_device_offset; return TRUE; } } // method 3: signature match log_info("iidx:camhook", "Did not find predefined hook address, try signature match"); // --- addr_hook_a --- uint8_t *addr_hook_a_ptr = reinterpret_cast(find_pattern( iidx_module, "488BC4565741564883EC5048C740D8FEFFFFFF", "XXXXXXXXXXXXXXXXXXX", 0, 0)); if (addr_hook_a_ptr == nullptr) { log_warning("iidx:camhook", "failed to find hook: addr_hook_a"); log_warning("iidx:camhook", "hint: this feature REQUIRES a compatible DLL!"); return FALSE; } addr_hook_a = (uint64_t) (addr_hook_a_ptr - (uint8_t*) iidx_module); // addr_hook_b uint8_t* addr_hook_b_ptr = reinterpret_cast(find_pattern( iidx_module, "E8000000004885C07439E8", "X????XXXXXX", 0, 0)); if (addr_hook_b_ptr == nullptr) { log_warning("iidx:camhook", "failed to find hook: addr_hook_b"); return FALSE; } // displace with the content of wildcard bytes int32_t disp_b = *((int32_t *) (addr_hook_b_ptr + 1)); addr_hook_b = (addr_hook_b_ptr - (uint8_t*) iidx_module) + disp_b + 5; // --- addr_textures --- uint8_t* addr_textures_ptr = reinterpret_cast(find_pattern( iidx_module, "E800000000488BC8488BD34883C420", "X????XXXXXXXXXX", 0, 0)); if (addr_textures_ptr == nullptr) { log_warning("iidx:camhook", "failed to find hook: addr_textures (part 1)"); return FALSE; } // displace with the content of wildcard bytes int32_t disp_textures = *((int32_t *) (addr_textures_ptr + 1)); addr_textures_ptr += disp_textures + 5; uint32_t search_from = (addr_textures_ptr - (uint8_t*) iidx_module); addr_textures_ptr = reinterpret_cast(find_pattern_from( iidx_module, "488D0D", "XXX", 0, 0, search_from)); if (addr_textures_ptr == nullptr) { log_warning("iidx:camhook", "failed to find hook: addr_textures (part 2)"); return FALSE; } // displace again with the next integer disp_textures = *((int32_t *) (addr_textures_ptr + 3)); addr_textures = (addr_textures_ptr - (uint8_t*) iidx_module) + disp_textures + 7; // addr_device_offset search_from = (addr_hook_a_ptr - (uint8_t*) iidx_module); uint8_t *addr_device_ptr = reinterpret_cast(find_pattern_from( iidx_module, "488B89", "XXX", 3, 0, search_from)); addr_device_offset = *addr_device_ptr; if (addr_textures_ptr == nullptr) { log_warning("iidx:camhook", "failed to find hook: addr_textures (part 3)"); return FALSE; } return TRUE; } static void **__fastcall camera_hook_a(PBYTE a1) { std::call_once(hook_a_init, [&]{ device = *reinterpret_cast(a1 + addr_device_offset); auto const textures = *reinterpret_cast((uint8_t*)iidx_module + addr_textures); texture_a = textures; texture_b = textures + 2; init_local_camera(); }); return camera_hook_a_orig(a1); } static LPDIRECT3DTEXTURE9 camera_hook_b(void* a1, int idx) { if (idx == 0 && top_camera) { return top_camera->GetTexture(); } if (idx == 1 && front_camera) { return front_camera->GetTexture(); } return camera_hook_b_orig(a1, idx); } bool create_hooks() { auto *hook_a_ptr = reinterpret_cast( reinterpret_cast(iidx_module) + addr_hook_a); if (!detour::trampoline( reinterpret_cast(hook_a_ptr), camera_hook_a, &camera_hook_a_orig)) { log_warning("iidx:camhook", "failed to trampoline hook_a"); return FALSE; } auto *hook_b_ptr = reinterpret_cast( reinterpret_cast(iidx_module) + addr_hook_b); if (!detour::trampoline( reinterpret_cast(hook_b_ptr), camera_hook_b, &camera_hook_b_orig)) { log_warning("iidx:camhook", "failed to trampoline hook_b"); return FALSE; } return TRUE; } HRESULT create_d3d_device_manager() { UINT resetToken = 0; IDirect3DDeviceManager9 *pD3DManager = nullptr; HRESULT hr = DXVA2CreateDirect3DDeviceManager9(&resetToken, &pD3DManager); if (FAILED(hr)) { goto done; } hr = pD3DManager->ResetDevice(device, resetToken); if (FAILED(hr)) { goto done; } s_pD3DManager = pD3DManager; (s_pD3DManager)->AddRef(); done: if (SUCCEEDED(hr)) { log_misc("iidx:camhook", "Created DeviceManager for DXVA"); } else { log_warning("iidx:camhook", "Cannot create DXVA DeviceManager: {}", hr); } SafeRelease(&pD3DManager); return hr; } bool init_local_camera() { HRESULT hr = S_OK; IMFAttributes *pAttributes = nullptr; IMFActivate **ppDevices = nullptr; uint32_t numDevices; // Initialize an attribute store to specify enumeration parameters. hr = WrappedMFCreateAttributes(&pAttributes, 1); // Ask for source type = video capture devices. if (SUCCEEDED(hr)) { hr = pAttributes->SetGUID( MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID ); } // Enumerate devices. if (SUCCEEDED(hr)) { hr = WrappedMFEnumDeviceSources(pAttributes, &ppDevices, &numDevices); } log_info("iidx:camhook", "MFEnumDeviceSources returned {} device(s)", numDevices); if (numDevices > 0) { hr = create_d3d_device_manager(); } if (SUCCEEDED(hr)) { // get options std::string top_camera_id(""); std::string front_camera_id(""); auto options = games::get_options(eamuse_get_game()); if (options->at(launcher::Options::IIDXCamHookTopId).is_active()) { top_camera_id = options->at(launcher::Options::IIDXCamHookTopId).value_text(); } if (options->at(launcher::Options::IIDXCamHookFrontId).is_active()) { front_camera_id = options->at(launcher::Options::IIDXCamHookFrontId).value_text(); } int preferred_top_index = -1; int preferred_front_index = -1; log_info("iidx:camhook", "-------- Listing all cameras from MFEnumDeviceSources --------"); // print camera names first for user for (size_t i = 0; i < numDevices; i++) { // get camera activation auto pActivate = ppDevices[i]; // print friendly name PWCHAR friendly = nullptr; UINT32 friendly_len = 0; hr = pActivate->GetAllocatedString( MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME, &friendly, &friendly_len); if (SUCCEEDED(hr) && friendly != nullptr) { log_info("iidx:camhook", "Camera {} Name: {}", i, ws2s(friendly)); CoTaskMemFree(friendly); friendly = nullptr; } // get symlink name PWCHAR symlink = nullptr; UINT32 symlink_len = 0; hr = pActivate->GetAllocatedString( MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK, &symlink, &symlink_len); if (FAILED(hr) || symlink == nullptr) { log_info("iidx:camhook", "Camera {}: failed to get vidcap symlink name", i); continue; } const auto symlink_s = ws2s(symlink); log_info("iidx:camhook", "Camera {} ID: {}", i, symlink_s); if (!top_camera_id.empty() && (symlink_s.find(top_camera_id) != std::string::npos)) { preferred_top_index = i; } else if (!front_camera_id.empty() && (symlink_s.find(front_camera_id) != std::string::npos)) { preferred_front_index = i; } if (symlink != nullptr) { CoTaskMemFree(symlink); symlink = nullptr; } } log_info("iidx:camhook", "-------- End of cameras from MFEnumDeviceSources --------"); // pick out preferred top/front cameras log_misc("iidx:camhook", "Adding user-preferred cameras"); for (size_t i = 0; i < numDevices && LOCAL_CAMERA_LIST.size() < 2; i++) { auto pActivate = ppDevices[i]; if ((int)i == preferred_top_index && top_camera == nullptr) { log_misc("iidx:camhook", "Adding user-preferred top camera {} / '{}'", i, top_camera_id); top_camera = add_top_camera(pActivate); } else if ((int)i == preferred_front_index && front_camera == nullptr) { log_misc("iidx:camhook", "Adding user-preferred front camera {} / '{}'", i, front_camera_id); front_camera = add_front_camera(pActivate); } } // fill out the rest of cameras log_misc("iidx:camhook", "Adding other cameras"); for (size_t i = 0; i < numDevices && LOCAL_CAMERA_LIST.size() < 2; i++) { auto pActivate = ppDevices[i]; if ((int)i == preferred_top_index || (int)i == preferred_front_index) { // we already tried these above; don't use them here continue; } if (!FLIP_CAMS) { // top camera first, then front if (top_camera == nullptr) { top_camera = add_top_camera(pActivate); } else if (front_camera == nullptr) { front_camera = add_front_camera(pActivate); } } else { // front first, then top if (front_camera == nullptr) { front_camera = add_front_camera(pActivate); } else if (top_camera == nullptr) { top_camera = add_top_camera(pActivate); } } } } if (LOCAL_CAMERA_LIST.size() == 0) { goto done; } camera_config_load(); if (top_camera) { top_camera->StartCapture(); } if (front_camera) { front_camera->StartCapture(); } CAMERA_READY = true; done: SafeRelease(&pAttributes); for (DWORD i = 0; i < numDevices; i++) { SafeRelease(&ppDevices[i]); } CoTaskMemFree(ppDevices); return SUCCEEDED(hr); } IIDXLocalCamera* add_top_camera(IMFActivate* pActivate) { auto top_camera = new IIDXLocalCamera("top", TDJ_CAMERA_PREFER_16_9, pActivate, s_pD3DManager, device, texture_a); if (top_camera->m_initialized) { LOCAL_CAMERA_LIST.push_back(top_camera); } else { top_camera->Release(); top_camera = nullptr; } return top_camera; } IIDXLocalCamera* add_front_camera(IMFActivate* pActivate) { auto front_camera = new IIDXLocalCamera("front", TDJ_CAMERA_PREFER_16_9, pActivate, s_pD3DManager, device, texture_b); if (front_camera->m_initialized) { LOCAL_CAMERA_LIST.push_back(front_camera); } else { front_camera->Release(); front_camera = nullptr; } return front_camera; } bool init_camera_hooks() { bool result = find_camera_hooks(); if (result) { log_misc( "iidx:camhook", "found hooks:\n hook_a 0x{:x}\n hook_b 0x{:x}\n textures 0x{:x}\n device_offset 0x{:x}", addr_hook_a, addr_hook_b, addr_textures, addr_device_offset); result = create_hooks(); if (result) { init_mf_library(); } } return result; } void camera_release() { if (top_camera) { top_camera->Release(); top_camera = nullptr; } if (front_camera) { front_camera->Release(); front_camera = nullptr; } SafeRelease(&s_pD3DManager); } bool camera_config_load() { CAMERA_CONFIG_PATH = fileutils::get_config_file_path("iidx::camhook", "spicetools_camera_control.json"); try { // read config file std::string config = fileutils::text_read(CAMERA_CONFIG_PATH); if (!config.empty()) { // parse document rapidjson::Document doc; doc.Parse(config.c_str()); // check parse error auto error = doc.GetParseError(); if (error) { log_warning("iidx:camhook", "config parse error: {}", error); return false; } // verify root is a dict if (!doc.IsObject()) { log_warning("iidx:camhook", "config not found"); return false; } const std::string root("/sp2x_cameras"); auto numCameras = LOCAL_CAMERA_LIST.size(); for (size_t i = 0; i < numCameras; i++) { auto *camera = LOCAL_CAMERA_LIST.at(i); auto symLink = camera->GetSymLink(); const auto cameraNode = rapidjson::Pointer(root + "/" + symLink).Get(doc); if (cameraNode && cameraNode->IsObject()) { log_info("iidx:camhook", "Parsing config for: {}", symLink); // Media type auto mediaTypePointer = rapidjson::Pointer(root + "/" + symLink + "/MediaType").Get(doc); if (mediaTypePointer && mediaTypePointer->IsString()) { std::string mediaType = mediaTypePointer->GetString(); if (mediaType.length() > 0) { camera->m_selectedMediaTypeDescription = mediaType; camera->m_useAutoMediaType = false; } else { camera->m_useAutoMediaType = true; } } // Draw mode auto drawModePointer = rapidjson::Pointer(root + "/" + symLink + "/DrawMode").Get(doc); if (drawModePointer && drawModePointer->IsString()) { std::string drawModeString = drawModePointer->GetString(); for (int j = 0; j < DRAW_MODE_SIZE; j++) { if (DRAW_MODE_LABELS[j].compare(drawModeString) == 0) { camera->m_drawMode = (LocalCameraDrawMode) j; break; } } } // Flip auto flipHorizontalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipHorizontal").Get(doc); if (flipHorizontalPointer && flipHorizontalPointer->IsBool()) { camera->m_flipHorizontal = flipHorizontalPointer->GetBool(); } auto flipVerticalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipVertical").Get(doc); if (flipVerticalPointer && flipVerticalPointer->IsBool()) { camera->m_flipVertical = flipVerticalPointer->GetBool(); } // Allow manual control auto manualControlPointer = rapidjson::Pointer(root + "/" + symLink + "/AllowManualControl").Get(doc); if (manualControlPointer && manualControlPointer->IsBool() && manualControlPointer->GetBool()) { camera->m_allowManualControl = true; } // Camera control // if m_allowManualControl is false, SetCameraControlProp will fail, but run through this code // anyway to exercise parsing code for (int propIndex = 0; propIndex < CAMERA_CONTROL_PROP_SIZE; propIndex++) { std::string label = CAMERA_CONTROL_LABELS[propIndex]; CameraControlProp prop = {}; camera->GetCameraControlProp(propIndex, &prop); auto valuePointer = rapidjson::Pointer(root + "/" + symLink + "/" + label + "/value").Get(doc); auto flagsPointer = rapidjson::Pointer(root + "/" + symLink + "/" + label + "/flags").Get(doc); if (valuePointer && valuePointer->IsInt() && flagsPointer && flagsPointer->IsInt()) { prop.value = (long)valuePointer->GetInt(); prop.valueFlags = (long)flagsPointer->GetInt(); camera->SetCameraControlProp(propIndex, prop.value, prop.valueFlags); } } log_misc("iidx:camhook", " >> done"); } else { log_misc("iidx:camhook", "No previous config for: {}", symLink); } } } } catch (const std::exception& e) { log_warning("iidx:camhook", "exception occurred while config: {}", e.what()); return false; } return true; } bool camera_config_save() { log_info("iidx:camhook", "saving config"); // create document rapidjson::Document doc; std::string config = fileutils::text_read(CAMERA_CONFIG_PATH); if (!config.empty()) { doc.Parse(config.c_str()); log_misc("iidx:camhook", "existing config file found"); } if (!doc.IsObject()) { log_misc("iidx:camhook", "clearing out config file"); doc.SetObject(); } auto numCameras = LOCAL_CAMERA_LIST.size(); for (size_t i = 0; i < numCameras; i++) { auto *camera = LOCAL_CAMERA_LIST.at(i); auto symLink = camera->GetSymLink(); std::string root("/sp2x_cameras/" + symLink + "/"); // Media type if (camera->m_useAutoMediaType) { rapidjson::Pointer(root + "MediaType").Set(doc, ""); } else { rapidjson::Pointer(root + "MediaType").Set(doc, camera->m_selectedMediaTypeDescription); } // Draw Mode rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[camera->m_drawMode]); // Flip rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal); rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical); // Manual control rapidjson::Pointer(root + "AllowManualControl").Set(doc, camera->m_allowManualControl); // Camera control for (int propIndex = 0; propIndex < CAMERA_CONTROL_PROP_SIZE; propIndex++) { std::string label = CAMERA_CONTROL_LABELS[propIndex]; CameraControlProp prop = {}; camera->GetCameraControlProp(propIndex, &prop); rapidjson::Pointer(root + label + "/value").Set(doc, (int)prop.value); rapidjson::Pointer(root + label + "/flags").Set(doc, (int)prop.valueFlags); } } // build JSON rapidjson::StringBuffer buffer; rapidjson::PrettyWriter writer(buffer); doc.Accept(writer); // save to file if (fileutils::write_config_file("iidx::camhook", CAMERA_CONFIG_PATH, buffer.GetString())) { } else { log_warning("iidx:camhook", "unable to save config file"); } return true; } bool camera_config_reset() { return false; } } #endif