From fdc7adff7f0560923633c5438a4be7d2f55a0f38 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sun, 11 Oct 2026 01:04:14 -0700 Subject: [PATCH] add card connect support (#923) There are no servers to test this, but: * touch, touch with mouse * in theory card reader should work * printer works (RGB, front and back) * test/service button, lights What is (painfully) missing: * windowed mode Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/spice2x/CMakeLists.txt | 4 + src/spice2x/acioemu/device.cpp | 7 +- src/spice2x/acioemu/handle.cpp | 9 +- src/spice2x/acioemu/handle.h | 4 + src/spice2x/acioemu/icca.cpp | 24 +- src/spice2x/acioemu/icca.h | 2 +- src/spice2x/games/cardconnect/bi2x_hook.cpp | 371 ++++++++++++ src/spice2x/games/cardconnect/bi2x_hook.h | 5 + src/spice2x/games/cardconnect/cardconnect.cpp | 59 ++ src/spice2x/games/cardconnect/cardconnect.h | 15 + src/spice2x/games/cardconnect/io.cpp | 40 ++ src/spice2x/games/cardconnect/io.h | 33 ++ src/spice2x/games/cardconnect/printer.cpp | 554 ++++++++++++++++++ src/spice2x/games/cardconnect/printer.h | 6 + src/spice2x/games/io.cpp | 8 + src/spice2x/games/shared/printer.cpp | 162 +++-- src/spice2x/games/shared/printer.h | 14 + src/spice2x/launcher/launcher.cpp | 40 +- src/spice2x/launcher/options.cpp | 68 +++ src/spice2x/launcher/options.h | 8 + src/spice2x/misc/eamuse.cpp | 2 + src/spice2x/util/execexe.cpp | 2 +- 22 files changed, 1358 insertions(+), 79 deletions(-) create mode 100644 src/spice2x/games/cardconnect/bi2x_hook.cpp create mode 100644 src/spice2x/games/cardconnect/bi2x_hook.h create mode 100644 src/spice2x/games/cardconnect/cardconnect.cpp create mode 100644 src/spice2x/games/cardconnect/cardconnect.h create mode 100644 src/spice2x/games/cardconnect/io.cpp create mode 100644 src/spice2x/games/cardconnect/io.h create mode 100644 src/spice2x/games/cardconnect/printer.cpp create mode 100644 src/spice2x/games/cardconnect/printer.h diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index dc2135a8..a6d91cd5 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -557,6 +557,10 @@ set(SOURCE_FILES ${SOURCE_FILES} games/udn/udn.cpp games/udn/io.cpp games/udn/bi2x_hook.cpp + games/cardconnect/cardconnect.cpp + games/cardconnect/io.cpp + games/cardconnect/bi2x_hook.cpp + games/cardconnect/printer.cpp # hooks hooks/audio/acm.cpp diff --git a/src/spice2x/acioemu/device.cpp b/src/spice2x/acioemu/device.cpp index 6e8dacfe..4918694b 100644 --- a/src/spice2x/acioemu/device.cpp +++ b/src/spice2x/acioemu/device.cpp @@ -46,17 +46,14 @@ MessageData *ACIODeviceEmu::create_msg(uint8_t addr, uint16_t code, uint8_t pid, } // allocate data - auto data_raw = new uint8_t[MSG_HEADER_SIZE + data_size]; + auto msg = new MessageData {}; // set header - auto msg = (MessageData *) &data_raw[0]; set_header(msg, addr, code, pid, (uint8_t) data_size); // set data if (data) { - memcpy(data_raw + MSG_HEADER_SIZE, data, data_size); - } else { - memset(data_raw + MSG_HEADER_SIZE, 0, data_size); + memcpy(msg->cmd.raw, data, data_size); } // return prepared message diff --git a/src/spice2x/acioemu/handle.cpp b/src/spice2x/acioemu/handle.cpp index 4ebe3ead..16d26de6 100644 --- a/src/spice2x/acioemu/handle.cpp +++ b/src/spice2x/acioemu/handle.cpp @@ -14,15 +14,20 @@ bool acioemu::ACIOHandle::open(LPCWSTR lpFileName) { return false; } + std::lock_guard lock(mutex); log_info("acioemu", "Opened {} (ACIO)", ws2s(com_port)); // ACIO device - acio_emu.add_device(new acioemu::ICCADevice(false, true, icca_node_count)); + if (!initialized) { + acio_emu.add_device(new acioemu::ICCADevice(false, true, icca_node_count)); + initialized = true; + } return true; } int acioemu::ACIOHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) { + std::lock_guard lock(mutex); auto buffer = reinterpret_cast(lpBuffer); // read from emu @@ -42,6 +47,7 @@ int acioemu::ACIOHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) { } int acioemu::ACIOHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) { + std::lock_guard lock(mutex); auto buffer = reinterpret_cast(lpBuffer); // write to emu @@ -64,6 +70,7 @@ int acioemu::ACIOHandle::device_io( } size_t acioemu::ACIOHandle::bytes_available() { + std::lock_guard lock(mutex); return acio_emu.bytes_available(); } diff --git a/src/spice2x/acioemu/handle.h b/src/spice2x/acioemu/handle.h index 5caa3d79..b5cce769 100644 --- a/src/spice2x/acioemu/handle.h +++ b/src/spice2x/acioemu/handle.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "acioemu/acioemu.h" #include "hooks/devicehook.h" @@ -12,6 +14,8 @@ namespace acioemu { uint8_t icca_node_count; acioemu::ACIOEmu acio_emu; + std::mutex mutex; + bool initialized = false; public: ACIOHandle(LPCWSTR lpCOMPort, uint8_t iccaNodeCount = 2); diff --git a/src/spice2x/acioemu/icca.cpp b/src/spice2x/acioemu/icca.cpp index eb9eddb5..64eaa04f 100644 --- a/src/spice2x/acioemu/icca.cpp +++ b/src/spice2x/acioemu/icca.cpp @@ -46,9 +46,9 @@ ICCADevice::ICCADevice(bool flip_order, bool keypad_thread, uint8_t node_count) // keypad thread for faster polling if (keypad_thread) { - this->keypad_thread = new std::thread([this]() { + this->keypad_thread = std::jthread([this](std::stop_token stop_token) { timeutils::PreciseSleepTimer timer; - while (this->cards) { + while (!stop_token.stop_requested()) { for (int unit = 0; unit < this->node_count; unit++) { this->update_keypad(unit, false); } @@ -61,11 +61,15 @@ ICCADevice::ICCADevice(bool flip_order, bool keypad_thread, uint8_t node_count) ICCADevice::~ICCADevice() { // stop thread - delete keypad_thread; + if (keypad_thread.joinable()) { + keypad_thread.request_stop(); + keypad_thread.join(); + } // delete cards in array for (int i = 0; i < node_count; i++) { - delete cards[i]; + delete[] cards[i]; + delete[] keypad_last[i]; } // delete the rest @@ -107,7 +111,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in, if (avs::game::is_model({"LDJ", "TBS", "UJK", "XIF"}) || games::sdvx::is_valkyrie_model()) { this->set_version(msg, 0x3, 0, 1, 7, 0, "ICCA"); - } else if (avs::game::is_model("VFG")) { + } else if (avs::game::is_model({"VFG", "TCS"})) { this->set_version(msg, 0x3, 0, 1, 7, 0, "ICCB"); } else { this->set_version(msg, 0x3, 0, 1, 6, 0, "ICCA"); @@ -160,7 +164,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in, break; case 0x12: // EJECT if (this->cards[unit] != nullptr) { - delete this->cards[unit]; + delete[] this->cards[unit]; } this->cards[unit] = nullptr; this->hold[unit] = false; @@ -267,7 +271,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in, memcpy(msg->cmd.raw, this->cards[unit], 8); // delete card - delete this->cards[unit]; + delete[] this->cards[unit]; this->cards[unit] = nullptr; this->hold[unit] = false; } @@ -326,7 +330,9 @@ bool ICCADevice::parse_msg(MessageData *msg_in, } //log_info("icca", "counter[{}] = {}", unit, this->counter[unit]); - auto msg = this->create_msg_status(msg_in, this->counter[unit]); + // Card Connect waits for a successful reply without retrying this command. + auto msg = this->create_msg_status( + msg_in, avs::game::is_model("TCS") ? 0 : this->counter[unit]); write_msg(msg, response_buffer); delete msg; break; @@ -363,7 +369,7 @@ void ICCADevice::update_card(int unit) { if (difftime(t_now, this->cards_time[unit]) >= 10.f) { if (this->cards[unit] != nullptr) { - delete this->cards[unit]; + delete[] this->cards[unit]; } this->cards[unit] = nullptr; this->hold[unit] = false; diff --git a/src/spice2x/acioemu/icca.h b/src/spice2x/acioemu/icca.h index 2ca83bec..86e9162c 100644 --- a/src/spice2x/acioemu/icca.h +++ b/src/spice2x/acioemu/icca.h @@ -17,7 +17,7 @@ namespace acioemu { private: bool type_new; bool flip_order; - std::thread *keypad_thread; + std::jthread keypad_thread; std::mutex keypad_mutex; uint8_t **cards; time_t *cards_time; diff --git a/src/spice2x/games/cardconnect/bi2x_hook.cpp b/src/spice2x/games/cardconnect/bi2x_hook.cpp new file mode 100644 index 00000000..a715d3be --- /dev/null +++ b/src/spice2x/games/cardconnect/bi2x_hook.cpp @@ -0,0 +1,371 @@ +#include "bi2x_hook.h" + +#if SPICE64 + +#include +#include +#include +#include +#include + +#include "io.h" +#include "misc/eamuse.h" +#include "rawinput/rawinput.h" +#include "util/detour.h" +#include "util/logging.h" +#include "util/tapeled.h" + +namespace games::cardconnect { + + struct AIO_SCI_COMM { + uint8_t data[0xf8]; + }; + + struct AIO_NMGR_IOB2 { + uint8_t data[0x9f8]; + }; + + struct AIO_IOB2_BI2X_TCS { + uint8_t data[0x350]; + }; + + struct AIO_IOB2_BI2X_WRFIRM { + uint8_t data[0x20f48]; + }; + +#pragma pack(push, 1) + struct AIO_IOB2_BI2X_TCS__INPUT { + uint8_t DevIoCounter; + uint8_t bTest; + uint8_t bService; + uint8_t bCoinSw; + uint8_t bCoinJam; + uint8_t CoinCount; + }; + + struct AIO_IOB2_BI2X_TCS__DEVSTATUS { + uint8_t InputCounter; + uint8_t OutputCounter; + uint8_t TapeLedCounter; + AIO_IOB2_BI2X_TCS__INPUT Input; + uint8_t reserved[17]; + }; +#pragma pack(pop) + + // The supplied native DLL copies 26 bytes, even though the managed wrapper requests 27. + static_assert(sizeof(AIO_IOB2_BI2X_TCS__DEVSTATUS) == 26); + static_assert(offsetof(AIO_IOB2_BI2X_TCS__DEVSTATUS, Input) == 3); + static_assert(offsetof(AIO_IOB2_BI2X_TCS__INPUT, bTest) == 1); + static_assert(offsetof(AIO_IOB2_BI2X_TCS__INPUT, bService) == 2); + static_assert(offsetof(AIO_IOB2_BI2X_TCS__INPUT, bCoinSw) == 3); + static_assert(offsetof(AIO_IOB2_BI2X_TCS__INPUT, CoinCount) == 5); + + static AIO_IOB2_BI2X_TCS *(__fastcall *aioIob2Bi2xTCS_Create_orig)( + AIO_NMGR_IOB2 *, uint32_t) = nullptr; + static uint32_t (__fastcall *aioIob2Bi2xTCS_GetDeviceStatus_orig)( + AIO_IOB2_BI2X_TCS *, AIO_IOB2_BI2X_TCS__DEVSTATUS *, uint32_t) = nullptr; + static void (__fastcall *aioIob2Bi2xTCS_IoReset_orig)( + AIO_IOB2_BI2X_TCS *, uint32_t) = nullptr; + static void (__fastcall *aioIob2Bi2xTCS_SetWatchDogTimer_orig)( + AIO_IOB2_BI2X_TCS *, uint8_t) = nullptr; + static void (__fastcall *aioIob2Bi2xTCS_ControlCoinBlocker_orig)( + AIO_IOB2_BI2X_TCS *, uint32_t, bool) = nullptr; + static void (__fastcall *aioIob2Bi2xTCS_AddCounter_orig)( + AIO_IOB2_BI2X_TCS *, uint32_t, uint32_t) = nullptr; + static void (__fastcall *aioIob2Bi2xTCS_SetPhotoLight_orig)( + AIO_IOB2_BI2X_TCS *, bool) = nullptr; + static void (__fastcall *aioIob2Bi2xTCS_SetIccrLed_orig)( + AIO_IOB2_BI2X_TCS *, uint32_t) = nullptr; + static void (__fastcall *aioIob2Bi2xTCS_SetTapeLedData_orig)( + AIO_IOB2_BI2X_TCS *, uint32_t, uint8_t *) = nullptr; + + static AIO_SCI_COMM *(__fastcall *aioIob2Bi2x_OpenSciUsbCdc_orig)(uint32_t) = nullptr; + static AIO_IOB2_BI2X_WRFIRM *(__fastcall *aioIob2Bi2x_CreateWriteFirmContext_orig)( + uint32_t, uint32_t) = nullptr; + static void (__fastcall *aioIob2Bi2x_DestroyWriteFirmContext_orig)( + AIO_IOB2_BI2X_WRFIRM *) = nullptr; + static int32_t (__fastcall *aioIob2Bi2x_WriteFirmGetState_orig)( + AIO_IOB2_BI2X_WRFIRM *) = nullptr; + static AIO_NMGR_IOB2 *(__fastcall *aioNMgrIob2_Create_orig)( + AIO_SCI_COMM *, uint32_t) = nullptr; + static void (__fastcall *aioNMgrIob_BeginManage_orig)(AIO_NMGR_IOB2 *) = nullptr; + + static void (__fastcall *aioNodeMgr_Destroy_orig)(AIO_NMGR_IOB2 *) = nullptr; + static int32_t (__fastcall *aioNodeMgr_GetState_orig)(AIO_NMGR_IOB2 *) = nullptr; + static bool (__fastcall *aioNodeMgr_IsReady_orig)(AIO_NMGR_IOB2 *, int32_t) = nullptr; + static void (__fastcall *aioNodeCtl_Destroy_orig)(AIO_IOB2_BI2X_TCS *) = nullptr; + static int32_t (__fastcall *aioNodeCtl_GetState_orig)(AIO_IOB2_BI2X_TCS *) = nullptr; + static bool (__fastcall *aioNodeCtl_IsReady_orig)(AIO_IOB2_BI2X_TCS *, int32_t) = nullptr; + static void (__fastcall *aioSci_Destroy_orig)(AIO_SCI_COMM *) = nullptr; + + static std::unique_ptr aio_sci; + static std::unique_ptr aio_manager; + static std::unique_ptr aio_board; + static std::unique_ptr aio_firmware; + static uint8_t input_counter = 0; + static bool coin_pressed = false; + + static void write_light(size_t index, float value) { + auto &lights = get_lights(); + GameAPI::Lights::writeLight(RI_MGR, lights[index], value); + } + + static AIO_IOB2_BI2X_TCS *__fastcall aioIob2Bi2xTCS_Create( + AIO_NMGR_IOB2 *manager, uint32_t device_id) { + if (!aio_manager || manager != aio_manager.get()) { + return aioIob2Bi2xTCS_Create_orig(manager, device_id); + } + if (!aio_board) { + aio_board = std::make_unique(); + input_counter = 0; + coin_pressed = false; + log_info("cardconnect::bi2x", "created emulated BI2X-TCS board"); + } + return aio_board.get(); + } + + static uint32_t __fastcall aioIob2Bi2xTCS_GetDeviceStatus( + AIO_IOB2_BI2X_TCS *board, AIO_IOB2_BI2X_TCS__DEVSTATUS *status, + uint32_t status_size) { + if (!aio_board || board != aio_board.get()) { + return aioIob2Bi2xTCS_GetDeviceStatus_orig(board, status, status_size); + } + if (!status && status_size > 0) { + log_warning("cardconnect::bi2x", "GetDeviceStatus called with a null output buffer"); + return 0; + } + + auto &buttons = get_buttons(); + const bool coin = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::CoinMech]); + if (coin && !coin_pressed) { + eamuse_coin_insert(); + } + coin_pressed = coin; + + AIO_IOB2_BI2X_TCS__DEVSTATUS current {}; + current.InputCounter = ++input_counter; + current.Input.DevIoCounter = input_counter; + current.Input.bTest = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Test]) ? 1 : 0; + current.Input.bService = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Service]) ? 1 : 0; + current.Input.bCoinSw = coin ? 1 : 0; + current.Input.CoinCount = static_cast(eamuse_coin_get_stock()); + + RI_MGR->devices_flush_output(); + + const auto copy_size = std::min(status_size, sizeof(current)); + if (copy_size > 0) { + std::memcpy(status, ¤t, copy_size); + } + return copy_size; + } + + static void __fastcall aioIob2Bi2xTCS_IoReset(AIO_IOB2_BI2X_TCS *board, uint32_t bits) { + if (!aio_board || board != aio_board.get()) { + return aioIob2Bi2xTCS_IoReset_orig(board, bits); + } + } + + static void __fastcall aioIob2Bi2xTCS_SetWatchDogTimer( + AIO_IOB2_BI2X_TCS *board, uint8_t count) { + if (!aio_board || board != aio_board.get()) { + return aioIob2Bi2xTCS_SetWatchDogTimer_orig(board, count); + } + } + + static void __fastcall aioIob2Bi2xTCS_ControlCoinBlocker( + AIO_IOB2_BI2X_TCS *board, uint32_t slot, bool open) { + if (!aio_board || board != aio_board.get()) { + return aioIob2Bi2xTCS_ControlCoinBlocker_orig(board, slot, open); + } + if (slot != 0) { + log_warning("cardconnect::bi2x", "unknown coin blocker slot {}", slot); + return; + } + eamuse_coin_set_block(!open); + } + + static void __fastcall aioIob2Bi2xTCS_AddCounter( + AIO_IOB2_BI2X_TCS *board, uint32_t counter, uint32_t count) { + if (!aio_board || board != aio_board.get()) { + return aioIob2Bi2xTCS_AddCounter_orig(board, counter, count); + } + // This drives the physical bookkeeping meter, not the input coin counter. + } + + static void __fastcall aioIob2Bi2xTCS_SetPhotoLight(AIO_IOB2_BI2X_TCS *board, bool on) { + if (!aio_board || board != aio_board.get()) { + return aioIob2Bi2xTCS_SetPhotoLight_orig(board, on); + } + write_light(Lights::PhotoLight, on ? 1.f : 0.f); + } + + static void __fastcall aioIob2Bi2xTCS_SetIccrLed(AIO_IOB2_BI2X_TCS *board, uint32_t rgb) { + if (!aio_board || board != aio_board.get()) { + return aioIob2Bi2xTCS_SetIccrLed_orig(board, rgb); + } + write_light(Lights::CardReaderR, ((rgb >> 16) & 0xff) / 255.f); + write_light(Lights::CardReaderG, ((rgb >> 8) & 0xff) / 255.f); + write_light(Lights::CardReaderB, (rgb & 0xff) / 255.f); + } + + static void __fastcall aioIob2Bi2xTCS_SetTapeLedData( + AIO_IOB2_BI2X_TCS *board, uint32_t tape, uint8_t *data) { + if (!aio_board || board != aio_board.get()) { + return aioIob2Bi2xTCS_SetTapeLedData_orig(board, tape, data); + } + if (!data || tape >= 2) { + log_warning("cardconnect::bi2x", "invalid tape LED data for channel {}", tape); + return; + } + if (tapeledutils::is_enabled()) { + const auto color = tapeledutils::pick_color_from_led_tape(data, 15); + const auto first = tape == 0 ? Lights::SideLeftR : Lights::SideRightR; + write_light(first, color.r); + write_light(first + 1, color.g); + write_light(first + 2, color.b); + } + } + + static AIO_SCI_COMM *__fastcall aioIob2Bi2x_OpenSciUsbCdc(uint32_t serial_number) { + if (!aio_sci) { + aio_sci = std::make_unique(); + } + return aio_sci.get(); + } + + static AIO_IOB2_BI2X_WRFIRM *__fastcall aioIob2Bi2x_CreateWriteFirmContext( + uint32_t serial_number, uint32_t iob_mask) { + if (!aio_firmware) { + aio_firmware = std::make_unique(); + } + return aio_firmware.get(); + } + + static void __fastcall aioIob2Bi2x_DestroyWriteFirmContext(AIO_IOB2_BI2X_WRFIRM *context) { + if (!aio_firmware || context != aio_firmware.get()) { + return aioIob2Bi2x_DestroyWriteFirmContext_orig(context); + } + aio_firmware.reset(); + } + + static int32_t __fastcall aioIob2Bi2x_WriteFirmGetState(AIO_IOB2_BI2X_WRFIRM *context) { + if (!aio_firmware || context != aio_firmware.get()) { + return aioIob2Bi2x_WriteFirmGetState_orig(context); + } + return 8; + } + + static AIO_NMGR_IOB2 *__fastcall aioNMgrIob2_Create(AIO_SCI_COMM *sci, uint32_t mode) { + if (!aio_sci || sci != aio_sci.get()) { + return aioNMgrIob2_Create_orig(sci, mode); + } + if (!aio_manager) { + aio_manager = std::make_unique(); + } + return aio_manager.get(); + } + + static void __fastcall aioNMgrIob_BeginManage(AIO_NMGR_IOB2 *manager) { + if (!aio_manager || manager != aio_manager.get()) { + return aioNMgrIob_BeginManage_orig(manager); + } + } + + static void __fastcall aioNodeMgr_Destroy(AIO_NMGR_IOB2 *manager) { + if (!aio_manager || manager != aio_manager.get()) { + return aioNodeMgr_Destroy_orig(manager); + } + aio_manager.reset(); + } + + static int32_t __fastcall aioNodeMgr_GetState(AIO_NMGR_IOB2 *manager) { + if (!aio_manager || manager != aio_manager.get()) { + return aioNodeMgr_GetState_orig(manager); + } + return 1; + } + + static bool __fastcall aioNodeMgr_IsReady(AIO_NMGR_IOB2 *manager, int32_t state) { + if (!aio_manager || manager != aio_manager.get()) { + return aioNodeMgr_IsReady_orig(manager, state); + } + return state >= 1; + } + + static void __fastcall aioNodeCtl_Destroy(AIO_IOB2_BI2X_TCS *board) { + if (!aio_board || board != aio_board.get()) { + return aioNodeCtl_Destroy_orig(board); + } + aio_board.reset(); + } + + static int32_t __fastcall aioNodeCtl_GetState(AIO_IOB2_BI2X_TCS *board) { + if (!aio_board || board != aio_board.get()) { + return aioNodeCtl_GetState_orig(board); + } + return 1; + } + + static bool __fastcall aioNodeCtl_IsReady(AIO_IOB2_BI2X_TCS *board, int32_t state) { + if (!aio_board || board != aio_board.get()) { + return aioNodeCtl_IsReady_orig(board, state); + } + return state >= 1; + } + + static void __fastcall aioSci_Destroy(AIO_SCI_COMM *sci) { + if (!aio_sci || sci != aio_sci.get()) { + return aioSci_Destroy_orig(sci); + } + aio_sci.reset(); + } + + void bi2x_hook_init() { + log_info("cardconnect::bi2x", "initializing native BI2X-TCS hooks"); + + const auto video = "libaio-iob2_video.dll"; + detour::trampoline(video, "aioIob2Bi2xTCS_Create", + aioIob2Bi2xTCS_Create, &aioIob2Bi2xTCS_Create_orig); + detour::trampoline(video, "aioIob2Bi2xTCS_GetDeviceStatus", + aioIob2Bi2xTCS_GetDeviceStatus, &aioIob2Bi2xTCS_GetDeviceStatus_orig); + detour::trampoline(video, "aioIob2Bi2xTCS_IoReset", + aioIob2Bi2xTCS_IoReset, &aioIob2Bi2xTCS_IoReset_orig); + detour::trampoline(video, "aioIob2Bi2xTCS_SetWatchDogTimer", + aioIob2Bi2xTCS_SetWatchDogTimer, &aioIob2Bi2xTCS_SetWatchDogTimer_orig); + detour::trampoline(video, "aioIob2Bi2xTCS_ControlCoinBlocker", + aioIob2Bi2xTCS_ControlCoinBlocker, &aioIob2Bi2xTCS_ControlCoinBlocker_orig); + detour::trampoline(video, "aioIob2Bi2xTCS_AddCounter", + aioIob2Bi2xTCS_AddCounter, &aioIob2Bi2xTCS_AddCounter_orig); + detour::trampoline(video, "aioIob2Bi2xTCS_SetPhotoLight", + aioIob2Bi2xTCS_SetPhotoLight, &aioIob2Bi2xTCS_SetPhotoLight_orig); + detour::trampoline(video, "aioIob2Bi2xTCS_SetIccrLed", + aioIob2Bi2xTCS_SetIccrLed, &aioIob2Bi2xTCS_SetIccrLed_orig); + detour::trampoline(video, "aioIob2Bi2xTCS_SetTapeLedData", + aioIob2Bi2xTCS_SetTapeLedData, &aioIob2Bi2xTCS_SetTapeLedData_orig); + + const auto iob = "libaio-iob.dll"; + detour::trampoline(iob, "aioIob2Bi2x_OpenSciUsbCdc", + aioIob2Bi2x_OpenSciUsbCdc, &aioIob2Bi2x_OpenSciUsbCdc_orig); + detour::trampoline(iob, "aioIob2Bi2x_CreateWriteFirmContext", + aioIob2Bi2x_CreateWriteFirmContext, &aioIob2Bi2x_CreateWriteFirmContext_orig); + detour::trampoline(iob, "aioIob2Bi2x_DestroyWriteFirmContext", + aioIob2Bi2x_DestroyWriteFirmContext, &aioIob2Bi2x_DestroyWriteFirmContext_orig); + detour::trampoline(iob, "aioIob2Bi2x_WriteFirmGetState", + aioIob2Bi2x_WriteFirmGetState, &aioIob2Bi2x_WriteFirmGetState_orig); + detour::trampoline(iob, "aioNMgrIob2_Create", + aioNMgrIob2_Create, &aioNMgrIob2_Create_orig); + detour::trampoline(iob, "aioNMgrIob_BeginManage", + aioNMgrIob_BeginManage, &aioNMgrIob_BeginManage_orig); + + const auto aio = "libaio.dll"; + detour::trampoline(aio, "aioNodeMgr_Destroy", aioNodeMgr_Destroy, &aioNodeMgr_Destroy_orig); + detour::trampoline(aio, "aioNodeMgr_GetState", aioNodeMgr_GetState, &aioNodeMgr_GetState_orig); + detour::trampoline(aio, "aioNodeMgr_IsReady", aioNodeMgr_IsReady, &aioNodeMgr_IsReady_orig); + detour::trampoline(aio, "aioNodeCtl_Destroy", aioNodeCtl_Destroy, &aioNodeCtl_Destroy_orig); + detour::trampoline(aio, "aioNodeCtl_GetState", aioNodeCtl_GetState, &aioNodeCtl_GetState_orig); + detour::trampoline(aio, "aioNodeCtl_IsReady", aioNodeCtl_IsReady, &aioNodeCtl_IsReady_orig); + detour::trampoline(aio, "aioSci_Destroy", aioSci_Destroy, &aioSci_Destroy_orig); + } +} + +#endif diff --git a/src/spice2x/games/cardconnect/bi2x_hook.h b/src/spice2x/games/cardconnect/bi2x_hook.h new file mode 100644 index 00000000..41c15417 --- /dev/null +++ b/src/spice2x/games/cardconnect/bi2x_hook.h @@ -0,0 +1,5 @@ +#pragma once + +namespace games::cardconnect { + void bi2x_hook_init(); +} diff --git a/src/spice2x/games/cardconnect/cardconnect.cpp b/src/spice2x/games/cardconnect/cardconnect.cpp new file mode 100644 index 00000000..520b3fae --- /dev/null +++ b/src/spice2x/games/cardconnect/cardconnect.cpp @@ -0,0 +1,59 @@ +#include "cardconnect.h" + +#include + +#include "acioemu/handle.h" +#include "bi2x_hook.h" +#include "printer.h" +#include "hooks/graphics/graphics.h" +#include "util/execexe.h" +#include "util/fileutils.h" +#include "util/libutils.h" +#include "util/logging.h" +#include "util/unity_player.h" + +namespace games::cardconnect { + std::string CARDCONNECT_INJECT_ARGS = ""; + + static const std::wstring reader_port = L"\\\\.\\COM2"; + + void CardConnectGame::attach() { + Game::attach(); + + // EXECEXE redirects Unity's raw-data writes under the game directory. + fileutils::dir_create_recursive_log("cardconnect", "game/dev/raw/log"); + if (!fileutils::bin_write("game/dev/raw/log/output_log.txt", nullptr, 0)) { + log_warning("cardconnect", "failed to create Unity output log"); + } + + auto *reader = new acioemu::ACIOHandle(reader_port.c_str(), 1); + devicehook_init_trampoline(); + devicehook_add(reader); + + execexe::init(); + execexe::init_port_hook(reader_port, reader); + +#if SPICE64 + libutils::load_library("libaio.dll"); + libutils::load_library("libaio-iob.dll"); + libutils::load_library("libaio-iob2_video.dll"); + bi2x_hook_init(); + printer_attach(); +#endif + + if (GRAPHICS_SHOW_CURSOR) { + unity_utils::force_show_cursor(true); + } + + unity_utils::set_args( + std::format("{} {}{}", + GetCommandLineA(), + CARDCONNECT_INJECT_ARGS, + unity_utils::get_unity_player_args())); + } + + void CardConnectGame::detach() { + Game::detach(); + devicehook_dispose(); + } +} diff --git a/src/spice2x/games/cardconnect/cardconnect.h b/src/spice2x/games/cardconnect/cardconnect.h new file mode 100644 index 00000000..4ff2d7c7 --- /dev/null +++ b/src/spice2x/games/cardconnect/cardconnect.h @@ -0,0 +1,15 @@ +#pragma once + +#include "games/game.h" + +namespace games::cardconnect { + extern std::string CARDCONNECT_INJECT_ARGS; + + class CardConnectGame : public games::Game { + public: + CardConnectGame() : Game("Card Connect") {} + + virtual void attach() override; + virtual void detach() override; + }; +} diff --git a/src/spice2x/games/cardconnect/io.cpp b/src/spice2x/games/cardconnect/io.cpp new file mode 100644 index 00000000..f714ecbb --- /dev/null +++ b/src/spice2x/games/cardconnect/io.cpp @@ -0,0 +1,40 @@ +#include "io.h" + +std::vector