Initial re-upload of spice2x-24-08-24

This commit is contained in:
2024-08-28 11:10:34 -04:00
commit caa9e02285
1181 changed files with 380065 additions and 0 deletions

246
games/iidx/bi2a.cpp Normal file
View File

@@ -0,0 +1,246 @@
#include "bi2a.h"
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "util/utils.h"
#include "iidx.h"
#include "io.h"
using namespace acioemu;
games::iidx::IIDXFMSerialHandle::FMSerialDevice::FMSerialDevice() {
this->node_count = 1;
}
bool games::iidx::IIDXFMSerialHandle::FMSerialDevice::parse_msg(
MessageData *msg_in,
circular_buffer<uint8_t> *response_buffer
) {
#ifdef ACIOEMU_LOG
log_info("iidx", "BI2A ADDR: {}, CMD: 0x{:04x}", msg_in->addr, msg_in->cmd.code);
#endif
// check command
switch (msg_in->cmd.code) {
case ACIO_CMD_GET_VERSION: {
// send version data
auto msg = this->create_msg(msg_in, MSG_VERSION_SIZE);
this->set_version(msg, 0x03, 0, 4, 2, 0, "BI2A");
write_msg(msg, response_buffer);
delete msg;
break;
}
case 0x0153:
if (DISABLE_ESPEC_IO) {
return false;
}
[[fallthrough]]; // to 0x0152
case 0x0152: { // STATUS
// check input data length
if (msg_in->cmd.data_size == 0x30) {
// LED TICKER
IIDX_LED_TICKER_LOCK.lock();
if (!IIDXIO_LED_TICKER_READONLY)
memcpy(IIDXIO_LED_TICKER, &msg_in->cmd.raw[0x17], 9);
IIDX_LED_TICKER_LOCK.unlock();
// NEON LIGHT
bool neon_light = msg_in->cmd.raw[0x24] != 0;
write_top_neon(neon_light ? (uint8_t) 0xFF : (uint8_t) 0);
// SPOT LIGHT
uint8_t spot_light_bits = 0;
for (size_t i = 0; i < 8; i++) {
auto index = i > 3 ? i + 1 : i;
if (msg_in->cmd.raw[0x20 + index] != 0)
spot_light_bits |= 1 << i;
}
write_top_lamp(spot_light_bits);
// SWLED (DECK)
uint16_t lamp_bits = 0;
for (size_t i = 0; i < 14; i++) {
if (msg_in->cmd.raw[0x07 + i] != 0)
lamp_bits |= 1 << i;
}
write_lamp(lamp_bits);
// SWLED (PANEL)
uint8_t led_bits = 0;
for (size_t i = 0; i < 4; i++) {
if (msg_in->cmd.raw[0x03 + i] != 0)
led_bits |= 1 << i;
}
write_led(led_bits);
// flush device output
RI_MGR->devices_flush_output();
}
// sleep - otherwise the IO thread will go too hard on the CPU
Sleep(1);
// generate message
auto msg = this->create_msg(msg_in, 0x2E);
// get buttons
auto &buttons = get_buttons();
// player 1 buttons
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_1)))
ARRAY_SETB(msg->cmd.raw, 151);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_2)))
ARRAY_SETB(msg->cmd.raw, 167);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_3)))
ARRAY_SETB(msg->cmd.raw, 183);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_4)))
ARRAY_SETB(msg->cmd.raw, 199);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_5)))
ARRAY_SETB(msg->cmd.raw, 215);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_6)))
ARRAY_SETB(msg->cmd.raw, 231);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_7)))
ARRAY_SETB(msg->cmd.raw, 247);
// player 2 buttons
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_1)))
ARRAY_SETB(msg->cmd.raw, 263);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_2)))
ARRAY_SETB(msg->cmd.raw, 279);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_3)))
ARRAY_SETB(msg->cmd.raw, 295);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_4)))
ARRAY_SETB(msg->cmd.raw, 311);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_5)))
ARRAY_SETB(msg->cmd.raw, 327);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_6)))
ARRAY_SETB(msg->cmd.raw, 343);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_7)))
ARRAY_SETB(msg->cmd.raw, 359);
// player 1 start
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_Start)))
ARRAY_SETB(msg->cmd.raw, 79);
// player 2 start
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_Start)))
ARRAY_SETB(msg->cmd.raw, 78);
// VEFX
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::VEFX)))
ARRAY_SETB(msg->cmd.raw, 77);
// EFFECT
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::Effect)))
ARRAY_SETB(msg->cmd.raw, 76);
// service
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::Service)))
ARRAY_SETB(msg->cmd.raw, 10);
// test
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::Test)))
ARRAY_SETB(msg->cmd.raw, 11);
// turntables
msg->cmd.raw[16] = get_tt(0, true);
msg->cmd.raw[17] = get_tt(1, true);
// slider
msg->cmd.raw[0] |= get_slider(0) << 4;
msg->cmd.raw[2] |= get_slider(1) << 4;
msg->cmd.raw[4] |= get_slider(2) << 4;
msg->cmd.raw[6] |= get_slider(3) << 4;
msg->cmd.raw[7] |= get_slider(4) << 4;
// coin
this->coin_counter += eamuse_coin_consume_stock();
msg->cmd.raw[8] |= this->coin_counter;
/*
* TODO
* bit 9 appears to be the coin mech
*/
// write message
write_msg(msg, response_buffer);
delete msg;
break;
}
case ACIO_CMD_CLEAR:
case ACIO_CMD_STARTUP:
case 0x0120: // WD COMMAND
case 0xFF: // BROADCAST
{
// send status 0
auto msg = this->create_msg_status(msg_in, 0);
write_msg(msg, response_buffer);
delete msg;
break;
}
default:
return false;
}
// mark as handled
return true;
}
bool games::iidx::IIDXFMSerialHandle::open(LPCWSTR lpFileName) {
if (wcscmp(lpFileName, L"COM2") != 0) {
return false;
}
log_info("iidx", "Opened COM2 (FM DEVICE)");
// ACIO device
acio_emu.add_device(new FMSerialDevice());
return true;
}
int games::iidx::IIDXFMSerialHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
// read from emu
DWORD bytes_read = 0;
while (bytes_read < nNumberOfBytesToRead) {
auto cur_byte = acio_emu.read();
if (cur_byte.has_value()) {
buffer[bytes_read++] = cur_byte.value();
} else {
break;
}
}
// return amount of bytes read
return (int) bytes_read;
}
int games::iidx::IIDXFMSerialHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
auto buffer = reinterpret_cast<const uint8_t *>(lpBuffer);
// write to emu
for (DWORD i = 0; i < nNumberOfBytesToWrite; i++) {
acio_emu.write(buffer[i]);
}
// return all data written
return (int) nNumberOfBytesToWrite;
}
int games::iidx::IIDXFMSerialHandle::device_io(DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize,
LPVOID lpOutBuffer, DWORD nOutBufferSize) {
return -1;
}
bool games::iidx::IIDXFMSerialHandle::close() {
log_info("iidx", "Closed COM2 (FM DEVICE)");
return true;
}

30
games/iidx/bi2a.h Normal file
View File

@@ -0,0 +1,30 @@
#pragma once
#include "acioemu/acioemu.h"
#include "hooks/devicehook.h"
namespace games::iidx {
class IIDXFMSerialHandle : public CustomHandle {
private:
acioemu::ACIOEmu acio_emu;
class FMSerialDevice : public acioemu::ACIODeviceEmu {
private:
uint8_t coin_counter = 0;
public:
FMSerialDevice();
bool parse_msg(acioemu::MessageData *msg_in, circular_buffer<uint8_t> *response_buffer) override;
};
public:
bool open(LPCWSTR lpFileName) override;
int read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) override;
int write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) override;
int device_io(DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer,
DWORD nOutBufferSize) override;
bool close() override;
};
}

253
games/iidx/bi2x.cpp Normal file
View File

