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>
This commit is contained in:
bicarus
2026-10-11 01:04:14 -07:00
committed by GitHub
parent cf5d87d4d5
commit fdc7adff7f
22 changed files with 1358 additions and 79 deletions
+4
View File
@@ -557,6 +557,10 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/udn/udn.cpp games/udn/udn.cpp
games/udn/io.cpp games/udn/io.cpp
games/udn/bi2x_hook.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
hooks/audio/acm.cpp hooks/audio/acm.cpp
+2 -5
View File
@@ -46,17 +46,14 @@ MessageData *ACIODeviceEmu::create_msg(uint8_t addr, uint16_t code, uint8_t pid,
} }
// allocate data // allocate data
auto data_raw = new uint8_t[MSG_HEADER_SIZE + data_size]; auto msg = new MessageData {};
// set header // set header
auto msg = (MessageData *) &data_raw[0];
set_header(msg, addr, code, pid, (uint8_t) data_size); set_header(msg, addr, code, pid, (uint8_t) data_size);
// set data // set data
if (data) { if (data) {
memcpy(data_raw + MSG_HEADER_SIZE, data, data_size); memcpy(msg->cmd.raw, data, data_size);
} else {
memset(data_raw + MSG_HEADER_SIZE, 0, data_size);
} }
// return prepared message // return prepared message
+7
View File
@@ -14,15 +14,20 @@ bool acioemu::ACIOHandle::open(LPCWSTR lpFileName) {
return false; return false;
} }
std::lock_guard<std::mutex> lock(mutex);
log_info("acioemu", "Opened {} (ACIO)", ws2s(com_port)); log_info("acioemu", "Opened {} (ACIO)", ws2s(com_port));
// ACIO device // ACIO device
if (!initialized) {
acio_emu.add_device(new acioemu::ICCADevice(false, true, icca_node_count)); acio_emu.add_device(new acioemu::ICCADevice(false, true, icca_node_count));
initialized = true;
}
return true; return true;
} }
int acioemu::ACIOHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) { int acioemu::ACIOHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
std::lock_guard<std::mutex> lock(mutex);
auto buffer = reinterpret_cast<uint8_t *>(lpBuffer); auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
// read from emu // read from emu
@@ -42,6 +47,7 @@ int acioemu::ACIOHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
} }
int acioemu::ACIOHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) { int acioemu::ACIOHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
std::lock_guard<std::mutex> lock(mutex);
auto buffer = reinterpret_cast<const uint8_t *>(lpBuffer); auto buffer = reinterpret_cast<const uint8_t *>(lpBuffer);
// write to emu // write to emu
@@ -64,6 +70,7 @@ int acioemu::ACIOHandle::device_io(
} }
size_t acioemu::ACIOHandle::bytes_available() { size_t acioemu::ACIOHandle::bytes_available() {
std::lock_guard<std::mutex> lock(mutex);
return acio_emu.bytes_available(); return acio_emu.bytes_available();
} }
+4
View File
@@ -1,5 +1,7 @@
#pragma once #pragma once
#include <mutex>
#include "acioemu/acioemu.h" #include "acioemu/acioemu.h"
#include "hooks/devicehook.h" #include "hooks/devicehook.h"
@@ -12,6 +14,8 @@ namespace acioemu {
uint8_t icca_node_count; uint8_t icca_node_count;
acioemu::ACIOEmu acio_emu; acioemu::ACIOEmu acio_emu;
std::mutex mutex;
bool initialized = false;
public: public:
ACIOHandle(LPCWSTR lpCOMPort, uint8_t iccaNodeCount = 2); ACIOHandle(LPCWSTR lpCOMPort, uint8_t iccaNodeCount = 2);
+15 -9
View File
@@ -46,9 +46,9 @@ ICCADevice::ICCADevice(bool flip_order, bool keypad_thread, uint8_t node_count)
// keypad thread for faster polling // keypad thread for faster polling
if (keypad_thread) { if (keypad_thread) {
this->keypad_thread = new std::thread([this]() { this->keypad_thread = std::jthread([this](std::stop_token stop_token) {
timeutils::PreciseSleepTimer timer; timeutils::PreciseSleepTimer timer;
while (this->cards) { while (!stop_token.stop_requested()) {
for (int unit = 0; unit < this->node_count; unit++) { for (int unit = 0; unit < this->node_count; unit++) {
this->update_keypad(unit, false); this->update_keypad(unit, false);
} }
@@ -61,11 +61,15 @@ ICCADevice::ICCADevice(bool flip_order, bool keypad_thread, uint8_t node_count)
ICCADevice::~ICCADevice() { ICCADevice::~ICCADevice() {
// stop thread // stop thread
delete keypad_thread; if (keypad_thread.joinable()) {
keypad_thread.request_stop();
keypad_thread.join();
}
// delete cards in array // delete cards in array
for (int i = 0; i < node_count; i++) { for (int i = 0; i < node_count; i++) {
delete cards[i]; delete[] cards[i];
delete[] keypad_last[i];
} }
// delete the rest // delete the rest
@@ -107,7 +111,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in,
if (avs::game::is_model({"LDJ", "TBS", "UJK", "XIF"}) || if (avs::game::is_model({"LDJ", "TBS", "UJK", "XIF"}) ||
games::sdvx::is_valkyrie_model()) { games::sdvx::is_valkyrie_model()) {
this->set_version(msg, 0x3, 0, 1, 7, 0, "ICCA"); 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"); this->set_version(msg, 0x3, 0, 1, 7, 0, "ICCB");
} else { } else {
this->set_version(msg, 0x3, 0, 1, 6, 0, "ICCA"); this->set_version(msg, 0x3, 0, 1, 6, 0, "ICCA");
@@ -160,7 +164,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in,
break; break;
case 0x12: // EJECT case 0x12: // EJECT
if (this->cards[unit] != nullptr) { if (this->cards[unit] != nullptr) {
delete this->cards[unit]; delete[] this->cards[unit];
} }
this->cards[unit] = nullptr; this->cards[unit] = nullptr;
this->hold[unit] = false; this->hold[unit] = false;
@@ -267,7 +271,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in,
memcpy(msg->cmd.raw, this->cards[unit], 8); memcpy(msg->cmd.raw, this->cards[unit], 8);
// delete card // delete card
delete this->cards[unit]; delete[] this->cards[unit];
this->cards[unit] = nullptr; this->cards[unit] = nullptr;
this->hold[unit] = false; this->hold[unit] = false;
} }
@@ -326,7 +330,9 @@ bool ICCADevice::parse_msg(MessageData *msg_in,
} }
//log_info("icca", "counter[{}] = {}", unit, this->counter[unit]); //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); write_msg(msg, response_buffer);
delete msg; delete msg;
break; break;
@@ -363,7 +369,7 @@ void ICCADevice::update_card(int unit) {
if (difftime(t_now, this->cards_time[unit]) >= 10.f) { if (difftime(t_now, this->cards_time[unit]) >= 10.f) {
if (this->cards[unit] != nullptr) { if (this->cards[unit] != nullptr) {
delete this->cards[unit]; delete[] this->cards[unit];
} }
this->cards[unit] = nullptr; this->cards[unit] = nullptr;
this->hold[unit] = false; this->hold[unit] = false;
+1 -1
View File
@@ -17,7 +17,7 @@ namespace acioemu {
private: private:
bool type_new; bool type_new;
bool flip_order; bool flip_order;
std::thread *keypad_thread; std::jthread keypad_thread;
std::mutex keypad_mutex; std::mutex keypad_mutex;
uint8_t **cards; uint8_t **cards;
time_t *cards_time; time_t *cards_time;
+371
View File
@@ -0,0 +1,371 @@
#include "bi2x_hook.h"
#if SPICE64
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <memory>
#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_COMM> aio_sci;
static std::unique_ptr<AIO_NMGR_IOB2> aio_manager;
static std::unique_ptr<AIO_IOB2_BI2X_TCS> aio_board;
static std::unique_ptr<AIO_IOB2_BI2X_WRFIRM> 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<AIO_IOB2_BI2X_TCS>();
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<uint8_t>(eamuse_coin_get_stock());
RI_MGR->devices_flush_output();
const auto copy_size = std::min<uint32_t>(status_size, sizeof(current));
if (copy_size > 0) {
std::memcpy(status, &current, 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<AIO_SCI_COMM>();
}
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<AIO_IOB2_BI2X_WRFIRM>();
}
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<AIO_NMGR_IOB2>();
}
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
@@ -0,0 +1,5 @@
#pragma once
namespace games::cardconnect {
void bi2x_hook_init();
}
@@ -0,0 +1,59 @@
#include "cardconnect.h"
#include <format>
#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();
}
}
@@ -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;
};
}
+40
View File
@@ -0,0 +1,40 @@
#include "io.h"
std::vector<Button> &games::cardconnect::get_buttons() {
static std::vector<Button> buttons;
if (buttons.empty()) {
buttons = GameAPI::Buttons::getButtons("Card Connect");
GameAPI::Buttons::sortButtons(
&buttons,
"Test",
"Service",
"Coin Mech"
);
}
return buttons;
}
std::vector<Light> &games::cardconnect::get_lights() {
static std::vector<Light> lights;
if (lights.empty()) {
lights = GameAPI::Lights::getLights("Card Connect");
GameAPI::Lights::sortLights(
&lights,
"Photo Light",
"Card Reader R",
"Card Reader G",
"Card Reader B",
"Side Left R",
"Side Left G",
"Side Left B",
"Side Right R",
"Side Right G",
"Side Right B"
);
}
return lights;
}
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <vector>
#include "cfg/api.h"
namespace games::cardconnect {
namespace Buttons {
enum {
Test,
Service,
CoinMech,
};
}
namespace Lights {
enum {
PhotoLight,
CardReaderR,
CardReaderG,
CardReaderB,
SideLeftR,
SideLeftG,
SideLeftB,
SideRightR,
SideRightG,
SideRightB,
};
}
std::vector<Button> &get_buttons();
std::vector<Light> &get_lights();
}
+554
View File
@@ -0,0 +1,554 @@
#include "printer.h"
#if SPICE64
#include <algorithm>
#include <array>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <mutex>
#include <vector>
#include "games/shared/printer.h"
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
namespace games::cardconnect {
// Settings and native protocol values.
bool PRINTER_DISABLED = false;
static constexpr uint16_t READY = 0;
static constexpr uint16_t INVALID_PARAMETER = 1006;
static constexpr uint16_t BUSY = 1500;
static constexpr uint16_t PRINTING = 2100;
static constexpr uint16_t OUTPUT_ERROR = 2504;
enum PrinterInfoTag : uint16_t {
INFO_PAPER = 0,
INFO_FIRMWARE = 3,
INFO_PRINT_COUNT = 4,
INFO_HOLO_REMAINING = 6,
INFO_MEMORY = 16,
INFO_FRONT_MODE = 19,
INFO_BACK_MODE = 20,
INFO_IR_CORRECTION = 22,
};
static constexpr uint16_t FORMAT_RGB = 3;
static constexpr uint16_t FORMAT_BGR = 4;
static constexpr uint16_t COLOR_COMPONENTS = 3;
static constexpr uint16_t COLOR_DEPTH = 8;
static constexpr uint16_t CARD_WIDTH = 662;
static constexpr uint16_t CARD_HEIGHT = 1024;
static constexpr uint32_t RIBBON_REMAINING = 4096;
static constexpr auto PRINT_TIME = std::chrono::seconds(5);
// Native reply layouts.
#pragma pack(push, 1)
struct FirmwareRecord {
std::array<uint8_t, 34> reserved;
uint8_t major;
uint8_t minor;
std::array<uint8_t, 2> reserved_tail;
};
struct FirmwareReply {
uint8_t count;
std::array<FirmwareRecord, 4> records;
};
struct PrintCountReply {
uint32_t total_count;
std::array<uint8_t, 16> reserved;
uint32_t ribbon_remaining;
std::array<uint8_t, 8> reserved_tail;
};
struct HoloRemainingReply {
uint32_t remaining;
std::array<uint8_t, 28> reserved;
};
struct MemoryReply {
uint16_t reserved;
uint16_t remaining;
uint16_t completed;
uint16_t copies;
std::array<uint8_t, 10> reserved_tail;
};
struct PrintIDReply {
uint16_t remaining;
std::array<uint8_t, 4> reserved;
uint16_t status;
};
#pragma pack(pop)
static_assert(sizeof(FirmwareRecord) == 38);
static_assert(offsetof(FirmwareRecord, major) == 34);
static_assert(offsetof(FirmwareRecord, minor) == 35);
static_assert(sizeof(FirmwareReply) == 153);
static_assert(offsetof(FirmwareReply, records) == 1);
static_assert(sizeof(PrintCountReply) == 32);
static_assert(offsetof(PrintCountReply, ribbon_remaining) == 20);
static_assert(sizeof(HoloRemainingReply) == 32);
static_assert(sizeof(MemoryReply) == 18);
static_assert(offsetof(MemoryReply, remaining) == 2);
static_assert(offsetof(MemoryReply, completed) == 4);
static_assert(offsetof(MemoryReply, copies) == 6);
static_assert(sizeof(PrintIDReply) == 8);
static_assert(offsetof(PrintIDReply, status) == 6);
// One printer, shared by the game's threads.
struct PrintJob {
uint16_t id = 0;
uint16_t copies = 1;
uint16_t remaining = 0;
uint16_t width = 0;
uint16_t height = 0;
uint16_t format = FORMAT_RGB;
std::array<std::vector<uint8_t>, 2> sides;
uint16_t error = READY;
bool receiving = false;
// Submitted jobs stay busy while waiting for this baseline.
bool awaiting_counter_baseline = false;
std::chrono::steady_clock::time_point next_copy;
};
static std::mutex printer_mutex;
static PrintJob job;
static uint16_t next_page_id = 0;
static uint32_t total_count = 0;
static uint32_t cutter_offset = 0;
// Write the result; non-ready codes return FALSE.
static BOOL set_result(uint16_t *result, uint16_t code = READY) {
if (!result) {
log_warning("cardconnect::printer", "missing result buffer");
return FALSE;
}
*result = code;
return code == READY;
}
static BOOL invalid_parameter(uint16_t *result, const char *operation) {
log_warning("cardconnect::printer", "{}: invalid parameter or job state", operation);
return set_result(result, INVALID_PARAMETER);
}
// Caller holds printer_mutex. The baseline gates completion.
static void update_printing() {
if (job.awaiting_counter_baseline) {
return;
}
const auto now = std::chrono::steady_clock::now();
while (job.remaining && now >= job.next_copy) {
++total_count;
--job.remaining;
job.next_copy += PRINT_TIME;
log_info("cardconnect::printer", "job {}: total count={}, remaining={}",
job.id, total_count, job.remaining);
}
}
// Caller holds printer_mutex.
static uint16_t poll_status() {
update_printing();
if (job.error) {
return job.error;
}
return job.remaining ? PRINTING : READY;
}
// Driver lifetime.
static void __stdcall chcusb_MakeThread(uint16_t count) {
log_info("cardconnect::printer", "initializing printer with {} thread slots", count);
}
static BOOL __stdcall chcusb_open(uint16_t *result) {
return set_result(result);
}
static void __stdcall chcusb_close() {
// Closing a worker must not cancel a submitted job.
std::lock_guard lock(printer_mutex);
if (job.receiving) {
log_warning("cardconnect::printer", "discarding incomplete print job {}", job.id);
job = {};
}
}
static BOOL __stdcall chcusb_ReleaseThread(uint16_t *result) {
std::lock_guard lock(printer_mutex);
job = {};
return set_result(result);
}
// Status and printer information.
static BOOL __stdcall chcusb_status(uint16_t *result) {
std::lock_guard lock(printer_mutex);
return set_result(result, poll_status());
}
static BOOL __stdcall chcusb_getPrinterInfo(uint16_t tag, void *buffer, uint16_t *length) {
std::lock_guard lock(printer_mutex);
update_printing();
// Build the requested reply.
std::array<uint8_t, sizeof(FirmwareReply)> data {};
uint16_t size;
switch (tag) {
case INFO_FIRMWARE: {
FirmwareReply reply {};
reply.count = static_cast<uint8_t>(reply.records.size());
for (auto &record : reply.records) {
record.major = 1;
}
size = sizeof(reply);
std::memcpy(data.data(), &reply, size);
break;
}
case INFO_PRINT_COUNT: {
PrintCountReply reply {};
reply.total_count = total_count;
reply.ribbon_remaining = RIBBON_REMAINING;
size = sizeof(reply);
std::memcpy(data.data(), &reply, size);
break;
}
case INFO_HOLO_REMAINING: {
HoloRemainingReply reply {};
reply.remaining = RIBBON_REMAINING;
size = sizeof(reply);
std::memcpy(data.data(), &reply, size);
break;
}
case INFO_MEMORY: {
MemoryReply reply {};
reply.remaining = job.remaining;
reply.completed = static_cast<uint16_t>(job.copies - job.remaining);
reply.copies = job.copies;
size = sizeof(reply);
std::memcpy(data.data(), &reply, size);
break;
}
default:
log_warning("cardconnect::printer", "unsupported printer info tag {}", tag);
return FALSE;
}
// Check capacity before writing.
if (!buffer || !length || *length < size) {
log_warning("cardconnect::printer", "invalid buffer for printer info tag {}", tag);
if (length) {
*length = size;
}
return FALSE;
}
std::memcpy(buffer, data.data(), size);
*length = size;
// CoMain needs its post-cleanup baseline before any copy finishes.
if (tag == INFO_PRINT_COUNT && job.awaiting_counter_baseline) {
job.awaiting_counter_baseline = false;
job.next_copy = std::chrono::steady_clock::now() + PRINT_TIME;
log_info("cardconnect::printer", "job {}: counter baseline={}, starting print timer",
job.id, total_count);
}
return TRUE;
}
// Paper and coating settings do not alter saved RGB data.
static BOOL __stdcall chcusb_setPrinterInfo(
uint16_t tag, const void *buffer, uint16_t *length, uint16_t *result) {
uint16_t expected = 0;
switch (tag) {
case INFO_PAPER:
expected = 10;
break;
case INFO_FRONT_MODE:
case INFO_BACK_MODE:
case INFO_IR_CORRECTION:
// The managed caller sends one byte, not two.
expected = 1;
break;
}
if (!expected || !buffer || !length || *length != expected) {
log_warning("cardconnect::printer",
"setPrinterInfo: tag={}, length={}, expected={}, buffer={}, length pointer={}",
tag, length ? *length : 0, expected, buffer != nullptr, length != nullptr);
return invalid_parameter(result, "setPrinterInfo");
}
return set_result(result);
}
// Configure the next job.
static BOOL __stdcall chcusb_imageformat(
uint16_t format, uint16_t components, uint16_t depth,
uint16_t width, uint16_t height, uint16_t *result) {
std::lock_guard lock(printer_mutex);
update_printing();
if ((format != FORMAT_RGB && format != FORMAT_BGR) ||
components != COLOR_COMPONENTS || depth != COLOR_DEPTH ||
width != CARD_WIDTH || height != CARD_HEIGHT || job.receiving || job.remaining) {
return invalid_parameter(result, "imageformat");
}
job.width = width;
job.height = height;
job.format = format;
return set_result(result);
}
static BOOL __stdcall chcusb_copies(uint16_t copies, uint16_t *result) {
std::lock_guard lock(printer_mutex);
update_printing();
if (!copies || job.receiving || job.remaining) {
return invalid_parameter(result, "copies");
}
job.copies = copies;
return set_result(result);
}
// Receive front and back image data.
static BOOL __stdcall chcusb_startpage(uint16_t mode, uint16_t *page_id, uint16_t *result) {
std::lock_guard lock(printer_mutex);
update_printing();
if (job.receiving || job.remaining) {
log_warning("cardconnect::printer", "startpage while printer is busy");
return set_result(result, BUSY);
}
if (mode != 0 || !page_id || !result || !job.width || !job.height) {
return invalid_parameter(result, "startpage");
}
// Keep zero reserved for "no job".
if (++next_page_id == 0) {
++next_page_id;
}
job.id = next_page_id;
job.error = READY;
job.receiving = true;
job.sides = {};
*page_id = job.id;
log_info("cardconnect::printer", "receiving job {} ({} copies)", job.id, job.copies);
return set_result(result);
}
static BOOL __stdcall chcusb_write(
const uint8_t *data, uint32_t *size, uint16_t *result) {
std::lock_guard lock(printer_mutex);
const size_t expected = static_cast<size_t>(job.width) * job.height * COLOR_COMPONENTS;
if (!job.receiving || !data || !size || !result || !*size) {
return invalid_parameter(result, "write");
}
// Fill the front before accepting the back.
auto &side = job.sides[job.sides[0].size() == expected ? 1 : 0];
if (*size > expected - side.size()) {
return invalid_parameter(result, "write size");
}
side.insert(side.end(), data, data + *size);
return set_result(result);
}
// Accept coating masks without saving them.
static BOOL __stdcall discard_mask(
const uint8_t *data, uint32_t *size, uint16_t *result) {
std::lock_guard lock(printer_mutex);
if (!job.receiving || !data || !size ||
*size != static_cast<uint32_t>(job.width) * job.height) {
return invalid_parameter(result, "mask write");
}
return set_result(result);
}
static BOOL __stdcall chcusb_endpage(uint16_t *result) {
std::lock_guard lock(printer_mutex);
const size_t expected = static_cast<size_t>(job.width) * job.height * COLOR_COMPONENTS;
// Both sides must be complete.
if (!result || !job.receiving ||
job.sides[0].size() != expected || job.sides[1].size() != expected) {
return invalid_parameter(result, "endpage");
}
// Image files use RGB channel order.
if (job.format == FORMAT_BGR) {
for (auto &side : job.sides) {
for (size_t i = 0; i < side.size(); i += COLOR_COMPONENTS) {
std::swap(side[i], side[i + 2]);
}
}
}
// Save every copy before reporting success.
job.receiving = false;
for (uint16_t i = 0; i < job.copies; ++i) {
if (!games::shared::printer_write_images({
{job.sides[0].data(), job.width, job.height, COLOR_COMPONENTS, "_front"},
{job.sides[1].data(), job.width, job.height, COLOR_COMPONENTS, "_back"}})) {
job.error = OUTPUT_ERROR;
job.sides = {};
log_warning("cardconnect::printer", "failed to save job {}", job.id);
return set_result(result, job.error);
}
}
// Wait for the game's counter baseline.
job.sides = {};
job.remaining = job.copies;
job.awaiting_counter_baseline = true;
log_info("cardconnect::printer", "submitted job {}, waiting for counter baseline", job.id);
return set_result(result);
}
// Job progress and cancellation.
static BOOL __stdcall chcusb_getPrintIDStatus(
uint16_t page_id, void *buffer, uint16_t *result) {
std::lock_guard lock(printer_mutex);
const auto status = poll_status();
if (!buffer || page_id != job.id) {
return invalid_parameter(result, "getPrintIDStatus");
}
PrintIDReply reply {};
reply.remaining = job.remaining;
reply.status = status;
std::memcpy(buffer, &reply, sizeof(reply));
return set_result(result);
}
static BOOL __stdcall chcusb_cancelCopies(uint16_t page_id, uint16_t *result) {
std::lock_guard lock(printer_mutex);
if (page_id != job.id) {
return invalid_parameter(result, "cancelCopies");
}
update_printing();
job.remaining = 0;
job.receiving = false;
job.awaiting_counter_baseline = false;
job.sides = {};
return set_result(result);
}
static BOOL __stdcall chcusb_resetPrinter(uint16_t *result) {
std::lock_guard lock(printer_mutex);
update_printing();
job = {};
return set_result(result);
}
// Physical-printer controls.
static BOOL __stdcall chcusb_setIcctable(
[[maybe_unused]] const char *input_profile,
[[maybe_unused]] const char *output_profile,
[[maybe_unused]] uint16_t rendering_intents,
[[maybe_unused]] const void *input_tone_r,
[[maybe_unused]] const void *input_tone_g,
[[maybe_unused]] const void *input_tone_b,
[[maybe_unused]] const void *output_tone_r,
[[maybe_unused]] const void *output_tone_g,
[[maybe_unused]] const void *output_tone_b,
uint16_t *result) {
// Keep the original RGB colors.
return set_result(result);
}
static void __stdcall chcusb_setCutterOffset(uint32_t offset, uint16_t *result) {
std::lock_guard lock(printer_mutex);
cutter_offset = offset;
set_result(result);
}
static BOOL __stdcall chcusb_getCutterOffset(uint32_t *offset, uint16_t *result) {
std::lock_guard lock(printer_mutex);
if (!offset) {
return invalid_parameter(result, "getCutterOffset");
}
*offset = cutter_offset;
return set_result(result);
}
static BOOL __stdcall chcusb_getErrorLog(uint16_t index, void *buffer, uint16_t *result) {
if (!buffer || index != 0) {
return invalid_parameter(result, "getErrorLog");
}
std::memset(buffer, 0, 16);
return set_result(result);
}
template<typename T>
static void hook(const char *name, T function) {
T original = nullptr;
detour::trampoline("C320Dusb.dll", name, function, &original);
}
void printer_attach() {
if (PRINTER_DISABLED) {
return;
}
log_info("cardconnect::printer", "enabling C320D printer emulation");
games::shared::printer_init_output({"_front", "_back"});
libutils::load_library("game\\tcs_data\\Plugins\\C320Dusb.dll");
// Connections and status.
hook("chcusb_MakeThread", chcusb_MakeThread);
hook("chcusb_open", chcusb_open);
hook("chcusb_close", chcusb_close);
hook("chcusb_ReleaseThread", chcusb_ReleaseThread);
hook("chcusb_status", chcusb_status);
hook("chcusb_getPrinterInfo", chcusb_getPrinterInfo);
hook("chcusb_setPrinterInfo", chcusb_setPrinterInfo);
// Job setup and image transfer.
hook("chcusb_imageformat", chcusb_imageformat);
hook("chcusb_copies", chcusb_copies);
hook("chcusb_startpage", chcusb_startpage);
hook("chcusb_write", chcusb_write);
hook("chcusb_writeLaminate", discard_mask);
hook("chcusb_writeHolo", discard_mask);
hook("chcusb_writeIred", discard_mask);
hook("chcusb_endpage", chcusb_endpage);
// Completion and device controls.
hook("chcusb_getPrintIDStatus", chcusb_getPrintIDStatus);
hook("chcusb_cancelCopies", chcusb_cancelCopies);
hook("chcusb_resetPrinter", chcusb_resetPrinter);
hook("chcusb_setIcctable", chcusb_setIcctable);
hook("chcusb_setCutterOffset", chcusb_setCutterOffset);
hook("chcusb_getCutterOffset", chcusb_getCutterOffset);
hook("chcusb_getErrorLog", chcusb_getErrorLog);
}
}
#else
namespace games::cardconnect {
bool PRINTER_DISABLED = false;
}
#endif
+6
View File
@@ -0,0 +1,6 @@
#pragma once
namespace games::cardconnect {
extern bool PRINTER_DISABLED;
void printer_attach();
}
+8
View File
@@ -38,6 +38,7 @@
#include "mfg/io.h" #include "mfg/io.h"
#include "pc/io.h" #include "pc/io.h"
#include "udn/io.h" #include "udn/io.h"
#include "cardconnect/io.h"
namespace games { namespace games {
@@ -341,6 +342,13 @@ namespace games {
buttons_help.insert({ udn, udn::get_buttons_help() }); buttons_help.insert({ udn, udn::get_buttons_help() });
lights.insert({ udn, udn::get_lights() }); lights.insert({ udn, udn::get_lights() });
file_hints[udn].push_back({"game/dancearound_Data"}); file_hints[udn].push_back({"game/dancearound_Data"});
// Card Connect
const std::string cardconnect("Card Connect");
games.push_back(cardconnect);
buttons.insert({ cardconnect, cardconnect::get_buttons() });
lights.insert({ cardconnect, cardconnect::get_lights() });
file_hints[cardconnect].push_back({"game/tcs.exe"});
} }
const std::vector<std::string> &get_games() { const std::vector<std::string> &get_games() {
+97 -55
View File
@@ -2,6 +2,7 @@
#include <vector> #include <vector>
#include <thread> #include <thread>
#include <limits>
#include "avs/game.h" #include "avs/game.h"
#include "hooks/sleephook.h" #include "hooks/sleephook.h"
@@ -264,7 +265,9 @@ namespace games::shared {
typedef void (CALLBACK *PFNACBfunc)(DWORD dwErr, short PrintFlag, short PrintUsbNo, long nJid, int CopyNumber); typedef void (CALLBACK *PFNACBfunc)(DWORD dwErr, short PrintFlag, short PrintUsbNo, long nJid, int CopyNumber);
static inline std::string get_image_out_path(bool clear, std::string path, const std::string &format) { static std::vector<std::string> get_image_out_paths(
bool clear, std::string path, const std::string &format,
const std::vector<std::string> &suffixes, bool overwrite = false) {
// check path // check path
if (path.empty()) { if (path.empty()) {
@@ -277,28 +280,99 @@ namespace games::shared {
path = std::string(path.c_str(), path.length() - 1); path = std::string(path.c_str(), path.length() - 1);
// find non-existing filename // find non-existing filename
static std::string prefix = "printer_";
for (int n = 0; n < 4096; n++) { for (int n = 0; n < 4096; n++) {
std::ostringstream filename_s; std::vector<std::string> filenames;
filename_s << path << "\\" << prefix << n << "." << format; bool exists = false;
std::string filename = filename_s.str(); for (const auto &suffix : suffixes) {
if (clear && fileutils::file_exists(filename.c_str())) { auto filename = fmt::format("{}\\printer_{}{}.{}", path, n, suffix, format);
if (fileutils::file_exists(filename.c_str())) {
exists = true;
if (clear) {
log_info("printer", "deleting {}...", filename); log_info("printer", "deleting {}...", filename);
DeleteFile(filename.c_str()); if (!DeleteFileA(filename.c_str())) {
if (PRINTER_OVERWRITE_FILE) { log_warning("printer", "could not delete {}: {}", filename, GetLastError());
return filename; return {};
} }
} }
else if (!fileutils::file_exists(filename.c_str())) }
return filename; filenames.push_back(std::move(filename));
}
if ((!clear && !exists) || overwrite) {
return filenames;
}
} }
// error
if (!clear) { if (!clear) {
log_fatal("printer", "could not find path"); log_warning("printer", "could not find an unused printer output filename");
}
return {};
} }
return "DUMMY"; bool printer_write_image(const uint8_t *data, int width, int height, int components,
const std::string &suffix) {
return printer_write_images({{data, width, height, components, suffix}});
}
bool printer_write_images(std::initializer_list<PrinterImage> images) {
if (images.size() == 0 || PRINTER_PATH.empty() || PRINTER_FORMAT.empty()) {
log_warning("printer", "printer output is uninitialized or contains no images");
return false;
}
std::vector<std::string> suffixes;
for (const auto &image : images) {
if (!image.data || image.width <= 0 || image.height <= 0 ||
(image.components != 1 && image.components != 3) ||
image.width > std::numeric_limits<int>::max() / image.components / image.height) {
log_warning("printer", "invalid image supplied for printer output");
return false;
}
suffixes.push_back(image.suffix);
}
bool all_saved = true;
for (const auto &path : PRINTER_PATH) {
for (const auto &format : PRINTER_FORMAT) {
const auto image_paths = get_image_out_paths(
PRINTER_OVERWRITE_FILE, path, format, suffixes, PRINTER_OVERWRITE_FILE);
if (image_paths.size() != images.size()) {
log_warning("printer", "could not allocate printer output paths in {}", path);
all_saved = false;
continue;
}
size_t index = 0;
for (const auto &image : images) {
const auto &image_path = image_paths[index++];
bool success = false;
if (format == "png") {
success = stbi_write_png(
image_path.c_str(), image.width, image.height, image.components,
image.data, image.width * image.components);
} else if (format == "bmp") {
success = stbi_write_bmp(image_path.c_str(), image.width, image.height,
image.components, image.data);
} else if (format == "tga") {
success = stbi_write_tga(image_path.c_str(), image.width, image.height,
image.components, image.data);
} else if (format == "jpg") {
success = stbi_write_jpg(
image_path.c_str(), image.width, image.height, image.components,
image.data, PRINTER_JPG_QUALITY);
}
if (success) {
log_info("printer", "printer emulation has written an image to {}", image_path);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("Printer: saved {}", fileutils::basename(image_path)));
} else {
all_saved = false;
log_warning("printer", "printer emulation failed to write image to {}", image_path);
overlay::notifications::add(
overlay::notifications::Severity::Error,
fmt::format("Printer: failed to write {}", fileutils::basename(image_path)));
}
}
}
}
return all_saved;
} }
static inline bool process_image_print(const CPDBandImageParams *pBandImage) { static inline bool process_image_print(const CPDBandImageParams *pBandImage) {
@@ -372,43 +446,8 @@ namespace games::shared {
} }
} }
// iterate folders // preserve the legacy caller's success behavior after reporting file errors
log_misc("printer", "writing files..."); printer_write_image(image_data, image_width, image_height, 3);
for (const auto &path : PRINTER_PATH) {
for (const auto &format : PRINTER_FORMAT) {
// get image path
std::string image_path = get_image_out_path(PRINTER_OVERWRITE_FILE, path, format);
bool success = false;
// call write function depending on format
if (format == "png" && stbi_write_png(
image_path.c_str(), image_width, image_height, 3, image_data, image_width * 3))
success = true;
if (format == "bmp" && stbi_write_bmp(
image_path.c_str(), image_width, image_height, 3, image_data))
success = true;
if (format == "tga" && stbi_write_tga(
image_path.c_str(), image_width, image_height, 3, image_data))
success = true;
if (format == "jpg" && stbi_write_jpg(
image_path.c_str(), image_width, image_height, 3, image_data, PRINTER_JPG_QUALITY))
success = true;
// logging
if (success) {
log_info("printer", "printer emulation has written an image to {}", image_path);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("Printer: saved {}", fileutils::basename(image_path)));
} else {
log_warning("printer", "printer emulation failed to write image to {}", image_path);
overlay::notifications::add(
overlay::notifications::Severity::Error,
fmt::format("Printer: failed to write {}", fileutils::basename(image_path)));
}
}
}
// clean up // clean up
delete[] image_data; delete[] image_data;
@@ -555,9 +594,7 @@ namespace games::shared {
log_info("printer", "CPUXInit called"); log_info("printer", "CPUXInit called");
} }
void printer_attach() { void printer_init_output(std::initializer_list<std::string> suffixes) {
log_info("printer", "SpiceTools Printer");
// default parameters // default parameters
if (PRINTER_PATH.empty()) { if (PRINTER_PATH.empty()) {
PRINTER_PATH.emplace_back("."); PRINTER_PATH.emplace_back(".");
@@ -588,10 +625,15 @@ namespace games::shared {
PRINTER_CLEAR = false; PRINTER_CLEAR = false;
for (const auto &path : PRINTER_PATH) { for (const auto &path : PRINTER_PATH) {
for (const auto &format : PRINTER_FORMAT) { for (const auto &format : PRINTER_FORMAT) {
get_image_out_path(true, path, format); get_image_out_paths(true, path, format, suffixes);
} }
} }
} }
}
void printer_attach() {
log_info("printer", "SpiceTools Printer");
printer_init_output();
// IAT hooks // IAT hooks
detour::iat_try("CPU9CheckPaperRemain", CPU9CheckPaperRemain); detour::iat_try("CPU9CheckPaperRemain", CPU9CheckPaperRemain);
+14
View File
@@ -2,6 +2,8 @@
#include <vector> #include <vector>
#include <string> #include <string>
#include <cstdint>
#include <initializer_list>
#include <windows.h> #include <windows.h>
@@ -15,4 +17,16 @@ namespace games::shared {
extern bool PRINTER_OVERWRITE_FILE; extern bool PRINTER_OVERWRITE_FILE;
void printer_attach(); void printer_attach();
struct PrinterImage {
const uint8_t *data;
int width;
int height;
int components;
std::string suffix;
};
void printer_init_output(std::initializer_list<std::string> suffixes = {""});
bool printer_write_images(std::initializer_list<PrinterImage> images);
bool printer_write_image(const uint8_t *data, int width, int height, int components,
const std::string &suffix = "");
} }
+38 -2
View File
@@ -73,6 +73,8 @@
#include "games/mfg/mfg.h" #include "games/mfg/mfg.h"
#include "games/pc/pc.h" #include "games/pc/pc.h"
#include "games/udn/udn.h" #include "games/udn/udn.h"
#include "games/cardconnect/cardconnect.h"
#include "games/cardconnect/printer.h"
#include "games/museca/museca.h" #include "games/museca/museca.h"
#include "hooks/avshook.h" #include "hooks/avshook.h"
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
@@ -248,6 +250,7 @@ int main_implementation(int argc, char *argv[]) {
bool attach_mfg = false; bool attach_mfg = false;
bool attach_pc = false; bool attach_pc = false;
bool attach_udn = false; bool attach_udn = false;
bool attach_cardconnect = false;
bool attach_museca = false; bool attach_museca = false;
bool show_cursor_if_no_touch = false; bool show_cursor_if_no_touch = false;
@@ -787,6 +790,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::LoadUDNModule].value_bool()) { if (options[launcher::Options::LoadUDNModule].value_bool()) {
attach_udn = true; attach_udn = true;
} }
if (options[launcher::Options::LoadCardConnectModule].value_bool()) {
attach_cardconnect = true;
}
if (options[launcher::Options::LoadMusecaModule].value_bool()) { if (options[launcher::Options::LoadMusecaModule].value_bool()) {
attach_museca = true; attach_museca = true;
} }
@@ -879,7 +885,8 @@ int main_implementation(int argc, char *argv[]) {
// printer overwrite // printer overwrite
if (options[launcher::Options::SDVXPrinterOutputOverwrite].value_bool() || if (options[launcher::Options::SDVXPrinterOutputOverwrite].value_bool() ||
options[launcher::Options::LovePlusPrinterOutputOverwrite].value_bool()) { options[launcher::Options::LovePlusPrinterOutputOverwrite].value_bool() ||
options[launcher::Options::CardConnectPrinterOutputOverwrite].value_bool()) {
games::shared::PRINTER_OVERWRITE_FILE = true; games::shared::PRINTER_OVERWRITE_FILE = true;
} }
@@ -890,6 +897,9 @@ int main_implementation(int argc, char *argv[]) {
for (auto &path : options[launcher::Options::LovePlusPrinterOutputPath].values_text()) { for (auto &path : options[launcher::Options::LovePlusPrinterOutputPath].values_text()) {
games::shared::PRINTER_PATH.push_back(path); games::shared::PRINTER_PATH.push_back(path);
} }
for (auto &path : options[launcher::Options::CardConnectPrinterOutputPath].values_text()) {
games::shared::PRINTER_PATH.push_back(path);
}
// printer file format // printer file format
for (auto &path : options[launcher::Options::SDVXPrinterOutputFormat].values_text()) { for (auto &path : options[launcher::Options::SDVXPrinterOutputFormat].values_text()) {
@@ -898,17 +908,24 @@ int main_implementation(int argc, char *argv[]) {
for (auto &path : options[launcher::Options::LovePlusPrinterOutputFormat].values_text()) { for (auto &path : options[launcher::Options::LovePlusPrinterOutputFormat].values_text()) {
games::shared::PRINTER_FORMAT.push_back(path); games::shared::PRINTER_FORMAT.push_back(path);
} }
for (auto &format : options[launcher::Options::CardConnectPrinterOutputFormat].values_text()) {
games::shared::PRINTER_FORMAT.push_back(format);
}
// printer JPG quality // printer JPG quality
if (options[launcher::Options::SDVXPrinterJPGQuality].is_active()) { if (options[launcher::Options::SDVXPrinterJPGQuality].is_active()) {
games::shared::PRINTER_JPG_QUALITY = options[launcher::Options::SDVXPrinterJPGQuality].value_uint32(); games::shared::PRINTER_JPG_QUALITY = options[launcher::Options::SDVXPrinterJPGQuality].value_uint32();
} else if (options[launcher::Options::LovePlusPrinterJPGQuality].is_active()) { } else if (options[launcher::Options::LovePlusPrinterJPGQuality].is_active()) {
games::shared::PRINTER_JPG_QUALITY = options[launcher::Options::LovePlusPrinterJPGQuality].value_uint32(); games::shared::PRINTER_JPG_QUALITY = options[launcher::Options::LovePlusPrinterJPGQuality].value_uint32();
} else if (options[launcher::Options::CardConnectPrinterJPGQuality].is_active()) {
games::shared::PRINTER_JPG_QUALITY =
options[launcher::Options::CardConnectPrinterJPGQuality].value_uint32();
} }
// printer output clear // printer output clear
if (options[launcher::Options::SDVXPrinterOutputClear].value_bool() || if (options[launcher::Options::SDVXPrinterOutputClear].value_bool() ||
options[launcher::Options::LovePlusPrinterOutputClear].value_bool()) { options[launcher::Options::LovePlusPrinterOutputClear].value_bool() ||
options[launcher::Options::CardConnectPrinterOutputClear].value_bool()) {
games::shared::PRINTER_CLEAR = true; games::shared::PRINTER_CLEAR = true;
} }
@@ -1498,6 +1515,12 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::UDNNoIO].is_active()) { if (options[launcher::Options::UDNNoIO].is_active()) {
games::udn::UDN_NO_IO = options[launcher::Options::UDNNoIO].value_bool(); games::udn::UDN_NO_IO = options[launcher::Options::UDNNoIO].value_bool();
} }
if (options[launcher::Options::CardConnectArgs].is_active()) {
games::cardconnect::CARDCONNECT_INJECT_ARGS =
options[launcher::Options::CardConnectArgs].value_text();
}
games::cardconnect::PRINTER_DISABLED =
options[launcher::Options::CardConnectNoPrinter].value_bool();
if (options[launcher::Options::spice2x_EnableSMXStage].value_bool()) { if (options[launcher::Options::spice2x_EnableSMXStage].value_bool()) {
rawinput::ENABLE_SMX_STAGE = true; rawinput::ENABLE_SMX_STAGE = true;
} }
@@ -2322,6 +2345,14 @@ int main_implementation(int argc, char *argv[]) {
break; break;
} }
// Card Connect
if (check_dll("kamunity.dll") && fileutils::file_exists("game/tcs.exe")) {
avs::game::DLL_NAME = "kamunity.dll";
attach_io = true;
attach_cardconnect = true;
break;
}
// Busou Shinki: Armored Princess Battle Conductor // Busou Shinki: Armored Princess Battle Conductor
if (check_dll("kamunity.dll") && fileutils::file_exists("game/bsac_app.exe")) { if (check_dll("kamunity.dll") && fileutils::file_exists("game/bsac_app.exe")) {
avs::game::DLL_NAME = "kamunity.dll"; avs::game::DLL_NAME = "kamunity.dll";
@@ -2491,6 +2522,11 @@ int main_implementation(int argc, char *argv[]) {
avs::core::HEAP_SIZE = 1073741824; // 1GB avs::core::HEAP_SIZE = 1073741824; // 1GB
games.push_back(new games::udn::UDNGame()); games.push_back(new games::udn::UDNGame());
} }
if (attach_cardconnect) {
show_cursor_if_no_touch = true;
avs::core::set_default_heap_size("kamunity.dll");
games.push_back(new games::cardconnect::CardConnectGame());
}
// apply user heap size, if defined // apply user heap size, if defined
if (user_heap_size > 0) { if (user_heap_size > 0) {
+68
View File
@@ -1527,6 +1527,14 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = std::invoke([]()
.game_name = "Museca", .game_name = "Museca",
.category = "Advanced Game Options", .category = "Advanced Game Options",
}; };
c[launcher::Options::LoadCardConnectModule] = {
.title = "Force Load Card Connect Module",
.name = "cardconnect",
.desc = "Manually enable Card Connect module.",
.type = OptionType::Bool,
.game_name = "Card Connect",
.category = "Advanced Game Options",
};
c[launcher::Options::PathToModules] = { c[launcher::Options::PathToModules] = {
.title = "Modules Folder Override", .title = "Modules Folder Override",
.name = "modules", .name = "modules",
@@ -2477,6 +2485,66 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = std::invoke([]()
.game_name = "DANCE aROUND", .game_name = "DANCE aROUND",
.category = "Advanced Game Options", .category = "Advanced Game Options",
}; };
c[launcher::Options::CardConnectArgs] = {
.title = "Card Connect Arguments Override",
.name = "cardconnectargs",
.desc = "Command line arguments passed to the game.",
.type = OptionType::Text,
.setting_name = "",
.game_name = "Card Connect",
.category = "Advanced Game Options",
};
c[launcher::Options::CardConnectNoPrinter] = {
.title = "Card Connect Disable Printer Emulation",
.name = "cardconnectnoprinter",
.desc = "Leave the native printer driver unhooked for use with a real printer.",
.type = OptionType::Bool,
.game_name = "Card Connect",
.category = "Cab Peripherals",
};
c[launcher::Options::CardConnectPrinterOutputPath] = {
.title = "Card Connect Printer Output Path",
.name = "cardconnectprinterpath",
.desc = "Folder for front/back color images. Defaults to the working directory.",
.type = OptionType::Text,
.game_name = "Card Connect",
.category = "Cab Peripherals",
.picker = OptionPickerType::DirectoryPath,
};
c[launcher::Options::CardConnectPrinterOutputClear] = {
.title = "Card Connect Printer Output Clear",
.name = "cardconnectprinterclear",
.desc = "Delete saved front/back images in the output directory on startup.",
.type = OptionType::Bool,
.game_name = "Card Connect",
.category = "Cab Peripherals",
};
c[launcher::Options::CardConnectPrinterOutputOverwrite] = {
.title = "Card Connect Printer Output Overwrite",
.name = "cardconnectprinteroverwrite",
.desc = "Always overwrite the same front/back image pair.",
.type = OptionType::Bool,
.game_name = "Card Connect",
.category = "Cab Peripherals",
};
c[launcher::Options::CardConnectPrinterOutputFormat] = {
.title = "Card Connect Printer Output Format",
.name = "cardconnectprinterformat",
.desc = "File format for printer output. Defaults to PNG.",
.type = OptionType::Text,
.setting_name = "(png/bmp/tga/jpg)",
.game_name = "Card Connect",
.category = "Cab Peripherals",
};
c[launcher::Options::CardConnectPrinterJPGQuality] = {
.title = "Card Connect Printer JPG Quality",
.name = "cardconnectprinterjpgquality",
.desc = "Quality setting in percent if JPG format is used.",
.type = OptionType::Integer,
.setting_name = "(1-100)",
.game_name = "Card Connect",
.category = "Cab Peripherals",
};
c[launcher::Options::spice2x_LightsOverallBrightness] = { c[launcher::Options::spice2x_LightsOverallBrightness] = {
.title = "Lights Brightness Adjustment", .title = "Lights Brightness Adjustment",
.name = "sp2x-lights-brightness", .name = "sp2x-lights-brightness",
+8
View File
@@ -151,6 +151,7 @@ namespace launcher {
LoadMFGModule, LoadMFGModule,
LoadPCModule, LoadPCModule,
LoadUDNModule, LoadUDNModule,
LoadCardConnectModule,
LoadMusecaModule, LoadMusecaModule,
PathToModules, PathToModules,
ScreenshotFolder, ScreenshotFolder,
@@ -253,6 +254,13 @@ namespace launcher {
PCKnobMode, PCKnobMode,
UDNArgs, UDNArgs,
UDNNoIO, UDNNoIO,
CardConnectArgs,
CardConnectNoPrinter,
CardConnectPrinterOutputPath,
CardConnectPrinterOutputClear,
CardConnectPrinterOutputOverwrite,
CardConnectPrinterOutputFormat,
CardConnectPrinterJPGQuality,
spice2x_LightsOverallBrightness, spice2x_LightsOverallBrightness,
spice2x_WindowBorder, spice2x_WindowBorder,
spice2x_WindowSize, spice2x_WindowSize,
+2
View File
@@ -736,6 +736,8 @@ void eamuse_autodetect_game() {
eamuse_set_game("DANCE aROUND"); eamuse_set_game("DANCE aROUND");
else if (avs::game::is_model("NDD")) else if (avs::game::is_model("NDD"))
eamuse_set_game("Silent Scope: Bone Eater"); eamuse_set_game("Silent Scope: Bone Eater");
else if (avs::game::is_model("TCS"))
eamuse_set_game("Card Connect");
else { else {
log_warning("eamuse", "unknown game model: {}", avs::game::MODEL); log_warning("eamuse", "unknown game model: {}", avs::game::MODEL);
eamuse_set_game("unknown"); eamuse_set_game("unknown");
+1 -1
View File
@@ -62,7 +62,7 @@ namespace execexe {
static HANDLE WINAPI execexe_CreateFileW_hook(LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, static HANDLE WINAPI execexe_CreateFileW_hook(LPCWSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) { DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) {
if (lpFileName == port_name && acio->open(lpFileName)) { if (lpFileName == port_name) {
if (!port_opened) { if (!port_opened) {
port_opened = acio->open(port_name.c_str()); port_opened = acio->open(port_name.c_str());
} else { } else {