mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-10-11 12:23:50 -07:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
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;
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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, ¤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<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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace games::cardconnect {
|
||||||
|
extern bool PRINTER_DISABLED;
|
||||||
|
void printer_attach();
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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);
|
||||||
log_info("printer", "deleting {}...", filename);
|
if (fileutils::file_exists(filename.c_str())) {
|
||||||
DeleteFile(filename.c_str());
|
exists = true;
|
||||||
if (PRINTER_OVERWRITE_FILE) {
|
if (clear) {
|
||||||
return filename;
|
log_info("printer", "deleting {}...", filename);
|
||||||
|
if (!DeleteFileA(filename.c_str())) {
|
||||||
|
log_warning("printer", "could not delete {}: {}", filename, GetLastError());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filenames.push_back(std::move(filename));
|
||||||
|
}
|
||||||
|
if ((!clear && !exists) || overwrite) {
|
||||||
|
return filenames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!clear) {
|
||||||
|
log_warning("printer", "could not find an unused printer output filename");
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
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)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (!fileutils::file_exists(filename.c_str()))
|
|
||||||
return filename;
|
|
||||||
}
|
}
|
||||||
|
return all_saved;
|
||||||
// error
|
|
||||||
if (!clear) {
|
|
||||||
log_fatal("printer", "could not find path");
|
|
||||||
}
|
|
||||||
|
|
||||||
return "DUMMY";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@@ -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 = "");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user