@@ -0,0 +1,253 @@
#include "bi2x.h"
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "util/utils.h"
#include "iidx.h"
#include "io.h"
using namespace acioemu;
games::iidx::BI2XSerialHandle::BI2XDevice::BI2XDevice() {
this->node_count = 1;
}
bool games::iidx::BI2XSerialHandle::BI2XDevice::parse_msg(
MessageData *msg_in,
circular_buffer<uint8_t> *response_buffer
) {
#ifdef ACIOEMU_LOG
log_info("iidx", "BI2X ADDR: {}, CMD: 0x{:04x}", msg_in->addr, msg_in->cmd.code);
#endif
// check command
switch (msg_in->cmd.code) {
case ACIO_CMD_GET_VERSION: {
// send version data
auto msg = this->create_msg(msg_in, MSG_VERSION_SIZE);
this->set_version(msg, 0x03, 0, 4, 2, 0, "BI2X");
write_msg(msg, response_buffer);
delete msg;
break;
}
case 0x0153:
if (DISABLE_ESPEC_IO) {
return false;
}
[[fallthrough]]; // to 0x0152
case 0x0152: { // STATUS
// check input data length
if (msg_in->cmd.data_size == 0x30) {
// LED TICKER
IIDX_LED_TICKER_LOCK.lock();
if (!IIDXIO_LED_TICKER_READONLY)
memcpy(IIDXIO_LED_TICKER, &msg_in->cmd.raw[0x17], 9);
IIDX_LED_TICKER_LOCK.unlock();
// NEON LIGHT
bool neon_light = msg_in->cmd.raw[0x24] != 0;
write_top_neon(neon_light ? (uint8_t) 0xFF : (uint8_t) 0);
// SPOT LIGHT
uint8_t spot_light_bits = 0;
for (size_t i = 0; i < 8; i++) {
auto index = i > 3 ? i + 1 : i;
if (msg_in->cmd.raw[0x20 + index] != 0)
spot_light_bits |= 1 << i;
}
write_top_lamp(spot_light_bits);
// SWLED (DECK)
uint16_t lamp_bits = 0;
for (size_t i = 0; i < 14; i++) {
if (msg_in->cmd.raw[0x07 + i] != 0)
lamp_bits |= 1 << i;
}
write_lamp(lamp_bits);
// SWLED (PANEL)
uint8_t led_bits = 0;
for (size_t i = 0; i < 4; i++) {
if (msg_in->cmd.raw[0x03 + i] != 0)
led_bits |= 1 << i;
}
write_led(led_bits);
// flush device output
RI_MGR->devices_flush_output();
}
// sleep - otherwise the IO thread will go too hard on the CPU
Sleep(1);
// generate message
auto msg = this->create_msg(msg_in, 0x2E);
// get buttons
auto &buttons = get_buttons();
// player 1 buttons
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_1)))
ARRAY_SETB(msg->cmd.raw, 151);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_2)))
ARRAY_SETB(msg->cmd.raw, 167);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_3)))
ARRAY_SETB(msg->cmd.raw, 183);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_4)))
ARRAY_SETB(msg->cmd.raw, 199);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_5)))
ARRAY_SETB(msg->cmd.raw, 215);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_6)))
ARRAY_SETB(msg->cmd.raw, 231);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_7)))
ARRAY_SETB(msg->cmd.raw, 247);
// player 2 buttons
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_1)))
ARRAY_SETB(msg->cmd.raw, 263);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_2)))
ARRAY_SETB(msg->cmd.raw, 279);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_3)))
ARRAY_SETB(msg->cmd.raw, 295);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_4)))
ARRAY_SETB(msg->cmd.raw, 311);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_5)))
ARRAY_SETB(msg->cmd.raw, 327);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_6)))
ARRAY_SETB(msg->cmd.raw, 343);
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_7)))
ARRAY_SETB(msg->cmd.raw, 359);
// player 1 start
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_Start)))
ARRAY_SETB(msg->cmd.raw, 79);
// player 2 start
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_Start)))
ARRAY_SETB(msg->cmd.raw, 78);
// VEFX
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::VEFX)))
ARRAY_SETB(msg->cmd.raw, 77);
// EFFECT
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::Effect)))
ARRAY_SETB(msg->cmd.raw, 76);
// service
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::Service)))
ARRAY_SETB(msg->cmd.raw, 10);
// test
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::Test)))
ARRAY_SETB(msg->cmd.raw, 11);
// turntables
msg->cmd.raw[16] = get_tt(0, true);
msg->cmd.raw[17] = get_tt(1, true);
// slider
msg->cmd.raw[0] |= get_slider(0) << 4;
msg->cmd.raw[2] |= get_slider(1) << 4;
msg->cmd.raw[4] |= get_slider(2) << 4;
msg->cmd.raw[6] |= get_slider(3) << 4;
msg->cmd.raw[7] |= get_slider(4) << 4;
// coin
this->coin_counter += eamuse_coin_consume_stock();
msg->cmd.raw[8] |= this->coin_counter;
/*
* TODO
* bit 9 appears to be the coin mech
*/
// write message
write_msg(msg, response_buffer);
delete msg;
break;
}
case ACIO_CMD_CLEAR:
case ACIO_CMD_STARTUP:
case 0x0120: // WD COMMAND
case 0xFF: // BROADCAST
{
// send status 0
auto msg = this->create_msg_status(msg_in, 0);
write_msg(msg, response_buffer);
delete msg;
break;
}
default:
return false;
}
// mark as handled
return true;
}
bool games::iidx::BI2XSerialHandle::open(LPCWSTR lpFileName) {
if (wcscmp(lpFileName, L"COM3") != 0) {
return false;
}
log_info("iidx", "Opened COM3 (BIO2)");
// ACIO device
//acio_emu.add_device(new FMSerialDevice());
return true;
}
int games::iidx::BI2XSerialHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
//auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
// read from emu
/*
DWORD bytes_read = 0;
while (bytes_read < nNumberOfBytesToRead) {
auto cur_byte = acio_emu.read();
if (cur_byte.has_value()) {
buffer[bytes_read++] = cur_byte.value();
} else {
break;
}
}
*/
// return amount of bytes read
//return (int) bytes_read;
return (int) nNumberOfBytesToRead;
}
int games::iidx::BI2XSerialHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
auto buffer = reinterpret_cast<const uint8_t *>(lpBuffer);
log_info("iidx", "BIO2 Data: {}", bin2hex(buffer, nNumberOfBytesToWrite));
// write to emu
/*
for (DWORD i = 0; i < nNumberOfBytesToWrite; i++) {
acio_emu.write(buffer[i]);
}
*/
// return all data written
return (int) nNumberOfBytesToWrite;
}
int games::iidx::BI2XSerialHandle::device_io(DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize,
LPVOID lpOutBuffer, DWORD nOutBufferSize) {
return -1;
}
bool games::iidx::BI2XSerialHandle::close() {
log_info("iidx", "Closed COM3 (BIO2)");
return true;
}

30
games/iidx/bi2x.h Normal file
View File

@@ -0,0 +1,30 @@
#pragma once
#include "acioemu/acioemu.h"
#include "hooks/devicehook.h"
namespace games::iidx {
class BI2XSerialHandle : public CustomHandle {
private:
acioemu::ACIOEmu acio_emu;
class BI2XDevice : public acioemu::ACIODeviceEmu {
private:
uint8_t coin_counter = 0;
public:
BI2XDevice();
bool parse_msg(acioemu::MessageData *msg_in, circular_buffer<uint8_t> *response_buffer) override;
};
public:
bool open(LPCWSTR lpFileName) override;
int read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) override;
int write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) override;
int device_io(DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer,
DWORD nOutBufferSize) override;
bool close() override;
};
}

547
games/iidx/bi2x_hook.cpp Normal file
View File

