Update to spice2x-25-04-25 (pre-apply)

> broken commit
This commit is contained in:
[ ]
2025-05-07 00:25:31 +09:00
parent 04dee88276
commit c94de456b5
543 changed files with 78491 additions and 91698 deletions
+5
View File
@@ -32,6 +32,11 @@ bool games::iidx::IIDXFMSerialHandle::FMSerialDevice::parse_msg(
delete msg;
break;
}
case 0x0153:
if (DISABLE_ESPEC_IO) {
return false;
}
[[fallthrough]]; // to 0x0152
case 0x0152: { // STATUS
// check input data length
+5
View File
@@ -32,6 +32,11 @@ bool games::iidx::BI2XSerialHandle::BI2XDevice::parse_msg(
delete msg;
break;
}
case 0x0153:
if (DISABLE_ESPEC_IO) {
return false;
}
[[fallthrough]]; // to 0x0152
case 0x0152: { // STATUS
// check input data length
+9 -23
View File
@@ -10,6 +10,7 @@
#include "launcher/options.h"
#include "io.h"
#include "iidx.h"
#include "util/tapeled.h"
namespace games::iidx {
constexpr bool BI2X_PASSTHROUGH = false;
@@ -416,12 +417,9 @@ namespace games::iidx {
static struct TapeLedMapping {
size_t data_size;
int index_r, index_g, index_b;
float avg_mult;
TapeLedMapping(size_t data_size, int index_r, int index_g, int index_b)
: data_size(data_size), index_r(index_r), index_g(index_g), index_b(index_b) {
avg_mult = 1.f / (data_size * 255);
}
: data_size(data_size), index_r(index_r), index_g(index_g), index_b(index_b) {}
} mapping[] = {
{ 19, Lights::StageLeftAvgR, Lights::StageLeftAvgG, Lights::StageLeftAvgB },
@@ -444,32 +442,20 @@ namespace games::iidx {
};
// check index bounds
if (index < std::size(mapping)) {
if (tapeledutils::is_enabled() && index < std::size(mapping)) {
auto &map = mapping[index];
// calculate average color
size_t avg_ri = 0;
size_t avg_gi = 0;
size_t avg_bi = 0;
for (size_t i = 0; i < map.data_size; i++) {
auto color = &data[i * 3];
avg_ri += color[0];
avg_gi += color[1];
avg_bi += color[2];
}
float avg_r = avg_ri * map.avg_mult;
float avg_g = avg_gi * map.avg_mult;
float avg_b = avg_bi * map.avg_mult;
// pick a color to use
const auto rgb = tapeledutils::pick_color_from_led_tape(data, map.data_size);
// set average color
// program the lights into API
auto &lights = get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], avg_r);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], avg_g);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], avg_b);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
}
if (This != custom_node) {
// send tape data to real device
return AIO_IOB2_BI2X_TDJ__SetTapeLedData_orig(This, index, data);
}
+668
View File
@@ -0,0 +1,668 @@
#include "camera.h"
#if SPICE64
#include <d3d9.h>
#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<IIDXLocalCamera*> 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<uint8_t *>(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<uint8_t *>(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<uint8_t *>(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<uint8_t *>(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<uint8_t *>(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<LPDIRECT3DDEVICE9EX*>(a1 + addr_device_offset);
auto const textures = *reinterpret_cast<LPDIRECT3DTEXTURE9**>((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<uint16_t *>(
reinterpret_cast<intptr_t>(iidx_module) + addr_hook_a);
if (!detour::trampoline(
reinterpret_cast<camera_hook_a_t>(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<uint16_t *>(
reinterpret_cast<intptr_t>(iidx_module) + addr_hook_b);
if (!detour::trampoline(
reinterpret_cast<camera_hook_b_t>(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<rapidjson::StringBuffer> 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
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#if SPICE64
#include <vector>
#include "games/iidx/local_camera.h"
namespace games::iidx {
extern std::vector<IIDXLocalCamera*> LOCAL_CAMERA_LIST;
extern bool CAMERA_READY;
bool init_local_camera();
bool init_camera_hooks();
void camera_release();
bool camera_config_load();
bool camera_config_save();
bool camera_config_reset();
IIDXLocalCamera* add_top_camera(IMFActivate* pActivate);
IIDXLocalCamera* add_front_camera(IMFActivate* pActivate);
}
#endif
+151 -236
View File
@@ -1,26 +1,20 @@
// set version to Windows 7 to enable Media Foundation functions
#ifdef _WIN32_WINNT
#undef _WIN32_WINNT
#endif
#define _WIN32_WINNT 0x0601
// turn on C-style COM objects
#define CINTERFACE
#include "iidx.h"
#include <mfobjects.h>
#include <mfidl.h>
#include "acioemu/handle.h"
#include "avs/core.h"
#include "avs/game.h"
#include "cfg/configurator.h"
#include "games/io.h"
#include "games/iidx/legacy_camera.h"
#include "hooks/avshook.h"
#include "hooks/cfgmgr32hook.h"
#include "hooks/devicehook.h"
#include "hooks/graphics/graphics.h"
#ifdef SPICE64
#include "hooks/graphics/nvenc_hook.h"
#endif
#include "hooks/setupapihook.h"
#include "hooks/sleephook.h"
#include "launcher/options.h"
@@ -31,6 +25,7 @@
#include "util/fileutils.h"
#include "util/libutils.h"
#include "util/memutils.h"
#include "util/sigscan.h"
#include "util/utils.h"
#include "bi2a.h"
@@ -38,9 +33,6 @@
#include "ezusb.h"
#include "io.h"
static VTBL_TYPE(IMFActivate, GetAllocatedString) GetAllocatedString_orig = nullptr;
static decltype(MFEnumDeviceSources) *MFEnumDeviceSources_orig = nullptr;
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
@@ -57,12 +49,19 @@ namespace games::iidx {
// settings
bool FLIP_CAMS = false;
bool DISABLE_CAMS = false;
bool TDJ_CAMERA = false;
bool TDJ_CAMERA_PREFER_16_9 = true;
bool TDJ_MODE = false;
bool FORCE_720P = false;
bool DISABLE_ESPEC_IO = false;
bool NATIVE_TOUCH = false;
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
std::optional<std::string> ASIO_DRIVER = std::nullopt;
uint8_t DIGITAL_TT_SENS = 4;
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE = std::nullopt;
std::optional<std::string> SCREEN_MODE = std::nullopt;
std::optional<std::string> TDJ_CAMERA_OVERRIDE = std::nullopt;
// states
static HKEY real_asio_reg_handle = nullptr;
@@ -101,9 +100,25 @@ namespace games::iidx {
*phkResult = DEVICE_ASIO_REG_HANDLE;
log_info("iidx::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
return RegOpenKeyExA_orig(real_asio_reg_handle, ASIO_DRIVER.value().c_str(), ulOptions, samDesired,
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(
"iidx::asio",
"failed to open registry subkey '{}', error=0x{:x}",
ASIO_DRIVER.value().c_str(), result);
log_warning(
"iidx::asio",
"due to improper ASIO setting, game will likely fail to launch; double check -iidxasio and the registry",
ASIO_DRIVER.value().c_str(), result);
}
return result;
}
}
@@ -130,20 +145,20 @@ namespace games::iidx {
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
}
/*
* Dirty Workaround for IO Device
* Game gets registry object via SetupDiOpenDevRegKey, then looks up "PortName" via RegQueryValueExA
* We ignore the first and just cheat on the second.
*/
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCTSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
LPBYTE lpData, LPDWORD lpcbData)
{
if (lpValueName != nullptr && lpData != nullptr && lpcbData != nullptr) {
// ASIO hack
if (hKey == DEVICE_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
log_info("iidx::asio", "RegQueryValueExA({}, \"{}\")", fmt::ptr((void *) hKey), lpValueName);
if (_stricmp(lpValueName, "Description") == 0) {
memcpy(lpData, ASIO_DRIVER.value().c_str(), ASIO_DRIVER.value().size() + 1);
// newer iidx does a comparison against hardcoded string "XONAR SOUND CARD(64)" (same as sdvx)
// 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 {
@@ -151,6 +166,11 @@ namespace games::iidx {
}
}
/*
* Dirty Workaround for IO Device
* Game gets registry object via SetupDiOpenDevRegKey, then looks up "PortName" via RegQueryValueExA
* We ignore the first and just cheat on the second.
*/
// check for port name lookup
if (_stricmp(lpValueName, "PortName") == 0) {
const char port[] = "COM2";
@@ -172,195 +192,24 @@ namespace games::iidx {
return RegCloseKey_orig(hKey);
}
/*
* Camera related stuff
*/
static std::wstring CAMERA0_ID;
static std::wstring CAMERA1_ID;
static HRESULT WINAPI GetAllocatedString_hook(IMFActivate* This, REFGUID guidKey, LPWSTR *ppwszValue,
UINT32 *pcchLength)
{
// call the original
HRESULT result = GetAllocatedString_orig(This, guidKey, ppwszValue, pcchLength);
// log the cam name
log_misc("iidx::cam", "obtained {}", ws2s(*ppwszValue));
// try first camera
wchar_t *pwc = nullptr;
if (CAMERA0_ID.length() == 23) {
pwc = wcsstr(*ppwszValue, CAMERA0_ID.c_str());
}
// try second camera if first wasn't found
if (!pwc && CAMERA1_ID.length() == 23) {
pwc = wcsstr(*ppwszValue, CAMERA1_ID.c_str());
}
// check if camera could be identified
if (pwc) {
// fake the USB IDs
pwc[4] = L'2';
pwc[5] = L'8';
pwc[6] = L'8';
pwc[7] = L'c';
pwc[13] = L'0';
pwc[14] = L'0';
pwc[15] = L'0';
pwc[16] = L'2';
pwc[21] = L'0';
pwc[22] = L'0';
log_misc("iidx::cam", "replaced {}", ws2s(*ppwszValue));
}
// return original result
return result;
}
static void hook_camera_vtable(IMFActivate *camera) {
// hook allocated string method for camera identification
memutils::VProtectGuard camera_guard(camera->lpVtbl);
camera->lpVtbl->GetAllocatedString = GetAllocatedString_hook;
}
static void hook_camera(size_t no, std::wstring camera_id, std::string camera_instance) {
// logic based on camera no
if (no == 0) {
// don't hook if camera 0 is already hooked
if (CAMERA0_ID.length() > 0) {
return;
}
// save the camera ID
CAMERA0_ID = camera_id;
// cfgmgr hook
CFGMGR32_HOOK_SETTING camera_setting;
camera_setting.device_instance = 0xDEADBEEF;
camera_setting.parent_instance = ~camera_setting.device_instance;
camera_setting.device_id = "USB\\VEN_1022&DEV_7908";
camera_setting.device_node_id = "USB\\VID_288C&PID_0002&MI_00\\?&????????&?&????";
if (camera_instance.length() == 17) {
for (int i = 0; i < 17; i++) {
camera_setting.device_node_id[28 + i] = camera_instance[i];
}
}
cfgmgr32hook_add(camera_setting);
log_info("iidx::cam", "hooked camera 1 @ {}", ws2s(camera_id));
}
else if (no == 1) {
// don't hook if camera 1 is already hooked
if (CAMERA1_ID.length() > 0) {
return;
}
// save the camera ID
CAMERA1_ID = camera_id;
// cfgmgr hook
CFGMGR32_HOOK_SETTING camera_setting;
camera_setting.device_instance = 0xBEEFDEAD;
camera_setting.parent_instance = ~camera_setting.device_instance;
camera_setting.device_id = "USB\\VEN_1022&DEV_7914";
camera_setting.device_node_id = "USB\\VID_288C&PID_0002&MI_00\\?&????????&?&????";
if (camera_instance.length() == 17) {
for (int i = 0; i < 17; i++) {
camera_setting.device_node_id[28 + i] = camera_instance[i];
}
}
cfgmgr32hook_add(camera_setting);
log_info("iidx::cam", "hooked camera 2 @ {}", ws2s(camera_id));
}
}
static HRESULT WINAPI MFEnumDeviceSources_hook(IMFAttributes *pAttributes, IMFActivate ***pppSourceActivate,
UINT32 *pcSourceActivate) {
// call original function
HRESULT result_orig = MFEnumDeviceSources_orig(pAttributes, pppSourceActivate, pcSourceActivate);
// check for capture devices
if (FAILED(result_orig) || !*pcSourceActivate) {
return result_orig;
}
// iterate cameras
size_t cam_hook_num = 0;
for (size_t cam_num = 0; cam_num < *pcSourceActivate && cam_hook_num < 2; cam_num++) {
// flip
size_t cam_num_flipped = cam_num;
if (FLIP_CAMS) {
cam_num_flipped = *pcSourceActivate - cam_num - 1;
}
// get camera
IMFActivate *camera = (*pppSourceActivate)[cam_num_flipped];
// save original method for later use
if (GetAllocatedString_orig == nullptr) {
GetAllocatedString_orig = camera->lpVtbl->GetAllocatedString;
}
// hook allocated string method for camera identification
hook_camera_vtable(camera);
// get camera link
LPWSTR camera_link_lpwstr;
UINT32 camera_link_length;
if (SUCCEEDED(GetAllocatedString_orig(
camera,
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK,
&camera_link_lpwstr,
&camera_link_length))) {
// cut name to make ID
std::wstring camera_link_ws = std::wstring(camera_link_lpwstr);
std::wstring camera_id = camera_link_ws.substr(8, 23);
log_info("iidx::cam", "found video capture device: {}", ws2s(camera_link_ws));
// only support cameras that start with \\?\usb
if (!string_begins_with(camera_link_ws, L"\\\\?\\usb")) {
continue;
}
// get camera instance
std::string camera_link = ws2s(camera_link_ws);
std::string camera_instance = camera_link.substr(32, 17);
// hook the camera
hook_camera(cam_hook_num, camera_id, camera_instance);
// increase camera hook number
cam_hook_num++;
} else {
log_warning("iidx::cam", "failed to open camera {}", cam_num_flipped);
}
}
// return result
return result_orig;
}
static bool log_hook(void *user, const std::string &data, logger::Style style, std::string &out) {
if (data.empty() || data[0] != '[') {
return false;
}
// get rid of the log spam
if (data.find(" I:graphic: adapter mode ") != std::string::npos) {
out.clear();
return true;
} else if (data.find(" W:afputils: CDirectX::SetRenderState ") != std::string::npos) {
out.clear();
return true;
} else if (data.find("\" layer ID 0 is not layer ID.") != std::string::npos) {
out.clear();
return true;
} else if (data.find(" W:touch: missing trigger:") != std::string::npos) {
out.clear();
return true;
} else {
return false;
}
@@ -394,7 +243,6 @@ namespace games::iidx {
void IIDXGame::attach() {
Game::attach();
// IO boards
auto options = games::get_options(eamuse_get_game());
if (!options->at(launcher::Options::IIDXBIO2FW).value_bool()) {
@@ -440,20 +288,24 @@ namespace games::iidx {
if (aio != nullptr) {
// check TDJ mode
TDJ_MODE |= fileutils::text_read("C:\\000rom.txt") == "TDJ-JA";
TDJ_MODE |= fileutils::text_read("D:\\001rom.txt") == "TDJ";
// force TDJ mode
if (TDJ_MODE) {
// ensure game starts in the desired mode
hooks::avs::set_rom("/c_drv/000rom.txt", "TDJ-JA");
hooks::avs::set_rom("/d_drv/001rom.txt", "TDJ");
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
devicehook_init(avs::core::DLL_INSTANCE);
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends("beatmania IIDX", "main", avs::game::DLL_INSTANCE);
if (!NATIVE_TOUCH) {
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends("beatmania IIDX", "main", avs::game::DLL_INSTANCE);
}
// prevent crash on TDJ mode without correct DLL
if (!GetModuleHandle("nvcuda.dll")) {
@@ -498,6 +350,8 @@ namespace games::iidx {
}
}
apply_audio_hacks();
// ASIO device hook
RegCloseKey_orig = detour::iat_try(
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
@@ -514,22 +368,7 @@ namespace games::iidx {
// check if cam hook should be enabled
if (!DISABLE_CAMS) {
// camera media framework hook
MFEnumDeviceSources_orig = detour::iat_try(
"MFEnumDeviceSources", MFEnumDeviceSources_hook, avs::game::DLL_INSTANCE);
// camera settings
SETUPAPI_SETTINGS settings3 {};
settings3.class_guid[0] = 0x00000000;
settings3.class_guid[1] = 0x00000000;
settings3.class_guid[2] = 0x00000000;
settings3.class_guid[3] = 0x00000000;
const char property3[] = "USB Composite Device";
memcpy(settings3.property_devicedesc, property3, sizeof(property3));
settings3.property_address[0] = 1;
settings3.property_address[1] = 7;
setupapihook_add(settings3);
init_legacy_camera_hook(FLIP_CAMS);
}
// init cfgmgr32 hooks
@@ -545,18 +384,38 @@ namespace games::iidx {
SetEnvironmentVariable("CONNECT_CAMERA", "0");
}
if (SCREEN_MODE.has_value()) {
SetEnvironmentVariable("SCREEN_MODE", SCREEN_MODE.value().c_str());
}
// windowed subscreen, enabled by default, unless turned off by user
auto options = games::get_options(eamuse_get_game());
if (GRAPHICS_WINDOWED && !options->at(launcher::Options::spice2x_IIDXNoSub).value_bool()) {
GRAPHICS_IIDX_WSUB = true;
}
#ifdef SPICE64
this->detect_sound_output_device();
#endif
// check bad model name
if (!cfg::CONFIGURATOR_STANDALONE && avs::game::is_model("TDJ")) {
log_fatal(
"iidx",
"BAD MODEL NAME ERROR\n\n\n"
"!!! model name set to TDJ, this is WRONG and will break your game !!!\n"
"If you are trying to boot IIDX with Lightning Model mode, please do the following instead:\n"
" Revert your changes to XML file so it says <model __type=\"str\">LDJ</model>\n"
" In SpiceCfg, enable 'IIDX TDJ Mode' or provide -iidxtdj flag in command line\n"
" Apply any applicable settings / patches / hex edits\n"
"!!! !!!\n"
"!!! If you are trying to boot IIDX with Lightning Model mode, !!!\n"
"!!! please do the following instead: !!!\n"
"!!! !!!\n"
"!!! Revert your changes to XML file so it says !!!\n"
"!!! <model __type=\"str\">LDJ</model> !!!\n"
"!!! !!!\n"
"!!! In SpiceCfg, enable 'IIDX TDJ Mode' or provide -iidxtdj flag !!!\n"
"!!! in command line !!!\n"
"!!! !!!\n"
"!!! Apply any applicable settings / patches / hex edits !!!\n"
"!!! !!!\n"
"!!! model name set to TDJ, this is WRONG and will break your game !!!\n\n\n"
);
}
@@ -577,6 +436,7 @@ namespace games::iidx {
"using user-supplied \"{}\" for SOUND_OUTPUT_DEVICE",
SOUND_OUTPUT_DEVICE.value());
SetEnvironmentVariable("SOUND_OUTPUT_DEVICE", SOUND_OUTPUT_DEVICE.value().c_str());
SOUND_OUTPUT_DEVICE_IN_EFFECT = SOUND_OUTPUT_DEVICE;
return;
}
@@ -607,12 +467,8 @@ namespace games::iidx {
device = "asio";
}
log_info("iidx", "SOUND_OUTPUT_DEVICE set to {}", device);
log_info(
"iidx",
"SOUND_OUTPUT_DEVICE only affects IIDX27+ and ignored by older games. "
"If {} is not what you wanted, you can override it with -iidxsounddevice option",
device);
SetEnvironmentVariable("SOUND_OUTPUT_DEVICE", device);
SOUND_OUTPUT_DEVICE_IN_EFFECT = device;
}
uint32_t get_pad() {
@@ -864,4 +720,63 @@ namespace games::iidx {
bool is_tdj_fhd() {
return TDJ_MODE && avs::game::is_ext(2022101900, MAXINT);
}
void apply_audio_hacks() {
#ifdef SPICE64
const auto has_XONAR_SOUND_CARD = find_pattern(
avs::game::DLL_INSTANCE,
"584F4E415220534F554E442043415244", // XONAR SOUND CARD
"XXXXXXXXXXXXXXXX",
0,
0);
const auto has_SOUND_OUTPUT_DEVICE = find_pattern(
avs::game::DLL_INSTANCE,
"534F554E445F4F55545055545F444556494345", // SOUND_OUTPUT_DEVICE
"XXXXXXXXXXXXXXXXXXX",
0,
0);
// attempt to detect ASIO support
// 25-26: has neither (no patch needed - WASAPI Exclusive by default)
// 27-30: has both (envvar will be respected, ASIO or WASAPI)
// 31+: only has XONAR (ASIO by default, signature patch can be used to force WASAPI - for now)
if (!has_SOUND_OUTPUT_DEVICE && !has_XONAR_SOUND_CARD) {
log_info("iidx", "This game only uses WASAPI audio engine");
return;
}
if (has_SOUND_OUTPUT_DEVICE && has_XONAR_SOUND_CARD) {
log_info("iidx", "This game accepts SOUND_OUTPUT_DEVICE environment variable");
return;
}
log_info("iidx", "This game supports ASIO but does not accept SOUND_OUTPUT_DEVICE environment variable");
if (SOUND_OUTPUT_DEVICE_IN_EFFECT.has_value() && SOUND_OUTPUT_DEVICE_IN_EFFECT.value() == "wasapi") {
intptr_t result = replace_pattern(
avs::game::DLL_INSTANCE,
"FF5008E8????ECFF83780803740D",
"??????BB00000000EB169090????",
0, 0);
if (result == 0) {
log_warning(
"iidx",
"Failed to force WASAPI as audio engine using signature matching. "
"Unless patches are applied, game will default to ASIO");
} else {
log_info(
"iidx",
"Successfully forced WASAPI as audio engine using signature matching @ 0x{:x}.",
result);
}
}
#endif
}
}
+9
View File
@@ -8,14 +8,22 @@
namespace games::iidx {
// settings
extern bool FLIP_CAMS;
extern bool DISABLE_CAMS;
extern bool TDJ_CAMERA;
extern bool TDJ_CAMERA_PREFER_16_9;
extern std::optional<std::string> TDJ_CAMERA_OVERRIDE;
extern bool TDJ_MODE;
extern bool FORCE_720P;
extern bool DISABLE_ESPEC_IO;
extern bool NATIVE_TOUCH;
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
extern std::optional<std::string> ASIO_DRIVER;
extern uint8_t DIGITAL_TT_SENS;
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
extern std::optional<std::string> SCREEN_MODE;
// state
extern char IIDXIO_LED_TICKER[10];
@@ -45,4 +53,5 @@ namespace games::iidx {
unsigned char get_slider(uint8_t slider);
const char* get_16seg();
bool is_tdj_fhd();
void apply_audio_hacks();
}
+12
View File
@@ -45,6 +45,18 @@ std::vector<Button> &games::iidx::get_buttons() {
return buttons;
}
std::string games::iidx::get_buttons_help() {
// keep to max 100 characters wide
return
" P1 EFCT P2 \n"
" VEFX \n"
" 2 4 6 2 4 6 \n"
" TT 1 3 5 7 1 3 5 7 TT \n"
"\n"
"For controllers, use Analog tab for turntable (TT)."
;
}
std::vector<Analog> &games::iidx::get_analogs() {
static std::vector<Analog> analogs;
+1
View File
@@ -158,6 +158,7 @@ namespace games::iidx {
// getters
std::vector<Button> &get_buttons();
std::string get_buttons_help();
std::vector<Analog> &get_analogs();
std::vector<Light> &get_lights();
}
+235
View File
@@ -0,0 +1,235 @@
#include "games/iidx/legacy_camera.h"
// set version to Windows 7 to enable Media Foundation functions
#ifdef _WIN32_WINNT
#undef _WIN32_WINNT
#endif
#define _WIN32_WINNT 0x0601
// turn on C-style COM objects
#define CINTERFACE
#include <mfobjects.h>
#include <mfidl.h>
#include <string>
#include "avs/game.h"
#include "cfg/configurator.h"
#include "hooks/cfgmgr32hook.h"
#include "hooks/setupapihook.h"
#include "util/detour.h"
#include "util/memutils.h"
#include "util/logging.h"
#include "util/utils.h"
static VTBL_TYPE(IMFActivate, GetAllocatedString) GetAllocatedString_orig = nullptr;
static decltype(MFEnumDeviceSources) *MFEnumDeviceSources_orig = nullptr;
static bool should_flip_cams = false;
namespace games::iidx {
/*
* Camera related stuff
*/
static std::wstring CAMERA0_ID;
static std::wstring CAMERA1_ID;
static HRESULT WINAPI GetAllocatedString_hook(IMFActivate* This, REFGUID guidKey, LPWSTR *ppwszValue,
UINT32 *pcchLength)
{
// call the original
HRESULT result = GetAllocatedString_orig(This, guidKey, ppwszValue, pcchLength);
// log the cam name
log_misc("iidx::cam", "obtained {}", ws2s(*ppwszValue));
// try first camera
wchar_t *pwc = nullptr;
if (CAMERA0_ID.length() == 23) {
pwc = wcsstr(*ppwszValue, CAMERA0_ID.c_str());
}
// try second camera if first wasn't found
if (!pwc && CAMERA1_ID.length() == 23) {
pwc = wcsstr(*ppwszValue, CAMERA1_ID.c_str());
}
// check if camera could be identified
if (pwc) {
// fake the USB IDs
pwc[4] = L'2';
pwc[5] = L'8';
pwc[6] = L'8';
pwc[7] = L'c';
pwc[13] = L'0';
pwc[14] = L'0';
pwc[15] = L'0';
pwc[16] = L'2';
pwc[21] = L'0';
pwc[22] = L'0';
log_misc("iidx::cam", "replaced {}", ws2s(*ppwszValue));
}
// return original result
return result;
}
static void hook_camera(size_t no, std::wstring camera_id, std::string camera_instance) {
// logic based on camera no
if (no == 0) {
// don't hook if camera 0 is already hooked
if (CAMERA0_ID.length() > 0) {
return;
}
// save the camera ID
CAMERA0_ID = camera_id;
// cfgmgr hook
CFGMGR32_HOOK_SETTING camera_setting;
camera_setting.device_instance = 0xDEADBEEF;
camera_setting.parent_instance = ~camera_setting.device_instance;
camera_setting.device_id = "USB\\VEN_1022&DEV_7908";
camera_setting.device_node_id = "USB\\VID_288C&PID_0002&MI_00\\?&????????&?&????";
if (camera_instance.length() == 17) {
for (int i = 0; i < 17; i++) {
camera_setting.device_node_id[28 + i] = camera_instance[i];
}
}
cfgmgr32hook_add(camera_setting);
log_info("iidx::cam", "hooked camera 1 @ {}", ws2s(camera_id));
}
else if (no == 1) {
// don't hook if camera 1 is already hooked
if (CAMERA1_ID.length() > 0) {
return;
}
// save the camera ID
CAMERA1_ID = camera_id;
// cfgmgr hook
CFGMGR32_HOOK_SETTING camera_setting;
camera_setting.device_instance = 0xBEEFDEAD;
camera_setting.parent_instance = ~camera_setting.device_instance;
camera_setting.device_id = "USB\\VEN_1022&DEV_7914";
camera_setting.device_node_id = "USB\\VID_288C&PID_0002&MI_00\\?&????????&?&????";
if (camera_instance.length() == 17) {
for (int i = 0; i < 17; i++) {
camera_setting.device_node_id[28 + i] = camera_instance[i];
}
}
cfgmgr32hook_add(camera_setting);
log_info("iidx::cam", "hooked camera 2 @ {}", ws2s(camera_id));
}
}
static void hook_camera_vtable(IMFActivate *camera) {
// hook allocated string method for camera identification
memutils::VProtectGuard camera_guard(camera->lpVtbl);
camera->lpVtbl->GetAllocatedString = GetAllocatedString_hook;
}
static HRESULT WINAPI MFEnumDeviceSources_hook(IMFAttributes *pAttributes, IMFActivate ***pppSourceActivate,
UINT32 *pcSourceActivate) {
// call original function
HRESULT result_orig = MFEnumDeviceSources_orig(pAttributes, pppSourceActivate, pcSourceActivate);
// check for capture devices
if (FAILED(result_orig) || !*pcSourceActivate) {
return result_orig;
}
// iterate cameras
size_t cam_hook_num = 0;
for (size_t cam_num = 0; cam_num < *pcSourceActivate && cam_hook_num < 2; cam_num++) {
// flip
size_t cam_num_flipped = cam_num;
if (should_flip_cams) {
cam_num_flipped = *pcSourceActivate - cam_num - 1;
}
// get camera
IMFActivate *camera = (*pppSourceActivate)[cam_num_flipped];
// save original method for later use
if (GetAllocatedString_orig == nullptr) {
GetAllocatedString_orig = camera->lpVtbl->GetAllocatedString;
}
// hook allocated string method for camera identification
hook_camera_vtable(camera);
// get camera link
LPWSTR camera_link_lpwstr;
UINT32 camera_link_length;
if (SUCCEEDED(GetAllocatedString_orig(
camera,
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK,
&camera_link_lpwstr,
&camera_link_length))) {
// cut name to make ID
std::wstring camera_link_ws = std::wstring(camera_link_lpwstr);
std::wstring camera_id = camera_link_ws.substr(8, 23);
log_info("iidx::cam", "found video capture device: {}", ws2s(camera_link_ws));
// only support cameras that start with \\?\usb
if (!string_begins_with(camera_link_ws, L"\\\\?\\usb")) {
continue;
}
// get camera instance
std::string camera_link = ws2s(camera_link_ws);
std::string camera_instance = camera_link.substr(32, 17);
// hook the camera
hook_camera(cam_hook_num, camera_id, camera_instance);
// increase camera hook number
cam_hook_num++;
} else {
log_warning("iidx::cam", "failed to open camera {}", cam_num_flipped);
}
}
// return result
return result_orig;
}
void init_legacy_camera_hook(bool flip_cams) {
should_flip_cams = flip_cams;
// camera media framework hook
MFEnumDeviceSources_orig = detour::iat_try(
"MFEnumDeviceSources", MFEnumDeviceSources_hook, avs::game::DLL_INSTANCE);
// camera settings
SETUPAPI_SETTINGS settings3 {};
settings3.class_guid[0] = 0x00000000;
settings3.class_guid[1] = 0x00000000;
settings3.class_guid[2] = 0x00000000;
settings3.class_guid[3] = 0x00000000;
const char property3[] = "USB Composite Device";
memcpy(settings3.property_devicedesc, property3, sizeof(property3));
settings3.property_address[0] = 1;
settings3.property_address[1] = 7;
setupapihook_add(settings3);
}
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
namespace games::iidx {
void init_legacy_camera_hook(bool);
}
+872
View File
@@ -0,0 +1,872 @@
#include "games/iidx/local_camera.h"
#if SPICE64
#include "util/logging.h"
#include "util/utils.h"
#include "mf_wrappers.h"
std::string CAMERA_CONTROL_LABELS[] = {
"Pan",
"Tilt",
"Roll",
"Zoom",
"Exposure",
"Iris",
"Focus"
};
std::string DRAW_MODE_LABELS[] = {
"Stretch",
"Crop",
"Letterbox",
"Crop to 4:3",
"Letterbox to 4:3",
};
// static HRESULT printTextureLevelDesc(LPDIRECT3DTEXTURE9 texture) {
// HRESULT hr = S_OK;
// D3DSURFACE_DESC desc;
// hr = texture->GetLevelDesc(0, &desc);
// log_info("iidx:camhook", "Texture Desc Size: {}x{} Res Type: {} Format: {} Usage: {}", desc.Width, desc.Height, (int) desc.Type, (int) desc.Format, (int) desc.Usage);
// return hr;
// }
LONG TARGET_SURFACE_WIDTH = 1280;
LONG TARGET_SURFACE_HEIGHT = 720;
double RATIO_16_9 = 16.0 / 9.0;
double RATIO_4_3 = 4.0 / 3.0;
namespace games::iidx {
IIDXLocalCamera::IIDXLocalCamera(
std::string name,
BOOL prefer_16_by_9,
IMFActivate *pActivate,
IDirect3DDeviceManager9 *pD3DManager,
LPDIRECT3DDEVICE9EX device,
LPDIRECT3DTEXTURE9 *texture_target
):
m_nRefCount(1),
m_name(name),
m_prefer_16_by_9(prefer_16_by_9),
m_device(device),
m_texture_target(texture_target),
m_texture_original(*texture_target)
{
InitializeCriticalSection(&m_critsec);
HRESULT hr = S_OK;
IMFAttributes *pAttributes = nullptr;
EnterCriticalSection(&m_critsec);
log_info("iidx:camhook", "[{}] Creating camera", m_name);
// Get friendly name for log purposes
WCHAR *pwszFriendlyName = nullptr;
UINT32 m_cchFriendlyName = 0;
hr = pActivate->GetAllocatedString(
MF_DEVSOURCE_ATTRIBUTE_FRIENDLY_NAME,
&pwszFriendlyName,
&m_cchFriendlyName
);
if (SUCCEEDED(hr) && pwszFriendlyName != nullptr) {
log_misc("iidx:camhook", "[{}] Name: {}", m_name, ws2s(pwszFriendlyName));
m_friendly_name = ws2s(pwszFriendlyName);
CoTaskMemFree(pwszFriendlyName);
pwszFriendlyName = nullptr;
}
// Retrive symlink of Camera for control configurations
hr = pActivate->GetAllocatedString(
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK,
&m_pwszSymbolicLink,
&m_cchSymbolicLink
);
if (FAILED(hr)) { goto done; }
log_misc("iidx:camhook", "[{}] Symlink: {}", m_name, GetSymLink());
// Create the media source object.
hr = pActivate->ActivateObject(IID_PPV_ARGS(&m_pSource));
if (FAILED(hr)) { goto done; }
// Retain reference to the camera
m_pSource->AddRef();
log_misc("iidx:camhook", "[{}] Activated", m_name);
// Create an attribute store to hold initialization settings.
hr = WrappedMFCreateAttributes(&pAttributes, 2);
if (FAILED(hr)) { goto done; }
hr = pAttributes->SetUnknown(MF_SOURCE_READER_D3D_MANAGER, pD3DManager);
if (FAILED(hr)) { goto done; }
hr = pAttributes->SetUINT32(MF_SOURCE_READER_DISABLE_DXVA, FALSE);
if (FAILED(hr)) { goto done; }
// TODO: Color space conversion
// if (SUCCEEDED(hr)) {
// hr = pAttributes->SetUINT32(MF_SOURCE_READER_ENABLE_ADVANCED_VIDEO_PROCESSING, TRUE);
// }
// if (SUCCEEDED(hr)) {
// hr = pAttributes->SetUINT32(MF_READWRITE_DISABLE_CONVERTERS, FALSE);
// }
// Create the source reader.
hr = WrappedMFCreateSourceReaderFromMediaSource(
m_pSource,
pAttributes,
&m_pSourceReader
);
if (FAILED(hr)) { goto done; }
log_misc("iidx:camhook", "[{}] Created source reader", m_name);
hr = InitTargetTexture();
if (FAILED(hr)) { goto done; }
// Camera should be still usable even if camera control is not supported
InitCameraControl();
done:
if (SUCCEEDED(hr)) {
m_initialized = true;
log_misc("iidx:camhook", "[{}] Initialized", m_name);
} else {
log_warning("iidx:camhook", "[{}] Failed to create camera: {}", m_name, hr);
}
SafeRelease(&pAttributes);
LeaveCriticalSection(&m_critsec);
}
HRESULT IIDXLocalCamera::StartCapture() {
HRESULT hr = S_OK;
IMFMediaType *pType = nullptr;
if (!m_initialized) {
log_warning("iidx:camhook", "[{}] Camera not initialized", m_name);
return E_FAIL;
}
// Try to find a suitable output type.
log_misc("iidx:camhook", "[{}] Find best media type", m_name);
UINT32 bestWidth = 0;
double bestFrameRate = 0;
// The loop should terminate by MF_E_NO_MORE_TYPES
// Adding a hard limit just in case
for (DWORD i = 0; i < 1000; i++) {
hr = m_pSourceReader->GetNativeMediaType(
(DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM,
i,
&pType
);
if (FAILED(hr)) {
if (hr != MF_E_NO_MORE_TYPES) {
log_warning("iidx:camhook", "[{}] Cannot get media type {} {}", m_name, i, hr);
}
break;
}
hr = TryMediaType(pType, &bestWidth, &bestFrameRate);
if (SUCCEEDED(hr)) {
MediaTypeInfo info = GetMediaTypeInfo(pType);
m_mediaTypeInfos.push_back(info);
if (hr == S_OK) {
m_pAutoMediaType = pType;
}
} else {
// Invalid media type (e.g. no conversion function)
SafeRelease(&pType);
}
}
// Sort available media types
std::sort(m_mediaTypeInfos.begin(), m_mediaTypeInfos.end(), [](const MediaTypeInfo &a, const MediaTypeInfo &b) {
if (a.width != b.width) {
return a.width > b.width;
}
if (a.height != b.height) {
return a.height > b.height;
}
if (a.frameRate != b.frameRate) {
return (int)a.frameRate > (int)b.frameRate;
}
return a.subtype.Data1 > b.subtype.Data1;
});
if (!m_pAutoMediaType) {
m_pAutoMediaType = m_mediaTypeInfos.front().p_mediaType;
}
IMFMediaType *pSelectedMediaType = nullptr;
// Find media type specified by user configurations
if (!m_useAutoMediaType && m_selectedMediaTypeDescription.length() > 0) {
log_info("iidx:camhook", "[{}] Use media type from config {}", m_name, m_selectedMediaTypeDescription);
auto it = std::find_if(m_mediaTypeInfos.begin(), m_mediaTypeInfos.end(), [this](const MediaTypeInfo &item){
return item.description.compare(this->m_selectedMediaTypeDescription) == 0;
});
if (it != m_mediaTypeInfos.end()) {
pSelectedMediaType = (*it).p_mediaType;
}
}
hr = S_OK;
if (!pSelectedMediaType) {
pSelectedMediaType = m_pAutoMediaType;
}
if (SUCCEEDED(hr)) {
hr = ChangeMediaType(pSelectedMediaType);
}
if (SUCCEEDED(hr)) {
log_info("iidx:camhook", "[{}] Creating thread", m_name);
CreateThread();
}
return hr;
}
HRESULT IIDXLocalCamera::ChangeMediaType(IMFMediaType *pType) {
HRESULT hr = S_OK;
MediaTypeInfo info = GetMediaTypeInfo(pType);
log_info("iidx:camhook", "[{}] Changing media type: {}", m_name, info.description);
auto it = std::find_if(m_mediaTypeInfos.begin(), m_mediaTypeInfos.end(), [pType](const MediaTypeInfo &item) {
return item.p_mediaType == pType;
});
m_selectedMediaTypeIndex = it - m_mediaTypeInfos.begin();
m_selectedMediaTypeDescription = info.description;
if (SUCCEEDED(hr)) {
hr = m_pSourceReader->SetCurrentMediaType(
(DWORD)MF_SOURCE_READER_FIRST_VIDEO_STREAM,
NULL,
pType
);
}
if (SUCCEEDED(hr)) {
m_cameraWidth = info.width;
m_cameraHeight = info.height;
UpdateDrawRect();
}
return hr;
}
void IIDXLocalCamera::UpdateDrawRect() {
double cameraRatio = (double)m_cameraWidth / m_cameraHeight;
RECT cameraRect = {0, 0, m_cameraWidth, m_cameraHeight};
RECT targetRect = {0, 0, TARGET_SURFACE_WIDTH, TARGET_SURFACE_HEIGHT};
switch (m_drawMode) {
case DrawModeStretch: {
CopyRect(&m_rcSource, &cameraRect);
CopyRect(&m_rcDest, &targetRect);
break;
}
case DrawModeCrop: {
if (cameraRatio > RATIO_16_9) {
// take full source height, crop left/right
LONG croppedWidth = m_cameraHeight * RATIO_16_9;
m_rcSource.left = (LONG)(m_cameraWidth - croppedWidth) / 2;
m_rcSource.top = 0;
m_rcSource.right = m_rcSource.left + croppedWidth;
m_rcSource.bottom = m_cameraHeight;
} else {
// take full source width, crop top/bottom
LONG croppedHeight = m_cameraWidth / RATIO_16_9;
m_rcSource.left = 0;
m_rcSource.top = (LONG)(m_cameraHeight - croppedHeight) / 2;
m_rcSource.right = m_cameraWidth;
m_rcSource.bottom = m_rcSource.top + croppedHeight;
}
CopyRect(&m_rcDest, &targetRect);
break;
}
case DrawModeLetterbox: {
CopyRect(&m_rcSource, &cameraRect);
if (cameraRatio > RATIO_16_9) {
// take full dest width, empty top/bottom
LONG boxedHeight = TARGET_SURFACE_WIDTH / cameraRatio;
m_rcDest.left = 0;
m_rcDest.top = (LONG)(TARGET_SURFACE_HEIGHT - boxedHeight) / 2;
m_rcDest.right = TARGET_SURFACE_WIDTH;
m_rcDest.bottom = m_rcDest.top + boxedHeight;
} else {
// take full dest height, empty top/bottom
LONG boxedWidth = TARGET_SURFACE_HEIGHT * cameraRatio;
m_rcDest.left = (LONG)(TARGET_SURFACE_WIDTH - boxedWidth) / 2;
m_rcDest.top = 0;
m_rcDest.right = m_rcDest.left + boxedWidth;
m_rcDest.bottom = TARGET_SURFACE_HEIGHT;
}
break;
}
case DrawModeCrop4_3: {
if (cameraRatio > RATIO_4_3) {
// take full source height, crop left/right
LONG croppedWidth = m_cameraHeight * RATIO_4_3;
m_rcSource.left = (LONG)(m_cameraWidth - croppedWidth) / 2;
m_rcSource.top = 0;
m_rcSource.right = m_rcSource.left + croppedWidth;
m_rcSource.bottom = m_cameraHeight;
} else {
// take full source width, crop top/bottom
LONG croppedHeight = m_cameraWidth / RATIO_4_3;
m_rcSource.left = 0;
m_rcSource.top = (LONG)(m_cameraHeight - croppedHeight) / 2;
m_rcSource.right = m_cameraWidth;
m_rcSource.bottom = m_rcSource.top + croppedHeight;
}
CopyRect(&m_rcDest, &targetRect);
break;
}
case DrawModeLetterbox4_3: {
CopyRect(&m_rcSource, &cameraRect);
if (cameraRatio > RATIO_4_3) {
// take full dest width, empty top/bottom
LONG boxedHeight = TARGET_SURFACE_HEIGHT / RATIO_4_3;
m_rcDest.left = 0;
m_rcDest.top = (LONG)(TARGET_SURFACE_HEIGHT - boxedHeight) / 2;
m_rcDest.right = TARGET_SURFACE_WIDTH;
m_rcDest.bottom = m_rcDest.top + boxedHeight;
} else {
// take full dest height, empty top/bottom
LONG boxedWidth = TARGET_SURFACE_WIDTH * RATIO_4_3;
m_rcDest.left = (LONG)(TARGET_SURFACE_WIDTH - boxedWidth) / 2;
m_rcDest.top = 0;
m_rcDest.right = m_rcDest.left + boxedWidth;
m_rcDest.bottom = TARGET_SURFACE_HEIGHT;
}
break;
}
}
// ensure the rects are valid
IntersectRect(&m_rcSource, &m_rcSource, &cameraRect);
IntersectRect(&m_rcDest, &m_rcDest, &targetRect);
log_info(
"iidx:camhook", "[{}] Update draw rect mode={} src=({}, {}, {}, {}) dest=({}, {}, {}, {})",
m_name,
DRAW_MODE_LABELS[m_drawMode],
m_rcSource.left,
m_rcSource.top,
m_rcSource.right,
m_rcSource.bottom,
m_rcDest.left,
m_rcDest.top,
m_rcDest.right,
m_rcDest.bottom
);
m_device->ColorFill(m_pDestSurf, &targetRect, D3DCOLOR_XRGB(0, 0, 0));
}
void IIDXLocalCamera::CreateThread() {
// Create thread
m_drawThread = new std::thread([this]() {
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_BELOW_NORMAL);
double accumulator = 0.0;
while (this->m_active) {
this->Render();
double frameTimeMicroSec = (1000000.0 / this->m_frameRate);
int floorFrameTimeMicroSec = floor(frameTimeMicroSec);
// This maybe an overkill but who knows
accumulator += (frameTimeMicroSec - floorFrameTimeMicroSec);
if (accumulator > 1.0) {
accumulator -= 1.0;
floorFrameTimeMicroSec += 1;
}
std::this_thread::sleep_for(std::chrono::microseconds(floorFrameTimeMicroSec));
}
});
}
LPDIRECT3DTEXTURE9 IIDXLocalCamera::GetTexture() {
return m_texture;
}
IAMCameraControl* IIDXLocalCamera::GetCameraControl() {
return m_pCameraControl;
}
HRESULT IIDXLocalCamera::InitCameraControl() {
HRESULT hr = S_OK;
log_misc("iidx:camhook", "[{}] Init camera control", m_name);
hr = m_pSource->QueryInterface(IID_IAMCameraControl, (void**)&m_pCameraControl);
if (FAILED(hr)) {
// The device does not support IAMCameraControl
log_warning("iidx:camhook", "[{}] Camera control not supported", m_name);
return E_FAIL;
}
for (size_t i = 0; i < CAMERA_CONTROL_PROP_SIZE; i++) {
long minValue = 0;
long maxValue = 0;
long delta = 0;
long defaultValue = 0;
long defFlags = 0;
long value = 0;
long valueFlags = 0;
m_pCameraControl->GetRange(
i,
&minValue,
&maxValue,
&delta,
&defaultValue,
&defFlags
);
m_pCameraControl->Get(
i,
&value,
&valueFlags
);
m_controlProps.push_back({
minValue,
maxValue,
delta,
defaultValue,
defFlags,
value,
valueFlags,
});
CameraControlProp prop = m_controlProps.at(i);
log_misc(
"iidx:camhook", "[{}] >> {} range=({}, {}) default={} delta={} dFlags={} value={} vFlags={}",
m_name,
CAMERA_CONTROL_LABELS[i],
prop.minValue, prop.maxValue,
prop.defaultValue,
prop.delta,
prop.defFlags,
prop.value,
prop.valueFlags
);
}
m_controlOptionsInitialized = true;
return hr;
}
HRESULT IIDXLocalCamera::GetCameraControlProp(int index, CameraControlProp *pProp) {
if (!m_controlOptionsInitialized) {
return E_FAIL;
}
auto targetProp = m_controlProps.at(index);
pProp->minValue = targetProp.minValue;
pProp->maxValue = targetProp.maxValue;
pProp->defaultValue = targetProp.defaultValue;
pProp->delta = targetProp.delta;
pProp->defFlags = targetProp.defFlags;
pProp->value = targetProp.value;
pProp->valueFlags = targetProp.valueFlags;
return S_OK;
}
HRESULT IIDXLocalCamera::SetCameraControlProp(int index, long value, long flags) {
if (!m_controlOptionsInitialized || !m_allowManualControl) {
return E_FAIL;
}
if (index < 0 || index >= CAMERA_CONTROL_PROP_SIZE) {
return E_INVALIDARG;
}
auto targetProp = &(m_controlProps.at(index));
HRESULT hr = m_pCameraControl->Set(index, value, flags);
if (SUCCEEDED(hr)) {
m_pCameraControl->Get(
index,
&targetProp->value,
&targetProp->valueFlags
);
}
return hr;
}
HRESULT IIDXLocalCamera::ResetCameraControlProps() {
log_info("iidx:camhook", "[{}] Reset camera control", m_name);
for (size_t i = 0; i < CAMERA_CONTROL_PROP_SIZE; i++) {
CameraControlProp prop = m_controlProps.at(i);
SetCameraControlProp(i, prop.defaultValue, prop.defFlags);
}
return S_OK;
}
std::string IIDXLocalCamera::GetName() {
return m_name;
}
std::string IIDXLocalCamera::GetFriendlyName() {
return m_friendly_name;
}
std::string IIDXLocalCamera::GetSymLink() {
if (!m_pwszSymbolicLink) {
return "(unknown)";
}
return ws2s(m_pwszSymbolicLink);
}
MediaTypeInfo IIDXLocalCamera::GetMediaTypeInfo(IMFMediaType *pType) {
MediaTypeInfo info = {};
HRESULT hr = S_OK;
MFRatio frameRate = { 0, 0 };
info.p_mediaType = pType;
// Find the video subtype.
hr = pType->GetGUID(MF_MT_SUBTYPE, &info.subtype);
if (FAILED(hr)) { goto done; }
// Get the frame size.
hr = MFGetAttributeSize(pType, MF_MT_FRAME_SIZE, &info.width, &info.height);
if (FAILED(hr)) { goto done; }
// Get frame rate
hr = MFGetAttributeRatio(
pType,
MF_MT_FRAME_RATE,
(UINT32*)&frameRate.Numerator,
(UINT32*)&frameRate.Denominator
);
if (FAILED(hr)) { goto done; }
info.frameRate = frameRate.Numerator / frameRate.Denominator;
info.description = fmt::format(
"{}x{} @{}FPS {}",
info.width,
info.height,
(int)info.frameRate,
GetVideoFormatName(info.subtype)
);
done:
return info;
}
std::string IIDXLocalCamera::GetVideoFormatName(GUID subtype) {
if (subtype == MFVideoFormat_YUY2) {
return "YUY2";
}
if (subtype == MFVideoFormat_NV12) {
return "NV12";
}
if (subtype == MFVideoFormat_MJPG) {
return "MJPG";
}
return "Unknown";
}
/**
* Return values:
* S_OK: this is a "better" media type than the existing one
* S_FALSE: valid media type, but not "better"
* E_*: invalid meia type
*/
HRESULT IIDXLocalCamera::TryMediaType(IMFMediaType *pType, UINT32 *pBestWidth, double *pBestFrameRate) {
HRESULT hr = S_OK;
UINT32 width = 0, height = 0;
GUID subtype = { 0, 0, 0, 0 };
MFRatio frameRate = { 0, 0 };
hr = pType->GetGUID(MF_MT_SUBTYPE, &subtype);
if (FAILED(hr)) {
log_warning("iidx:camhook", "[{}] Failed to get subtype: {}", m_name, hr);
return hr;
}
hr = MFGetAttributeSize(pType, MF_MT_FRAME_SIZE, &width, &height);
if (FAILED(hr)) {
log_warning("iidx:camhook", "[{}] Failed to get frame size: {}", m_name, hr);
return hr;
}
// Only support format with converters
// TODO: verify conversion support with DXVA
if (subtype != MFVideoFormat_YUY2 && subtype != MFVideoFormat_NV12) {
return E_FAIL;
}
// Frame rate
hr = MFGetAttributeRatio(
pType,
MF_MT_FRAME_RATE,
(UINT32*)&frameRate.Numerator,
(UINT32*)&frameRate.Denominator
);
if (FAILED(hr)) {
log_warning("iidx:camhook", "[{}] Failed to get frame rate: {}", m_name, hr);
return hr;
}
double frameRateValue = frameRate.Numerator / frameRate.Denominator;
// Filter by aspect ratio
auto aspect_ratio = 4.f / 3.f;
if (m_prefer_16_by_9) {
aspect_ratio = 16.f / 9.f;
}
if (fabs((height * aspect_ratio) - width) > 0.01f) {
return S_FALSE;
}
// If we have 1280x720 already, only try for better frame rate
if ((*pBestWidth >= (UINT32)TARGET_SURFACE_WIDTH) && (width > *pBestWidth) && (frameRateValue < *pBestFrameRate)) {
return S_FALSE;
}
// Check if this format has better resolution / frame rate
if ((width > *pBestWidth) || (width >= (UINT32)TARGET_SURFACE_WIDTH && frameRateValue >= *pBestFrameRate)) {
// log_misc(
// "iidx:camhook", "Better media type {} ({}x{}) @({} FPS)",
// GetVideoFormatName(subtype),
// width,
// height,
// (int)frameRateValue
// );
*pBestWidth = width;
*pBestFrameRate = frameRateValue;
return S_OK;
}
return S_FALSE;
}
HRESULT IIDXLocalCamera::InitTargetTexture() {
HRESULT hr = S_OK;
// Create a new destination texture
hr = m_device->CreateTexture(TARGET_SURFACE_WIDTH, TARGET_SURFACE_HEIGHT, 1, D3DUSAGE_RENDERTARGET, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, &m_texture, NULL);
if (FAILED(hr)) { goto done; }
// Create a D3D9 surface for the destination texture so that camera sample can be drawn onto it
hr = m_texture->GetSurfaceLevel(0, &m_pDestSurf);
if (FAILED(hr)) { goto done; }
// Make the game use this new texture as camera stream source
*m_texture_target = m_texture;
m_active = TRUE;
// Create texture for colour conversion
hr = m_device->CreateTexture(TARGET_SURFACE_WIDTH, TARGET_SURFACE_HEIGHT, 1, D3DUSAGE_RENDERTARGET, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, &m_conversionTexture, NULL);
if (FAILED(hr)) { goto done; }
hr = m_conversionTexture->GetSurfaceLevel(0, &m_pConversionSurf);
if (FAILED(hr)) { goto done; }
// Create texture for transformation
hr = m_device->CreateTexture(TARGET_SURFACE_WIDTH, TARGET_SURFACE_HEIGHT, 1, D3DUSAGE_DYNAMIC, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, &m_transformTexture, NULL);
if (FAILED(hr)) { goto done; }
hr = m_transformTexture->GetSurfaceLevel(0, &m_pTransformSurf);
if (FAILED(hr)) { goto done; }
// Create texture for transformation result so that we don't have to screw our brain doing in-memory flipping
hr = m_device->CreateTexture(TARGET_SURFACE_WIDTH, TARGET_SURFACE_HEIGHT, 1, D3DUSAGE_DYNAMIC, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, &m_transformResultTexture, NULL);
if (FAILED(hr)) { goto done; }
hr = m_transformResultTexture->GetSurfaceLevel(0, &m_pTransformResultSurf);
if (FAILED(hr)) { goto done; }
// printTextureLevelDesc(m_texture);
// printTextureLevelDesc(m_texture_original);
done:
if (SUCCEEDED(hr)) {
log_misc("iidx:camhook", "[{}] Created texture", m_name);
} else {
log_warning("iidx:camhook", "[{}] Failed to create texture: {}", m_name, hr);
}
return hr;
}
HRESULT IIDXLocalCamera::FlushDrawCommands() {
IDirect3DQuery9* pEventQuery = nullptr;
// It is necessary to flush the command queue
// or the data is not ready for the receiver to read.
// Adapted from : https://msdn.microsoft.com/en-us/library/windows/desktop/bb172234%28v=vs.85%29.aspx
// Also see : http://www.ogre3d.org/forums/viewtopic.php?f=5&t=50486
m_device->CreateQuery(D3DQUERYTYPE_EVENT, &pEventQuery);
if (pEventQuery) {
pEventQuery->Issue(D3DISSUE_END);
while (S_FALSE == pEventQuery->GetData(NULL, 0, D3DGETDATA_FLUSH));
pEventQuery->Release(); // Must be released or causes a leak and reference count increment
}
return S_OK;
}
HRESULT IIDXLocalCamera::DrawSample(IMFMediaBuffer *pSrcBuffer) {
if (!m_active) {
return E_FAIL;
}
// snap variables now so they don't change while inside the critical section
const auto flip_h = m_flipHorizontal;
const auto flip_v = m_flipVertical;
EnterCriticalSection(&m_critsec);
HRESULT hr = S_OK;
IDirect3DSurface9 *pCameraSurf = NULL;
hr = WrappedMFGetService(pSrcBuffer, MR_BUFFER_SERVICE, IID_PPV_ARGS(&pCameraSurf));
if (flip_h || flip_v) {
// Stretch Camera content to texture and perform color space conversion
hr = m_device->StretchRect(pCameraSurf, &m_rcSource, m_pConversionSurf, &m_rcDest, D3DTEXF_LINEAR);
// Copy converted camera content to dynamic texture for vertical/horizontal flipping
hr = m_device->StretchRect(m_pConversionSurf, NULL, m_pTransformSurf, NULL, D3DTEXF_NONE);
// Transform
D3DLOCKED_RECT srcLockedRect;
hr = m_pTransformSurf->LockRect(&srcLockedRect, NULL, D3DLOCK_NOSYSLOCK | D3DLOCK_READONLY);
D3DLOCKED_RECT destLockedRect;
hr = m_pTransformResultSurf->LockRect(&destLockedRect, NULL, D3DLOCK_NOSYSLOCK);
BYTE* pSrc = (BYTE*)srcLockedRect.pBits;
BYTE* pDest = (BYTE*)destLockedRect.pBits;
const int pixelSize = 4;
if (flip_v) {
pDest += destLockedRect.Pitch * (TARGET_SURFACE_HEIGHT - 1);
}
for (int y = 0; y < TARGET_SURFACE_HEIGHT; y++) {
for (int x = 0; x < TARGET_SURFACE_WIDTH; x++) {
memcpy(
pDest + x * pixelSize,
pSrc + (flip_h ? (TARGET_SURFACE_WIDTH - x - 1) : x) * pixelSize,
pixelSize
);
}
pSrc += srcLockedRect.Pitch;
if (flip_v) {
pDest -= destLockedRect.Pitch;
} else {
pDest += destLockedRect.Pitch;
}
}
m_pTransformSurf->UnlockRect();
m_pTransformResultSurf->UnlockRect();
// Stretch camera texture to transform surface
hr = m_device->StretchRect(m_pTransformResultSurf, NULL, m_pDestSurf, NULL, D3DTEXF_NONE);
} else {
// No transformation needed, stretch to destination texture directly
hr = m_device->StretchRect(pCameraSurf, &m_rcSource, m_pDestSurf, &m_rcDest, D3DTEXF_LINEAR);
}
FlushDrawCommands();
if (FAILED(hr)) {
log_warning("iidx:camhook", "Error in DrawSample {}", hr);
}
SafeRelease(&pCameraSurf);
LeaveCriticalSection(&m_critsec);
return hr;
}
HRESULT IIDXLocalCamera::ReadSample() {
HRESULT hr;
DWORD streamIndex, flags;
LONGLONG llTimeStamp;
IMFSample *pSample = nullptr;
IMFMediaBuffer *pBuffer = nullptr;
hr = m_pSourceReader->ReadSample(
MF_SOURCE_READER_FIRST_VIDEO_STREAM,
0,
&streamIndex,
&flags,
&llTimeStamp,
&pSample
);
if (pSample) {
// Draw to D3D
pSample->GetBufferByIndex(0, &pBuffer);
hr = DrawSample(pBuffer);
}
SafeRelease(&pBuffer);
SafeRelease(&pSample);
return hr;
}
LPDIRECT3DTEXTURE9 IIDXLocalCamera::Render() {
if (!m_active) {
return nullptr;
}
HRESULT hr = ReadSample();
if (FAILED(hr)) {
return nullptr;
}
return m_texture;
}
ULONG IIDXLocalCamera::Release() {
log_info("iidx:camhook", "[{}] Release camera", m_name);
m_active = false;
ULONG uCount = InterlockedDecrement(&m_nRefCount);
*m_texture_target = m_texture_original;
for (size_t i = 0; i < m_mediaTypeInfos.size(); i++) {
SafeRelease(&(m_mediaTypeInfos.at(i).p_mediaType));
}
SafeRelease(&m_pDestSurf);
SafeRelease(&m_pTransformSurf);
SafeRelease(&m_pConversionSurf);
SafeRelease(&m_pTransformResultSurf);
SafeRelease(&m_texture);
SafeRelease(&m_conversionTexture);
SafeRelease(&m_transformTexture);
SafeRelease(&m_transformResultTexture);
if (m_pSource) {
m_pSource->Shutdown();
m_pSource->Release();
}
CoTaskMemFree(m_pwszSymbolicLink);
m_pwszSymbolicLink = NULL;
m_cchSymbolicLink = 0;
if (uCount == 0) {
delete this;
}
// For thread safety, return a temporary variable.
return uCount;
}
}
#endif
+187
View File
@@ -0,0 +1,187 @@
#pragma once
#if SPICE64
#include <d3d9.h>
#include <dxva2api.h>
#include <mutex>
#include <mferror.h>
#include <string>
#include <thread>
#include <strmif.h>
#include <vector>
#include "mf_wrappers.h"
#define CAMERA_CONTROL_PROP_SIZE 7
#define DRAW_MODE_SIZE 5
template <class T> void SafeRelease(T **ppT)
{
if (*ppT)
{
(*ppT)->Release();
*ppT = nullptr;
}
}
typedef void (*IMAGE_TRANSFORM_FN)(
BYTE* pDest,
LONG lDestStride,
const BYTE* pSrc,
LONG lSrcStride,
DWORD dwWidthInPixels,
DWORD dwHeightInPixels
);
struct CameraControlProp {
long minValue = 0;
long maxValue = 0;
long delta = 0;
long defaultValue = 0;
long defFlags = 0;
long value = 0;
long valueFlags = 0;
};
struct MediaTypeInfo {
GUID subtype = GUID_NULL;
UINT32 width = 0;
UINT32 height = 0;
double frameRate = 0.0;
IMFMediaType* p_mediaType = nullptr;
std::string description = "";
LONG *plStride = nullptr;
};
typedef enum {
DrawModeStretch = 0,
DrawModeCrop = 1,
DrawModeLetterbox = 2,
DrawModeCrop4_3 = 3,
DrawModeLetterbox4_3 = 4,
} LocalCameraDrawMode;
extern std::string CAMERA_CONTROL_LABELS[];
extern std::string DRAW_MODE_LABELS[];
namespace games::iidx {
class IIDXLocalCamera {
protected:
virtual ~IIDXLocalCamera() {};
ULONG m_nRefCount;
CRITICAL_SECTION m_critsec;
std::string m_name;
std::string m_friendly_name;
BOOL m_prefer_16_by_9;
WCHAR *m_pwszSymbolicLink = nullptr;
UINT32 m_cchSymbolicLink = 0;
// For reading frames from Camera
IMFMediaSource *m_pSource = nullptr;
IMFSourceReader *m_pSourceReader = nullptr;
// Camera Format information
double m_frameRate = 0;
LONG m_cameraWidth;
LONG m_cameraHeight;
// Draw rectangles
RECT m_rcSource;
RECT m_rcDest;
// Thread to draw texture asynchorously
std::thread *m_drawThread = nullptr;
// DirectX9 DeviceEx
LPDIRECT3DDEVICE9EX m_device;
// Address to hook camera texture onto the game
LPDIRECT3DTEXTURE9 *m_texture_target = nullptr;
// Target texture (to be shown in the game)
LPDIRECT3DTEXTURE9 m_texture = nullptr;
IDirect3DSurface9 *m_pDestSurf = nullptr;
// Texture for color space conversion
LPDIRECT3DTEXTURE9 m_conversionTexture = nullptr;
IDirect3DSurface9 *m_pConversionSurf = nullptr;
// Texture for custom transform (e.g. horizontal flip)
LPDIRECT3DTEXTURE9 m_transformTexture = nullptr;
IDirect3DSurface9 *m_pTransformSurf = nullptr;
// Texture to store "transformed" camera content
LPDIRECT3DTEXTURE9 m_transformResultTexture = nullptr;
IDirect3DSurface9 *m_pTransformResultSurf = nullptr;
// Storing original texture for clean up
LPDIRECT3DTEXTURE9 m_texture_original = nullptr;
IAMCameraControl *m_pCameraControl = nullptr;
// Camera Control
std::vector<CameraControlProp> m_controlProps = {};
BOOL m_controlOptionsInitialized = false;
public:
// True if first part of the setup steps (those in the constructor) succeeded
BOOL m_initialized = false;
// True if all the setup steps succeeded
BOOL m_active = false;
// Media type select
std::vector<MediaTypeInfo> m_mediaTypeInfos = {};
int m_selectedMediaTypeIndex = 0;
bool m_useAutoMediaType = true;
IMFMediaType *m_pAutoMediaType = nullptr;
std::string m_selectedMediaTypeDescription = "";
bool m_allowManualControl = false;
LocalCameraDrawMode m_drawMode = DrawModeCrop4_3;
// Render processing
bool m_flipHorizontal = false;
bool m_flipVertical = false;
IIDXLocalCamera(
std::string name,
BOOL prefer_16_by_9,
IMFActivate *pActivate,
IDirect3DDeviceManager9 *pD3DManager,
LPDIRECT3DDEVICE9EX device,
LPDIRECT3DTEXTURE9 *texture_target
);
LPDIRECT3DTEXTURE9 GetTexture();
ULONG Release();
IAMCameraControl* GetCameraControl();
HRESULT GetCameraControlProp(int index, CameraControlProp *pProp);
HRESULT SetCameraControlProp(int index, long value, long flags);
HRESULT ResetCameraControlProps();
HRESULT FlushDrawCommands();
std::string GetName();
std::string GetFriendlyName();
std::string GetSymLink();
HRESULT ChangeMediaType(IMFMediaType *pType);
HRESULT StartCapture();
void UpdateDrawRect();
private:
HRESULT CreateD3DDeviceManager();
void CreateThread();
MediaTypeInfo GetMediaTypeInfo(IMFMediaType *pType);
std::string GetVideoFormatName(GUID subtype);
HRESULT TryMediaType(IMFMediaType *pType, UINT32 *pBestWidth, double *pBestFrameRate);
HRESULT InitTargetTexture();
HRESULT InitCameraControl();
HRESULT DrawSample(IMFMediaBuffer *pSrcBuffer);
HRESULT ReadSample();
LPDIRECT3DTEXTURE9 Render();
};
}
#endif
+116
View File
@@ -0,0 +1,116 @@
#include "mf_wrappers.h"
#include "util/libutils.h"
#include "util/logging.h"
namespace games::iidx {
static bool INITIALIZED = false;
static HMODULE mf_dll = nullptr;
static HMODULE mfreadwrite_dll = nullptr;
static HMODULE mfplat_dll = nullptr;
typedef HRESULT (__stdcall * MFCreateAttributes_t)(
_Out_ IMFAttributes** ppMFAttributes,
_In_ UINT32 cInitialSize
);
typedef HRESULT (__stdcall * MFEnumDeviceSources_t)(
_In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
_Out_ UINT32* pcSourceActivate
);
typedef HRESULT (__stdcall * MFCreateSourceReaderFromMediaSource_t)(
_In_ IMFMediaSource *pMediaSource,
_In_opt_ IMFAttributes *pAttributes,
_Out_ IMFSourceReader **ppSourceReader
);
typedef HRESULT (__stdcall * MFGetService_t)(
IUnknown* punkObject,
REFGUID guidService,
REFIID riid,
_Outptr_ LPVOID* ppvObject
);
static MFCreateAttributes_t MFCreateAttributes = nullptr;
static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr;
static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr;
static MFGetService_t MFGetService = nullptr;
void init_mf_library() {
// why was all of this needed?
//
// when iidx camhook was initially implemented, we linked to mf.lib, mfreadwrite.lib, and mfplat.lib
// this made Unity-based really unhappy, causing them to skip over the logic that loads mf library
// causing videos to not play ("Initializing Microsoft Media Foundation failed." in the cmd prompt)
//
// as a result, the static linking to mf libs were removed, and we are now doing the mess that is this file
if (INITIALIZED) {
return;
}
INITIALIZED = true;
log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - BEGIN");
mf_dll = libutils::load_library("mf.dll", true);
mfreadwrite_dll = libutils::load_library("mfreadwrite.dll", true);
mfplat_dll = libutils::load_library("mfplat.dll", true);
MFCreateAttributes = (MFCreateAttributes_t)
libutils::get_proc(mfplat_dll, "MFCreateAttributes");
if (!MFCreateAttributes) {
log_fatal("mf_wrappers", "MFCreateAttributes failed to hook");
}
MFEnumDeviceSources = (MFEnumDeviceSources_t)
libutils::get_proc(mf_dll, "MFEnumDeviceSources");
if (!MFEnumDeviceSources) {
log_fatal("mf_wrappers", "MFEnumDeviceSources failed to hook");
}
MFCreateSourceReaderFromMediaSource = (MFCreateSourceReaderFromMediaSource_t)
libutils::get_proc(mfreadwrite_dll, "MFCreateSourceReaderFromMediaSource");
if (!MFCreateSourceReaderFromMediaSource) {
log_fatal("mf_wrappers", "MFCreateSourceReaderFromMediaSource failed to hook");
}
MFGetService = (MFGetService_t)libutils::get_proc(mf_dll, "MFGetService");
if (!MFGetService) {
log_fatal("mf_wrappers", "MFGetService failed to hook");
}
log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - DONE");
}
HRESULT WrappedMFCreateAttributes (
_Out_ IMFAttributes** ppMFAttributes,
_In_ UINT32 cInitialSize) {
return MFCreateAttributes(ppMFAttributes, cInitialSize);
}
HRESULT WrappedMFEnumDeviceSources (
_In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
_Out_ UINT32* pcSourceActivate) {
return MFEnumDeviceSources(pAttributes, pppSourceActivate, pcSourceActivate);
}
HRESULT WrappedMFCreateSourceReaderFromMediaSource (
_In_ IMFMediaSource *pMediaSource,
_In_opt_ IMFAttributes *pAttributes,
_Out_ IMFSourceReader **ppSourceReader) {
return MFCreateSourceReaderFromMediaSource(pMediaSource, pAttributes, ppSourceReader);
}
HRESULT WrappedMFGetService (
IUnknown* punkObject,
REFGUID guidService,
REFIID riid,
_Outptr_ LPVOID* ppvObject) {
return MFGetService(punkObject, guidService, riid, ppvObject);
}
}
+30
View File
@@ -0,0 +1,30 @@
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mfobjects.h>
#pragma once
namespace games::iidx {
void init_mf_library();
HRESULT WrappedMFCreateAttributes (
_Out_ IMFAttributes** ppMFAttributes,
_In_ UINT32 cInitialSize);
HRESULT WrappedMFEnumDeviceSources (
_In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
_Out_ UINT32* pcSourceActivate);
HRESULT WrappedMFCreateSourceReaderFromMediaSource (
_In_ IMFMediaSource *pMediaSource,
_In_opt_ IMFAttributes *pAttributes,
_Out_ IMFSourceReader **ppSourceReader);
HRESULT WrappedMFGetService (
IUnknown* punkObject,
REFGUID guidService,
REFIID riid,
_Outptr_ LPVOID* ppvObject);
}
+345
View File
@@ -0,0 +1,345 @@
#include "poke.h"
#include <thread>
#include "windows.h"
#include "cfg/screen_resize.h"
#include "games/io.h"
#include "games/iidx/iidx.h"
#include "hooks/graphics/graphics.h"
#include "launcher/shutdown.h"
#include "misc/eamuse.h"
#include "overlay/overlay.h"
#include "overlay/windows/generic_sub.h"
#include "rawinput/rawinput.h"
#include "touch/touch.h"
#include "util/libutils.h"
#include "util/logging.h"
#define POKE_NATIVE_TOUCH 0
#if POKE_NATIVE_TOUCH
static HINSTANCE USER32_INSTANCE = nullptr;
typedef BOOL (WINAPI *InitializeTouchInjection_t)(UINT32, DWORD);
typedef BOOL (WINAPI *InjectTouchInput_t)(UINT32, POINTER_TOUCH_INFO*);
static InitializeTouchInjection_t pInitializeTouchInjection = nullptr;
static InjectTouchInput_t pInjectTouchInput = nullptr;
#endif
namespace games::iidx::poke {
static std::thread *THREAD = nullptr;
static bool THREAD_RUNNING = false;
struct KeypadMapping {
char character;
uint16_t state;
};
static KeypadMapping KEYPAD_MAPPINGS[] = {
{ '0', 1 << EAM_IO_KEYPAD_0 },
{ '1', 1 << EAM_IO_KEYPAD_1 },
{ '2', 1 << EAM_IO_KEYPAD_2 },
{ '3', 1 << EAM_IO_KEYPAD_3 },
{ '4', 1 << EAM_IO_KEYPAD_4 },
{ '5', 1 << EAM_IO_KEYPAD_5 },
{ '6', 1 << EAM_IO_KEYPAD_6 },
{ '7', 1 << EAM_IO_KEYPAD_7 },
{ '8', 1 << EAM_IO_KEYPAD_8 },
{ '9', 1 << EAM_IO_KEYPAD_9 },
{ 'A', 1 << EAM_IO_KEYPAD_00 },
// Touch panel keypad does not have the decimal key.
// This will be used for toggling the keypad instead
{ 'D', 1 << EAM_IO_KEYPAD_DECIMAL },
};
const int IIDX_KEYPAD_BUTTON_SIZE = 90;
const int IIDX_KEYPAD_GAP = 15;
const int IIDX_KEYPAD_TOP = 570;
const int IIDX_KEYPAD_LEFT_1P = 90;
const int IIDX_KEYPAD_LEFT_2P = 1530;
// <KeypadID>/<Key> mapped to 1080p coordinate space
static const std::unordered_map<std::string, int> IIDX_KEYPAD_POSITION_X {
{"0/0", (int)(IIDX_KEYPAD_LEFT_1P + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"0/1", (int)(IIDX_KEYPAD_LEFT_1P + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"0/2", (int)(IIDX_KEYPAD_LEFT_1P + IIDX_KEYPAD_BUTTON_SIZE * 1.5 + IIDX_KEYPAD_GAP)},
{"0/3", (int)(IIDX_KEYPAD_LEFT_1P + IIDX_KEYPAD_BUTTON_SIZE * 2.5 + IIDX_KEYPAD_GAP * 2)},
{"0/4", (int)(IIDX_KEYPAD_LEFT_1P + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"0/5", (int)(IIDX_KEYPAD_LEFT_1P + IIDX_KEYPAD_BUTTON_SIZE * 1.5 + IIDX_KEYPAD_GAP)},
{"0/6", (int)(IIDX_KEYPAD_LEFT_1P + IIDX_KEYPAD_BUTTON_SIZE * 2.5 + IIDX_KEYPAD_GAP * 2)},
{"0/7", (int)(IIDX_KEYPAD_LEFT_1P + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"0/8", (int)(IIDX_KEYPAD_LEFT_1P + IIDX_KEYPAD_BUTTON_SIZE * 1.5 + IIDX_KEYPAD_GAP)},
{"0/9", (int)(IIDX_KEYPAD_LEFT_1P + IIDX_KEYPAD_BUTTON_SIZE * 2.5 + IIDX_KEYPAD_GAP * 2)},
{"0/A", (int)(IIDX_KEYPAD_LEFT_1P + IIDX_KEYPAD_BUTTON_SIZE * 1.5 + IIDX_KEYPAD_GAP)},
{"0/D", 60},
{"1/0", (int)(IIDX_KEYPAD_LEFT_2P + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"1/1", (int)(IIDX_KEYPAD_LEFT_2P + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"1/2", (int)(IIDX_KEYPAD_LEFT_2P + IIDX_KEYPAD_BUTTON_SIZE * 1.5 + IIDX_KEYPAD_GAP)},
{"1/3", (int)(IIDX_KEYPAD_LEFT_2P + IIDX_KEYPAD_BUTTON_SIZE * 2.5 + IIDX_KEYPAD_GAP * 2)},
{"1/4", (int)(IIDX_KEYPAD_LEFT_2P + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"1/5", (int)(IIDX_KEYPAD_LEFT_2P + IIDX_KEYPAD_BUTTON_SIZE * 1.5 + IIDX_KEYPAD_GAP)},
{"1/6", (int)(IIDX_KEYPAD_LEFT_2P + IIDX_KEYPAD_BUTTON_SIZE * 2.5 + IIDX_KEYPAD_GAP * 2)},
{"1/7", (int)(IIDX_KEYPAD_LEFT_2P + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"1/8", (int)(IIDX_KEYPAD_LEFT_2P + IIDX_KEYPAD_BUTTON_SIZE * 1.5 + IIDX_KEYPAD_GAP)},
{"1/9", (int)(IIDX_KEYPAD_LEFT_2P + IIDX_KEYPAD_BUTTON_SIZE * 2.5 + IIDX_KEYPAD_GAP * 2)},
{"1/A", (int)(IIDX_KEYPAD_LEFT_2P + IIDX_KEYPAD_BUTTON_SIZE * 1.5 + IIDX_KEYPAD_GAP)},
{"1/D", 1920 - 60},
};
static const std::unordered_map<std::string, int> IIDX_KEYPAD_POSITION_Y {
{"0/0", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP * 3 + IIDX_KEYPAD_BUTTON_SIZE * 3.5)},
{"0/1", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP * 2 + IIDX_KEYPAD_BUTTON_SIZE * 2.5)},
{"0/2", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP * 2 + IIDX_KEYPAD_BUTTON_SIZE * 2.5)},
{"0/3", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP * 2 + IIDX_KEYPAD_BUTTON_SIZE * 2.5)},
{"0/4", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP + IIDX_KEYPAD_BUTTON_SIZE * 1.5)},
{"0/5", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP + IIDX_KEYPAD_BUTTON_SIZE * 1.5)},
{"0/6", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP + IIDX_KEYPAD_BUTTON_SIZE * 1.5)},
{"0/7", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"0/8", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"0/9", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"0/A", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP * 3 + IIDX_KEYPAD_BUTTON_SIZE * 3.5)},
{"0/D", 50},
{"1/0", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP * 3 + IIDX_KEYPAD_BUTTON_SIZE * 3.5)},
{"1/1", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP * 2 + IIDX_KEYPAD_BUTTON_SIZE * 2.5)},
{"1/2", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP * 2 + IIDX_KEYPAD_BUTTON_SIZE * 2.5)},
{"1/3", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP * 2 + IIDX_KEYPAD_BUTTON_SIZE * 2.5)},
{"1/4", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP + IIDX_KEYPAD_BUTTON_SIZE * 1.5)},
{"1/5", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP + IIDX_KEYPAD_BUTTON_SIZE * 1.5)},
{"1/6", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP + IIDX_KEYPAD_BUTTON_SIZE * 1.5)},
{"1/7", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"1/8", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"1/9", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_BUTTON_SIZE * 0.5)},
{"1/A", (int)(IIDX_KEYPAD_TOP + IIDX_KEYPAD_GAP * 3 + IIDX_KEYPAD_BUTTON_SIZE * 3.5)},
{"1/D", 50},
};
#if POKE_NATIVE_TOUCH
void initialize_native_touch_library() {
if (USER32_INSTANCE == nullptr) {
USER32_INSTANCE = libutils::load_library("user32.dll");
}
pInitializeTouchInjection = libutils::try_proc<InitializeTouchInjection_t>(
USER32_INSTANCE, "InitializeTouchInjection");
pInjectTouchInput = libutils::try_proc<InjectTouchInput_t>(
USER32_INSTANCE, "InjectTouchInput");
}
void emulate_native_touch(TouchPoint tp, bool is_down) {
if (pInjectTouchInput == nullptr) {
return;
}
POINTER_TOUCH_INFO contact;
BOOL bRet = TRUE;
const int contact_offset = 2;
memset(&contact, 0, sizeof(POINTER_TOUCH_INFO));
contact.pointerInfo.pointerType = PT_TOUCH;
contact.pointerInfo.pointerId = 0;
contact.pointerInfo.ptPixelLocation.x = tp.x;
contact.pointerInfo.ptPixelLocation.y = tp.y;
if (is_down) {
contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
} else {
contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
}
contact.pointerInfo.dwTime = 0;
contact.pointerInfo.PerformanceCount = 0;
contact.touchFlags = TOUCH_FLAG_NONE;
contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
contact.orientation = 0;
contact.pressure = 1024;
contact.rcContact.top = tp.y - contact_offset;
contact.rcContact.bottom = tp.y + contact_offset;
contact.rcContact.left = tp.x - contact_offset;
contact.rcContact.right = tp.x + contact_offset;
bRet = InjectTouchInput(1, &contact);
if (!bRet) {
log_warning("poke", "error injecting native touch {}: ({}, {}) error: {}", is_down ? "down" : "up", tp.x, tp.y, GetLastError());
}
}
void emulate_native_touch_points(std::vector<TouchPoint> *touch_points) {
int i = 0;
for (auto &touch : *touch_points) {
emulate_native_touch(touch, true);
}
}
void clear_native_touch_points(std::vector<TouchPoint> *touch_points) {
for (auto &touch : *touch_points) {
emulate_native_touch(touch, false);
}
touch_points->clear();
}
#endif
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
std::vector<DWORD> touch_ids;
for (auto &touch : *touch_points) {
touch_ids.emplace_back(touch.id);
}
touch_remove_points(&touch_ids);
touch_points->clear();
}
void enable() {
// check if already running
if (THREAD)
return;
// create new thread
THREAD_RUNNING = true;
THREAD = new std::thread([] {
// log
log_info("poke", "enabled");
std::vector<TouchPoint> touch_points;
std::vector<uint16_t> last_keypad_state = {0, 0};
#if POKE_NATIVE_TOUCH
bool use_native = games::iidx::NATIVE_TOUCH;
if (use_native) {
initialize_native_touch_library();
if (pInitializeTouchInjection != nullptr) {
pInitializeTouchInjection(1, TOUCH_FEEDBACK_NONE);
}
}
#else
bool use_native = false;
#endif
// set variable to false to stop
while (THREAD_RUNNING) {
// clean up touch from last frame
if (touch_points.size() > 0) {
#if POKE_NATIVE_TOUCH
if (use_native) {
clear_native_touch_points(&touch_points);
} else {
clear_touch_points(&touch_points);
}
#else
clear_touch_points(&touch_points);
#endif
}
for (int unit = 0; unit < 2; unit++) {
// get keypad state
auto state = eamuse_get_keypad_state(unit);
if (state != 0) {
// add keys
for (auto &mapping : KEYPAD_MAPPINGS) {
if (state & mapping.state) {
if (last_keypad_state[unit] & mapping.state) {
// log_warning("poke", "ignoring hold {} {}", unit, mapping.character);
continue;
}
std::string handle = fmt::format("{0}/{1}", unit, mapping.character);
auto x_iter = IIDX_KEYPAD_POSITION_X.find(handle);
auto y_iter = IIDX_KEYPAD_POSITION_Y.find(handle);
if (x_iter != IIDX_KEYPAD_POSITION_X.end() && y_iter != IIDX_KEYPAD_POSITION_Y.end()) {
DWORD touch_id = (DWORD)(0xFFFFFF * unit + mapping.character);
float x = x_iter->second / 1920.0;
float y = y_iter->second / 1080.0;
if (use_native) {
x *= rawinput::TOUCHSCREEN_RANGE_X;
y *= rawinput::TOUCHSCREEN_RANGE_Y;
} else if (GRAPHICS_IIDX_WSUB) {
// Scale to windowed subscreen
x *= GRAPHICS_IIDX_WSUB_WIDTH;
y *= GRAPHICS_IIDX_WSUB_HEIGHT;
} else if (GENERIC_SUB_WINDOW_FULLSIZE || !overlay::OVERLAY->get_active()) {
// Overlay is not present, scale to main screen
if (GRAPHICS_WINDOWED) {
x *= SPICETOUCH_TOUCH_WIDTH;
y *= SPICETOUCH_TOUCH_HEIGHT;
} else {
x *= ImGui::GetIO().DisplaySize.x;
y *= ImGui::GetIO().DisplaySize.y;
}
} else {
// Overlay subscreen
x = (GENERIC_SUB_WINDOW_X + x * GENERIC_SUB_WINDOW_WIDTH);
y = (GENERIC_SUB_WINDOW_Y + y * GENERIC_SUB_WINDOW_HEIGHT);
// Scale to window size ratio
if (GRAPHICS_WINDOWED) {
x *= SPICETOUCH_TOUCH_WIDTH / ImGui::GetIO().DisplaySize.x;
y *= SPICETOUCH_TOUCH_HEIGHT / ImGui::GetIO().DisplaySize.y;
}
}
TouchPoint tp {
.id = touch_id,
.x = (LONG)x,
.y = (LONG)y,
.mouse = true,
};
touch_points.emplace_back(tp);
// log_warning("poke", "coords: {} {}", to_string(tp.x), to_string(tp.y));
}
}
} // for all keys
} // if state != 0
last_keypad_state[unit] = state;
} // for all units
if (touch_points.size() > 0) {
#if POKE_NATIVE_TOUCH
if (use_native) {
emulate_native_touch_points(&touch_points);
} else {
touch_write_points(&touch_points);
}
#else
touch_write_points(&touch_points);
#endif
}
// slow down
Sleep(50);
}
return nullptr;
});
}
void disable() {
if (!THREAD) {
return;
}
// stop old thread
THREAD_RUNNING = false;
THREAD->join();
// delete thread
delete THREAD;
THREAD = nullptr;
// log
log_info("poke", "disabled");
}
}
+6
View File
@@ -0,0 +1,6 @@
#pragma once
namespace games::iidx::poke {
void enable();
void disable();
}