@@ -0,0 +1,547 @@
#include "bi2x_hook.h"
#include <cstdint>
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
#include "rawinput/rawinput.h"
#include "misc/eamuse.h"
#include "games/io.h"
#include "launcher/options.h"
#include "io.h"
#include "iidx.h"
#include "util/tapeled.h"
namespace games::iidx {
constexpr bool BI2X_PASSTHROUGH = false;
/*
* class definitions
*/
struct AC_HNDLIF {
// dummy
uint8_t data[0x10];
};
struct AIO_NMGR_IOB2 {
uint8_t dummy0[0x50];
void (__fastcall *pAIO_NMGR_IOB_BeginManage)(int64_t a1);
uint8_t dummy1[0x9A0];
};
struct AIO_IOB2_BI2X_TDJ {
// who knows
uint8_t data[0x13F8];
};
struct AIO_IOB2_BI2X_TDJ__DEVSTATUS {
// of course you could work with variables here
uint8_t buffer[0xCA];
};
/*
* typedefs
*/
// libaio-iob2_video.dll
typedef AIO_IOB2_BI2X_TDJ* (__fastcall *aioIob2Bi2xTDJ_Create_t)(AIO_NMGR_IOB2 *nmgr, int a2);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__GetDeviceStatus_t)(AIO_IOB2_BI2X_TDJ *This,
AIO_IOB2_BI2X_TDJ__DEVSTATUS *a2);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__IoReset_t)(AIO_IOB2_BI2X_TDJ *This,
unsigned int a2);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__SetWatchDogTimer_t)(AIO_IOB2_BI2X_TDJ *This,
uint8_t a2);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__ControlCoinBlocker_t)(AIO_IOB2_BI2X_TDJ *This,
int index, uint8_t state);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__AddCounter_t)(AIO_IOB2_BI2X_TDJ *This,
int a2, int a3);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__SetStartLamp_t)(AIO_IOB2_BI2X_TDJ *This,
int player, uint8_t state);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__SetVefxButtonLamp_t)(AIO_IOB2_BI2X_TDJ *This,
uint8_t state);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__SetEffectButtonLamp_t)(AIO_IOB2_BI2X_TDJ *This,
uint8_t state);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__SetPlayerButtonLamp_t)(AIO_IOB2_BI2X_TDJ *This,
int player, int button, uint8_t state);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__SetWooferLED_t)(AIO_IOB2_BI2X_TDJ *This,
unsigned int index, uint32_t color);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__SetIccrLed_t)(AIO_IOB2_BI2X_TDJ *This,
unsigned int index, uint32_t color);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__SetTurnTableLed_t)(AIO_IOB2_BI2X_TDJ *This,
unsigned int index, uint32_t color);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__SetTurnTableResist_t)(AIO_IOB2_BI2X_TDJ *This,
unsigned int index, uint8_t resistance);
typedef void (__fastcall *AIO_IOB2_BI2X_TDJ__SetTapeLedData_t)(AIO_IOB2_BI2X_TDJ *This,
unsigned int index, uint8_t *data);
// libaio-iob.dll
typedef AC_HNDLIF* (__fastcall *aioIob2Bi2x_OpenSciUsbCdc_t)(uint8_t device_num);
typedef int64_t (__fastcall *aioIob2Bi2x_WriteFirmGetState_t)(int64_t a1);
typedef AIO_NMGR_IOB2** (__fastcall *aioNMgrIob2_Create_t)(AC_HNDLIF *a1, unsigned int a2);
/*
* function pointers
*/
// libaio-iob2_video.dll
static aioIob2Bi2xTDJ_Create_t aioIob2Bi2xTDJ_Create_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__GetDeviceStatus_t AIO_IOB2_BI2X_TDJ__GetDeviceStatus_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__IoReset_t AIO_IOB2_BI2X_TDJ__IoReset_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__SetWatchDogTimer_t AIO_IOB2_BI2X_TDJ__SetWatchDogTimer_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__ControlCoinBlocker_t AIO_IOB2_BI2X_TDJ__ControlCoinBlocker_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__AddCounter_t AIO_IOB2_BI2X_TDJ__AddCounter_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__SetStartLamp_t AIO_IOB2_BI2X_TDJ__SetStartLamp_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__SetVefxButtonLamp_t AIO_IOB2_BI2X_TDJ__SetVefxButtonLamp_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__SetEffectButtonLamp_t AIO_IOB2_BI2X_TDJ__SetEffectButtonLamp_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__SetPlayerButtonLamp_t AIO_IOB2_BI2X_TDJ__SetPlayerButtonLamp_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__SetWooferLED_t AIO_IOB2_BI2X_TDJ__SetWooferLED_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__SetIccrLed_t AIO_IOB2_BI2X_TDJ__SetIccrLed_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__SetTurnTableLed_t AIO_IOB2_BI2X_TDJ__SetTurnTableLed_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__SetTurnTableResist_t AIO_IOB2_BI2X_TDJ__SetTurnTableResist_orig = nullptr;
static AIO_IOB2_BI2X_TDJ__SetTapeLedData_t AIO_IOB2_BI2X_TDJ__SetTapeLedData_orig = nullptr;
// libaio-iob.dll
static aioIob2Bi2x_OpenSciUsbCdc_t aioIob2Bi2x_OpenSciUsbCdc_orig = nullptr;
static aioIob2Bi2x_WriteFirmGetState_t aioIob2Bi2x_WriteFirmGetState_orig = nullptr;
static aioNMgrIob2_Create_t aioNMgrIob2_Create_orig = nullptr;
/*
* variables
*/
AIO_IOB2_BI2X_TDJ *custom_node = nullptr;
AC_HNDLIF *acHndlif = nullptr;
AIO_NMGR_IOB2 *aioNmgrIob2 = nullptr;
/*
* implementations
*/
static AIO_IOB2_BI2X_TDJ* __fastcall aioIob2Bi2xTDJ_Create(AIO_NMGR_IOB2 *nmgr, int a2) {
if (!BI2X_PASSTHROUGH) {
custom_node = new AIO_IOB2_BI2X_TDJ;
memset(&custom_node->data, 0, sizeof(custom_node->data));
return custom_node;
} else {
// call original
return aioIob2Bi2xTDJ_Create_orig(nmgr, a2);
}
}
static void __fastcall AIO_IOB2_BI2X_TDJ__GetDeviceStatus(AIO_IOB2_BI2X_TDJ *This,
AIO_IOB2_BI2X_TDJ__DEVSTATUS *status) {
// flush raw input
RI_MGR->devices_flush_output();
// check handle
if (This == custom_node) {
// clear input data
memset(status, 0x00, sizeof(AIO_IOB2_BI2X_TDJ__DEVSTATUS));
} else {
// get data from real device
AIO_IOB2_BI2X_TDJ__GetDeviceStatus_orig(This, status);
}
// get buttons
auto &buttons = get_buttons();
// control buttons
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Test]))
status->buffer[4] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Service]))
status->buffer[5] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::CoinMech]))
status->buffer[6] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::VEFX]))
status->buffer[10] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Effect]))
status->buffer[11] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P1_Headphone]))
status->buffer[12] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P2_Headphone]))
status->buffer[13] = 0xFF;
// coin stock
status->buffer[22] += eamuse_coin_get_stock();
// player 1 buttons
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P1_Start]))
status->buffer[8] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P1_1]))
status->buffer[27] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P1_2]))
status->buffer[28] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P1_3]))
status->buffer[29] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P1_4]))
status->buffer[30] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P1_5]))
status->buffer[31] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P1_6]))
status->buffer[32] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P1_7]))
status->buffer[33] = 0xFF;
// player 2 buttons
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P2_Start]))
status->buffer[9] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P2_1]))
status->buffer[34] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P2_2]))
status->buffer[35] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P2_3]))
status->buffer[36] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P2_4]))
status->buffer[37] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P2_5]))
status->buffer[38] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P2_6]))
status->buffer[39] = 0xFF;
if (GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::P2_7]))
status->buffer[40] = 0xFF;
// turntables
status->buffer[20] += get_tt(0, false);
status->buffer[21] += get_tt(1, false);
return;
}
static void __fastcall AIO_IOB2_BI2X_TDJ__IoReset(AIO_IOB2_BI2X_TDJ *This,
unsigned int a2)
{
if (This != custom_node) {
return AIO_IOB2_BI2X_TDJ__IoReset_orig(This, a2);
}
}
static void __fastcall AIO_IOB2_BI2X_TDJ__SetWatchDogTimer(AIO_IOB2_BI2X_TDJ *This,
uint8_t a2)
{
if (This != custom_node) {
// comment this out if you want to disable the BI2X watchdog timer
return AIO_IOB2_BI2X_TDJ__SetWatchDogTimer_orig(This, a2);
}
}
static void __fastcall AIO_IOB2_BI2X_TDJ__ControlCoinBlocker(AIO_IOB2_BI2X_TDJ *This,
int index, uint8_t state) {
// coin blocker is closed when state is zero
eamuse_coin_set_block(state == 0);
if (This != custom_node) {
return AIO_IOB2_BI2X_TDJ__ControlCoinBlocker_orig(This, index, state);
}
}
static void __fastcall AIO_IOB2_BI2X_TDJ__AddCounter(AIO_IOB2_BI2X_TDJ *This,
int a2, int a3)
{
if (This != custom_node) {
return AIO_IOB2_BI2X_TDJ__AddCounter_orig(This, a2, a3);
}
}
static void __fastcall AIO_IOB2_BI2X_TDJ__SetStartLamp(AIO_IOB2_BI2X_TDJ *This,
int player, uint8_t state)
{
auto &lights = get_lights();
if (player == 0) {
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::P1_Start], state ? 1.f : 0.f);
} else if (player == 1) {
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::P2_Start], state ? 1.f : 0.f);
}
if (This != custom_node) {
return AIO_IOB2_BI2X_TDJ__SetStartLamp_orig(This, player, state);
}
}
static void __fastcall AIO_IOB2_BI2X_TDJ__SetVefxButtonLamp(AIO_IOB2_BI2X_TDJ *This,
uint8_t state)
{
auto &lights = get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::VEFX], state ? 1.f : 0.f);
if (This != custom_node) {
return AIO_IOB2_BI2X_TDJ__SetVefxButtonLamp_orig(This, state);
}
}
static void __fastcall AIO_IOB2_BI2X_TDJ__SetEffectButtonLamp(AIO_IOB2_BI2X_TDJ *This,
uint8_t state)
{
auto &lights = get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::Effect], state ? 1.f : 0.f);
if (This != custom_node) {
return AIO_IOB2_BI2X_TDJ__SetEffectButtonLamp_orig(This, state);
}
}
static void __fastcall AIO_IOB2_BI2X_TDJ__SetPlayerButtonLamp(AIO_IOB2_BI2X_TDJ *This,
int player, int button, uint8_t state)
{
auto &lights = get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::P1_1 + player * 7 + button], state ? 1.f : 0.f);
if (This != custom_node) {
return AIO_IOB2_BI2X_TDJ__SetPlayerButtonLamp_orig(This, player, button, state);
}
}
static void __fastcall AIO_IOB2_BI2X_TDJ__SetWooferLED(AIO_IOB2_BI2X_TDJ *This,
unsigned int index, uint32_t color)
{
uint32_t col_r = (color & 0xFF0000) >> 16;
uint32_t col_g = (color & 0x00FF00) >> 8;
uint32_t col_b = (color & 0x0000FF) >> 0;
auto &lights = get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::WooferR], col_r / 255.f);
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::WooferG], col_g / 255.f);
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::WooferB], col_b / 255.f);
if (This != custom_node) {
return AIO_IOB2_BI2X_TDJ__SetWooferLED_orig(This, index, color);
}
}
static void __fastcall AIO_IOB2_BI2X_TDJ__SetIccrLed(AIO_IOB2_BI2X_TDJ *This,
unsigned int index, uint32_t color)
{
uint32_t col_r = (color & 0xFF0000) >> 16;
uint32_t col_g = (color & 0x00FF00) >> 8;
uint32_t col_b = (color & 0x0000FF) >> 0;
auto &lights = get_lights();
if (index == 0) {
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::ICCR_P1_R], col_r / 255.f);
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::ICCR_P1_G], col_g / 255.f);
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::ICCR_P1_B], col_b / 255.f);
} else if (index == 1) {
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::ICCR_P2_R], col_r / 255.f);
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::ICCR_P2_G], col_g / 255.f);
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::ICCR_P2_B], col_b / 255.f);
}
if (This != custom_node) {
return AIO_IOB2_BI2X_TDJ__SetIccrLed_orig(This, index, color);
}
}
static void __fastcall AIO_IOB2_BI2X_TDJ__SetTurnTableLed(AIO_IOB2_BI2X_TDJ *This,
unsigned int index, uint32_t color)
{
uint32_t col_r = (color & 0xFF0000) >> 16;
uint32_t col_g = (color & 0x00FF00) >> 8;
uint32_t col_b = (color & 0x0000FF) >> 0;
auto &lights = get_lights();
if (index == 0) {
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::TT_P1_R], col_r / 255.f);
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::TT_P1_G], col_g / 255.f);
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::TT_P1_B], col_b / 255.f);
} else if (index == 1) {
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::TT_P2_R], col_r / 255.f);
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::TT_P2_G], col_g / 255.f);
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::TT_P2_B], col_b / 255.f);
}
if (This != custom_node) {
return AIO_IOB2_BI2X_TDJ__SetTurnTableLed_orig(This, index, color);
}
}
static void __fastcall AIO_IOB2_BI2X_TDJ__SetTurnTableResist(AIO_IOB2_BI2X_TDJ *This,
unsigned int index, uint8_t resistance)
{
auto &lights = get_lights();
if (index == 0) {
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::TT_P1_Resistance], resistance / 255.f);
} else if (index == 1) {
GameAPI::Lights::writeLight(RI_MGR, lights[Lights::TT_P2_Resistance], resistance / 255.f);
}
if (This != custom_node) {
return AIO_IOB2_BI2X_TDJ__SetTurnTableResist_orig(This, index, resistance);
}
}
static void __fastcall AIO_IOB2_BI2X_TDJ__SetTapeLedData(AIO_IOB2_BI2X_TDJ *This,
unsigned int index, uint8_t *data)
{
/*
* index mapping
*
* 0 - stage left - 57 bytes - 19 colors
* 1 - stage right - 57 bytes - 19 colors
* 2 - cabinet left - 135 bytes - 45 colors
* 3 - cabinet right - 135 bytes - 45 colors
* 4 - control panel under - 63 bytes - 21 colors
* 5 - ceiling left - 162 bytes - 54 colors
* 6 - title left - 33 bytes - 11 colors
* 7 - title right - 33 bytes - 11 colors
* 8 - ceiling right - 162 bytes - 54 colors
* 9 - touch panel left - 51 bytes - 17 colors
* 10 - touch panel right - 51 bytes - 17 colors
* 11 - side panel left inner - 204 bytes - 68 colors
* 12 - side panel left outer - 204 bytes - 68 colors
* 13 - side panel left - 183 bytes - 61 colors
* 14 - side panel right outer - 204 bytes - 68 colors
* 15 - side panel right inner - 204 bytes - 68 colors
* 16 - side panel right - 183 bytes - 61 colors
*
* data is stored in RGB order, 3 bytes per color
*
* TODO: expose this data via API
*/
// data mapping
static struct TapeLedMapping {
size_t data_size;
int index_r, index_g, index_b;
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) {}
} mapping[] = {
{ 19, Lights::StageLeftAvgR, Lights::StageLeftAvgG, Lights::StageLeftAvgB },
{ 19, Lights::StageRightAvgR, Lights::StageRightAvgG, Lights::StageRightAvgB },
{ 45, Lights::CabinetLeftAvgR, Lights::CabinetLeftAvgG, Lights::CabinetLeftAvgB },
{ 45, Lights::CabinetRightAvgR, Lights::CabinetRightAvgG, Lights::CabinetRightAvgB },
{ 21, Lights::ControlPanelUnderAvgR, Lights::ControlPanelUnderAvgG, Lights::ControlPanelUnderAvgB },
{ 54, Lights::CeilingLeftAvgR, Lights::CeilingLeftAvgG, Lights::CeilingLeftAvgB },
{ 11, Lights::TitleLeftAvgR, Lights::TitleLeftAvgG, Lights::TitleLeftAvgB },
{ 11, Lights::TitleRightAvgR, Lights::TitleRightAvgG, Lights::TitleRightAvgB },
{ 54, Lights::CeilingRightAvgR, Lights::CeilingRightAvgG, Lights::CeilingRightAvgB },
{ 17, Lights::TouchPanelLeftAvgR, Lights::TouchPanelLeftAvgG, Lights::TouchPanelLeftAvgB },
{ 17, Lights::TouchPanelRightAvgR, Lights::TouchPanelRightAvgG, Lights::TouchPanelRightAvgB },
{ 68, Lights::SidePanelLeftInnerAvgR, Lights::SidePanelLeftInnerAvgG, Lights::SidePanelLeftInnerAvgB },
{ 68, Lights::SidePanelLeftOuterAvgR, Lights::SidePanelLeftOuterAvgG, Lights::SidePanelLeftOuterAvgB },
{ 61, Lights::SidePanelLeftAvgR, Lights::SidePanelLeftAvgG, Lights::SidePanelLeftAvgB },
{ 68, Lights::SidePanelRightOuterAvgR, Lights::SidePanelRightOuterAvgG, Lights::SidePanelRightOuterAvgB },
{ 68, Lights::SidePanelRightInnerAvgR, Lights::SidePanelRightInnerAvgG, Lights::SidePanelRightInnerAvgB },
{ 61, Lights::SidePanelRightAvgR, Lights::SidePanelRightAvgG, Lights::SidePanelRightAvgB },
};
// check index bounds
if (tapeledutils::is_enabled() && index < std::size(mapping)) {
auto &map = mapping[index];
// pick a color to use
const auto rgb = tapeledutils::pick_color_from_led_tape(data, map.data_size);
// program the lights into API
auto &lights = get_lights();
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);
}
}
static AC_HNDLIF* __fastcall aioIob2Bi2X_OpenSciUsbCdc(uint8_t device_num) {
if (acHndlif == nullptr) {
acHndlif = new AC_HNDLIF;
memset(acHndlif->data, 0x0, sizeof(acHndlif->data));
}
log_info("bi2x_hook", "aioIob2Bi2x_OpenSciUsbCdc");
return acHndlif;
}
static void __fastcall AIO_NMGR_IOB_BeginManageStub(int64_t a1) {
log_info("bi2x_hook", "AIO_NMGR_IOB::BeginManage");
};
static AIO_NMGR_IOB2** __fastcall aioNMgrIob2_Create(AC_HNDLIF *a1, unsigned int a2) {
if (aioNmgrIob2 == nullptr) {
aioNmgrIob2 = new AIO_NMGR_IOB2;
memset(aioNmgrIob2, 0x0, sizeof(AIO_NMGR_IOB2));
aioNmgrIob2->pAIO_NMGR_IOB_BeginManage = AIO_NMGR_IOB_BeginManageStub;
}
log_info("bi2x_hook", "aioNMgrIob2_Create");
return &aioNmgrIob2;
}
static int64_t __fastcall aioIob2Bi2x_WriteFirmGetState(int64_t a1) {
log_info("bi2x_hook", "aioIob2Bi2x_WriteFirmGetState");
return 8;
}
void bi2x_hook_init() {
// avoid double init
static bool initialized = false;
if (initialized) {
return;
} else {
initialized = true;
}
// announce
log_info("bi2x_hook", "init");
// hook IOB2 video
const auto libaioIob2VideoDll = "libaio-iob2_video.dll";
detour::trampoline_try(libaioIob2VideoDll, "aioIob2Bi2xTDJ_Create",
aioIob2Bi2xTDJ_Create, &aioIob2Bi2xTDJ_Create_orig);
detour::trampoline_try(libaioIob2VideoDll, "?GetDeviceStatus@AIO_IOB2_BI2X_TDJ@@QEBAXAEAUDEVSTATUS@1@@Z",
AIO_IOB2_BI2X_TDJ__GetDeviceStatus, &AIO_IOB2_BI2X_TDJ__GetDeviceStatus_orig);
detour::trampoline_try(libaioIob2VideoDll, "?IoReset@AIO_IOB2_BI2X_TDJ@@QEAAXI@Z",
AIO_IOB2_BI2X_TDJ__IoReset, &AIO_IOB2_BI2X_TDJ__IoReset_orig);
detour::trampoline_try(libaioIob2VideoDll, "?SetWatchDogTimer@AIO_IOB2_BI2X_TDJ@@QEAAXE@Z",
AIO_IOB2_BI2X_TDJ__SetWatchDogTimer, &AIO_IOB2_BI2X_TDJ__SetWatchDogTimer_orig);
detour::trampoline_try(libaioIob2VideoDll, "?ControlCoinBlocker@AIO_IOB2_BI2X_TDJ@@QEAAXI_N@Z",
AIO_IOB2_BI2X_TDJ__ControlCoinBlocker, &AIO_IOB2_BI2X_TDJ__ControlCoinBlocker_orig);
detour::trampoline_try(libaioIob2VideoDll, "?AddCounter@AIO_IOB2_BI2X_TDJ@@QEAAXII@Z",
AIO_IOB2_BI2X_TDJ__AddCounter, &AIO_IOB2_BI2X_TDJ__AddCounter_orig);
detour::trampoline_try(libaioIob2VideoDll, "?SetStartLamp@AIO_IOB2_BI2X_TDJ@@QEAAXI_N@Z",
AIO_IOB2_BI2X_TDJ__SetStartLamp, &AIO_IOB2_BI2X_TDJ__SetStartLamp_orig);
detour::trampoline_try(libaioIob2VideoDll, "?SetVefxButtonLamp@AIO_IOB2_BI2X_TDJ@@QEAAX_N@Z",
AIO_IOB2_BI2X_TDJ__SetVefxButtonLamp, &AIO_IOB2_BI2X_TDJ__SetVefxButtonLamp_orig);
detour::trampoline_try(libaioIob2VideoDll, "?SetEffectButtonLamp@AIO_IOB2_BI2X_TDJ@@QEAAX_N@Z",
AIO_IOB2_BI2X_TDJ__SetEffectButtonLamp, &AIO_IOB2_BI2X_TDJ__SetEffectButtonLamp_orig);
detour::trampoline_try(libaioIob2VideoDll, "?SetPlayerButtonLamp@AIO_IOB2_BI2X_TDJ@@QEAAXII_N@Z",
AIO_IOB2_BI2X_TDJ__SetPlayerButtonLamp, &AIO_IOB2_BI2X_TDJ__SetPlayerButtonLamp_orig);
detour::trampoline_try(libaioIob2VideoDll, "?SetWooferLed@AIO_IOB2_BI2X_TDJ@@QEAAXI@Z",
AIO_IOB2_BI2X_TDJ__SetWooferLED, &AIO_IOB2_BI2X_TDJ__SetWooferLED_orig);
detour::trampoline_try(libaioIob2VideoDll, "?SetIccrLed@AIO_IOB2_BI2X_TDJ@@QEAAXII@Z",
AIO_IOB2_BI2X_TDJ__SetIccrLed, &AIO_IOB2_BI2X_TDJ__SetIccrLed_orig);
detour::trampoline_try(libaioIob2VideoDll, "?SetTurnTableLed@AIO_IOB2_BI2X_TDJ@@QEAAXII@Z",
AIO_IOB2_BI2X_TDJ__SetTurnTableLed, &AIO_IOB2_BI2X_TDJ__SetTurnTableLed_orig);
detour::trampoline_try(libaioIob2VideoDll, "?SetTurnTableResist@AIO_IOB2_BI2X_TDJ@@QEAAXIE@Z",
AIO_IOB2_BI2X_TDJ__SetTurnTableResist, &AIO_IOB2_BI2X_TDJ__SetTurnTableResist_orig);
detour::trampoline_try(libaioIob2VideoDll, "?SetTapeLedData@AIO_IOB2_BI2X_TDJ@@QEAAXIPEBX@Z",
AIO_IOB2_BI2X_TDJ__SetTapeLedData, &AIO_IOB2_BI2X_TDJ__SetTapeLedData_orig);
// hook IOB
const auto libaioIobDll = "libaio-iob.dll";
detour::trampoline_try(libaioIobDll, "aioIob2Bi2x_OpenSciUsbCdc",
aioIob2Bi2X_OpenSciUsbCdc, &aioIob2Bi2x_OpenSciUsbCdc_orig);
detour::trampoline_try(libaioIobDll, "aioIob2Bi2x_WriteFirmGetState",
aioIob2Bi2x_WriteFirmGetState, &aioIob2Bi2x_WriteFirmGetState_orig);
detour::trampoline_try(libaioIobDll, "aioNMgrIob2_Create",
aioNMgrIob2_Create, &aioNMgrIob2_Create_orig);
}
}

6
games/iidx/bi2x_hook.h Normal file
View File

@@ -0,0 +1,6 @@
#pragma once
namespace games::iidx {
void bi2x_hook_init();
}

545
games/iidx/camera.cpp Normal file
View File

@@ -0,0 +1,545 @@
#include "camera.h"
#include <d3d9.h>
#include <mfapi.h>
#include <mfidl.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 "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 = std::filesystem::path(_wgetenv(L"APPDATA")) / L"spicetools_camera_control.json";
bool CAMERA_READY = false;
bool parse_cmd_params() {
const auto remove_spaces = [](const char& c) {
return c == ' ';
};
log_misc("iidx::tdjcam", "Using user-supplied addresses (-iidxtdjcamhookoffset)");
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::tdjcam", "failed to parse -iidxtdjcamhookoffset");
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::tdjcam", "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::tdjcam", "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::tdjcam", "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::tdjcam", "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::tdjcam", "failed to find hook: addr_hook_a");
log_warning("iidx::tdjcam", "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::tdjcam", "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::tdjcam", "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::tdjcam", "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::tdjcam", "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::tdjcam", "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::tdjcam", "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::tdjcam", "Created DeviceManager for DXVA");
} else {
log_warning("iidx::tdjcam", "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 = MFCreateAttributes(&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 = MFEnumDeviceSources(pAttributes, &ppDevices, &numDevices);
}
log_info("iidx::tdjcam", "MFEnumDeviceSources returned {} device(s)", numDevices);
if (numDevices > 0) {
hr = create_d3d_device_manager();
}
if (SUCCEEDED(hr)) {
int cameraIndex = 0;
for (size_t i = 0; i < numDevices && LOCAL_CAMERA_LIST.size() < 2; i++) {
// get camera activation
auto pActivate = ppDevices[i];
if ((cameraIndex == 0 && !FLIP_CAMS) || (cameraIndex == 1 && FLIP_CAMS)) {
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);
cameraIndex++;
} else {
top_camera->Release();
top_camera = nullptr;
}
} else {
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);
cameraIndex++;
} else {
front_camera->Release();
front_camera = nullptr;
}
}
}
}
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);
}
bool init_camera_hooks() {
bool result = find_camera_hooks();
if (result) {
log_misc(
"iidx::tdjcam",
"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();
}
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() {
log_info("iidx::tdjcam", "loading config");
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::tdjcam", "config parse error: {}", error);
return false;
}
// verify root is a dict
if (!doc.IsObject()) {
log_warning("iidx::tdjcam", "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::tdjcam", "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;
}
}
}
// 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::tdjcam", " >> done");
} else {
log_misc("iidx::tdjcam", "No previous config for: {}", symLink);
}
}
}
} catch (const std::exception& e) {
log_warning("iidx::tdjcam", "exception occurred while config: {}", e.what());
return false;
}
return true;
}
bool camera_config_save() {
log_info("iidx::tdjcam", "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::tdjcam", "existing config file found");
}
if (!doc.IsObject()) {
log_misc("iidx::tdjcam", "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]);
// 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::text_write(CAMERA_CONFIG_PATH, buffer.GetString())) {
} else {
log_warning("iidx::tdjcam", "unable to save config file to {}", CAMERA_CONFIG_PATH.string());
}
return true;
}
bool camera_config_reset() {
return false;
}
}

17
games/iidx/camera.h Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#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();
}

277
games/iidx/ezusb.cpp Normal file
View File

@@ -0,0 +1,277 @@
#include "ezusb.h"
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "iidx.h"
#include "io.h"
bool games::iidx::EZUSBHandle::open(LPCWSTR lpFileName) {
return wcscmp(lpFileName, L"\\\\.\\Ezusb-0") == 0;
}
int games::iidx::EZUSBHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
log_warning("iidx", "EZUSB invalid read operation!");
return -1;
}
int games::iidx::EZUSBHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
log_warning("iidx", "EZUSB invalid write operation!");
return -1;
}
int games::iidx::EZUSBHandle::device_io(DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize,
LPVOID lpOutBuffer, DWORD nOutBufferSize) {
// initialize check
if (!init) {
// TODO: initialize input
init = true;
init_success = true;
}
if (init_success) {
switch (dwIoControlCode) {
// device descriptor
case 0x222004:
// check output buffer size
if (nOutBufferSize >= 18) {
// set descriptor data
*((char *) lpOutBuffer + 8) = 0x47;
*((char *) lpOutBuffer + 9) = 0x05;
*((char *) lpOutBuffer + 10) = 0x35;
*((char *) lpOutBuffer + 11) = 0x22;
// return output buffer size
return 18;
} else // buffer too small
return -1;
// vendor
case 0x222014:
// check input buffer size
if (nInBufferSize >= 10)
return 0;
else
return -1;
// data read
case 0x22204E:
// check input buffer size
if (nInBufferSize < 4)
return -1;
// read 0x01
if (*(DWORD *) lpInBuffer == 0x01) {
*((DWORD *) lpOutBuffer) = get_pad();
*((char *) lpOutBuffer + 4) = FPGA_CMD_STATE;
*((char *) lpOutBuffer + 7) = get_tt(1, false);
*((char *) lpOutBuffer + 8) = get_tt(0, false);
*((char *) lpOutBuffer + 9) = FPGA_COUNTER++;
*((char *) lpOutBuffer + 11) = 1; // 1 success; 0 failed
*((char *) lpOutBuffer + 13) = get_slider(1) << 4 | get_slider(0);
*((char *) lpOutBuffer + 14) = get_slider(3) << 4 | get_slider(2);
*((char *) lpOutBuffer + 15) = get_slider(4);
return (int) nOutBufferSize;
}
// read 0x03
if (*(DWORD *) lpInBuffer == 0x03) {
// check output buffer size
if (nOutBufferSize < 64)
return -1;
// null node read
if (FPGA_CUR_NODE == 0) {
*(int *) lpOutBuffer = 0;
return (int) nOutBufferSize;
}
// check node size
if (FPGA_CUR_NODE != 64)
return -1;
// check command
if (SRAM_CMD != 2)
return -1;
// check if page exists
if (SRAM_PAGE >= 12)
return -1;
// write page
*((char *) lpOutBuffer + 1) = (char) SRAM_PAGE;
// copy page
memcpy((uint8_t*) lpOutBuffer + 2, SRAM_DATA + 62 * SRAM_PAGE, 62);
SRAM_PAGE++;
// write node
*((char *) lpOutBuffer) = (char) FPGA_CUR_NODE;
return (int) nOutBufferSize;
}
// unknown
return -1;
// data write
case 0x222051:
// check in buffer size
if (nInBufferSize < 4)
return -1;
// write 0x00
if (*(DWORD *) lpInBuffer == 0x00) {
// check out buffer size
if (nOutBufferSize < 10)
return -1;
// get data
uint16_t lamp = *((uint16_t *) lpOutBuffer + 0);
uint8_t led = *((uint8_t *) lpOutBuffer + 6);
uint8_t top_lamp = *((uint8_t *) lpOutBuffer + 8);
uint8_t top_neon = *((uint8_t *) lpOutBuffer + 9);
// process data
write_lamp(lamp);
write_led(led);
write_top_lamp(top_lamp);
write_top_neon(top_neon);
// flush device output
RI_MGR->devices_flush_output();
char write_node = *((char *) lpOutBuffer + 2);
if (write_node != 0) {
switch (write_node) {
case 4:
switch (*((char *) lpOutBuffer + 3)) {
case 1: // init
FPGA_CMD_STATE = 65;
break;
case 2: // check
FPGA_CMD_STATE = 66;
break;
case 3: // write
{
//int write_data = *((short *) lpOutBuffer + 4);
break;
}
case 4: // write done
FPGA_CMD_STATE = 67;
break;
default: // unknown
return -1;
}
break;
case 5:
break;
case 9:
break;
case 64: // SRAM command
SRAM_CMD = *((char *) lpOutBuffer + 3);
switch (SRAM_CMD) {
case 2: // read
SRAM_PAGE = 0;
break;
case 3: // write
SRAM_PAGE = 0;
break;
case 4: // done
break;
default: // unknown
return -1;
}
break;
default: // unknown node
return -1;
}
}
// save node
FPGA_CUR_NODE = write_node;
// return out buffer size
return (int) nOutBufferSize;
}
// write 0x02
if (*(DWORD *) lpInBuffer == 0x02) {
// check out buffer size
if (nOutBufferSize < 64)
return -1;
int cmd = *(char *) lpOutBuffer;
switch (cmd) {
case 0: // null write
break;
case 4: // unknown
break;
case 5: // 16seg
{
// write to status
IIDX_LED_TICKER_LOCK.lock();
if (!IIDXIO_LED_TICKER_READONLY)
memcpy(IIDXIO_LED_TICKER, (char *) lpOutBuffer + 2, 9);
IIDX_LED_TICKER_LOCK.unlock();
break;
}
case 64: // SRAM write
{
// get page
static char page = *((char *) lpOutBuffer + 1);
page = (char) SRAM_PAGE++;
// check page
if (page >= 12)
return -1;
// copy data
memcpy(SRAM_DATA + 62 * page, (uint8_t*) lpOutBuffer + 2, 62);
break;
}
default: // unknown node
return -1;
}
// return out buffer size
return (int) nOutBufferSize;
}
// unknown write
return -1;
// firmware upload
case 0x22206D:
// check buffer size
if (nInBufferSize >= 2)
return (int) nOutBufferSize;
else
return -1;
// unknown control code
default:
log_warning("iidx", "EZUSB unknown: {}", dwIoControlCode);
return -1;
}
} else
return -1;
}
bool games::iidx::EZUSBHandle::close() {
return true;
}

32
games/iidx/ezusb.h Normal file
View File

@@ -0,0 +1,32 @@
#pragma once
#include "hooks/devicehook.h"
namespace games::iidx {
class EZUSBHandle : public CustomHandle {
private:
// state
bool init = false;
bool init_success = false;
char FPGA_CMD_STATE = 67;
char FPGA_COUNTER = 0;
int FPGA_CUR_NODE = 0;
int SRAM_CMD = 0;
int SRAM_PAGE = 0;
char SRAM_DATA[62 * 12]{};
public:
bool open(LPCWSTR lpFileName) override;
int read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) override;
int write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) override;
int device_io(DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer,
DWORD nOutBufferSize) override;
bool close() override;
};
}

733
games/iidx/iidx.cpp Normal file
View File

@@ -0,0 +1,733 @@
#include "iidx.h"
#include "acioemu/handle.h"
#include "avs/core.h"
#include "avs/game.h"
#include "cfg/configurator.h"
#include "games/io.h"
#ifdef SPICE64
#include "games/iidx/camera.h"
#endif
#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"
#include "touch/touch.h"
#include "misc/wintouchemu.h"
#include "misc/eamuse.h"
#include "util/detour.h"
#include "util/fileutils.h"
#include "util/libutils.h"
#include "util/memutils.h"
#include "util/sigscan.h"
#include "util/utils.h"
#include "bi2a.h"
#include "bi2x_hook.h"
#include "ezusb.h"
#include "io.h"
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
namespace games::iidx {
// constants
const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x3001);
const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x3002);
const char *ORIGINAL_ASIO_DEVICE_NAME = "XONAR SOUND CARD(64)";
// settings
bool 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> 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;
static HKEY real_asio_device_reg_handle = nullptr;
static uint16_t IIDXIO_TT_STATE[2]{};
static int8_t IIDXIO_TT_DIRECTION[2]{1, 1};
static bool IIDXIO_TT_PRESSED[2]{};
static bool IIDXIO_TT_ALT_PRESSED[2]{};
char IIDXIO_LED_TICKER[10] = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', '\x00'};
bool IIDXIO_LED_TICKER_READONLY = false;
std::mutex IIDX_LED_TICKER_LOCK;
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
if (lpSubKey != nullptr && phkResult != nullptr) {
if (hKey == HKEY_LOCAL_MACHINE &&
ASIO_DRIVER.has_value() &&
_stricmp(lpSubKey, "software\\asio") == 0)
{
*phkResult = PARENT_ASIO_REG_HANDLE;
return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle);
}
}
return RegOpenKeyA_orig(hKey, lpSubKey, phkResult);
}
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
PHKEY phkResult)
{
if (lpSubKey != nullptr && phkResult != nullptr) {
if (hKey == PARENT_ASIO_REG_HANDLE &&
ASIO_DRIVER.has_value() &&
_stricmp(lpSubKey, ORIGINAL_ASIO_DEVICE_NAME) == 0)
{
*phkResult = DEVICE_ASIO_REG_HANDLE;
log_info("iidx::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
return RegOpenKeyExA_orig(real_asio_reg_handle, ASIO_DRIVER.value().c_str(), ulOptions, samDesired,
&real_asio_device_reg_handle);
}
}
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
}
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
if (dwIndex == 0) {
auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName);
if (ret == ERROR_SUCCESS && lpName != nullptr) {
log_info("iidx::asio", "stubbing '{}' with '{}'", lpName, ORIGINAL_ASIO_DEVICE_NAME);
strncpy(lpName, ORIGINAL_ASIO_DEVICE_NAME, cchName);
}
return ret;
} else {
return ERROR_NO_MORE_ITEMS;
}
}
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
}
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCTSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
LPBYTE lpData, LPDWORD lpcbData)
{
if (lpValueName != nullptr && lpData != nullptr && lpcbData != nullptr) {
// 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) {
// 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 {
hKey = real_asio_device_reg_handle;
}
}
/*
* 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";
memcpy(lpData, port, sizeof(port));
return ERROR_SUCCESS;
}
}
// fallback
return RegQueryValueExA_orig(hKey, lpValueName, lpReserved, lpType, lpData, lpcbData);
}
static LONG WINAPI RegCloseKey_hook(HKEY hKey) {
if (hKey == PARENT_ASIO_REG_HANDLE || hKey == DEVICE_ASIO_REG_HANDLE) {
return ERROR_SUCCESS;
}
return RegCloseKey_orig(hKey);
}
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;
}
}
typedef void* (*aioIob2Bi2x_CreateWriteFirmContext_t)(unsigned int, int);
static aioIob2Bi2x_CreateWriteFirmContext_t aioIob2Bi2x_CreateWriteFirmContext_orig = nullptr;
static void* aioIob2Bi2x_CreateWriteFirmContext_hook(unsigned int a1, int a2) {
if (aioIob2Bi2x_CreateWriteFirmContext_orig) {
auto options = games::get_options(eamuse_get_game());
if (options->at(launcher::Options::IIDXBIO2FW).value_bool()) {
log_info("iidx", "CreateWriteFirmContext({}, {} -> 2)", a1, a2);
return aioIob2Bi2x_CreateWriteFirmContext_orig(a1, 2);
} else {
log_info("iidx", "CreateWriteFirmContext({}, {})", a1, a2);
return aioIob2Bi2x_CreateWriteFirmContext_orig(a1, a2);
}
}
log_warning("iidx", "CreateWriteFirmContext == nullptr");
return nullptr;
}
IIDXGame::IIDXGame() : Game("Beatmania IIDX") {
logger::hook_add(log_hook, this);
}
IIDXGame::~IIDXGame() {
logger::hook_remove(log_hook, this);
}
void IIDXGame::attach() {
Game::attach();
#ifdef SPICE64
if (find_pattern(
avs::game::DLL_INSTANCE,
"534F554E445F4F55545055545F444556494345", // SOUND_OUTPUT_DEVICE
"XXXXXXXXXXXXXXXXXXX",
0, 0)) {
log_misc("iidx", "This game accepts SOUND_OUTPUT_DEVICE environment variable");
} else {
log_warning("iidx", "This game does not accept SOUND_OUTPUT_DEVICE environment variable; it will be ignored");
log_warning("iidx", "Make sure you applied appropriate patches to use the correct sound device (ASIO or WASAPI)");
}
#endif
// IO boards
auto options = games::get_options(eamuse_get_game());
if (!options->at(launcher::Options::IIDXBIO2FW).value_bool()) {
// reduce boot wait time
hooks::sleep::init(1000, 1);
// add old IO board
SETUPAPI_SETTINGS settings1 {};
settings1.class_guid[0] = 0xAE18AA60;
settings1.class_guid[1] = 0x11D47F6A;
settings1.class_guid[2] = 0x0100DD97;
settings1.class_guid[3] = 0x59B92902;
const char property1[] = "Cypress EZ-USB (2235 - EEPROM missing)";
const char interface_detail1[] = "\\\\.\\Ezusb-0";
memcpy(settings1.property_devicedesc, property1, sizeof(property1));
memcpy(settings1.interface_detail, interface_detail1, sizeof(interface_detail1));
setupapihook_init(avs::game::DLL_INSTANCE);
setupapihook_add(settings1);
// IIDX <25 with EZUSB input device
devicehook_init();
devicehook_add(new EZUSBHandle());
// add new BIO2 I/O board
SETUPAPI_SETTINGS settings2 {};
settings2.class_guid[0] = 0x4D36E978;
settings2.class_guid[1] = 0x11CEE325;
settings2.class_guid[2] = 0x0008C1BF;
settings2.class_guid[3] = 0x1803E12B;
const char property2[] = "BIO2(VIDEO)";
const char interface_detail2[] = "COM2";
memcpy(settings2.property_devicedesc, property2, sizeof(property2));
memcpy(settings2.interface_detail, interface_detail2, sizeof(interface_detail2));
setupapihook_add(settings2);
// IIDX 25-27 BIO2 BI2A input device
devicehook_add(new IIDXFMSerialHandle());
}
// check for new I/O DLL
auto aio = libutils::try_library("libaio.dll");
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);
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")) {
DISABLE_CAMS = true;
}
}
// insert BI2X hooks
bi2x_hook_init();
// add card readers
devicehook_init(aio);
devicehook_add(new acioemu::ACIOHandle(L"COM1"));
// firmware upgrade hook
if (options->at(launcher::Options::IIDXBIO2FW).value_bool()) {
aioIob2Bi2x_CreateWriteFirmContext_orig = detour::iat(
"aioIob2Bi2x_CreateWriteFirmContext",
aioIob2Bi2x_CreateWriteFirmContext_hook);
//devicehook_add(new MITMHandle(L"#vid_1ccf&pid_8050", "", true));
} else {
/*
// add the BIO2 I/O board (with different firmware)
SETUPAPI_SETTINGS settings {};
settings.class_guid[0] = 0x0;
settings.class_guid[1] = 0x0;
settings.class_guid[2] = 0x0;
settings.class_guid[3] = 0x0;
const char property[] = "1CCF(8050)_000";
const char property_hardwareid[] = "USB\\VID_1CCF&PID_8050&MI_00\\000";
const char interface_detail[] = "COM3";
memcpy(settings.property_devicedesc, property, sizeof(property));
memcpy(settings.property_hardwareid, property_hardwareid, sizeof(property_hardwareid));
memcpy(settings.interface_detail, interface_detail, sizeof(interface_detail));
setupapihook_init(aio);
setupapihook_add(settings);
// IIDX 27 BIO2 BI2X input devce
devicehook_add(new BI2XSerialHandle());
*/
}
}
// ASIO device hook
RegCloseKey_orig = detour::iat_try(
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
RegEnumKeyA_orig = detour::iat_try(
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyA_orig = detour::iat_try(
"RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyExA_orig = detour::iat_try(
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
// IO device workaround
RegQueryValueExA_orig = detour::iat_try(
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
// check if cam hook should be enabled
if (!DISABLE_CAMS) {
init_legacy_camera_hook(FLIP_CAMS);
}
#ifdef SPICE64
if (TDJ_CAMERA) {
init_camera_hooks();
}
if (TDJ_MODE && !D3D9_DEVICE_HOOK_DISABLE) {
nvenc_hook::initialize();
}
#endif
// init cfgmgr32 hooks
cfgmgr32hook_init(avs::game::DLL_INSTANCE);
}
void IIDXGame::pre_attach() {
Game::pre_attach();
// environment variables must be set before the DLL is loaded as the VC++ runtime copies all
// environment variables at startup
if (DISABLE_CAMS) {
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"
"!!! !!!\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"
);
}
}
void IIDXGame::detach() {
Game::detach();
devicehook_dispose();
}
void IIDXGame::detect_sound_output_device() {
// if the user specified a value (other than auto), use it as the environment var
// probably "wasapi" or "asio", but it's not explicitly checked here for forward compat
if (SOUND_OUTPUT_DEVICE.has_value() && SOUND_OUTPUT_DEVICE.value() != "auto") {
log_info(
"iidx",
"using user-supplied \"{}\" for SOUND_OUTPUT_DEVICE",
SOUND_OUTPUT_DEVICE.value());
SetEnvironmentVariable("SOUND_OUTPUT_DEVICE", SOUND_OUTPUT_DEVICE.value().c_str());
return;
}
// automatic detection
bool use_asio = false;
log_misc("iidx", "auto-detect SOUND_OUTPUT_DEVICE...");
if (ASIO_DRIVER.has_value()) {
log_misc(
"iidx",
"-iidxasio is set to \"{}\", use asio for SOUND_OUTPUT_DEVICE",
ASIO_DRIVER.value());
use_asio = true;
} else {
HKEY subkey;
LSTATUS result;
result = RegOpenKey(HKEY_LOCAL_MACHINE, "SOFTWARE\\ASIO\\XONAR SOUND CARD(64)", &subkey);
if (result == ERROR_SUCCESS) {
RegCloseKey(subkey);
use_asio = true;
log_misc(
"iidx",
"found HKLM\\SOFTWARE\\ASIO\\XONAR SOUND CARD(64), using asio for SOUND_OUTPUT_DEVICE");
}
}
const char* device = "wasapi";
if (use_asio) {
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);
}
uint32_t get_pad() {
uint32_t pad = 0;
// get buttons
auto &buttons = get_buttons();
// player 1 buttons
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_1)))
pad |= 1 << 0x08;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_2)))
pad |= 1 << 0x09;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_3)))
pad |= 1 << 0x0A;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_4)))
pad |= 1 << 0x0B;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_5)))
pad |= 1 << 0x0C;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_6)))
pad |= 1 << 0x0D;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_7)))
pad |= 1 << 0x0E;
// player 2 buttons
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_1)))
pad |= 1 << 0x0F;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_2)))
pad |= 1 << 0x10;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_3)))
pad |= 1 << 0x11;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_4)))
pad |= 1 << 0x12;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_5)))
pad |= 1 << 0x13;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_6)))
pad |= 1 << 0x14;
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_7)))
pad |= 1 << 0x15;
// player 1 start
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P1_Start)))
pad |= 1 << 0x18;
// player 2 start
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::P2_Start)))
pad |= 1 << 0x19;
// VEFX
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::VEFX)))
pad |= 1 << 0x1A;
// EFFECT
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::Effect)))
pad |= 1 << 0x1B;
// test
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::Test)))
pad |= 1 << 0x1C;
// service
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::Service)))
pad |= 1 << 0x1D;
return ~(pad & 0xFFFFFF00);
}
void write_lamp(uint16_t lamp) {
// mapping
static const size_t mapping[] = {
Lights::P1_1,
Lights::P1_2,
Lights::P1_3,
Lights::P1_4,
Lights::P1_5,
Lights::P1_6,
Lights::P1_7,
Lights::P2_1,
Lights::P2_2,
Lights::P2_3,
Lights::P2_4,
Lights::P2_5,
Lights::P2_6,
Lights::P2_7,
};
// get lights
auto &lights = get_lights();
// bit scan
for (int i = 0; i < 14; i++) {
float value = (lamp & (1 << i)) ? 1.f : 0.f;
GameAPI::Lights::writeLight(RI_MGR, lights.at(mapping[i]), value);
}
}
void write_led(uint8_t led) {
// mapping
static const size_t mapping[] = {
Lights::P1_Start,
Lights::P2_Start,
Lights::VEFX,
Lights::Effect,
};
// get lights
auto &lights = get_lights();
// bit scan
for (int i = 0; i < 4; i++) {
auto value = (led & (1 << i)) ? 1.f : 0.f;
GameAPI::Lights::writeLight(RI_MGR, lights.at(mapping[i]), value);
}
}
void write_top_lamp(uint8_t top_lamp) {
// mapping
static const size_t mapping[] = {
Lights::SpotLight1,
Lights::SpotLight2,
Lights::SpotLight3,
Lights::SpotLight4,
Lights::SpotLight5,
Lights::SpotLight6,
Lights::SpotLight7,
Lights::SpotLight8,
};
// get lights
auto &lights = get_lights();
// bit scan
for (int i = 0; i < 8; i++) {
auto value = (top_lamp & (1 << i)) ? 1.f : 0.f;
GameAPI::Lights::writeLight(RI_MGR, lights.at(mapping[i]), value);
}
}
void write_top_neon(uint8_t top_neon) {
// get lights
auto &lights = get_lights();
// write value
auto value = top_neon > 0 ? 1.f : 0.f;
GameAPI::Lights::writeLight(RI_MGR, lights.at(Lights::NeonLamp), value);
}
unsigned char get_tt(int player, bool slow) {
// check change value for high/low precision
uint16_t change =
slow ? (uint16_t) (DIGITAL_TT_SENS / 4) : (uint16_t) DIGITAL_TT_SENS;
// check player number
if (player > 1)
return 0;
// get buttons
auto &buttons = get_buttons();
bool ttp = GameAPI::Buttons::getState(RI_MGR, buttons.at(
player != 0 ? Buttons::P2_TTPlus : Buttons::P1_TTPlus));
bool ttm = GameAPI::Buttons::getState(RI_MGR, buttons.at(
player != 0 ? Buttons::P2_TTMinus : Buttons::P1_TTMinus));
bool ttpm = GameAPI::Buttons::getState(RI_MGR, buttons.at(
player != 0 ? Buttons::P2_TTPlusMinus : Buttons::P1_TTPlusMinus));
bool ttpm_alt = GameAPI::Buttons::getState(RI_MGR, buttons.at(
player != 0 ? Buttons::P2_TTPlusMinusAlt : Buttons::P1_TTPlusMinusAlt));
// TT+
if (ttp)
IIDXIO_TT_STATE[player] += change;
// TT-
if (ttm)
IIDXIO_TT_STATE[player] -= change;
// TT+/-
bool ttpm_rising_edge = !IIDXIO_TT_PRESSED[player] && ttpm;
bool ttpm_alt_rising_edge = !IIDXIO_TT_ALT_PRESSED[player] && ttpm_alt;
if (ttpm_rising_edge || ttpm_alt_rising_edge) {
IIDXIO_TT_DIRECTION[player] *= -1;
}
if (ttpm || ttpm_alt) {
IIDXIO_TT_STATE[player] += (change * IIDXIO_TT_DIRECTION[player]);
}
IIDXIO_TT_PRESSED[player] = ttpm;
IIDXIO_TT_ALT_PRESSED[player] = ttpm_alt;
// raw input
auto &analogs = get_analogs();
auto &analog = analogs[player != 0 ? Analogs::TT_P2 : Analogs::TT_P1];
auto ret_value = IIDXIO_TT_STATE[player];
if (analog.isSet()) {
ret_value = IIDXIO_TT_STATE[player];
ret_value += (uint16_t) (GameAPI::Analogs::getState(RI_MGR, analog) * 1023.999f);
}
// return higher 8 bit
return (uint8_t) (ret_value >> 2);
}
unsigned char get_slider(uint8_t slider) {
// check slide
if (slider > 4)
return 0;
// get analog
auto &analogs = get_analogs();
Analog *analog = nullptr;
switch (slider) {
case 0:
analog = &analogs.at(Analogs::VEFX);
break;
case 1:
analog = &analogs.at(Analogs::LowEQ);
break;
case 2:
analog = &analogs.at(Analogs::HiEQ);
break;
case 3:
analog = &analogs.at(Analogs::Filter);
break;
case 4:
analog = &analogs.at(Analogs::PlayVolume);
break;
default:
break;
}
// if not set return max value
if (!analog || !analog->isSet()) {
return 0xF;
}
// return slide
return (unsigned char) (GameAPI::Analogs::getState(RI_MGR, *analog) * 15.999f);
}
const char* get_16seg() {
return IIDXIO_LED_TICKER;
}
bool is_tdj_fhd() {
return TDJ_MODE && avs::game::is_ext(2022101900, MAXINT);
}
}

56
games/iidx/iidx.h Normal file
View File

@@ -0,0 +1,56 @@
#pragma once
#include <mutex>
#include <optional>
#include "games/game.h"
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];
extern bool IIDXIO_LED_TICKER_READONLY;
extern std::mutex IIDX_LED_TICKER_LOCK;
class IIDXGame : public games::Game {
public:
IIDXGame();
virtual ~IIDXGame() override;
virtual void attach() override;
virtual void pre_attach() override;
virtual void detach() override;
private:
void detect_sound_output_device();
};
// helper methods
uint32_t get_pad();
void write_lamp(uint16_t lamp);
void write_led(uint8_t led);
void write_top_lamp(uint8_t top_lamp);
void write_top_neon(uint8_t top_neon);
unsigned char get_tt(int player, bool slow);
unsigned char get_slider(uint8_t slider);
const char* get_16seg();
bool is_tdj_fhd();
}

175
games/iidx/io.cpp Normal file
View File

@@ -0,0 +1,175 @@
#include "io.h"
std::vector<Button> &games::iidx::get_buttons() {
static std::vector<Button> buttons;
if (buttons.empty()) {
buttons = GameAPI::Buttons::getButtons("Beatmania IIDX");
GameAPI::Buttons::sortButtons(
&buttons,
"Service",
"Test",
"Coin Mech",
"P1 1",
"P1 2",
"P1 3",
"P1 4",
"P1 5",
"P1 6",
"P1 7",
"P1 TT+",
"P1 TT-",
"P1 TT+/-",
"P1 TT+/- Alternate",
"P1 Start",
"P2 1",
"P2 2",
"P2 3",
"P2 4",
"P2 5",
"P2 6",
"P2 7",
"P2 TT+",
"P2 TT-",
"P2 TT+/-",
"P2 TT+/- Alternate",
"P2 Start",
"EFFECT",
"VEFX",
"P1 Headphone",
"P2 Headphone"
);
}
return buttons;
}
std::vector<Analog> &games::iidx::get_analogs() {
static std::vector<Analog> analogs;
if (analogs.empty()) {
analogs = GameAPI::Analogs::getAnalogs("Beatmania IIDX");
GameAPI::Analogs::sortAnalogs(
&analogs,
"Turntable P1",
"Turntable P2",
"VEFX",
"Low-EQ",
"Hi-EQ",
"Filter",
"Play Volume"
);
}
return analogs;
}
std::vector<Light> &games::iidx::get_lights() {
static std::vector<Light> lights;
if (lights.empty()) {
lights = GameAPI::Lights::getLights("Beatmania IIDX");
GameAPI::Lights::sortLights(
&lights,
"P1 1",
"P1 2",
"P1 3",
"P1 4",
"P1 5",
"P1 6",
"P1 7",
"P2 1",
"P2 2",
"P2 3",
"P2 4",
"P2 5",
"P2 6",
"P2 7",
"P1 Start",
"P2 Start",
"VEFX",
"Effect",
"Spot Light 1",
"Spot Light 2",
"Spot Light 3",
"Spot Light 4",
"Spot Light 5",
"Spot Light 6",
"Spot Light 7",
"Spot Light 8",
"Neon Lamp",
"Woofer R",
"Woofer G",
"Woofer B",
"IC Card Reader P1 R",
"IC Card Reader P1 G",
"IC Card Reader P1 B",
"IC Card Reader P2 R",
"IC Card Reader P2 G",
"IC Card Reader P2 B",
"Turntable P1 R",
"Turntable P1 G",
"Turntable P1 B",
"Turntable P2 R",
"Turntable P2 G",
"Turntable P2 B",
"Turntable P1 Resistance",
"Turntable P2 Resistance",
"Stage Left Avg R",
"Stage Left Avg G",
"Stage Left Avg B",
"Stage Right Avg R",
"Stage Right Avg G",
"Stage Right Avg B",
"Cabinet Left Avg R",
"Cabinet Left Avg G",
"Cabinet Left Avg B",
"Cabinet Right Avg R",
"Cabinet Right Avg G",
"Cabinet Right Avg B",
"Control Panel Under Avg R",
"Control Panel Under Avg G",
"Control Panel Under Avg B",
"Ceiling Left Avg R",
"Ceiling Left Avg G",
"Ceiling Left Avg B",
"Title Left Avg R",
"Title Left Avg G",
"Title Left Avg B",
"Title Right Avg R",
"Title Right Avg G",
"Title Right Avg B",
"Ceiling Right Avg R",
"Ceiling Right Avg G",
"Ceiling Right Avg B",
"Touch Panel Left Avg R",
"Touch Panel Left Avg G",
"Touch Panel Left Avg B",
"Touch Panel Right Avg R",
"Touch Panel Right Avg G",
"Touch Panel Right Avg B",
"Side Panel Left Inner Avg R",
"Side Panel Left Inner Avg G",
"Side Panel Left Inner Avg B",
"Side Panel Left Outer Avg R",
"Side Panel Left Outer Avg G",
"Side Panel Left Outer Avg B",
"Side Panel Left Avg R",
"Side Panel Left Avg G",
"Side Panel Left Avg B",
"Side Panel Right Outer Avg R",
"Side Panel Right Outer Avg G",
"Side Panel Right Outer Avg B",
"Side Panel Right Inner Avg R",
"Side Panel Right Inner Avg G",
"Side Panel Right Inner Avg B",
"Side Panel Right Avg R",
"Side Panel Right Avg G",
"Side Panel Right Avg B"
);
}
return lights;
}

163
games/iidx/io.h Normal file
View File

@@ -0,0 +1,163 @@
#pragma once
#include <vector>
#include "cfg/api.h"
namespace games::iidx {
// all buttons in correct order
namespace Buttons {
enum {
Service,
Test,
CoinMech,
P1_1,
P1_2,
P1_3,
P1_4,
P1_5,
P1_6,
P1_7,
P1_TTPlus,
P1_TTMinus,
P1_TTPlusMinus,
P1_TTPlusMinusAlt,
P1_Start,
P2_1,
P2_2,
P2_3,
P2_4,
P2_5,
P2_6,
P2_7,
P2_TTPlus,
P2_TTMinus,
P2_TTPlusMinus,
P2_TTPlusMinusAlt,
P2_Start,
Effect,
VEFX,
P1_Headphone,
P2_Headphone,
};
}
// all analogs in correct order
namespace Analogs {
enum {
TT_P1,
TT_P2,
VEFX,
LowEQ,
HiEQ,
Filter,
PlayVolume
};
}
// all lights in correct order
namespace Lights {
enum {
P1_1,
P1_2,
P1_3,
P1_4,
P1_5,
P1_6,
P1_7,
P2_1,
P2_2,
P2_3,
P2_4,
P2_5,
P2_6,
P2_7,
P1_Start,
P2_Start,
VEFX,
Effect,
SpotLight1,
SpotLight2,
SpotLight3,
SpotLight4,
SpotLight5,
SpotLight6,
SpotLight7,
SpotLight8,
NeonLamp,
WooferR,
WooferG,
WooferB,
ICCR_P1_R,
ICCR_P1_G,
ICCR_P1_B,
ICCR_P2_R,
ICCR_P2_G,
ICCR_P2_B,
TT_P1_R,
TT_P1_G,
TT_P1_B,
TT_P2_R,
TT_P2_G,
TT_P2_B,
TT_P1_Resistance,
TT_P2_Resistance,
StageLeftAvgR,
StageLeftAvgG,
StageLeftAvgB,
StageRightAvgR,
StageRightAvgG,
StageRightAvgB,
CabinetLeftAvgR,
CabinetLeftAvgG,
CabinetLeftAvgB,
CabinetRightAvgR,
CabinetRightAvgG,
CabinetRightAvgB,
ControlPanelUnderAvgR,
ControlPanelUnderAvgG,
ControlPanelUnderAvgB,
CeilingLeftAvgR,
CeilingLeftAvgG,
CeilingLeftAvgB,
TitleLeftAvgR,
TitleLeftAvgG,
TitleLeftAvgB,
TitleRightAvgR,
TitleRightAvgG,
TitleRightAvgB,
CeilingRightAvgR,
CeilingRightAvgG,
CeilingRightAvgB,
TouchPanelLeftAvgR,
TouchPanelLeftAvgG,
TouchPanelLeftAvgB,
TouchPanelRightAvgR,
TouchPanelRightAvgG,
TouchPanelRightAvgB,
SidePanelLeftInnerAvgR,
SidePanelLeftInnerAvgG,
SidePanelLeftInnerAvgB,
SidePanelLeftOuterAvgR,
SidePanelLeftOuterAvgG,
SidePanelLeftOuterAvgB,
SidePanelLeftAvgR,
SidePanelLeftAvgG,
SidePanelLeftAvgB,
SidePanelRightOuterAvgR,
SidePanelRightOuterAvgG,
SidePanelRightOuterAvgB,
SidePanelRightInnerAvgR,
SidePanelRightInnerAvgG,
SidePanelRightInnerAvgB,
SidePanelRightAvgR,
SidePanelRightAvgG,
SidePanelRightAvgB,
};
}
// getters
std::vector<Button> &get_buttons();
std::vector<Analog> &get_analogs();
std::vector<Light> &get_lights();
}

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);
}
}

View File

@@ -0,0 +1,5 @@
#pragma once
namespace games::iidx {
void init_legacy_camera_hook(bool);
}

765
games/iidx/local_camera.cpp Normal file
View File

@@ -0,0 +1,765 @@
#include "games/iidx/local_camera.h"
#include "util/logging.h"
#include "util/utils.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::tdjcam", "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::tdjcam", "[{}] Creating camera", m_name);
// 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::tdjcam", "[{}] 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::tdjcam", "[{}] Activated", m_name);
// Create an attribute store to hold initialization settings.
hr = MFCreateAttributes(&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 = MFCreateSourceReaderFromMediaSource(
m_pSource,
pAttributes,
&m_pSourceReader
);
if (FAILED(hr)) { goto done; }
log_misc("iidx::tdjcam", "[{}] 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::tdjcam", "[{}] Initialized", m_name);
} else {
log_warning("iidx::tdjcam", "[{}] 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::tdjcam", "[{}] Camera not initialized", m_name);
return E_FAIL;
}
// Try to find a suitable output type.
log_misc("iidx::tdjcam", "[{}] 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::tdjcam", "[{}] 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::tdjcam", "[{}] 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::tdjcam", "[{}] Creating thread", m_name);
CreateThread();
}
return hr;
}
HRESULT IIDXLocalCamera::ChangeMediaType(IMFMediaType *pType) {
HRESULT hr = S_OK;
MediaTypeInfo info = GetMediaTypeInfo(pType);
log_info("iidx::tdjcam", "[{}] 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::tdjcam", "[{}] 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]() {
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::tdjcam", "[{}] 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::tdjcam", "[{}] 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::tdjcam", "[{}] >> {} 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::tdjcam", "[{}] 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::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::tdjcam", "[{}] Failed to get subtype: {}", m_name, hr);
return hr;
}
hr = MFGetAttributeSize(pType, MF_MT_FRAME_SIZE, &width, &height);
if (FAILED(hr)) {
log_warning("iidx::tdjcam", "[{}] 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::tdjcam", "[{}] 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::tdjcam", "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;
// printTextureLevelDesc(m_texture);
// printTextureLevelDesc(m_texture_original);
done:
if (SUCCEEDED(hr)) {
log_misc("iidx::tdjcam", "[{}] Created texture", m_name);
} else {
log_warning("iidx::tdjcam", "[{}] Failed to create texture: {}", m_name, hr);
}
return hr;
}
HRESULT IIDXLocalCamera::DrawSample(IMFMediaBuffer *pSrcBuffer) {
if (!m_active) {
return E_FAIL;
}
EnterCriticalSection(&m_critsec);
HRESULT hr = S_OK;
IDirect3DSurface9 *pCameraSurf = NULL;
IDirect3DQuery9* pEventQuery = nullptr;
hr = MFGetService(pSrcBuffer, MR_BUFFER_SERVICE, IID_PPV_ARGS(&pCameraSurf));
// Stretch camera texture to destination
hr = m_device->StretchRect(pCameraSurf, &m_rcSource, m_pDestSurf, &m_rcDest, D3DTEXF_LINEAR);
if (FAILED(hr)) { goto done; }
// 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
}
done:
if (FAILED(hr)) {
log_warning("iidx::tdjcam", "Error in DrawSample {}", GetLastError());
}
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::tdjcam", "[{}] Release camera", m_name);
m_active = false;
ULONG uCount = InterlockedDecrement(&m_nRefCount);
for (size_t i = 0; i < m_mediaTypeInfos.size(); i++) {
SafeRelease(&(m_mediaTypeInfos.at(i).p_mediaType));
}
SafeRelease(&m_pDestSurf);
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;
}
}

166
games/iidx/local_camera.h Normal file
View File

@@ -0,0 +1,166 @@
#pragma once
#include <d3d9.h>
#include <dxva2api.h>
#include <mutex>
#include <mfapi.h>
#include <mfidl.h>
#include <mferror.h>
#include <mfreadwrite.h>
#include <string>
#include <thread>
#include <strmif.h>
#include <vector>
#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;
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
LPDIRECT3DTEXTURE9 m_texture = nullptr;
IDirect3DSurface9 *m_pDestSurf = 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;
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();
std::string GetName();
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();
};
}

345
games/iidx/poke.cpp Normal file
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 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
games/iidx/poke.h Normal file
View File

@@ -0,0 +1,6 @@
#pragma once
namespace poke {
void enable();
void disable();
}